mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
🍱 Refactor
This commit is contained in:
parent
cb4b4f4b2e
commit
4f8e807a65
8 changed files with 198 additions and 170 deletions
|
@ -1,50 +1,81 @@
|
||||||
export interface ExtensionSettings {
|
import { JSX } from "preact"
|
||||||
|
|
||||||
|
export interface ExtensionSettings
|
||||||
|
{
|
||||||
redirect: boolean
|
redirect: boolean
|
||||||
targetPlatform: TargetPlatformName
|
targetPlatform: TargetPlatformName
|
||||||
urlResolver: YTUrlResolverName
|
urlResolver: YTUrlResolverName
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee', urlResolver: 'lbryInc' };
|
export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee', urlResolver: 'lbryInc' }
|
||||||
|
|
||||||
export function getExtensionSettingsAsync(): Promise<ExtensionSettings> {
|
export function getExtensionSettingsAsync(): Promise<ExtensionSettings>
|
||||||
return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any)));
|
{
|
||||||
|
return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type TargetPlatformName = 'madiator.com' | 'odysee' | 'app'
|
export type TargetPlatformName = 'madiator.com' | 'odysee' | 'app'
|
||||||
export interface TargetPlatformSettings {
|
export interface TargetPlatform
|
||||||
|
{
|
||||||
domainPrefix: string
|
domainPrefix: string
|
||||||
displayName: string
|
displayName: string
|
||||||
theme: string
|
theme: string
|
||||||
|
button: {
|
||||||
|
text: string
|
||||||
|
icon: string
|
||||||
|
style?:
|
||||||
|
{
|
||||||
|
icon?: JSX.CSSProperties
|
||||||
|
button?: JSX.CSSProperties
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const targetPlatformSettings: Record<TargetPlatformName, TargetPlatformSettings> = {
|
export const targetPlatformSettings: Record<TargetPlatformName, TargetPlatform> = {
|
||||||
'madiator.com': {
|
'madiator.com': {
|
||||||
domainPrefix: 'https://madiator.com/',
|
domainPrefix: 'https://madiator.com/',
|
||||||
displayName: 'Madiator.com',
|
displayName: 'Madiator.com',
|
||||||
theme: '#075656'
|
theme: '#075656',
|
||||||
|
button: {
|
||||||
|
text: 'Watch on',
|
||||||
|
icon: chrome.runtime.getURL('icons/lbry/madiator-logo.svg'),
|
||||||
|
style: {
|
||||||
|
button: { flexDirection: 'row-reverse' },
|
||||||
|
icon: { transform: 'scale(1.2)' }
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
odysee: {
|
odysee: {
|
||||||
domainPrefix: 'https://odysee.com/',
|
domainPrefix: 'https://odysee.com/',
|
||||||
displayName: 'Odysee',
|
displayName: 'Odysee',
|
||||||
theme: '#1e013b'
|
theme: '#1e013b',
|
||||||
|
button: {
|
||||||
|
text: 'Watch on Odysee',
|
||||||
|
icon: chrome.runtime.getURL('icons/lbry/odysee-logo.svg')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
domainPrefix: 'lbry://',
|
domainPrefix: 'lbry://',
|
||||||
displayName: 'LBRY App',
|
displayName: 'LBRY App',
|
||||||
theme: '#075656'
|
theme: '#075656',
|
||||||
|
button: {
|
||||||
|
text: 'Watch on LBRY',
|
||||||
|
icon: chrome.runtime.getURL('icons/lbry/lbry-logo.svg')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getTargetPlatfromSettingsEntiries = () => {
|
export const getTargetPlatfromSettingsEntiries = () =>
|
||||||
return Object.entries(targetPlatformSettings) as any as [Extract<keyof typeof targetPlatformSettings, string>, TargetPlatformSettings][]
|
{
|
||||||
|
return Object.entries(targetPlatformSettings) as any as [Extract<keyof typeof targetPlatformSettings, string>, TargetPlatform][]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type SourcePlatfromName = 'youtube.com' | 'yewtu.be'
|
export type SourcePlatfromName = 'youtube.com' | 'yewtu.be'
|
||||||
export interface SourcePlatfromSettings {
|
export interface SourcePlatfrom
|
||||||
|
{
|
||||||
hostnames: string[]
|
hostnames: string[]
|
||||||
htmlQueries: {
|
htmlQueries: {
|
||||||
mountButtonBefore: string,
|
mountButtonBefore: string,
|
||||||
|
@ -52,7 +83,7 @@ export interface SourcePlatfromSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sourcePlatfromSettings: Record<SourcePlatfromName, SourcePlatfromSettings> = {
|
export const sourcePlatfromSettings: Record<SourcePlatfromName, SourcePlatfrom> = {
|
||||||
"yewtu.be": {
|
"yewtu.be": {
|
||||||
hostnames: ['yewtu.be'],
|
hostnames: ['yewtu.be'],
|
||||||
htmlQueries: {
|
htmlQueries: {
|
||||||
|
@ -69,7 +100,8 @@ export const sourcePlatfromSettings: Record<SourcePlatfromName, SourcePlatfromSe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSourcePlatfromSettingsFromHostname(hostname: string) {
|
export function getSourcePlatfromSettingsFromHostname(hostname: string)
|
||||||
|
{
|
||||||
const values = Object.values(sourcePlatfromSettings)
|
const values = Object.values(sourcePlatfromSettings)
|
||||||
for (const settings of values)
|
for (const settings of values)
|
||||||
if (settings.hostnames.includes(hostname)) return settings
|
if (settings.hostnames.includes(hostname)) return settings
|
||||||
|
@ -139,6 +171,7 @@ export const ytUrlResolversSettings: Record<YTUrlResolverName, YTUrlResolver> =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getYtUrlResolversSettingsEntiries = () => {
|
export const getYtUrlResolversSettingsEntiries = () =>
|
||||||
|
{
|
||||||
return Object.entries(ytUrlResolversSettings) as any as [Extract<keyof typeof ytUrlResolversSettings, string>, YTUrlResolver][]
|
return Object.entries(ytUrlResolversSettings) as any as [Extract<keyof typeof ytUrlResolversSettings, string>, YTUrlResolver][]
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import chunk from 'lodash/chunk'
|
import chunk from 'lodash/chunk'
|
||||||
import groupBy from 'lodash/groupBy'
|
import groupBy from 'lodash/groupBy'
|
||||||
import { getExtensionSettingsAsync, Keys, SingleValueAtATime, Values, YtUrlResolveFunction, YtUrlResolveResponsePath, ytUrlResolversSettings } from '../settings'
|
import { getExtensionSettingsAsync, Keys, SingleValueAtATime, Values, YtUrlResolveFunction, YtUrlResolveResponsePath, ytUrlResolversSettings } from '../settings'
|
||||||
import { LbryURLCache } from './urlCache'
|
import { LbryPathnameCache } from './urlCache'
|
||||||
|
|
||||||
// const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS
|
// const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS
|
||||||
const QUERY_CHUNK_SIZE = 300
|
const QUERY_CHUNK_SIZE = 300
|
||||||
|
@ -57,6 +57,29 @@ export function getChannelId(channelURL: string)
|
||||||
return match ? match[1] : new URL(channelURL).searchParams.get('channel_id')
|
return match ? match[1] : new URL(channelURL).searchParams.get('channel_id')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseYouTubeURLTimeString(timeString: string)
|
||||||
|
{
|
||||||
|
const signs = timeString.replace(/[0-9]/g, '')
|
||||||
|
if (signs.length === 0) return timeString
|
||||||
|
const numbers = timeString.replace(/[^0-9]/g, '-').split('-')
|
||||||
|
let total = 0
|
||||||
|
for (let i = 0; i < signs.length; i++)
|
||||||
|
{
|
||||||
|
let t = parseInt(numbers[i])
|
||||||
|
switch (signs[i])
|
||||||
|
{
|
||||||
|
case 'd': t *= 24
|
||||||
|
case 'h': t *= 60
|
||||||
|
case 'm': t *= 60
|
||||||
|
case 's': break
|
||||||
|
default: return '0'
|
||||||
|
}
|
||||||
|
total += t
|
||||||
|
}
|
||||||
|
return total.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the array of YT channels from an OPML file
|
* Reads the array of YT channels from an OPML file
|
||||||
*
|
*
|
||||||
|
@ -113,7 +136,7 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres
|
||||||
await Promise.all(descriptorsWithIndex.map(async (descriptor, index) =>
|
await Promise.all(descriptorsWithIndex.map(async (descriptor, index) =>
|
||||||
{
|
{
|
||||||
if (!descriptor) return
|
if (!descriptor) return
|
||||||
const cache = await LbryURLCache.get(descriptor.id)
|
const cache = await LbryPathnameCache.get(descriptor.id)
|
||||||
|
|
||||||
// Cache can be null, if there is no lbry url yet
|
// Cache can be null, if there is no lbry url yet
|
||||||
if (cache !== undefined)
|
if (cache !== undefined)
|
||||||
|
@ -180,13 +203,13 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres
|
||||||
if (!apiResponse.ok)
|
if (!apiResponse.ok)
|
||||||
{
|
{
|
||||||
// Some API might not respond with 200 if it can't find the url
|
// Some API might not respond with 200 if it can't find the url
|
||||||
if (apiResponse.status === 404) await LbryURLCache.put(null, descriptor.id)
|
if (apiResponse.status === 404) await LbryPathnameCache.put(null, descriptor.id)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = followResponsePath<string>(await apiResponse.json(), urlResolverFunction.responsePath)
|
const value = followResponsePath<string>(await apiResponse.json(), urlResolverFunction.responsePath)
|
||||||
if (value) results[descriptor.index] = value
|
if (value) results[descriptor.index] = value
|
||||||
await LbryURLCache.put(value, descriptor.id)
|
await LbryPathnameCache.put(value, descriptor.id)
|
||||||
}
|
}
|
||||||
progressCount++
|
progressCount++
|
||||||
if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length)
|
if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length)
|
||||||
|
@ -213,7 +236,7 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres
|
||||||
{
|
{
|
||||||
const descriptor = descriptorsGroup[index]
|
const descriptor = descriptorsGroup[index]
|
||||||
if (value) results[descriptor.index] = value
|
if (value) results[descriptor.index] = value
|
||||||
await LbryURLCache.put(value, descriptor.id)
|
await LbryPathnameCache.put(value, descriptor.id)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
progressCount += descriptorsGroup.length
|
progressCount += descriptorsGroup.length
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
|
// This should only work in background
|
||||||
|
|
||||||
const openRequest = self.indexedDB?.open("yt-url-resolver-cache")
|
let db: IDBDatabase | null = null
|
||||||
|
|
||||||
if (openRequest)
|
// Throw if its not in the background
|
||||||
|
if (chrome.extension.getBackgroundPage() !== self) throw new Error()
|
||||||
|
|
||||||
|
if (typeof self.indexedDB !== 'undefined')
|
||||||
{
|
{
|
||||||
|
const openRequest = indexedDB.open("yt-url-resolver-cache")
|
||||||
openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt"))
|
openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt"))
|
||||||
|
|
||||||
// Delete Expired
|
// Delete Expired
|
||||||
openRequest.addEventListener('success', () =>
|
openRequest.addEventListener('success', () =>
|
||||||
{
|
{
|
||||||
const transaction = openRequest.result.transaction("store", "readwrite")
|
db = openRequest.result
|
||||||
|
const transaction = db.transaction("store", "readwrite")
|
||||||
const range = IDBKeyRange.upperBound(new Date())
|
const range = IDBKeyRange.upperBound(new Date())
|
||||||
|
|
||||||
const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range)
|
const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range)
|
||||||
|
@ -23,22 +29,24 @@ if (openRequest)
|
||||||
}
|
}
|
||||||
else console.warn(`IndexedDB not supported`)
|
else console.warn(`IndexedDB not supported`)
|
||||||
|
|
||||||
|
|
||||||
async function put(url: string | null, id: string): Promise<void>
|
async function put(url: string | null, id: string): Promise<void>
|
||||||
{
|
{
|
||||||
return await new Promise((resolve, reject) =>
|
return await new Promise((resolve, reject) =>
|
||||||
{
|
{
|
||||||
const store = openRequest.result.transaction("store", "readwrite").objectStore("store")
|
const store = db?.transaction("store", "readwrite").objectStore("store")
|
||||||
if (!store) return resolve()
|
if (!store) return resolve()
|
||||||
const request = store.put({ value: url, expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, id)
|
const request = store.put({ value: url, expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, id)
|
||||||
request.addEventListener('success', () => resolve())
|
request.addEventListener('success', () => resolve())
|
||||||
request.addEventListener('error', () => reject(request.error))
|
request.addEventListener('error', () => reject(request.error))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(id: string): Promise<string | null>
|
async function get(id: string): Promise<string | null>
|
||||||
{
|
{
|
||||||
return (await new Promise((resolve, reject) =>
|
return (await new Promise((resolve, reject) =>
|
||||||
{
|
{
|
||||||
const store = openRequest.result.transaction("store", "readonly").objectStore("store")
|
const store = db?.transaction("store", "readonly").objectStore("store")
|
||||||
if (!store) return resolve(null)
|
if (!store) return resolve(null)
|
||||||
const request = store.get(id)
|
const request = store.get(id)
|
||||||
request.addEventListener('success', () => resolve(request.result))
|
request.addEventListener('success', () => resolve(request.result))
|
||||||
|
@ -46,5 +54,5 @@ async function get(id: string): Promise<string | null>
|
||||||
}) as any)?.value
|
}) as any)?.value
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LbryURLCache = { put, get }
|
export const LbryPathnameCache = { put, get }
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": [
|
"scripts": [
|
||||||
"scripts/storageSetup.js",
|
"scripts/storageSetup.js",
|
||||||
"scripts/tabOnUpdated.js"
|
"scripts/background.js"
|
||||||
],
|
],
|
||||||
"persistent": false
|
"persistent": false
|
||||||
},
|
},
|
||||||
|
|
22
src/scripts/background.ts
Normal file
22
src/scripts/background.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { parseProtocolUrl } from '../common/lbry-url'
|
||||||
|
import { resolveById, YtIdResolverDescriptor } from '../common/yt'
|
||||||
|
async function resolveYT(descriptor: YtIdResolverDescriptor) {
|
||||||
|
const lbryProtocolUrl: string | null = await resolveById([descriptor]).then(a => a[0]);
|
||||||
|
const segments = parseProtocolUrl(lbryProtocolUrl || '', { encode: true });
|
||||||
|
if (segments.length === 0) return;
|
||||||
|
return segments.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGoingLbryPathnameRequest: Record<string, Promise<string | void>> = {}
|
||||||
|
async function lbryPathnameFromVideoId(videoId: string): Promise<string | void> {
|
||||||
|
// Don't create a new Promise for same ID until on going one is over.
|
||||||
|
const promise = onGoingLbryPathnameRequest[videoId] ?? (onGoingLbryPathnameRequest[videoId] = resolveYT({ id: videoId, type: 'video' }))
|
||||||
|
await promise
|
||||||
|
delete onGoingLbryPathnameRequest[videoId]
|
||||||
|
return await promise
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(({ videoId }: { videoId: string }, sender, sendResponse) => {
|
||||||
|
lbryPathnameFromVideoId(videoId).then((lbryPathname) => sendResponse(lbryPathname))
|
||||||
|
return true;
|
||||||
|
})
|
|
@ -1,9 +0,0 @@
|
||||||
import { TargetPlatformName } from '../common/settings'
|
|
||||||
import { YtIdResolverDescriptor } from '../common/yt'
|
|
||||||
export interface UpdateContext {
|
|
||||||
descriptor: YtIdResolverDescriptor
|
|
||||||
/** LBRY URL fragment */
|
|
||||||
lbryPathname: string
|
|
||||||
redirect: boolean
|
|
||||||
targetPlatform: TargetPlatformName
|
|
||||||
}
|
|
|
@ -1,55 +1,30 @@
|
||||||
import { h, JSX, render } from 'preact'
|
import { h, render } from 'preact'
|
||||||
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatformName, targetPlatformSettings } from '../common/settings'
|
import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatform, targetPlatformSettings } from '../common/settings'
|
||||||
import { resolveById, YtIdResolverDescriptor } from '../common/yt'
|
import { parseYouTubeURLTimeString } from '../common/yt'
|
||||||
import type { UpdateContext } from '../scripts/tabOnUpdated'
|
|
||||||
|
|
||||||
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t));
|
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t));
|
||||||
|
|
||||||
function pauseAllVideos() { document.querySelectorAll<HTMLVideoElement>('video').forEach(v => v.pause()); }
|
function pauseAllVideos() { document.querySelectorAll<HTMLVideoElement>('video').forEach(v => v.pause()); }
|
||||||
|
|
||||||
interface ButtonSettings {
|
interface WatchOnLbryButtonParameters
|
||||||
text: string
|
|
||||||
icon: string
|
|
||||||
style?:
|
|
||||||
{
|
{
|
||||||
icon?: JSX.CSSProperties
|
targetPlatform?: TargetPlatform
|
||||||
button?: JSX.CSSProperties
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonSettings: Record<TargetPlatformName, ButtonSettings> = {
|
|
||||||
app: {
|
|
||||||
text: 'Watch on LBRY',
|
|
||||||
icon: chrome.runtime.getURL('icons/lbry/lbry-logo.svg')
|
|
||||||
},
|
|
||||||
'madiator.com': {
|
|
||||||
text: 'Watch on',
|
|
||||||
icon: chrome.runtime.getURL('icons/lbry/madiator-logo.svg'),
|
|
||||||
style: {
|
|
||||||
button: { flexDirection: 'row-reverse' },
|
|
||||||
icon: { transform: 'scale(1.2)' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
odysee: {
|
|
||||||
text: 'Watch on Odysee',
|
|
||||||
icon: chrome.runtime.getURL('icons/lbry/odysee-logo.svg')
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ButtonParameters
|
|
||||||
{
|
|
||||||
targetPlatform?: TargetPlatformName
|
|
||||||
lbryPathname?: string
|
lbryPathname?: string
|
||||||
time?: number
|
time?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WatchOnLbryButton({ targetPlatform = 'app', lbryPathname, time }: ButtonParameters = {}) {
|
interface Target
|
||||||
if (!lbryPathname || !targetPlatform) return null;
|
{
|
||||||
const targetPlatformSetting = targetPlatformSettings[targetPlatform];
|
platfrom: TargetPlatform
|
||||||
const buttonSetting = buttonSettings[targetPlatform];
|
lbryPathname: string
|
||||||
|
time: number | null
|
||||||
|
}
|
||||||
|
|
||||||
const url = new URL(`${targetPlatformSetting.domainPrefix}${lbryPathname}`)
|
export function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryButtonParameters) {
|
||||||
if (time) url.searchParams.append('t', time.toFixed(0))
|
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' }}>
|
return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
|
||||||
<a href={`${url.toString()}`} onClick={pauseAllVideos} role='button'
|
<a href={`${url.toString()}`} onClick={pauseAllVideos} role='button'
|
||||||
|
@ -59,25 +34,45 @@ export function WatchOnLbryButton({ targetPlatform = 'app', lbryPathname, time }
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '12px',
|
gap: '12px',
|
||||||
borderRadius: '2px',
|
borderRadius: '2px',
|
||||||
backgroundColor: targetPlatformSetting.theme,
|
backgroundColor: targetPlatform.theme,
|
||||||
border: '0',
|
border: '0',
|
||||||
color: 'whitesmoke',
|
color: 'whitesmoke',
|
||||||
padding: '10px 16px',
|
padding: '10px 16px',
|
||||||
marginRight: '4px',
|
marginRight: '4px',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
...buttonSetting.style?.button,
|
...targetPlatform.button.style?.button,
|
||||||
}}>
|
}}>
|
||||||
<img src={buttonSetting.icon} height={16}
|
<img src={targetPlatform.button.icon} height={16}
|
||||||
style={{ transform: 'scale(1.5)', ...buttonSetting.style?.icon }} />
|
style={{ transform: 'scale(1.5)', ...targetPlatform.button.style?.icon }} />
|
||||||
<span>{buttonSetting.text}</span>
|
<span>{targetPlatform.button.text}</span>
|
||||||
</a>
|
</a>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mountPoint: HTMLDivElement | null = null
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectTo({ lbryPathname, platfrom }: Target)
|
||||||
|
{
|
||||||
|
const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`)
|
||||||
|
const time = new URL(location.href).searchParams.get('t')
|
||||||
|
|
||||||
|
if (time) url.searchParams.set('t', parseYouTubeURLTimeString(time))
|
||||||
|
|
||||||
|
if (platfrom === targetPlatformSettings.app)
|
||||||
|
{
|
||||||
|
pauseAllVideos();
|
||||||
|
location.assign(url);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
location.replace(url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns a mount point for the button */
|
/** Returns a mount point for the button */
|
||||||
async function findButtonMountPoint(): Promise<HTMLDivElement | void> {
|
async function findButtonMountPoint(): Promise<HTMLDivElement> {
|
||||||
let mountBefore: HTMLDivElement | null = null
|
let mountBefore: HTMLDivElement | null = null
|
||||||
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
||||||
if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`)
|
if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`)
|
||||||
|
@ -88,82 +83,24 @@ async function findButtonMountPoint(): Promise<HTMLDivElement | void> {
|
||||||
div.style.display = 'flex';
|
div.style.display = 'flex';
|
||||||
div.style.alignItems = 'center'
|
div.style.alignItems = 'center'
|
||||||
mountBefore.parentElement?.insertBefore(div, mountBefore)
|
mountBefore.parentElement?.insertBefore(div, mountBefore)
|
||||||
mountPoint = div
|
|
||||||
|
return div
|
||||||
}
|
}
|
||||||
|
|
||||||
let videoElement: HTMLVideoElement | null = null;
|
|
||||||
async function findVideoElement() {
|
async function findVideoElement() {
|
||||||
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
||||||
if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`)
|
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)
|
while(!(videoElement = document.querySelector(sourcePlatform.htmlQueries.videoPlayer))) await sleep(200)
|
||||||
|
|
||||||
videoElement.addEventListener('timeupdate', () => updateButton(ctxCache))
|
return videoElement
|
||||||
}
|
|
||||||
|
|
||||||
/** Compute the URL and determine whether or not a redirect should be performed. Delegates the redirect to callbacks. */
|
|
||||||
let ctxCache: UpdateContext | null = null
|
|
||||||
function handleURLChange (ctx: UpdateContext | null): void {
|
|
||||||
ctxCache = ctx
|
|
||||||
updateButton(ctx)
|
|
||||||
if (ctx?.redirect) redirectTo(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateButton(ctx: UpdateContext | null): void {
|
|
||||||
if (!mountPoint) return
|
|
||||||
if (!ctx) return render(<WatchOnLbryButton />, mountPoint)
|
|
||||||
if (ctx.descriptor.type !== 'video') return;
|
|
||||||
const lbryPathname = ctx.lbryPathname
|
|
||||||
const targetPlatform = ctx.targetPlatform
|
|
||||||
let time: number = videoElement?.currentTime ?? 0
|
|
||||||
if (time < 3) time = 0
|
|
||||||
if (time >= (videoElement?.duration ?? 0) - 1) time = 0
|
|
||||||
|
|
||||||
render(<WatchOnLbryButton targetPlatform={targetPlatform} lbryPathname={lbryPathname} time={time || undefined} />, mountPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
function redirectTo({ targetPlatform, lbryPathname }: UpdateContext): void {
|
|
||||||
|
|
||||||
const parseYouTubeTime = (timeString: string) => {
|
|
||||||
const signs = timeString.replace(/[0-9]/g, '')
|
|
||||||
if (signs.length === 0) return timeString
|
|
||||||
const numbers = timeString.replace(/[^0-9]/g, '-').split('-')
|
|
||||||
let total = 0
|
|
||||||
for (let i = 0; i < signs.length; i++) {
|
|
||||||
let t = parseInt(numbers[i])
|
|
||||||
switch (signs[i]) {
|
|
||||||
case 'd': t *= 24; case 'h': t *= 60; case 'm': t *= 60; case 's': break
|
|
||||||
default: return '0'
|
|
||||||
}
|
|
||||||
total += t
|
|
||||||
}
|
|
||||||
return total.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetPlatformSetting = targetPlatformSettings[targetPlatform];
|
|
||||||
const url = new URL(`${targetPlatformSetting.domainPrefix}${lbryPathname}`)
|
|
||||||
const time = new URL(location.href).searchParams.get('t')
|
|
||||||
|
|
||||||
if (time) url.searchParams.append('t', parseYouTubeTime(time))
|
|
||||||
|
|
||||||
if (targetPlatform === 'app')
|
|
||||||
{
|
|
||||||
pauseAllVideos();
|
|
||||||
location.assign(url);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
location.replace(url.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', async () =>
|
window.addEventListener('load', async () =>
|
||||||
{
|
{
|
||||||
// Listen History.pushState
|
|
||||||
{
|
|
||||||
const originalPushState = history.pushState
|
|
||||||
history.pushState = function(...params) { onPushState(); return originalPushState(...params) }
|
|
||||||
}
|
|
||||||
|
|
||||||
const settings = await getExtensionSettingsAsync()
|
const settings = await getExtensionSettingsAsync()
|
||||||
|
const [buttonMountPoint, videoElement] = await Promise.all([findButtonMountPoint(), findVideoElement()])
|
||||||
|
|
||||||
// Listen Settings Change
|
// Listen Settings Change
|
||||||
chrome.storage.onChanged.addListener(async (changes, areaName) => {
|
chrome.storage.onChanged.addListener(async (changes, areaName) => {
|
||||||
|
@ -171,24 +108,38 @@ window.addEventListener('load', async () =>
|
||||||
Object.assign(settings, changes)
|
Object.assign(settings, changes)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen History.pushState
|
||||||
|
{
|
||||||
|
const originalPushState = history.pushState
|
||||||
|
history.pushState = function(...params) { originalPushState(...params); afterPushState(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request Lbry pathname from background
|
||||||
|
// We should get this from background, so the caching works and we don't get erros in the future if yt decides to impliment CORS
|
||||||
|
const requestLbryPathname = async (videoId: string) => await new Promise<string | null>((resolve) => chrome.runtime.sendMessage({ videoId }, resolve))
|
||||||
|
|
||||||
|
let target: Target | null = null
|
||||||
async function updateByURL(url: URL)
|
async function updateByURL(url: URL)
|
||||||
{
|
{
|
||||||
if (url.pathname !== '/watch') return
|
if (url.pathname !== '/watch') return
|
||||||
|
|
||||||
const videoId = url.searchParams.get('v')
|
const videoId = url.searchParams.get('v')
|
||||||
if (!videoId) return
|
if (!videoId) return
|
||||||
const descriptor: YtIdResolverDescriptor = { id: videoId, type: 'video' }
|
const lbryPathname = await requestLbryPathname(videoId)
|
||||||
const lbryPathname = (await resolveById([descriptor]))[0]
|
|
||||||
if (!lbryPathname) return
|
if (!lbryPathname) return
|
||||||
updateButton({ descriptor, lbryPathname, redirect: settings.redirect, targetPlatform: settings.targetPlatform })
|
const time = videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null
|
||||||
|
target = { lbryPathname, platfrom: targetPlatformSettings[settings.targetPlatform], time }
|
||||||
|
|
||||||
|
if (settings.redirect) redirectTo(target)
|
||||||
|
else updateButton(buttonMountPoint, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPushState()
|
videoElement.addEventListener('timeupdate', () => updateButton(buttonMountPoint, target))
|
||||||
|
|
||||||
|
async function afterPushState()
|
||||||
{
|
{
|
||||||
await updateByURL(new URL(location.href))
|
await updateByURL(new URL(location.href))
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateByURL(new URL(location.href))
|
await updateByURL(new URL(location.href))
|
||||||
|
|
||||||
findButtonMountPoint().then(() => updateButton(ctxCache))
|
|
||||||
findVideoElement().then(() => updateButton(ctxCache))
|
|
||||||
})
|
})
|
|
@ -19,12 +19,12 @@ async function lbryChannelsFromFile(file: File) {
|
||||||
ext === 'xml' || ext == 'opml' ? getSubsFromOpml :
|
ext === 'xml' || ext == 'opml' ? getSubsFromOpml :
|
||||||
ext === 'csv' ? getSubsFromCsv :
|
ext === 'csv' ? getSubsFromCsv :
|
||||||
getSubsFromJson)(await getFileContent(file)))
|
getSubsFromJson)(await getFileContent(file)))
|
||||||
const lbryUrls = await resolveById(
|
const lbryPathnames = await resolveById(
|
||||||
Array.from(ids).map(id => ({ id, type: 'channel' } as const)),
|
Array.from(ids).map(id => ({ id, type: 'channel' } as const)),
|
||||||
(progress) => render(<YTtoLBRY progress={progress} />, document.getElementById('root')!));
|
(progress) => render(<YTtoLBRY progress={progress} />, document.getElementById('root')!));
|
||||||
const { targetPlatform: platform } = await getExtensionSettingsAsync();
|
const { targetPlatform: platform } = await getExtensionSettingsAsync();
|
||||||
const urlPrefix = targetPlatformSettings[platform].domainPrefix;
|
const urlPrefix = targetPlatformSettings[platform].domainPrefix;
|
||||||
return lbryUrls.map(channel => urlPrefix + channel);
|
return lbryPathnames.map(channel => urlPrefix + channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise<void> | void, progress: number }) {
|
function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise<void> | void, progress: number }) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue