/* setup player */ let player const titleBar = document.getElementById("title") 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) { const err = await res.text(); throw new Error(`Server error: ${err}`); } return res.json() }) .then(data => { return data }) .catch(error => { console.error("Fetch error:", error) }) } function toggleVis(el) { el.style.display = el.style.display ? null : "none" } 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 .replace(urlRegex, url => { const ytRegex = /^(https?:\/\/)?(www\.)?(youtu(be\.com|\.be|be-nocookie\.com))\//gi const href = url.replace(ytRegex, `${window.location.origin}/`) 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) }) } /* setup video details buttons */ const buttonDetails = document.getElementById("toggle-details") const divDetails = document.getElementById("details") let detailsLoaded = false async function toggleDetails() { if (!player) return if (detailsLoaded) { toggleVis(divDetails) return } // elements const channel = document.getElementById("details-channel") const likes = document.getElementById("details-likes") const views = document.getElementById("details-views") const date = document.getElementById("details-date") const desc = document.getElementById("details-desc") const tags = document.getElementById("details-tags") const comments = document.getElementById("details-comments") // req const d = (await getApiReq(`/details?id=${player.playerInfo.videoData.video_id}`)).items[0] // populate channel.textContent = d.snippet.channelTitle channel.href = `https://www.youtube.com/channel/${d.snippet.channelId}` views.textContent = new Intl.NumberFormat().format(d.statistics.viewCount) likes.textContent = new Intl.NumberFormat().format(d.statistics.likeCount) comments.textContent = new Intl.NumberFormat().format(d.statistics.commentCount) date.textContent = d.snippet.publishedAt if (d.snippet.description) desc.innerHTML = createAnchors(d.snippet.description.replaceAll("\n", "
")) tags.textContent = d.snippet.tags.join(", ") // done detailsLoaded = true toggleVis(divDetails) } /* setup video comments buttons */ const buttonComments = document.getElementById("toggle-comments") const divComments = document.getElementById("comments") const commentTemplate = document.getElementById("template-comment") let commentsLoaded = false function genComment(cd, parent, replies) { const c = commentTemplate.content.cloneNode(true) const author = c.querySelector(".author") const date = c.querySelector(".date") const dateM = c.querySelector(".date.modified") const body = c.querySelector(".body") const likes = c.querySelector(".likes") author.textContent = cd.authorDisplayName author.href = cd.authorChannelUrl date.textContent = cd.publishedAt if (cd.publishedAt === cd.updatedAt) dateM.remove() else dateM.textContent = `(Edited ${cd.updatedAt})` body.innerHTML = cd.textDisplay likes.textContent = new Intl.NumberFormat().format(cd.likeCount) if (replies) { const divReplies = c.querySelector(".replies") for (const reply of replies) genComment(reply.snippet, divReplies) } parent.append(c) } async function toggleComments() { if (!player) return if (commentsLoaded) { toggleVis(divComments) return } // req const d = (await getApiReq(`/comments?id=${player.playerInfo.videoData.video_id }`)) // populate for (const cdd of d.items) { const cd = cdd.snippet.topLevelComment.snippet const replies = cdd.replies ? cdd.replies.comments : null genComment(cd, divComments, replies) } // done commentsLoaded = true toggleVis(divComments) } buttonDetails.addEventListener("click", () => { toggleDetails() toggleComments() buttonDetails.remove() }) /* timestamps in comments */ function timestampLinkClick(e) { if (e.target.tagName !== "A") return 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) window.history.replaceState({}, "", url) /* reflect time in url bar */ window.scrollTo(0, 0) /* return to page top */ } document.addEventListener("click", timestampLinkClick)