From c895d53253b385a73a2126bbc093098ea5216b44 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sat, 2 Jul 2022 15:15:36 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8D=99=20new=20stuff=20and=20some=20chang?= =?UTF-8?q?es=20-=20channel=20buttons=20and=20redirect=20-=20in=20button?= =?UTF-8?q?=20mode=20if=20there=20is=20no=20target=20try=20to=20find=20lbr?= =?UTF-8?q?y=20url=20in=20the=20description=20-=20used=20a=20loop=20in=20c?= =?UTF-8?q?ontent=20script=20instead=20of=20events=20and=20stuff=20to=20ma?= =?UTF-8?q?ke=20it=20less=20confusing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 97 ---------- package.json | 10 - src/modules/yt/urlResolve.ts | 5 +- src/scripts/background.ts | 4 +- src/scripts/ytContent.tsx | 356 +++++++++++++++++++---------------- src/settings/index.ts | 33 ++-- 6 files changed, 217 insertions(+), 288 deletions(-) diff --git a/package-lock.json b/package-lock.json index a88d951..86aaa75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4826,91 +4826,6 @@ "timsort": "^0.3.0" } }, - "css-loader": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", - "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", - "dev": true, - "requires": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.7", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.3.5" - }, - "dependencies": { - "nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true - }, - "postcss": { - "version": "8.4.13", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", - "integrity": "sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==", - "dev": true, - "requires": { - "nanoid": "^3.3.3", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true - }, - "postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", - "dev": true, - "requires": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - } - }, - "postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.4" - } - }, - "postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "requires": { - "icss-utils": "^5.0.0" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, "css-modules-loader-core": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz", @@ -7923,12 +7838,6 @@ "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", "dev": true }, - "icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true - }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -14965,12 +14874,6 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", diff --git a/package.json b/package.json index 751765d..38d1c18 100644 --- a/package.json +++ b/package.json @@ -5,29 +5,19 @@ "scripts": { "build:assets": "cpx \"src/**/*.{json,png,svg}\" dist/", "watch:assets": "cpx --watch -v \"src/**/*.{json,png,svg}\" dist/", - "build:parcel": "cross-env NODE_ENV=production parcel build --no-source-maps --no-minify \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\" \"src/**/*.md\"", "watch:parcel": "parcel watch --no-hmr --no-source-maps \"src/**/*.{js,jsx,ts,tsx}\" \"src/**/*.html\" \"src/**/*.md\"", - "build:webext": "web-ext build --source-dir ./dist --overwrite-dest", - "clear:dist": "rm -r ./dist ; mkdir ./dist", - "build:base": "npm-run-all -l -p build:parcel build:assets", - "pick:manifest:v2": "cp -b ./manifest.v2.json ./dist/manifest.json", "pick:manifest:v3": "cp -b ./manifest.v3.json ./dist/manifest.json", - "build:v2": "npm run clear:dist ; npm run build:base && npm run pick:manifest:v2", "build:v3": "npm run clear:dist ; npm run build:base && npm run pick:manifest:v3", - "build": "rm -r ./build ; mkdir ./build && npm run build:v2 && zip -r ./build/manifest-v2.zip ./dist && npm run build:v3 && zip -r ./build/manifest-v3.zip ./dist", - "watch:v2": "npm run clear:dist ; npm run pick:manifest:v2 && npm-run-all -l -p watch:parcel watch:assets", "watch:v3": "npm run clear:dist ; npm run pick:manifest:v3 && npm-run-all -l -p watch:parcel watch:assets", - "watch": "npm run watch:v3", - "start:chrome": "web-ext run -t chromium --source-dir ./dist", "start:firefox": "web-ext run --source-dir ./dist", "test": "jest" diff --git a/src/modules/yt/urlResolve.ts b/src/modules/yt/urlResolve.ts index 212108e..4f0adf8 100644 --- a/src/modules/yt/urlResolve.ts +++ b/src/modules/yt/urlResolve.ts @@ -1,12 +1,13 @@ import { chunk } from "lodash" import path from "path" -import { getExtensionSettingsAsync, ytUrlResolversSettings } from "../../settings" +import { getExtensionSettingsAsync, SourcePlatform, ytUrlResolversSettings } from "../../settings" import { sign } from "../crypto" import { lbryUrlCache } from "./urlCache" const QUERY_CHUNK_SIZE = 100 -export type YtUrlResolveItem = { type: 'video' | 'channel', id: string } +export type ResolveUrlTypes = 'video' | 'channel' +export type YtUrlResolveItem = { type: ResolveUrlTypes, id: string } type Results = Record type Paramaters = YtUrlResolveItem[] diff --git a/src/scripts/background.ts b/src/scripts/background.ts index 93631bb..a6e3f30 100644 --- a/src/scripts/background.ts +++ b/src/scripts/background.ts @@ -23,6 +23,4 @@ chrome.runtime.onMessage.addListener(({ json }, sender, sendResponse) => { })() return true -}) - -chrome.tabs.onUpdated.addListener((tabId, changeInfo) => changeInfo.status === 'complete' && chrome.tabs.sendMessage(tabId, { message: 'url-changed' })) \ No newline at end of file +}) \ No newline at end of file diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index a986107..91d3ba9 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -1,198 +1,224 @@ import { h, render } from 'preact' import { parseYouTubeURLTimeString } from '../modules/yt' -import type { resolveById } from '../modules/yt/urlResolve' -import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatform, targetPlatformSettings } from '../settings' +import type { resolveById, ResolveUrlTypes } from '../modules/yt/urlResolve' +import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, getTargetPlatfromSettingsEntiries, SourcePlatform, sourcePlatfromSettings, 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
- - - {targetPlatform.button.text} - -
-} - -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) + const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)) + interface Target { + platform: TargetPlatform + lbryPathname: string + type: ResolveUrlTypes + time: number | null + } + + const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname) + if (!sourcePlatform) return + const targetPlatforms = getTargetPlatfromSettingsEntiries() + + const settings = await getExtensionSettingsAsync() // 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()) + const mountPoint = document.createElement('div') + mountPoint.style.display = 'flex' - 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 + function WatchOnLbryButton({ target }: { target?: Target }) { + if (!target) return null + const url = getLbryUrlByTarget(target) - 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 + return } - async function redirectTo({ lbryPathname, platfrom, time }: Target) { - const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`) + function updateButton(target: Target | null): void { + if (!target) return render(, mountPoint) - if (time) url.searchParams.set('t', time.toFixed(0)) + const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname) + if (!sourcePlatform) return render(, mountPoint) - findVideoElement().then((videoElement) => { - videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) - videoElement.pause() - }) + const mountBefore = document.querySelector(sourcePlatform.htmlQueries.mountButtonBefore[target.type]) + if (!mountBefore) return render(, mountPoint) - if (platfrom === targetPlatformSettings.app) { - if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true })) + mountBefore.parentElement?.insertBefore(mountPoint, mountBefore) + render(, mountPoint) + } - // 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()) + async function findVideoElementAwait() { + 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 + } + + async function getTargetsByURL(...urls: URL[]) { + const params: Parameters[0] = [] + const platform = targetPlatformSettings[settings.targetPlatform] + + const datas: Record = {} + + for (const url of urls) { + if (url.pathname === '/watch' && url.searchParams.has('v')) { + const id = url.searchParams.get('v')! + const type: ResolveUrlTypes = 'video' + params.push({ id, type }) + datas[id] = { url, type } + } + else if (url.pathname.startsWith('/channel/')) { + const id = url.pathname.substring("/channel/".length) + const type: ResolveUrlTypes = 'channel' + params.push({ id, type }) + datas[id] = { url, type } + } + 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) + const id = content.substring(startsAt, endsAt) + const type: ResolveUrlTypes = 'channel' + params.push({ id, type }) + datas[id] = { url, type } + } + } + + const results = Object.entries(await requestResolveById(params)) + const targets: Record = Object.fromEntries(results.map(([id, result]) => { + const data = datas[id] + + if (!result) return [ + data.url.href, + null + ] + + return [ + data.url.href, + { + type: data.type, + lbryPathname: result?.id, + platform, + time: data.type === 'channel' ? null : (data.url.searchParams.has('t') ? parseYouTubeURLTimeString(data.url.searchParams.get('t')!) : null) + } + ] + })) + + return targets + } + // 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 + } + + function getLbryUrlByTarget(target: Target) { + const url = new URL(`${target.platform.domainPrefix}${target.lbryPathname}`) + if (target.time) url.searchParams.set('t', target.time.toFixed(0)) + + return url + } + + let urlCache: URL | null = null + while (true) { + await sleep(500) + + const url = new URL(location.href); + let target = (await getTargetsByURL(url))[url.href] + + if (settings.redirect) { + if (!target) continue + if (url === urlCache) continue + + const lbryURL = getLbryUrlByTarget(target) + + findVideoElementAwait().then((videoElement) => { + videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) + videoElement.pause() + }) + + if (target.platform === targetPlatformSettings.app) { + if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true })) + // Its not gonna be able to replace anyway + // This was empty window doesnt stay open + location.replace(lbryURL) } else { - open(url.toString(), '_blank') + open(lbryURL, '_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() + if (!target) { + const descriptionElement = document.querySelector(sourcePlatform.htmlQueries.videoDescription) + if (descriptionElement) { + const anchors = Array.from(descriptionElement.querySelectorAll('a')) - const getTime = () => videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null + for (const anchor of anchors) { + const url = new URL(anchor.href) + let lbryURL: URL | null = null + if (sourcePlatform === sourcePlatfromSettings['youtube.com']) { + if (!targetPlatforms.some(([key, platform]) => url.searchParams.get('q')?.startsWith(platform.domainPrefix))) continue + lbryURL = new URL(url.searchParams.get('q')!) + } + else { + if (!targetPlatforms.some(([key, platform]) => url.href.startsWith(platform.domainPrefix))) continue + lbryURL = new URL(url.href) + } - 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) + if (lbryURL) { + target = { + lbryPathname: lbryURL.pathname.substring(1), + time: null, + type: 'video', + platform: targetPlatformSettings[settings.targetPlatform] + } + break + } + } + } } + + if (target) { + const videoElement = document.querySelector(sourcePlatform.htmlQueries.videoPlayer) + if (videoElement) target.time = videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null + } + + // We run it anyway with null target to hide the button + updateButton(target) } - await updater() + + urlCache = url } - await onModeChange() })() \ No newline at end of file diff --git a/src/settings/index.ts b/src/settings/index.ts index d2d8bc5..017bcdd 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -1,5 +1,6 @@ import type { JSX } from "preact" import { useEffect, useReducer } from "preact/hooks" +import type { ResolveUrlTypes } from "../modules/yt/urlResolve" export interface ExtensionSettings { redirect: boolean @@ -112,8 +113,9 @@ export const targetPlatformSettings = { const sourcePlatform = (o: { hostnames: string[] htmlQueries: { - mountButtonBefore: string, - videoPlayer: string + mountButtonBefore: Record, + videoPlayer: string, + videoDescription: string } }) => o export type SourcePlatform = ReturnType @@ -125,18 +127,27 @@ export function getSourcePlatfromSettingsFromHostname(hostname: string) { return null } export const sourcePlatfromSettings = { - "yewtu.be": sourcePlatform({ - hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'], - htmlQueries: { - mountButtonBefore: '#watch-on-youtube', - videoPlayer: '#player-container video' - } - }), "youtube.com": sourcePlatform({ hostnames: ['www.youtube.com'], htmlQueries: { - mountButtonBefore: 'ytd-video-owner-renderer~#subscribe-button', - videoPlayer: '#ytd-player video' + mountButtonBefore: { + video: 'ytd-video-owner-renderer~#subscribe-button', + channel: '#channel-header-container #buttons' + }, + videoPlayer: '#ytd-player video', + videoDescription: 'ytd-video-secondary-info-renderer #description' + } + }), + "yewtu.be": sourcePlatform({ + hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'], + htmlQueries: { + mountButtonBefore: + { + video: '#watch-on-youtube', + channel: '#subscribe' + }, + videoPlayer: '#player-container video', + videoDescription: '#descriptionWrapper' } }) }