mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
types added, names changed, rewrote most of ytContent.ts
- Added more types, so when there is an error it's more visible. - Default setting was using `lbry.tv` which doesn't exists anymore, so i made it odysee. - Changed `redirect` value name in the `LbrySettings` to `platform` which makes more sense to this version. - Changed `url` in UpdateContext to `pathname`. Using `url` for the full URL. - Rewrote most of the `ytContent.tsx` so the timestamp feature doesnt look like a patch.
This commit is contained in:
parent
8c4f3e60e0
commit
5c61db3ea0
6 changed files with 166 additions and 142 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,5 +2,7 @@
|
|||
dist
|
||||
node_modules
|
||||
web-ext-artifacts
|
||||
yarn-error.log
|
||||
.devcontainer
|
||||
|
||||
.DS_Store
|
||||
|
|
|
@ -1,16 +1,28 @@
|
|||
export interface LbrySettings {
|
||||
enabled: boolean
|
||||
redirect: keyof typeof redirectDomains
|
||||
export type PlatformName = 'madiator.com' | 'odysee' | 'app'
|
||||
|
||||
export interface PlatformSettings
|
||||
{
|
||||
domainPrefix: string
|
||||
display: string
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: LbrySettings = { enabled: true, redirect: 'lbry.tv' };
|
||||
|
||||
export const redirectDomains = {
|
||||
'madiator.com': { prefix: 'https://madiator.com/', display: 'madiator.com' },
|
||||
odysee: { prefix: 'https://odysee.com/', display: 'odysee' },
|
||||
app: { prefix: 'lbry://', display: 'App' },
|
||||
export const platformSettings: Record<PlatformName, PlatformSettings> = {
|
||||
'madiator.com': { domainPrefix: 'https://madiator.com/', display: 'madiator.com' },
|
||||
odysee: { domainPrefix: 'https://odysee.com/', display: 'odysee' },
|
||||
app: { domainPrefix: 'lbry://', display: 'App' },
|
||||
};
|
||||
|
||||
export const getPlatfromSettingsEntiries = () => {
|
||||
return Object.entries(platformSettings) as any as [Extract<keyof typeof platformSettings, string>, PlatformSettings][]
|
||||
}
|
||||
|
||||
export interface LbrySettings {
|
||||
enabled: boolean
|
||||
platform: PlatformName
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: LbrySettings = { enabled: true, platform: 'odysee' };
|
||||
|
||||
export function getSettingsAsync<K extends Array<keyof LbrySettings>>(...keys: K): Promise<Pick<LbrySettings, K[number]>> {
|
||||
return new Promise(resolve => chrome.storage.local.get(keys, o => resolve(o as any)));
|
||||
}
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
import { h, render } from 'preact';
|
||||
import { h, render } from 'preact'
|
||||
import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio'
|
||||
import { getPlatfromSettingsEntiries, LbrySettings, PlatformName } from '../common/settings'
|
||||
import { useLbrySettings } from '../common/useSettings'
|
||||
import './popup.sass'
|
||||
|
||||
import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio';
|
||||
import { redirectDomains } from '../common/settings';
|
||||
import { useLbrySettings } from '../common/useSettings';
|
||||
|
||||
import './popup.sass';
|
||||
|
||||
/** Utilty to set a setting in the browser */
|
||||
const setSetting = (setting: string, value: any) => chrome.storage.local.set({ [setting]: value });
|
||||
const setSetting = <K extends keyof LbrySettings>(setting: K, value: LbrySettings[K]) => chrome.storage.local.set({ [setting]: value });
|
||||
|
||||
/** Gets all the options for redirect destinations as selection options */
|
||||
const redirectOptions: SelectionOption[] = Object.entries(redirectDomains)
|
||||
const platformOptions: SelectionOption[] = getPlatfromSettingsEntiries()
|
||||
.map(([value, { display }]) => ({ value, display }));
|
||||
|
||||
function WatchOnLbryPopup() {
|
||||
const { enabled, redirect } = useLbrySettings();
|
||||
const { enabled, platform } = useLbrySettings();
|
||||
|
||||
return <div className='container'>
|
||||
<label className='radio-label'>Enable Redirection:</label>
|
||||
<ButtonRadio value={enabled ? 'YES' : 'NO'} options={['YES', 'NO']}
|
||||
onChange={enabled => setSetting('enabled', enabled.toLowerCase() === 'yes')} />
|
||||
<label className='radio-label'>Where would you like to redirect?</label>
|
||||
<ButtonRadio value={redirect as string} options={redirectOptions}
|
||||
onChange={redirect => setSetting('redirect', redirect)} />
|
||||
<ButtonRadio value={platform} options={platformOptions}
|
||||
onChange={(platform: PlatformName) => setSetting('platform', platform)} />
|
||||
<label className='radio-label'>Other useful tools:</label>
|
||||
<a href='/tools/YTtoLBRY.html' target='_blank'>
|
||||
<button type='button' className='btn1 button is-primary'>Subscriptions Converter</button>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { appRedirectUrl, parseProtocolUrl } from '../common/lbry-url';
|
||||
import { getSettingsAsync, LbrySettings } from '../common/settings';
|
||||
import { YTDescriptor, ytService } from '../common/yt';
|
||||
import { appRedirectUrl, parseProtocolUrl } from '../common/lbry-url'
|
||||
import { getSettingsAsync, PlatformName } from '../common/settings'
|
||||
import { YTDescriptor, ytService } from '../common/yt'
|
||||
export interface UpdateContext {
|
||||
descriptor: YTDescriptor
|
||||
/** LBRY URL fragment */
|
||||
url: string
|
||||
pathname: string
|
||||
enabled: boolean
|
||||
redirect: LbrySettings['redirect']
|
||||
platform: PlatformName
|
||||
}
|
||||
|
||||
async function resolveYT(descriptor: YTDescriptor) {
|
||||
|
@ -16,26 +16,26 @@ async function resolveYT(descriptor: YTDescriptor) {
|
|||
return segments.join('/');
|
||||
}
|
||||
|
||||
const urlCache: Record<string, string | undefined> = {};
|
||||
const pathnameCache: Record<string, string | undefined> = {};
|
||||
|
||||
async function ctxFromURL(url: string): Promise<UpdateContext | void> {
|
||||
if (!url || !(url.startsWith('https://www.youtube.com/watch?v=') || url.startsWith('https://www.youtube.com/channel/'))) return;
|
||||
url = new URL(url).href;
|
||||
const { enabled, redirect } = await getSettingsAsync('enabled', 'redirect');
|
||||
const { enabled, platform } = await getSettingsAsync('enabled', 'platform');
|
||||
const descriptor = ytService.getId(url);
|
||||
if (!descriptor) return; // couldn't get the ID, so we're done
|
||||
|
||||
const res = url in urlCache ? urlCache[url] : await resolveYT(descriptor);
|
||||
urlCache[url] = res;
|
||||
const res = url in pathnameCache ? pathnameCache[url] : await resolveYT(descriptor);
|
||||
pathnameCache[url] = res;
|
||||
if (!res) return; // couldn't find it on lbry, so we're done
|
||||
|
||||
return { descriptor, url: res, enabled, redirect };
|
||||
return { descriptor, pathname: res, enabled, platform };
|
||||
}
|
||||
|
||||
// handles lbry.tv -> lbry app redirect
|
||||
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, { url: tabUrl }) => {
|
||||
const { enabled, redirect } = await getSettingsAsync('enabled', 'redirect');
|
||||
if (!enabled || redirect !== 'app' || !changeInfo.url || !tabUrl?.startsWith('https://odysee.com/')) return;
|
||||
const { enabled, platform } = await getSettingsAsync('enabled', 'platform');
|
||||
if (!enabled || platform !== 'app' || !changeInfo.url || !tabUrl?.startsWith('https://odysee.com/')) return;
|
||||
|
||||
const url = appRedirectUrl(tabUrl, { encode: true });
|
||||
if (!url) return;
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
import { h, JSX, render } from 'preact';
|
||||
import { PlatformName, platformSettings } from '../common/settings'
|
||||
import type { UpdateContext } from '../scripts/tabOnUpdated'
|
||||
import { h, JSX, render } from 'preact'
|
||||
|
||||
import { parseProtocolUrl } from '../common/lbry-url';
|
||||
import { LbrySettings, redirectDomains } from '../common/settings';
|
||||
import { YTDescriptor, ytService } from '../common/yt';
|
||||
import { UpdateContext } from './tabOnUpdated';
|
||||
|
||||
interface UpdaterOptions {
|
||||
/** invoked if a redirect should be performed */
|
||||
onRedirect?(ctx: UpdateContext): void
|
||||
/** invoked if a URL is found */
|
||||
onURL?(ctx: UpdateContext): void
|
||||
}
|
||||
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t));
|
||||
|
||||
interface ButtonSettings {
|
||||
text: string
|
||||
|
@ -18,62 +10,35 @@ interface ButtonSettings {
|
|||
style?: JSX.CSSProperties
|
||||
}
|
||||
|
||||
const buttonSettings: Record<LbrySettings['redirect'], ButtonSettings> = {
|
||||
app: { text: 'Watch on LBRY', icon: chrome.runtime.getURL('icons/lbry/lbry-logo.svg') },
|
||||
'madiator.com': { text: 'Watch on LBRY', icon: chrome.runtime.getURL('icons/lbry/lbry-logo.svg') },
|
||||
const buttonSettings: Record<PlatformName, ButtonSettings> = {
|
||||
app: {
|
||||
text: 'Watch on LBRY',
|
||||
icon: chrome.runtime.getURL('icons/lbry/lbry-logo.svg')
|
||||
},
|
||||
'madiator.com': {
|
||||
text: 'Watch on LBRY',
|
||||
icon: chrome.runtime.getURL('icons/lbry/lbry-logo.svg')
|
||||
},
|
||||
odysee: {
|
||||
text: 'Watch on Odysee', icon: chrome.runtime.getURL('icons/lbry/odysee-logo.svg'),
|
||||
style: { backgroundColor: '#1e013b' },
|
||||
},
|
||||
};
|
||||
|
||||
function pauseVideo() { document.querySelectorAll<HTMLVideoElement>('video').forEach(v => v.pause()); }
|
||||
|
||||
function openApp(url: string) {
|
||||
pauseVideo();
|
||||
location.assign(url);
|
||||
interface ButtonParameters
|
||||
{
|
||||
platform?: PlatformName
|
||||
pathname?: string
|
||||
time?: number
|
||||
}
|
||||
|
||||
async function resolveYT(descriptor: YTDescriptor) {
|
||||
const lbryProtocolUrl: string | null = await ytService.resolveById(descriptor).then(a => a[0]);
|
||||
const segments = parseProtocolUrl(lbryProtocolUrl || '', { encode: true });
|
||||
if (segments.length === 0) return;
|
||||
return segments.join('/');
|
||||
}
|
||||
export function WatchOnLbryButton({ platform = 'app', pathname, time }: ButtonParameters) {
|
||||
if (!pathname || !platform) return null;
|
||||
const platformSetting = platformSettings[platform];
|
||||
const buttonSetting = buttonSettings[platform];
|
||||
|
||||
|
||||
|
||||
/** Compute the URL and determine whether or not a redirect should be performed. Delegates the redirect to callbacks. */
|
||||
async function handleURLChange(ctx: UpdateContext, { onRedirect, onURL }: UpdaterOptions): Promise<void> {
|
||||
if (onURL) onURL(ctx);
|
||||
if (ctx.enabled && onRedirect) onRedirect(ctx);
|
||||
}
|
||||
|
||||
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t));
|
||||
|
||||
/** Returns a mount point for the button */
|
||||
async function findMountPoint(): Promise<HTMLDivElement | void> {
|
||||
let ownerBar = document.querySelector('ytd-video-owner-renderer');
|
||||
for (let i = 0; !ownerBar && i < 50; i++) {
|
||||
await sleep(200);
|
||||
ownerBar = document.querySelector('ytd-video-owner-renderer');
|
||||
}
|
||||
|
||||
if (!ownerBar) return;
|
||||
const div = document.createElement('div');
|
||||
div.style.display = 'flex';
|
||||
ownerBar.insertAdjacentElement('afterend', div);
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
function WatchOnLbryButton({ redirect = 'app', url: pathname, time }: { redirect?: LbrySettings['redirect'], url?: string, time?: string }) {
|
||||
if (!pathname) return null;
|
||||
const domain = redirectDomains[redirect];
|
||||
const buttonSetting = buttonSettings[redirect];
|
||||
|
||||
const url = new URL(`${domain.prefix}${pathname}`)
|
||||
if (time) url.searchParams.append('t', time)
|
||||
const url = new URL(`${platformSetting.domainPrefix}${pathname}`)
|
||||
if (time) url.searchParams.append('t', time.toFixed(0))
|
||||
|
||||
return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
|
||||
<a href={`${url.toString()}`} onClick={pauseVideo} role='button'
|
||||
|
@ -96,57 +61,102 @@ function WatchOnLbryButton({ redirect = 'app', url: pathname, time }: { redirect
|
|||
</div>;
|
||||
}
|
||||
|
||||
|
||||
const mountPointPromise = findMountPoint();
|
||||
|
||||
let ctxCache: UpdateContext | undefined
|
||||
const handle = (ctx: UpdateContext) => (ctxCache = ctx) && ctx.url && handleURLChange(ctx, {
|
||||
async onURL({ descriptor: { type }, url, redirect }) {
|
||||
const mountPoint = await mountPointPromise;
|
||||
if (type !== 'video' || !mountPoint) return;
|
||||
render(<WatchOnLbryButton url={url} redirect={redirect} />, mountPoint);
|
||||
},
|
||||
onRedirect({ redirect, url }) {
|
||||
const domain = redirectDomains[redirect];
|
||||
if (redirect === 'app') return openApp(domain.prefix + url);
|
||||
location.replace(domain.prefix + url);
|
||||
},
|
||||
});
|
||||
|
||||
{(async () => {
|
||||
let videoElement: HTMLVideoElement | null = null;
|
||||
let renderingButton = false
|
||||
|
||||
while(!(videoElement = document.querySelector('video'))) await sleep(200)
|
||||
|
||||
const handleTimeChange = () => {
|
||||
if (renderingButton) return
|
||||
if (!videoElement) return
|
||||
if (!ctxCache?.url) return
|
||||
const time = (videoElement.currentTime ?? 0).toFixed(0)
|
||||
const { url, redirect } = ctxCache
|
||||
|
||||
renderingButton = true
|
||||
mountPointPromise.then(mountPoint => mountPoint && render(<WatchOnLbryButton time={time} url={url} redirect={redirect} />, mountPoint))
|
||||
.then(() => renderingButton = false)
|
||||
let mountPoint: HTMLDivElement | null = null
|
||||
/** Returns a mount point for the button */
|
||||
async function findButtonMountPoint(): Promise<HTMLDivElement | void> {
|
||||
let ownerBar = document.querySelector('ytd-video-owner-renderer');
|
||||
for (let i = 0; !ownerBar && i < 50; i++) {
|
||||
await sleep(200);
|
||||
ownerBar = document.querySelector('ytd-video-owner-renderer');
|
||||
}
|
||||
|
||||
videoElement.addEventListener('timeupdate', handleTimeChange)
|
||||
})()}
|
||||
if (!ownerBar) return;
|
||||
const div = document.createElement('div');
|
||||
div.style.display = 'flex';
|
||||
ownerBar.insertAdjacentElement('afterend', div);
|
||||
|
||||
// handle the location on load of the page
|
||||
chrome.runtime.sendMessage({ url: location.href }, async (ctx: UpdateContext) => handle(ctx));
|
||||
mountPoint = div
|
||||
}
|
||||
|
||||
let videoElement: HTMLVideoElement | null = null;
|
||||
async function findVideoElement() {
|
||||
while(!(videoElement = document.querySelector('#ytd-player video'))) await sleep(200)
|
||||
videoElement.addEventListener('timeupdate', () => updateButton(ctxCache))
|
||||
}
|
||||
|
||||
function pauseVideo() { document.querySelectorAll<HTMLVideoElement>('video').forEach(v => v.pause()); }
|
||||
|
||||
function openApp(url: string) {
|
||||
pauseVideo();
|
||||
location.assign(url);
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
ctxCache = ctx
|
||||
updateButton(ctx)
|
||||
if (ctx?.enabled) redirectTo(ctx)
|
||||
}
|
||||
|
||||
function updateButton(ctx: UpdateContext | null) {
|
||||
if (!mountPoint) return
|
||||
if (!ctx) return render(<WatchOnLbryButton />, mountPoint)
|
||||
if (ctx.descriptor.type !== 'video') return;
|
||||
const time = videoElement?.currentTime ?? 0
|
||||
const pathname = ctx.pathname
|
||||
const platform = ctx.platform
|
||||
|
||||
render(<WatchOnLbryButton platform={platform} pathname={pathname} time={time} />, mountPoint)
|
||||
}
|
||||
|
||||
function redirectTo({ platform, pathname }: UpdateContext) {
|
||||
|
||||
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 's': case 'm': t *= 60; case 'h': t *= 60; case 'd': t *= 24; break
|
||||
default: return '0'
|
||||
}
|
||||
total += t
|
||||
}
|
||||
return total.toString()
|
||||
}
|
||||
|
||||
const platformSetting = platformSettings[platform];
|
||||
const url = new URL(`${platformSetting.domainPrefix}${pathname}`)
|
||||
const time = new URL(location.href).searchParams.get('t')
|
||||
if (time) url.searchParams.append('t', parseYouTubeTime(time))
|
||||
|
||||
if (platform === 'app') return openApp(url.toString());
|
||||
location.replace(url.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
findButtonMountPoint().then(() => updateButton(ctxCache))
|
||||
findVideoElement().then(() => updateButton(ctxCache))
|
||||
|
||||
|
||||
/** Request UpdateContext from background */
|
||||
const requestCtxFromUrl = async (url: string) => await new Promise<UpdateContext | null>((resolve) => chrome.runtime.sendMessage({ url }, resolve))
|
||||
|
||||
/** Handle the location on load of the page */
|
||||
requestCtxFromUrl(location.href).then((ctx) => handleURLChange(ctx))
|
||||
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
chrome.runtime.onMessage.addListener(async (ctx: UpdateContext) => {
|
||||
mountPointPromise.then(mountPoint => mountPoint && render(<WatchOnLbryButton />, mountPoint))
|
||||
handle(ctx);
|
||||
});
|
||||
chrome.runtime.onMessage.addListener(async (ctx: UpdateContext) => handleURLChange(ctx));
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName !== 'local' || !changes.redirect) return;
|
||||
chrome.runtime.sendMessage({ url: location.href }, async (ctx: UpdateContext) => handle(ctx));
|
||||
});
|
||||
/** On settings change */
|
||||
chrome.storage.onChanged.addListener(async (changes, areaName) => {
|
||||
if (areaName !== 'local') return;
|
||||
if (changes.platform) handleURLChange(await requestCtxFromUrl(location.href))
|
||||
});
|
|
@ -1,10 +1,10 @@
|
|||
import { Fragment, h, JSX, render } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { h, render } from 'preact'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { getSettingsAsync, platformSettings } from '../common/settings'
|
||||
import { getFileContent, ytService } from '../common/yt'
|
||||
import readme from './README.md'
|
||||
|
||||
import { getSettingsAsync, redirectDomains } from '../common/settings';
|
||||
import { getFileContent, ytService } from '../common/yt';
|
||||
|
||||
import readme from './README.md';
|
||||
|
||||
/**
|
||||
* Parses the subscription file and queries the API for lbry channels
|
||||
|
@ -18,8 +18,8 @@ async function lbryChannelsFromFile(file: File) {
|
|||
|
||||
const ids = new Set((ext === 'xml' || ext == 'opml' ? ytService.readOpml(content) : ytService.readJson(content)))
|
||||
const lbryUrls = await ytService.resolveById(...Array.from(ids).map(id => ({ id, type: 'channel' } as const)));
|
||||
const { redirect } = await getSettingsAsync('redirect');
|
||||
const urlPrefix = redirectDomains[redirect].prefix;
|
||||
const { platform } = await getSettingsAsync('platform');
|
||||
const urlPrefix = platformSettings[platform].domainPrefix;
|
||||
return lbryUrls.map(channel => urlPrefix + channel);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue