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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
|
|
|
@ -5,7 +5,8 @@ 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[]
|
||||
|
||||
|
|
|
@ -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' }))
|
||||
})
|
|
@ -1,123 +1,154 @@
|
|||
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
|
||||
}
|
||||
|
||||
interface Source {
|
||||
platform: SourcePlatform
|
||||
id: string
|
||||
type: ResolveUrlTypes
|
||||
time: number | null
|
||||
}
|
||||
|
||||
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 buttonMountPoint = document.createElement('div')
|
||||
buttonMountPoint.style.display = 'flex'
|
||||
|
||||
const playerButtonMountPoint = document.createElement('div')
|
||||
playerButtonMountPoint.style.display = 'flex'
|
||||
|
||||
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')) {
|
||||
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
|
||||
return {
|
||||
id: url.searchParams.get('v')!,
|
||||
platform,
|
||||
time: url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null,
|
||||
type: 'video'
|
||||
}
|
||||
}
|
||||
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/')) {
|
||||
// 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 startsAt = content.indexOf(prefix) + prefix.length
|
||||
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
|
||||
}
|
||||
|
||||
async function redirectTo({ lbryPathname, platfrom, time }: Target) {
|
||||
const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`)
|
||||
async function getTargetsBySources(...sources: Source[]) {
|
||||
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) => {
|
||||
videoElement.addEventListener('play', () => videoElement.pause(), { once: true })
|
||||
videoElement.pause()
|
||||
})
|
||||
return [
|
||||
source.id,
|
||||
{
|
||||
type: result.type,
|
||||
lbryPathname: result.id,
|
||||
platform,
|
||||
time: source.time
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
|
||||
location.replace(url.toString())
|
||||
return targets
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// 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') {
|
||||
console.error("Background error on:", params)
|
||||
throw new Error("Background error.")
|
||||
}
|
||||
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 { useEffect, useReducer } from "preact/hooks"
|
||||
import type { ResolveUrlTypes } from "../modules/yt/urlResolve"
|
||||
|
||||
export interface ExtensionSettings {
|
||||
redirect: boolean
|
||||
|
@ -55,7 +56,7 @@ const targetPlatform = (o: {
|
|||
displayName: string
|
||||
theme: string
|
||||
button: {
|
||||
text: string
|
||||
platformNameText: string,
|
||||
icon: string
|
||||
style?:
|
||||
{
|
||||
|
@ -75,7 +76,7 @@ export const targetPlatformSettings = {
|
|||
displayName: 'Odysee',
|
||||
theme: 'linear-gradient(130deg, #c63d59, #f77937)',
|
||||
button: {
|
||||
text: 'Watch on Odysee',
|
||||
platformNameText: 'Odysee',
|
||||
icon: chrome.runtime.getURL('assets/icons/lbry/odysee-logo.svg')
|
||||
}
|
||||
})
|
||||
|
@ -86,8 +87,13 @@ export const targetPlatformSettings = {
|
|||
const sourcePlatform = (o: {
|
||||
hostnames: string[]
|
||||
htmlQueries: {
|
||||
mountButtonBefore: string,
|
||||
videoPlayer: string
|
||||
mountPoints: {
|
||||
mountButtonBefore: Record<ResolveUrlTypes, string>,
|
||||
mountPlayerButtonBefore: string | null,
|
||||
}
|
||||
videoPlayer: string,
|
||||
videoDescription: string
|
||||
channelLinks: string | null
|
||||
}
|
||||
}) => o
|
||||
export type SourcePlatform = ReturnType<typeof sourcePlatform>
|
||||
|
@ -102,15 +108,32 @@ export const sourcePlatfromSettings = {
|
|||
"youtube.com": sourcePlatform({
|
||||
hostnames: ['www.youtube.com'],
|
||||
htmlQueries: {
|
||||
mountButtonBefore: 'ytd-video-owner-renderer~#subscribe-button',
|
||||
videoPlayer: '#ytd-player video'
|
||||
mountPoints: {
|
||||
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({
|
||||
hostnames: ['yewtu.be', 'vid.puffyan.us', 'invidio.xamh.de', 'invidious.kavin.rocks'],
|
||||
htmlQueries: {
|
||||
mountButtonBefore: '#watch-on-youtube',
|
||||
videoPlayer: '#player-container video'
|
||||
mountPoints: {
|
||||
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