import { h, render } from 'preact'
import { parseYouTubeURLTimeString } from '../modules/yt'
import type { resolveById } from '../modules/yt/urlResolve'
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatform, targetPlatformSettings } from '../settings'
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t))
interface WatchOnLbryButtonParameters {
targetPlatform?: TargetPlatform
lbryPathname?: string
time?: number
}
interface Target {
platfrom: TargetPlatform
lbryPathname: string
time: number | null
}
function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryButtonParameters) {
if (!lbryPathname || !targetPlatform) return null
const url = new URL(`${targetPlatform.domainPrefix}${lbryPathname}`)
if (time) url.searchParams.set('t', time.toFixed(0))
return
}
function updateButton(mountPoint: HTMLDivElement, target: Target | null): void {
if (!target) return render(, mountPoint)
render(, mountPoint)
}
/** Returns a mount point for the button */
async function findButtonMountPoint(): Promise {
const id = 'watch-on-lbry-button-container'
let mountBefore: HTMLDivElement | null = null
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`)
const exits: HTMLDivElement | null = document.querySelector(`#${id}`)
if (exits) return exits
while (!(mountBefore = document.querySelector(sourcePlatform.htmlQueries.mountButtonBefore))) await sleep(200)
const div = document.createElement('div')
div.id = id
div.style.display = 'flex'
div.style.alignItems = 'center'
mountBefore.parentElement?.insertBefore(div, mountBefore)
return div
}
async function findVideoElement() {
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`)
let videoElement: HTMLVideoElement | null = null
while (!(videoElement = document.querySelector(sourcePlatform.htmlQueries.videoPlayer))) await sleep(200)
return videoElement
}
// We should get this from background, so the caching works and we don't get errors in the future if yt decides to impliment CORS
async function requestResolveById(...params: Parameters): ReturnType {
const json = await new Promise((resolve) => chrome.runtime.sendMessage({ json: JSON.stringify(params) }, resolve))
if (json === 'error') throw new Error("Background error.")
return json ? JSON.parse(json) : null
}
// Start
(async () => {
const settings = await getExtensionSettingsAsync()
let updater: (() => Promise)
// Listen Settings Change
chrome.storage.onChanged.addListener(async (changes, areaName) => {
if (areaName !== 'local') return
Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue])))
if (changes.redirect) await onModeChange()
await updater()
})
/*
* Gets messages from background script which relays tab update events. This is because there's no sensible way to detect
* history.pushState changes from a content script
*/
// Listen URL Change
chrome.runtime.onMessage.addListener(({ message }, sender) => message === 'url-changed' && updater())
async function getTargetByURL(url: URL) {
if (url.pathname === '/watch' && url.searchParams.has('v')) {
const videoId = url.searchParams.get('v')!
const result = await requestResolveById([{ id: videoId, type: 'video' }])
const target: Target | null = result?.[videoId] ? { lbryPathname: result[videoId].id, platfrom: targetPlatformSettings[settings.targetPlatform], time: null } : null
return target
}
else if (url.pathname.startsWith('/channel/')) {
await requestResolveById([{ id: url.pathname.substring("/channel/".length), type: 'channel' }])
}
else if (url.pathname.startsWith('/c/') || url.pathname.startsWith('/user/')) {
// We have to download the page content again because these parts of the page are not responsive
// yt front end sucks anyway
const content = await (await fetch(location.href)).text()
const prefix = `https://www.youtube.com/feeds/videos.xml?channel_id=`
const suffix = `"`
const startsAt = content.indexOf(prefix) + prefix.length
const endsAt = content.indexOf(suffix, startsAt)
await requestResolveById([{ id: content.substring(startsAt, endsAt), type: 'channel' }])
}
return null
}
async function redirectTo({ lbryPathname, platfrom, time }: Target) {
const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`)
if (time) url.searchParams.set('t', time.toFixed(0))
findVideoElement().then((videoElement) => {
videoElement.addEventListener('play', () => videoElement.pause(), { once: true })
videoElement.pause()
})
if (platfrom === targetPlatformSettings.app) {
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true }))
// On redirect with app, people might choose to cancel browser's dialog
// So we dont destroy the current window automatically for them
// And also we are keeping the same window for less distiraction
if (settings.redirect) {
location.replace(url.toString())
}
else {
open(url.toString(), '_blank')
if (window.history.length === 1) window.close()
else window.history.back()
}
}
else
location.replace(url.toString())
}
let removeVideoTimeUpdateListener: (() => void) | null = null
async function onModeChange() {
let target: Target | null = null
if (settings.redirect)
updater = async () => {
const url = new URL(location.href)
target = await getTargetByURL(url)
if (!target) return
target.time = url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null
redirectTo(target)
}
else {
const mountPoint = await findButtonMountPoint()
const videoElement = await findVideoElement()
const getTime = () => videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null
const onTimeUpdate = () => target && updateButton(mountPoint, Object.assign(target, { time: getTime() }))
removeVideoTimeUpdateListener?.call(null)
videoElement.addEventListener('timeupdate', onTimeUpdate)
removeVideoTimeUpdateListener = () => videoElement.removeEventListener('timeupdate', onTimeUpdate)
updater = async () => {
const url = new URL(location.href)
target = await getTargetByURL(url)
if (target) target.time = getTime()
updateButton(mountPoint, target)
}
}
await updater()
}
await onModeChange()
})()