Skip to content

Commit

Permalink
Merge pull request #70 from DeepDoge/patch-1
Browse files Browse the repository at this point in the history
Update ytContent.tsx
  • Loading branch information
kodxana authored Dec 11, 2021
2 parents 285d46b + dfdfe67 commit 6ac58a5
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 119 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
dist
node_modules
web-ext-artifacts
yarn-error.log
.devcontainer

.DS_Store
28 changes: 20 additions & 8 deletions src/common/settings.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
export type PlatformName = 'madiator.com' | 'odysee' | 'app'

export interface PlatformSettings
{
domainPrefix: string
display: string
}

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
redirect: keyof typeof redirectDomains
platform: PlatformName
}

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 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)));
Expand Down
15 changes: 14 additions & 1 deletion src/common/yt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const ytService = {
*/
readOpml(opmlContents: string): string[] {
const opml = new DOMParser().parseFromString(opmlContents, 'application/xml');

opmlContents = ''
return Array.from(opml.querySelectorAll('outline > outline'))
.map(outline => outline.getAttribute('xmlUrl'))
.filter((url): url is string => !!url)
Expand All @@ -73,9 +73,22 @@ export const ytService = {
*/
readJson(jsonContents: string): string[] {
const subscriptions: YtSubscription[] = JSON.parse(jsonContents);
jsonContents = ''
return subscriptions.map(sub => sub.snippet.resourceId.channelId);
},

/**
* Reads an array of YT channel IDs from the YT subscriptions CSV file
*
* @param csvContent a CSV file as a string
* @returns the channel IDs
*/
readCsv(csvContent: string): string[] {
const rows = csvContent.split('\n')
csvContent = ''
return rows.map((row) => row.substr(0, row.indexOf(',')))
},

/**
* Extracts the channelID from a YT URL.
*
Expand Down
20 changes: 10 additions & 10 deletions src/popup/popup.tsx
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
24 changes: 12 additions & 12 deletions src/scripts/tabOnUpdated.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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;
Expand Down
196 changes: 118 additions & 78 deletions src/scripts/ytContent.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,47 @@
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
}
import { PlatformName, platformSettings } from '../common/settings'
import type { UpdateContext } from '../scripts/tabOnUpdated'
import { h, JSX, render } from 'preact'

const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t));

interface ButtonSettings {
text: string
icon: string
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);
}

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('/');
}

/** 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);
interface ButtonParameters
{
platform?: PlatformName
pathname?: string
time?: number
}

/** Returns a mount point for the button */
async function findMountPoint(): Promise<HTMLDivElement | void> {
const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t));
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');
}
export function WatchOnLbryButton({ platform = 'app', pathname, time }: ButtonParameters) {
if (!pathname || !platform) return null;
const platformSetting = platformSettings[platform];
const buttonSetting = buttonSettings[platform];

if (!ownerBar) return;
const div = document.createElement('div');
div.style.display = 'flex';
ownerBar.insertAdjacentElement('afterend', div);
return div;
}
const url = new URL(`${platformSetting.domainPrefix}${pathname}`)
if (time) url.searchParams.append('t', time.toFixed(0))

function WatchOnLbryButton({ redirect = 'app', url }: { redirect?: LbrySettings['redirect'], url?: string }) {
if (!url) return null;
const domain = redirectDomains[redirect];
const buttonSetting = buttonSettings[redirect];
return <div style={{ display: 'flex', justifyContent: 'center', flexDirection: 'column' }}>
<a href={domain.prefix + url} onClick={pauseVideo} role='button'
<a href={`${url.toString()}`} onClick={pauseVideo} role='button'
children={<div>
<img src={buttonSetting.icon} height={10} width={14}
style={{ marginRight: 12, transform: 'scale(1.75)' }} />
Expand All @@ -88,36 +61,103 @@ function WatchOnLbryButton({ redirect = 'app', url }: { redirect?: LbrySettings[
</div>;
}

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');
}

if (!ownerBar) return;
const div = document.createElement('div');
div.style.display = 'flex';
ownerBar.insertAdjacentElement('afterend', div);

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()); }

const mountPointPromise = findMountPoint();
function openApp(url: string) {
pauseVideo();
location.assign(url);
}

const handle = (ctx: UpdateContext) => 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);
},
});
/** 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 'd': t *= 24; case 'h': t *= 60; case 'm': t *= 60; case 's': 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))

// handle the location on load of the page
chrome.runtime.sendMessage({ url: location.href }, async (ctx: UpdateContext) => handle(ctx));
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))
if (!ctx.url) return;
handle(ctx);
});

chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local' || !changes.redirect) return;
chrome.runtime.sendMessage({ url: location.href }, async (ctx: UpdateContext) => handle(ctx));
});
chrome.runtime.onMessage.addListener(async (ctx: UpdateContext) => handleURLChange(ctx));

/** On settings change */
chrome.storage.onChanged.addListener(async (changes, areaName) => {
if (areaName !== 'local') return;
if (changes.platform) handleURLChange(await requestCtxFromUrl(location.href))
});
Loading

0 comments on commit 6ac58a5

Please sign in to comment.