mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 09:37:26 +00:00
🍙 new stuff and some changes
- channel buttons and redirect - in button mode if there is no target try to find lbry url in the description - used a loop in content script instead of events and stuff to make it less confusing
This commit is contained in:
parent
66cb8ccccf
commit
c895d53253
6 changed files with 217 additions and 288 deletions
97
package-lock.json
generated
97
package-lock.json
generated
|
@ -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",
|
||||
|
|
10
package.json
10
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"
|
||||
|
|
|
@ -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<string, YtUrlResolveItem>
|
||||
type Paramaters = YtUrlResolveItem[]
|
||||
|
||||
|
|
|
@ -24,5 +24,3 @@ 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' }))
|
|
@ -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 <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
|
||||
<a href={`${url.toString()}`} role='button'
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '12px',
|
||||
borderRadius: '2px',
|
||||
backgroundColor: targetPlatform.theme,
|
||||
backgroundImage: targetPlatform.theme,
|
||||
fontWeight: 'bold',
|
||||
border: '0',
|
||||
color: 'whitesmoke',
|
||||
padding: '10px 16px',
|
||||
marginRight: '4px',
|
||||
fontSize: '14px',
|
||||
textDecoration: 'none',
|
||||
...targetPlatform.button.style?.button,
|
||||
}}>
|
||||
<img src={targetPlatform.button.icon} height={16}
|
||||
style={{ transform: 'scale(1.5)', ...targetPlatform.button.style?.icon }} />
|
||||
<span>{targetPlatform.button.text}</span>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
function updateButton(mountPoint: HTMLDivElement, target: Target | null): void {
|
||||
if (!target) return render(<WatchOnLbryButton />, mountPoint)
|
||||
render(<WatchOnLbryButton targetPlatform={target.platfrom} lbryPathname={target.lbryPathname} time={target.time ?? undefined} />, mountPoint)
|
||||
}
|
||||
|
||||
/** Returns a mount point for the button */
|
||||
async function findButtonMountPoint(): Promise<HTMLDivElement> {
|
||||
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<typeof resolveById>): ReturnType<typeof resolveById> {
|
||||
const json = await new Promise<string | null | 'error'>((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<void>)
|
||||
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 <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
|
||||
<a href={`${url.href}`} target={target.platform === targetPlatformSettings.app ? '' : '_blank'} role='button'
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '12px',
|
||||
borderRadius: '2px',
|
||||
backgroundColor: target.platform.theme,
|
||||
backgroundImage: target.platform.theme,
|
||||
fontWeight: 'bold',
|
||||
border: '0',
|
||||
color: 'whitesmoke',
|
||||
padding: '10px 16px',
|
||||
marginRight: target.type === 'channel' ? '10px' : '4px',
|
||||
fontSize: '14px',
|
||||
textDecoration: 'none',
|
||||
...target.platform.button.style?.button,
|
||||
}}>
|
||||
<img src={target.platform.button.icon} height={16}
|
||||
style={{ transform: 'scale(1.5)', ...target.platform.button.style?.icon }} />
|
||||
<span>{target.platform.button.text}</span>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
async function redirectTo({ lbryPathname, platfrom, time }: Target) {
|
||||
const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`)
|
||||
function updateButton(target: Target | null): void {
|
||||
if (!target) return render(<WatchOnLbryButton />, mountPoint)
|
||||
|
||||
if (time) url.searchParams.set('t', time.toFixed(0))
|
||||
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
||||
if (!sourcePlatform) return render(<WatchOnLbryButton />, 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(<WatchOnLbryButton />, mountPoint)
|
||||
|
||||
if (platfrom === targetPlatformSettings.app) {
|
||||
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true }))
|
||||
mountBefore.parentElement?.insertBefore(mountPoint, mountBefore)
|
||||
render(<WatchOnLbryButton target={target} />, 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<typeof requestResolveById>[0] = []
|
||||
const platform = targetPlatformSettings[settings.targetPlatform]
|
||||
|
||||
const datas: Record<string, { url: URL, type: ResolveUrlTypes }> = {}
|
||||
|
||||
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<string, Target | null> = 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<typeof resolveById>): ReturnType<typeof resolveById> {
|
||||
const json = await new Promise<string | null | 'error'>((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<HTMLAnchorElement>('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<HTMLVideoElement>(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()
|
||||
})()
|
|
@ -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<ResolveUrlTypes, string>,
|
||||
videoPlayer: string,
|
||||
videoDescription: string
|
||||
}
|
||||
}) => o
|
||||
export type SourcePlatform = ReturnType<typeof sourcePlatform>
|
||||
|
@ -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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue