mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
33cfbd3f71
6 changed files with 302 additions and 264 deletions
97
package-lock.json
generated
97
package-lock.json
generated
|
@ -4826,91 +4826,6 @@
|
||||||
"timsort": "^0.3.0"
|
"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": {
|
"css-modules-loader-core": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz",
|
"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=",
|
"integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=",
|
||||||
"dev": true
|
"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": {
|
"ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
@ -14965,12 +14874,6 @@
|
||||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
|
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
|
||||||
"dev": true
|
"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": {
|
"source-map-resolve": {
|
||||||
"version": "0.5.3",
|
"version": "0.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
|
"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": {
|
"scripts": {
|
||||||
"build:assets": "cpx \"src/**/*.{json,png,svg}\" dist/",
|
"build:assets": "cpx \"src/**/*.{json,png,svg}\" dist/",
|
||||||
"watch:assets": "cpx --watch -v \"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\"",
|
"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\"",
|
"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",
|
"build:webext": "web-ext build --source-dir ./dist --overwrite-dest",
|
||||||
|
|
||||||
"clear:dist": "rm -r ./dist ; mkdir ./dist",
|
"clear:dist": "rm -r ./dist ; mkdir ./dist",
|
||||||
|
|
||||||
"build:base": "npm-run-all -l -p build:parcel build:assets",
|
"build:base": "npm-run-all -l -p build:parcel build:assets",
|
||||||
|
|
||||||
"pick:manifest:v2": "cp -b ./manifest.v2.json ./dist/manifest.json",
|
"pick:manifest:v2": "cp -b ./manifest.v2.json ./dist/manifest.json",
|
||||||
"pick:manifest:v3": "cp -b ./manifest.v3.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: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: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",
|
"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: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:v3": "npm run clear:dist ; npm run pick:manifest:v3 && npm-run-all -l -p watch:parcel watch:assets",
|
||||||
|
|
||||||
"watch": "npm run watch:v3",
|
"watch": "npm run watch:v3",
|
||||||
|
|
||||||
"start:chrome": "web-ext run -t chromium --source-dir ./dist",
|
"start:chrome": "web-ext run -t chromium --source-dir ./dist",
|
||||||
"start:firefox": "web-ext run --source-dir ./dist",
|
"start:firefox": "web-ext run --source-dir ./dist",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { lbryUrlCache } from "./urlCache"
|
||||||
|
|
||||||
const QUERY_CHUNK_SIZE = 100
|
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 Results = Record<string, YtUrlResolveItem>
|
||||||
type Paramaters = YtUrlResolveItem[]
|
type Paramaters = YtUrlResolveItem[]
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,4 @@ chrome.runtime.onMessage.addListener(({ json }, sender, sendResponse) => {
|
||||||
})()
|
})()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => changeInfo.status === 'complete' && chrome.tabs.sendMessage(tabId, { message: 'url-changed' }))
|
|
|
@ -1,123 +1,154 @@
|
||||||
import { h, render } from 'preact'
|
import { h, render } from 'preact'
|
||||||
import { parseYouTubeURLTimeString } from '../modules/yt'
|
import { parseYouTubeURLTimeString } from '../modules/yt'
|
||||||
import type { resolveById } from '../modules/yt/urlResolve'
|
import type { resolveById, ResolveUrlTypes } from '../modules/yt/urlResolve'
|
||||||
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatform, targetPlatformSettings } from '../settings'
|
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 () => {
|
(async () => {
|
||||||
const settings = await getExtensionSettingsAsync()
|
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t))
|
||||||
let updater: (() => Promise<void>)
|
|
||||||
|
|
||||||
|
interface Target {
|
||||||
|
platform: TargetPlatform
|
||||||
|
lbryPathname: string
|
||||||
|
type: ResolveUrlTypes
|
||||||
|
time: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Source {
|
||||||
|
platform: SourcePlatform
|
||||||
|
id: string
|
||||||
|
type: ResolveUrlTypes
|
||||||
|
time: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPlatforms = getTargetPlatfromSettingsEntiries()
|
||||||
|
const settings = await getExtensionSettingsAsync()
|
||||||
// Listen Settings Change
|
// Listen Settings Change
|
||||||
chrome.storage.onChanged.addListener(async (changes, areaName) => {
|
chrome.storage.onChanged.addListener(async (changes, areaName) => {
|
||||||
if (areaName !== 'local') return
|
if (areaName !== 'local') return
|
||||||
Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue])))
|
Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue])))
|
||||||
if (changes.redirect) await onModeChange()
|
|
||||||
await updater()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
const buttonMountPoint = document.createElement('div')
|
||||||
* Gets messages from background script which relays tab update events. This is because there's no sensible way to detect
|
buttonMountPoint.style.display = 'flex'
|
||||||
* history.pushState changes from a content script
|
|
||||||
*/
|
const playerButtonMountPoint = document.createElement('div')
|
||||||
// Listen URL Change
|
playerButtonMountPoint.style.display = 'flex'
|
||||||
chrome.runtime.onMessage.addListener(({ message }, sender) => message === 'url-changed' && updater())
|
|
||||||
|
function WatchOnLbryButton({ source, target }: { source?: Source, target?: Target }) {
|
||||||
|
if (!target || !source) return null
|
||||||
|
const url = getLbryUrlByTarget(target)
|
||||||
|
|
||||||
|
return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
|
||||||
|
<a href={`${url.href}`} target='_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: source?.type === 'channel' ? '10px' : '4px',
|
||||||
|
fontSize: '14px',
|
||||||
|
textDecoration: 'none',
|
||||||
|
...target.platform.button.style?.button,
|
||||||
|
}}
|
||||||
|
onClick={() => findVideoElementAwait(source).then((videoElement) => {
|
||||||
|
videoElement.pause()
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<img src={target.platform.button.icon} height={16}
|
||||||
|
style={{ transform: 'scale(1.5)', ...target.platform.button.style?.icon }} />
|
||||||
|
<span>{target.type === 'channel' ? 'Channel on' : 'Watch on'} {target.platform.button.platformNameText}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function WatchOnLbryPlayerButton({ source, target }: { source?: Source, target?: Target }) {
|
||||||
|
if (!target || !source) return null
|
||||||
|
const url = getLbryUrlByTarget(target)
|
||||||
|
|
||||||
|
return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
|
||||||
|
<a href={`${url.href}`} target='_blank' role='button'
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
border: '0',
|
||||||
|
color: 'whitesmoke',
|
||||||
|
marginRight: '10px',
|
||||||
|
fontSize: '14px',
|
||||||
|
textDecoration: 'none',
|
||||||
|
...target.platform.button.style?.button,
|
||||||
|
}}
|
||||||
|
onClick={() => findVideoElementAwait(source).then((videoElement) => {
|
||||||
|
videoElement.pause()
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<img src={target.platform.button.icon} height={16}
|
||||||
|
style={{ transform: 'scale(1.5)', ...target.platform.button.style?.icon }} />
|
||||||
|
<span>{target.type === 'channel' ? 'Channel on' : 'Watch on'} {target.platform.button.platformNameText}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateButton(params: { source: Source, target: Target } | null): void {
|
||||||
|
if (!params) {
|
||||||
|
render(<WatchOnLbryButton />, buttonMountPoint)
|
||||||
|
render(<WatchOnLbryPlayerButton />, playerButtonMountPoint)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const mountPlayerButtonBefore = params.source.platform.htmlQueries.mountPoints.mountPlayerButtonBefore ?
|
||||||
|
document.querySelector(params.source.platform.htmlQueries.mountPoints.mountPlayerButtonBefore) :
|
||||||
|
null
|
||||||
|
if (!mountPlayerButtonBefore) render(<WatchOnLbryPlayerButton />, playerButtonMountPoint)
|
||||||
|
else {
|
||||||
|
if (mountPlayerButtonBefore.previousSibling !== playerButtonMountPoint)
|
||||||
|
mountPlayerButtonBefore.parentElement?.insertBefore(playerButtonMountPoint, mountPlayerButtonBefore)
|
||||||
|
render(<WatchOnLbryPlayerButton target={params.target} source={params.source} />, playerButtonMountPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mountButtonBefore = document.querySelector(params.source.platform.htmlQueries.mountPoints.mountButtonBefore[params.source.type])
|
||||||
|
if (!mountButtonBefore) render(<WatchOnLbryButton />, playerButtonMountPoint)
|
||||||
|
else {
|
||||||
|
if (mountButtonBefore.previousSibling !== buttonMountPoint)
|
||||||
|
mountButtonBefore.parentElement?.insertBefore(buttonMountPoint, mountButtonBefore)
|
||||||
|
render(<WatchOnLbryButton target={params.target} source={params.source} />, buttonMountPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findVideoElementAwait(source: Source) {
|
||||||
|
let videoElement: HTMLVideoElement | null = null
|
||||||
|
while (!(videoElement = document.querySelector(source.platform.htmlQueries.videoPlayer))) await sleep(200)
|
||||||
|
return videoElement
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSourceByUrl(url: URL): Promise<Source | null> {
|
||||||
|
const platform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
||||||
|
if (!platform) return null
|
||||||
|
|
||||||
async function getTargetByURL(url: URL) {
|
|
||||||
if (url.pathname === '/watch' && url.searchParams.has('v')) {
|
if (url.pathname === '/watch' && url.searchParams.has('v')) {
|
||||||
const videoId = url.searchParams.get('v')!
|
return {
|
||||||
const result = await requestResolveById([{ id: videoId, type: 'video' }])
|
id: url.searchParams.get('v')!,
|
||||||
const target: Target | null = result?.[videoId] ? { lbryPathname: result[videoId].id, platfrom: targetPlatformSettings[settings.targetPlatform], time: null } : null
|
platform,
|
||||||
|
time: url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null,
|
||||||
return target
|
type: 'video'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (url.pathname.startsWith('/channel/')) {
|
else if (url.pathname.startsWith('/channel/')) {
|
||||||
await requestResolveById([{ id: url.pathname.substring("/channel/".length), type: 'channel' }])
|
return {
|
||||||
|
id: url.pathname.substring("/channel/".length),
|
||||||
|
platform,
|
||||||
|
time: null,
|
||||||
|
type: 'channel'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (url.pathname.startsWith('/c/') || url.pathname.startsWith('/user/')) {
|
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
|
// We have to download the page content again because these parts of the page are not responsive
|
||||||
|
@ -127,56 +158,148 @@ async function requestResolveById(...params: Parameters<typeof resolveById>): Re
|
||||||
const suffix = `"`
|
const suffix = `"`
|
||||||
const startsAt = content.indexOf(prefix) + prefix.length
|
const startsAt = content.indexOf(prefix) + prefix.length
|
||||||
const endsAt = content.indexOf(suffix, startsAt)
|
const endsAt = content.indexOf(suffix, startsAt)
|
||||||
await requestResolveById([{ id: content.substring(startsAt, endsAt), type: 'channel' }])
|
const id = content.substring(startsAt, endsAt)
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
platform,
|
||||||
|
time: null,
|
||||||
|
type: 'channel'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function redirectTo({ lbryPathname, platfrom, time }: Target) {
|
async function getTargetsBySources(...sources: Source[]) {
|
||||||
const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`)
|
const params: Parameters<typeof requestResolveById>[0] = sources.map((source) => ({ id: source.id, type: source.type }))
|
||||||
|
const platform = targetPlatformSettings[settings.targetPlatform]
|
||||||
|
|
||||||
if (time) url.searchParams.set('t', time.toFixed(0))
|
const results = await requestResolveById(params)
|
||||||
|
const targets: Record<string, Target | null> = Object.fromEntries(
|
||||||
|
sources.map((source) => {
|
||||||
|
const result = results[source.id]
|
||||||
|
if (!result) return [
|
||||||
|
source.id,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
|
||||||
findVideoElement().then((videoElement) => {
|
return [
|
||||||
videoElement.addEventListener('play', () => videoElement.pause(), { once: true })
|
source.id,
|
||||||
videoElement.pause()
|
{
|
||||||
})
|
type: result.type,
|
||||||
|
lbryPathname: result.id,
|
||||||
|
platform,
|
||||||
|
time: source.time
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
location.replace(url.toString())
|
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
|
||||||
let removeVideoTimeUpdateListener: (() => void) | null = null
|
async function requestResolveById(...params: Parameters<typeof resolveById>): ReturnType<typeof resolveById> {
|
||||||
async function onModeChange() {
|
const json = await new Promise<string | null | 'error'>((resolve) => chrome.runtime.sendMessage({ json: JSON.stringify(params) }, resolve))
|
||||||
let target: Target | null = null
|
if (json === 'error') {
|
||||||
if (settings.redirect)
|
console.error("Background error on:", params)
|
||||||
updater = async () => {
|
throw new Error("Background error.")
|
||||||
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()
|
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: URL = (urlCache?.href === location.href) ? urlCache : new URL(location.href)
|
||||||
|
const source = await getSourceByUrl(new URL(location.href))
|
||||||
|
if (!source) continue
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (settings.redirect) {
|
||||||
|
const target = (await getTargetsBySources(source))[source.id]
|
||||||
|
if (!target) continue
|
||||||
|
if (url === urlCache) continue
|
||||||
|
|
||||||
|
const lbryURL = getLbryUrlByTarget(target)
|
||||||
|
|
||||||
|
// As soon as video play is ready and start playing, pause it.
|
||||||
|
findVideoElementAwait(source).then((videoElement) => {
|
||||||
|
videoElement.addEventListener('play', () => videoElement.pause(), { once: true })
|
||||||
|
videoElement.pause()
|
||||||
|
})
|
||||||
|
|
||||||
|
open(lbryURL, '_blank')
|
||||||
|
if (window.history.length === 1) window.close()
|
||||||
|
else window.history.back()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (urlCache !== url) updateButton(null)
|
||||||
|
let target = (await getTargetsBySources(source))[source.id]
|
||||||
|
|
||||||
|
// There is no target found via API try to check Video Description for LBRY links.
|
||||||
|
if (!target) {
|
||||||
|
const linksContainer =
|
||||||
|
source.type === 'video' ?
|
||||||
|
document.querySelector(source.platform.htmlQueries.videoDescription) :
|
||||||
|
source.platform.htmlQueries.channelLinks ? document.querySelector(source.platform.htmlQueries.channelLinks) : null
|
||||||
|
|
||||||
|
console.log(linksContainer)
|
||||||
|
|
||||||
|
if (linksContainer) {
|
||||||
|
const anchors = Array.from(linksContainer.querySelectorAll<HTMLAnchorElement>('a'))
|
||||||
|
|
||||||
|
for (const anchor of anchors) {
|
||||||
|
if (!anchor.href) continue
|
||||||
|
const url = new URL(anchor.href)
|
||||||
|
let lbryURL: URL | null = null
|
||||||
|
|
||||||
|
// Extract real link from youtube's redirect link
|
||||||
|
if (source.platform === sourcePlatfromSettings['youtube.com']) {
|
||||||
|
if (!targetPlatforms.some(([key, platform]) => url.searchParams.get('q')?.startsWith(platform.domainPrefix))) continue
|
||||||
|
lbryURL = new URL(url.searchParams.get('q')!)
|
||||||
|
}
|
||||||
|
// Just directly use the link itself on other platforms
|
||||||
|
else {
|
||||||
|
if (!targetPlatforms.some(([key, platform]) => url.href.startsWith(platform.domainPrefix))) continue
|
||||||
|
lbryURL = new URL(url.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lbryURL) {
|
||||||
|
target = {
|
||||||
|
lbryPathname: lbryURL.pathname.substring(1),
|
||||||
|
time: null,
|
||||||
|
type: lbryURL.pathname.substring(1).includes('/') ? 'video' : 'channel',
|
||||||
|
platform: targetPlatformSettings[settings.targetPlatform]
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target) updateButton(null)
|
||||||
|
else {
|
||||||
|
// If target is a video target add timestampt to it
|
||||||
|
if (target.type === 'video') {
|
||||||
|
const videoElement = document.querySelector<HTMLVideoElement>(source.platform.htmlQueries.videoPlayer)
|
||||||
|
if (videoElement) target.time = videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButton({ target, source })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
urlCache = url
|
||||||
}
|
}
|
||||||
|
|
||||||
await onModeChange()
|
|
||||||
})()
|
})()
|
|
@ -1,5 +1,6 @@
|
||||||
import type { JSX } from "preact"
|
import type { JSX } from "preact"
|
||||||
import { useEffect, useReducer } from "preact/hooks"
|
import { useEffect, useReducer } from "preact/hooks"
|
||||||
|
import type { ResolveUrlTypes } from "../modules/yt/urlResolve"
|
||||||
|
|
||||||
export interface ExtensionSettings {
|
export interface ExtensionSettings {
|
||||||
redirect: boolean
|
redirect: boolean
|
||||||
|
@ -55,7 +56,7 @@ const targetPlatform = (o: {
|
||||||
displayName: string
|
displayName: string
|
||||||
theme: string
|
theme: string
|
||||||
button: {
|
button: {
|
||||||
text: string
|
platformNameText: string,
|
||||||
icon: string
|
icon: string
|
||||||
style?:
|
style?:
|
||||||
{
|
{
|
||||||
|
@ -75,7 +76,7 @@ export const targetPlatformSettings = {
|
||||||
displayName: 'Odysee',
|
displayName: 'Odysee',
|
||||||
theme: 'linear-gradient(130deg, #c63d59, #f77937)',
|
theme: 'linear-gradient(130deg, #c63d59, #f77937)',
|
||||||
button: {
|
button: {
|
||||||
text: 'Watch on Odysee',
|
platformNameText: 'Odysee',
|
||||||
icon: chrome.runtime.getURL('assets/icons/lbry/odysee-logo.svg')
|
icon: chrome.runtime.getURL('assets/icons/lbry/odysee-logo.svg')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -86,8 +87,13 @@ export const targetPlatformSettings = {
|
||||||
const sourcePlatform = (o: {
|
const sourcePlatform = (o: {
|
||||||
hostnames: string[]
|
hostnames: string[]
|
||||||
htmlQueries: {
|
htmlQueries: {
|
||||||
mountButtonBefore: string,
|
mountPoints: {
|
||||||
videoPlayer: string
|
mountButtonBefore: Record<ResolveUrlTypes, string>,
|
||||||
|
mountPlayerButtonBefore: string | null,
|
||||||
|
}
|
||||||
|
videoPlayer: string,
|
||||||
|
videoDescription: string
|
||||||
|
channelLinks: string | null
|
||||||
}
|
}
|
||||||
}) => o
|
}) => o
|
||||||
export type SourcePlatform = ReturnType<typeof sourcePlatform>
|
export type SourcePlatform = ReturnType<typeof sourcePlatform>
|
||||||
|
@ -102,15 +108,32 @@ export const sourcePlatfromSettings = {
|
||||||
"youtube.com": sourcePlatform({
|
"youtube.com": sourcePlatform({
|
||||||
hostnames: ['www.youtube.com'],
|
hostnames: ['www.youtube.com'],
|
||||||
htmlQueries: {
|
htmlQueries: {
|
||||||
mountButtonBefore: 'ytd-video-owner-renderer~#subscribe-button',
|
mountPoints: {
|
||||||
videoPlayer: '#ytd-player video'
|
mountButtonBefore: {
|
||||||
|
video: 'ytd-video-owner-renderer~#subscribe-button',
|
||||||
|
channel: '#channel-header-container #buttons'
|
||||||
|
},
|
||||||
|
mountPlayerButtonBefore: 'ytd-player .ytp-right-controls',
|
||||||
|
},
|
||||||
|
videoPlayer: '#ytd-player video',
|
||||||
|
videoDescription: 'ytd-video-secondary-info-renderer #description',
|
||||||
|
channelLinks: '#channel-header #links-holder'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
"yewtu.be": sourcePlatform({
|
"yewtu.be": sourcePlatform({
|
||||||
hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'],
|
hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'],
|
||||||
htmlQueries: {
|
htmlQueries: {
|
||||||
mountButtonBefore: '#watch-on-youtube',
|
mountPoints: {
|
||||||
videoPlayer: '#player-container video'
|
mountButtonBefore:
|
||||||
|
{
|
||||||
|
video: '#watch-on-youtube',
|
||||||
|
channel: '#subscribe'
|
||||||
|
},
|
||||||
|
mountPlayerButtonBefore: null,
|
||||||
|
},
|
||||||
|
videoPlayer: '#player-container video',
|
||||||
|
videoDescription: '#descriptionWrapper',
|
||||||
|
channelLinks: null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue