/* 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)