/* static elements */ const titleBar = document.getElementById("title") const buttonDetails = document.getElementById("toggle-details") const divDetails = document.getElementById("details") const buttonComments = document.getElementById("toggle-comments") const divComments = document.getElementById("comments") const commentTemplate = document.getElementById("template-comment") /* state */ let player let detailsLoaded = false let commentsLoaded = false /* setup player */ function onPlayerReady(_e) { document.title = titleBar.textContent = player.videoTitle } function onYouTubeIframeAPIReady() { player = new YT.Player("player", { host: "https://www.youtube-nocookie.com", events: { "onReady": onPlayerReady, }, }) } /* helpers */ async function getApiReq(path) { return fetch("/api" + path) .then(async res => { if (!res.ok) throw new Error("Server error:", await res.text()) return res.json() }) .catch(e => console.error(e)) } function toggleVis(el) { const v = !!el.style.display el.style.display = v ? null : "none" return v } // TODO this func could be moved to server function createAnchors(text) { function _a(href, innerText) { return `${innerText ?? href}` } const urlRegex = /https?:\/\/[a-z0-9#$%&-./:=?@_~]+[a-z0-9/]/gi const channelRegex = /[^/]@[a-z0-9.-_]+/gi const timestampRegex = /[0-9]+:[0-9]+/g return text .replaceAll("\n", "
") .replace(urlRegex, url => { const ytWatchRegex = /^(https?:\/\/)?(www\.)?(youtu(be\.com|\.be|be-nocookie\.com))\/watch/gi const href = url.replace(ytWatchRegex, `${window.location.origin}/watch`) return _a(href, url) }) .replace(channelRegex, channel => { return _a(`https://youtube.com/${channel}`, channel) }) .replace(timestampRegex, ts => { const t = ts.split(":").reverse().reduce((a, v, i) => a + v * Math.pow(60, i), 0) /* calculate seconds from timestamp */ return _a(`${window.location.origin}/watch?v=${player.playerInfo.videoData.video_id}&t=${t}`, ts) }) } function fillFields(data, fieldContainer) { // see html if this doesn't make sense const fieldElements = fieldContainer.querySelectorAll("[data-f]") const fieldElementsMap = {} for (const fe of fieldElements) { const f = fe.dataset.f fieldElementsMap[f] = fe if (fe.dataset.skip !== undefined) continue const v = data[f] switch (fe.tagName) { case "DIV": fe.innerHTML = fe.dataset.anchor !== undefined ? createAnchors(v) : v break case "SPAN": // only used for stats fe.textContent = new Intl.NumberFormat().format(v) break case "TIME": fe.textContent = fe.datetime = v break case "A": // only used for channel links fe.href = `https://www.youtube.com/${v}` default: fe.textContent = v } } return fieldElementsMap } /* setup video details */ async function toggleDetails() { if (!player) return false if (detailsLoaded) return toggleVis(divDetails) // data const data = await getApiReq(`/details?id=${player.playerInfo.videoData.video_id}`) // pop const fieldElements = fillFields(data, divDetails) fieldElements.author.href = `https://www.youtube.com/channel/${data.channel}` fieldElements.tags.textContent = data.tags.join(", ") // done detailsLoaded = true return toggleVis(divDetails) } /* setup video comments */ async function toggleComments() { if (!player) return false if (commentsLoaded) return toggleVis(divComments) // data const data = await getApiReq(`/comments?id=${player.playerInfo.videoData.video_id }`) // pop function _genComment(commentData, parent) { const c = commentTemplate.content.cloneNode(true) const fieldElements = fillFields(commentData, c) if (commentData.published === commentData.updated) fieldElements.updated.parentElement.remove() if (commentData.replies) { const divReplies = c.querySelector(".replies") for (const rc of commentData.replies) _genComment(rc, divReplies) } parent.append(c) } for (const commentData of data) _genComment(commentData, divComments) // done commentsLoaded = true return toggleVis(divComments) } /* details button */ buttonDetails.addEventListener("click", () => { toggleDetails() toggleComments() buttonDetails.remove() }) /* timestamps in comments */ function timestampLinkClick(e) { if (e.target.tagName !== "A") return // FIXME this needs to check origin as well const url = new URL(e.target.href) if (url.pathname !== "/watch" || url.searchParams.get("v") !== player.playerInfo.videoData.video_id) return e.preventDefault() const t = url.searchParams.get("t") if (!t) return player.seekTo(t) // change url to match window in case clicking on youtube.com link // FIXME get ridded of replace youtube links w/ host links url.protocol = window.location.protocol url.hostname = window.location.hostname url.port = window.location.port // reflect url and scroll to top of page window.history.replaceState({}, "", url) window.scrollTo(0, 0) } document.addEventListener("click", timestampLinkClick)