From 1e7293826a60b83017bf52d2e31a831f06fe886c Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:41:30 +0000 Subject: [PATCH 01/20] =?UTF-8?q?=F0=9F=8D=99=20Refactor,=20moved=20many?= =?UTF-8?q?=20backend=20stuff=20to=20content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/tabOnUpdated.ts | 34 +-------------------------- src/scripts/ytContent.tsx | 47 +++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 48 deletions(-) diff --git a/src/scripts/tabOnUpdated.ts b/src/scripts/tabOnUpdated.ts index cf67bfc..06e3644 100644 --- a/src/scripts/tabOnUpdated.ts +++ b/src/scripts/tabOnUpdated.ts @@ -39,36 +39,4 @@ async function ctxFromURL(href: string): Promise { await promise delete ctxFromURLOnGoingPromise[descriptor.id] return await promise -} - -// handles lbry.tv -> lbry app redirect -chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, { url: tabUrl }) => { - const { redirect, targetPlatform } = await getExtensionSettingsAsync('redirect', 'targetPlatform'); - if (!redirect || targetPlatform !== 'app' || !changeInfo.url || !tabUrl?.startsWith('https://odysee.com/')) return; - - const url = appRedirectUrl(tabUrl, { encode: true }); - if (!url) return; - chrome.tabs.update(tabId, { url }); - alert('Opened link in LBRY App!'); // Better for UX since sometimes LBRY App doesn't take focus, if that is fixed, this can be removed - chrome.tabs.executeScript(tabId, { - code: `if (window.history.length === 1) { - window.close(); - } else { - window.history.back(); - } - document.querySelectorAll('video').forEach(v => v.pause()); - ` - }); -}); - -chrome.runtime.onMessage.addListener(({ url }: { url: string }, sender, sendResponse) => { - ctxFromURL(url).then(ctx => { - sendResponse(ctx); - }) - return true; -}) - -// relay youtube link changes to the content script -chrome.tabs.onUpdated.addListener((tabId, changeInfo, { url }) => { - if (url) ctxFromURL(url).then(ctx => chrome.tabs.sendMessage(tabId, ctx)); -}); +} \ No newline at end of file diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index 3281698..86e9f99 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -1,6 +1,7 @@ -import { getSourcePlatfromSettingsFromHostname, TargetPlatformName, targetPlatformSettings } from '../common/settings' +import { ExtensionSettings, getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatformName, targetPlatformSettings } from '../common/settings' import type { UpdateContext } from '../scripts/tabOnUpdated' import { h, JSX, render } from 'preact' +import { YtIdResolverDescriptor, ytService } from '../common/yt' const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)); @@ -159,21 +160,37 @@ function redirectTo({ targetPlatform, lbryPathname }: UpdateContext): void { findButtonMountPoint().then(() => updateButton(ctxCache)) findVideoElement().then(() => updateButton(ctxCache)) +async function onPageLoad() +{ + // Listen History.pushState + { + const originalPushState = history.pushState + history.pushState = function(...params) { onPushState(); return originalPushState(...params) } + } -/** Request UpdateContext from background */ -const requestCtxFromUrl = async (url: string) => await new Promise((resolve) => chrome.runtime.sendMessage({ url }, resolve)) + const settings = await getExtensionSettingsAsync('redirect', 'targetPlatform', 'urlResolver') + + // Listen Settings Change + chrome.storage.onChanged.addListener(async (changes, areaName) => { + if (areaName !== 'local') return; + Object.assign(settings, changes) + }); -/** Handle the location on load of the page */ -requestCtxFromUrl(location.href).then((ctx) => handleURLChange(ctx)) + async function updateByURL(url: URL) + { + if (url.pathname !== '/watch') return + const videoId = url.searchParams.get('v') + if (!videoId) return + const descriptor: YtIdResolverDescriptor = { id: videoId, type: 'video' } + const lbryPathname = (await ytService.resolveById([descriptor]))[0] + if (!lbryPathname) return + updateButton({ descriptor, lbryPathname, redirect: settings.redirect, targetPlatform: settings.targetPlatform }) + } -/* - * 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) => handleURLChange(ctx)); + async function onPushState() + { + await updateByURL(new URL(location.href)) + } -/** On settings change */ -chrome.storage.onChanged.addListener(async (changes, areaName) => { - if (areaName !== 'local') return; - if (changes.targetPlatform) handleURLChange(await requestCtxFromUrl(location.href)) -}); \ No newline at end of file + await updateByURL(new URL(location.href)) +} \ No newline at end of file From 205a8fd151f7a667fc2b35f92632e7c07ef03dde Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:42:35 +0000 Subject: [PATCH 02/20] =?UTF-8?q?=F0=9F=8D=99=20Refactor=20removed=20pick?= =?UTF-8?q?=20from=20getExtensionSettings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/settings.ts | 5 +++-- src/scripts/storageSetup.ts | 2 +- src/scripts/tabOnUpdated.ts | 2 +- src/scripts/ytContent.tsx | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/common/settings.ts b/src/common/settings.ts index 2200048..c5908c8 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -6,8 +6,9 @@ export interface ExtensionSettings { export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee', urlResolver: 'lbryInc' }; -export function getExtensionSettingsAsync>(...keys: K): Promise> { - return new Promise(resolve => chrome.storage.local.get(keys, o => resolve(o as any))); +export function getExtensionSettingsAsync(): Promise +{ + return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any))); } diff --git a/src/scripts/storageSetup.ts b/src/scripts/storageSetup.ts index f574108..7f90e83 100644 --- a/src/scripts/storageSetup.ts +++ b/src/scripts/storageSetup.ts @@ -2,7 +2,7 @@ import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync } from ' /** Reset settings to default value and update the browser badge text */ async function initSettings() { - const settings = await getExtensionSettingsAsync(...Object.keys(DEFAULT_SETTINGS) as Array); + const settings = await getExtensionSettingsAsync(); // get all the values that aren't set and use them as a change set const invalidEntries = (Object.entries(DEFAULT_SETTINGS) as Array<[keyof ExtensionSettings, ExtensionSettings[keyof ExtensionSettings]]>) diff --git a/src/scripts/tabOnUpdated.ts b/src/scripts/tabOnUpdated.ts index 06e3644..ae767b9 100644 --- a/src/scripts/tabOnUpdated.ts +++ b/src/scripts/tabOnUpdated.ts @@ -33,7 +33,7 @@ async function ctxFromURL(href: string): Promise { const res = await resolveYT(descriptor) if (!res) return // couldn't find it on lbry, so we're done - const { redirect, targetPlatform } = await getExtensionSettingsAsync('redirect', 'targetPlatform') + const { redirect, targetPlatform } = await getExtensionSettingsAsync() return { descriptor, lbryPathname: res, redirect, targetPlatform } })()) await promise diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index 86e9f99..f1fd9b1 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -168,7 +168,7 @@ async function onPageLoad() history.pushState = function(...params) { onPushState(); return originalPushState(...params) } } - const settings = await getExtensionSettingsAsync('redirect', 'targetPlatform', 'urlResolver') + const settings = await getExtensionSettingsAsync() // Listen Settings Change chrome.storage.onChanged.addListener(async (changes, areaName) => { From 4cdcc4c9a4be96aafb80921dc885809ebb1aa009 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:43:46 +0000 Subject: [PATCH 03/20] =?UTF-8?q?=F0=9F=8D=B1=20Refactor,=20removed=20not?= =?UTF-8?q?=20used=20backend=20code.=20bugfixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/tabOnUpdated.ts | 37 ++----------------------------------- src/tools/YTtoLBRY.tsx | 2 +- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/src/scripts/tabOnUpdated.ts b/src/scripts/tabOnUpdated.ts index ae767b9..17e1bb5 100644 --- a/src/scripts/tabOnUpdated.ts +++ b/src/scripts/tabOnUpdated.ts @@ -1,42 +1,9 @@ -import { appRedirectUrl, parseProtocolUrl } from '../common/lbry-url' -import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatformName } from '../common/settings' -import { YtIdResolverDescriptor, ytService } from '../common/yt' +import { TargetPlatformName } from '../common/settings' +import { YtIdResolverDescriptor } from '../common/yt' export interface UpdateContext { descriptor: YtIdResolverDescriptor /** LBRY URL fragment */ lbryPathname: string redirect: boolean targetPlatform: TargetPlatformName -} - -async function resolveYT(descriptor: YtIdResolverDescriptor) { - 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('/'); -} - -const ctxFromURLOnGoingPromise: Record> = {} -async function ctxFromURL(href: string): Promise { - if (!href) return; - - const url = new URL(href); - if (!getSourcePlatfromSettingsFromHostname(url.hostname)) return - if (!(url.pathname.startsWith('/watch') || url.pathname.startsWith('/channel'))) return - - const descriptor = ytService.getId(href); - if (!descriptor) return; // couldn't get the ID, so we're done - - // Don't create a new Promise for same ID until on going one is over. - const promise = ctxFromURLOnGoingPromise[descriptor.id] ?? (ctxFromURLOnGoingPromise[descriptor.id] = (async () => { - // NOTE: API call cached by resolveYT method automatically - const res = await resolveYT(descriptor) - if (!res) return // couldn't find it on lbry, so we're done - - const { redirect, targetPlatform } = await getExtensionSettingsAsync() - return { descriptor, lbryPathname: res, redirect, targetPlatform } - })()) - await promise - delete ctxFromURLOnGoingPromise[descriptor.id] - return await promise } \ No newline at end of file diff --git a/src/tools/YTtoLBRY.tsx b/src/tools/YTtoLBRY.tsx index 1c053da..ac38427 100644 --- a/src/tools/YTtoLBRY.tsx +++ b/src/tools/YTtoLBRY.tsx @@ -22,7 +22,7 @@ async function lbryChannelsFromFile(file: File) { const lbryUrls = await ytService.resolveById( Array.from(ids).map(id => ({ id, type: 'channel' } as const)), (progress) => render(, document.getElementById('root')!)); - const { targetPlatform: platform } = await getExtensionSettingsAsync('targetPlatform'); + const { targetPlatform: platform } = await getExtensionSettingsAsync(); const urlPrefix = targetPlatformSettings[platform].domainPrefix; return lbryUrls.map(channel => urlPrefix + channel); } From 719ff06caf2cc461fa3f7ff0c2288c28a9c069f7 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:54:28 +0000 Subject: [PATCH 04/20] =?UTF-8?q?=F0=9F=8D=A3=20Refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/settings.ts | 3 +- src/common/yt.ts | 139 +++++++++++++++++--------------------- src/scripts/ytContent.tsx | 12 ++-- 3 files changed, 69 insertions(+), 85 deletions(-) diff --git a/src/common/settings.ts b/src/common/settings.ts index c5908c8..de9065a 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -6,8 +6,7 @@ export interface ExtensionSettings { export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee', urlResolver: 'lbryInc' }; -export function getExtensionSettingsAsync(): Promise -{ +export function getExtensionSettingsAsync(): Promise { return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any))); } diff --git a/src/common/yt.ts b/src/common/yt.ts index 6d255e2..a0277ea 100644 --- a/src/common/yt.ts +++ b/src/common/yt.ts @@ -17,6 +17,11 @@ interface YtExportedJsonSubscription { }; } +export interface YtIdResolverDescriptor { + id: string + type: 'channel' | 'video' +} + /** * @param file to load * @returns a promise with the file as a string @@ -33,10 +38,58 @@ export function getFileContent(file: File): Promise { }); } -export interface YtIdResolverDescriptor { - id: string - type: 'channel' | 'video' -} +export const ytService = (() => { + /** + * Extracts the channelID from a YT URL. + * + * Handles these two types of YT URLs: + * * /feeds/videos.xml?channel_id=* + * * /channel/* + */ + function getChannelId(channelURL: string) { + const match = channelURL.match(/channel\/([^\s?]*)/); + return match ? match[1] : new URL(channelURL).searchParams.get('channel_id'); + } + + /** + * Reads the array of YT channels from an OPML file + * + * @param opmlContents an opml file as as tring + * @returns the channel IDs + */ + function 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) + .map(url => getChannelId(url)) + .filter((url): url is string => !!url); // we don't want it if it's empty + } + + /** + * Reads an array of YT channel IDs from the YT subscriptions JSON file + * + * @param jsonContents a JSON file as a string + * @returns the channel IDs + */ + function readJson(jsonContents: string): string[] { + const subscriptions: YtExportedJsonSubscription[] = 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 + */ + function readCsv(csvContent: string): string[] { + const rows = csvContent.split('\n') + csvContent = '' + return rows.slice(1).map((row) => row.substring(0, row.indexOf(','))) + } const URLResolverCache = (() => { @@ -98,80 +151,12 @@ const URLResolverCache = (() => return { put, get } })() -export const ytService = { - /** - * Reads the array of YT channels from an OPML file - * - * @param opmlContents an opml file as as tring - * @returns the channel IDs - */ - 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) - .map(url => ytService.getChannelId(url)) - .filter((url): url is string => !!url); // we don't want it if it's empty - }, - - /** - * Reads an array of YT channel IDs from the YT subscriptions JSON file - * - * @param jsonContents a JSON file as a string - * @returns the channel IDs - */ - readJson(jsonContents: string): string[] { - const subscriptions: YtExportedJsonSubscription[] = 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.slice(1).map((row) => row.substring(0, row.indexOf(','))) - }, - - /** - * Extracts the channelID from a YT URL. - * - * Handles these two types of YT URLs: - * * /feeds/videos.xml?channel_id=* - * * /channel/* - */ - getChannelId(channelURL: string) { - const match = channelURL.match(/channel\/([^\s?]*)/); - return match ? match[1] : new URL(channelURL).searchParams.get('channel_id'); - }, - - /** Extracts the video ID from a YT URL */ - getVideoId(url: string) { - const regex = /watch\/?\?.*v=([^\s&]*)/; - const match = url.match(regex); - return match ? match[1] : null; // match[1] is the videoId - }, - - getId(url: string): YtIdResolverDescriptor | null { - const videoId = ytService.getVideoId(url); - if (videoId) return { id: videoId, type: 'video' }; - const channelId = ytService.getChannelId(url); - if (channelId) return { id: channelId, type: 'channel' }; - return null; - }, - /** * @param descriptorsWithIndex YT resource IDs to check * @returns a promise with the list of channels that were found on lbry */ - async resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> { + async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> { const descriptorsWithIndex: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({...descriptor, index})) descriptors = null as any const results: (string | null)[] = [] @@ -196,7 +181,7 @@ export const ytService = { { const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any; - const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync('urlResolver') + const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] const url = new URL(`https://${urlResolverSetting.hostname}`); @@ -288,5 +273,7 @@ export const ytService = { })); return results - } -} + } + + return { readCsv, readJson, readOpml, resolveById } +})() diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index f1fd9b1..6fb60c3 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -155,12 +155,7 @@ function redirectTo({ targetPlatform, lbryPathname }: UpdateContext): void { location.replace(url.toString()); } - - -findButtonMountPoint().then(() => updateButton(ctxCache)) -findVideoElement().then(() => updateButton(ctxCache)) - -async function onPageLoad() +window.addEventListener('load', async () => { // Listen History.pushState { @@ -193,4 +188,7 @@ async function onPageLoad() } await updateByURL(new URL(location.href)) -} \ No newline at end of file + + findButtonMountPoint().then(() => updateButton(ctxCache)) + findVideoElement().then(() => updateButton(ctxCache)) +}) \ No newline at end of file From 7727d0415745eddd3a8d4cced823721f204e7cff Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:59:41 +0000 Subject: [PATCH 05/20] =?UTF-8?q?=F0=9F=A5=A1=20Refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/yt.ts | 90 ++++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 49 deletions(-) diff --git a/src/common/yt.ts b/src/common/yt.ts index a0277ea..63dcabe 100644 --- a/src/common/yt.ts +++ b/src/common/yt.ts @@ -91,65 +91,57 @@ export const ytService = (() => { return rows.slice(1).map((row) => row.substring(0, row.indexOf(','))) } -const URLResolverCache = (() => -{ - const openRequest = indexedDB.open("yt-url-resolver-cache") - - if (typeof self.indexedDB !== 'undefined') + const URLResolverCache = (() => { - openRequest.addEventListener('upgradeneeded', () => - { - const db = openRequest.result - const store = db.createObjectStore("store") - store.createIndex("expireAt", "expireAt") - }) + const openRequest = self.indexedDB?.open("yt-url-resolver-cache") - // Delete Expired - openRequest.addEventListener('success', () => + if (openRequest) { - const db = openRequest.result - const transaction = db.transaction("store", "readwrite") - const range = IDBKeyRange.upperBound(new Date()) + openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt")) - const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range) - expireAtCursorRequest.addEventListener('success', () => + // Delete Expired + openRequest.addEventListener('success', () => { - const expireCursor = expireAtCursorRequest.result - if (!expireCursor) return - expireCursor.delete() - expireCursor.continue() + const transaction = openRequest.result.transaction("store", "readwrite") + const range = IDBKeyRange.upperBound(new Date()) + + const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range) + expireAtCursorRequest.addEventListener('success', () => + { + const expireCursor = expireAtCursorRequest.result + if (!expireCursor) return + expireCursor.delete() + expireCursor.continue() + }) }) - }) - } - else console.warn(`IndexedDB not supported`) + } + else console.warn(`IndexedDB not supported`) - async function put(url: string | null, id: string) : Promise - { - return await new Promise((resolve, reject) => + async function put(url: string | null, id: string): Promise { - const db = openRequest.result - if (!db) return resolve() - const store = db.transaction("store", "readwrite").objectStore("store") - const putRequest = store.put({ value: url, expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, id) - putRequest.addEventListener('success', () => resolve()) - putRequest.addEventListener('error', () => reject(putRequest.error)) - }) - } - async function get(id: string): Promise - { - return (await new Promise((resolve, reject) => + return await new Promise((resolve, reject) => + { + const store = openRequest.result.transaction("store", "readwrite").objectStore("store") + if (!store) return resolve() + const request = store.put({ value: url, expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, id) + request.addEventListener('success', () => resolve()) + request.addEventListener('error', () => reject(request.error)) + }) + } + async function get(id: string): Promise { - const db = openRequest.result - if (!db) return resolve(null) - const store = db.transaction("store", "readonly").objectStore("store") - const getRequest = store.get(id) - getRequest.addEventListener('success', () => resolve(getRequest.result)) - getRequest.addEventListener('error', () => reject(getRequest.error)) - }) as any)?.value - } + return (await new Promise((resolve, reject) => + { + const store = openRequest.result.transaction("store", "readonly").objectStore("store") + if (!store) return resolve(null) + const request = store.get(id) + request.addEventListener('success', () => resolve(request.result)) + request.addEventListener('error', () => reject(request.error)) + }) as any)?.value + } - return { put, get } -})() + return { put, get } + })() /** From 75cb9cf01d986d65b2257eecd2b8382c3cf7b934 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 19:01:01 +0000 Subject: [PATCH 06/20] =?UTF-8?q?=F0=9F=8D=98=20Refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/{yt.ts => yt/index.ts} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/common/{yt.ts => yt/index.ts} (98%) diff --git a/src/common/yt.ts b/src/common/yt/index.ts similarity index 98% rename from src/common/yt.ts rename to src/common/yt/index.ts index 63dcabe..de868ee 100644 --- a/src/common/yt.ts +++ b/src/common/yt/index.ts @@ -1,6 +1,6 @@ -import chunk from 'lodash/chunk'; -import groupBy from 'lodash/groupBy'; -import { getExtensionSettingsAsync, Keys, SingleValueAtATime, Values, YtUrlResolveFunction, YTUrlResolver, YtUrlResolveResponsePath, ytUrlResolversSettings } from './settings' +import chunk from 'lodash/chunk' +import groupBy from 'lodash/groupBy' +import { getExtensionSettingsAsync, Keys, SingleValueAtATime, Values, YtUrlResolveFunction, YtUrlResolveResponsePath, ytUrlResolversSettings } from '../settings' // const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS const QUERY_CHUNK_SIZE = 300; From cb4b4f4b2eb9e85f9b83969e6dad86374700aec7 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 19:10:27 +0000 Subject: [PATCH 07/20] =?UTF-8?q?=F0=9F=A5=9E=20Refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/yt/index.ts | 401 +++++++++++++++++--------------------- src/common/yt/urlCache.ts | 50 +++++ src/scripts/ytContent.tsx | 8 +- src/tools/YTtoLBRY.tsx | 10 +- 4 files changed, 239 insertions(+), 230 deletions(-) create mode 100644 src/common/yt/urlCache.ts diff --git a/src/common/yt/index.ts b/src/common/yt/index.ts index de868ee..d7ca1da 100644 --- a/src/common/yt/index.ts +++ b/src/common/yt/index.ts @@ -1,271 +1,230 @@ import chunk from 'lodash/chunk' import groupBy from 'lodash/groupBy' import { getExtensionSettingsAsync, Keys, SingleValueAtATime, Values, YtUrlResolveFunction, YtUrlResolveResponsePath, ytUrlResolversSettings } from '../settings' +import { LbryURLCache } from './urlCache' // const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS -const QUERY_CHUNK_SIZE = 300; +const QUERY_CHUNK_SIZE = 300 -interface YtExportedJsonSubscription { - id: string; - etag: string; - title: string; - snippet: { - description: string; - resourceId: { - channelId: string; - }; - }; +interface YtExportedJsonSubscription +{ + id: string + etag: string + title: string + snippet: { + description: string + resourceId: { + channelId: string + } + } } -export interface YtIdResolverDescriptor { - id: string - type: 'channel' | 'video' +export interface YtIdResolverDescriptor +{ + id: string + type: 'channel' | 'video' } /** * @param file to load * @returns a promise with the file as a string */ -export function getFileContent(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.addEventListener('load', event => resolve(event.target?.result as string || '')); - reader.addEventListener('error', () => { - reader.abort(); - reject(new DOMException(`Could not read ${file.name}`)); - }); - reader.readAsText(file); - }); +export function getFileContent(file: File): Promise +{ + return new Promise((resolve, reject) => + { + const reader = new FileReader() + reader.addEventListener('load', event => resolve(event.target?.result as string || '')) + reader.addEventListener('error', () => + { + reader.abort() + reject(new DOMException(`Could not read ${file.name}`)) + }) + reader.readAsText(file) + }) } -export const ytService = (() => { - /** - * Extracts the channelID from a YT URL. - * - * Handles these two types of YT URLs: - * * /feeds/videos.xml?channel_id=* - * * /channel/* - */ - function getChannelId(channelURL: string) { - const match = channelURL.match(/channel\/([^\s?]*)/); - return match ? match[1] : new URL(channelURL).searchParams.get('channel_id'); - } +/** + * Extracts the channelID from a YT URL. + * + * Handles these two types of YT URLs: + * * /feeds/videos.xml?channel_id=* + * * /channel/* + */ +export function getChannelId(channelURL: string) +{ + const match = channelURL.match(/channel\/([^\s?]*)/) + return match ? match[1] : new URL(channelURL).searchParams.get('channel_id') +} - /** - * Reads the array of YT channels from an OPML file - * - * @param opmlContents an opml file as as tring - * @returns the channel IDs - */ - function readOpml(opmlContents: string): string[] { - const opml = new DOMParser().parseFromString(opmlContents, 'application/xml'); +/** + * Reads the array of YT channels from an OPML file + * + * @param opmlContents an opml file as as tring + * @returns the channel IDs + */ +export function getSubsFromOpml(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) - .map(url => getChannelId(url)) - .filter((url): url is string => !!url); // we don't want it if it's empty - } + .map(outline => outline.getAttribute('xmlUrl')) + .filter((url): url is string => !!url) + .map(url => getChannelId(url)) + .filter((url): url is string => !!url) // we don't want it if it's empty +} - /** - * Reads an array of YT channel IDs from the YT subscriptions JSON file - * - * @param jsonContents a JSON file as a string - * @returns the channel IDs - */ - function readJson(jsonContents: string): string[] { - const subscriptions: YtExportedJsonSubscription[] = JSON.parse(jsonContents); +/** + * Reads an array of YT channel IDs from the YT subscriptions JSON file + * + * @param jsonContents a JSON file as a string + * @returns the channel IDs + */ +export function getSubsFromJson(jsonContents: string): string[] +{ + const subscriptions: YtExportedJsonSubscription[] = JSON.parse(jsonContents) jsonContents = '' - return subscriptions.map(sub => sub.snippet.resourceId.channelId); - } + 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 - */ - function readCsv(csvContent: string): string[] { +/** + * 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 + */ +export function getSubsFromCsv(csvContent: string): string[] +{ const rows = csvContent.split('\n') csvContent = '' return rows.slice(1).map((row) => row.substring(0, row.indexOf(','))) - } +} - const URLResolverCache = (() => - { - const openRequest = self.indexedDB?.open("yt-url-resolver-cache") - - if (openRequest) - { - openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt")) - - // Delete Expired - openRequest.addEventListener('success', () => - { - const transaction = openRequest.result.transaction("store", "readwrite") - const range = IDBKeyRange.upperBound(new Date()) - - const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range) - expireAtCursorRequest.addEventListener('success', () => - { - const expireCursor = expireAtCursorRequest.result - if (!expireCursor) return - expireCursor.delete() - expireCursor.continue() - }) - }) - } - else console.warn(`IndexedDB not supported`) - - async function put(url: string | null, id: string): Promise - { - return await new Promise((resolve, reject) => - { - const store = openRequest.result.transaction("store", "readwrite").objectStore("store") - if (!store) return resolve() - const request = store.put({ value: url, expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, id) - request.addEventListener('success', () => resolve()) - request.addEventListener('error', () => reject(request.error)) - }) - } - async function get(id: string): Promise - { - return (await new Promise((resolve, reject) => - { - const store = openRequest.result.transaction("store", "readonly").objectStore("store") - if (!store) return resolve(null) - const request = store.get(id) - request.addEventListener('success', () => resolve(request.result)) - request.addEventListener('error', () => reject(request.error)) - }) as any)?.value - } - - return { put, get } - })() - - - /** - * @param descriptorsWithIndex YT resource IDs to check - * @returns a promise with the list of channels that were found on lbry - */ - async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> { - const descriptorsWithIndex: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({...descriptor, index})) +/** +* @param descriptorsWithIndex YT resource IDs to check +* @returns a promise with the list of channels that were found on lbry +*/ +export async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> +{ + const descriptorsWithIndex: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({ ...descriptor, index })) descriptors = null as any const results: (string | null)[] = [] - await Promise.all(descriptorsWithIndex.map(async (descriptor, index) => { - if (!descriptor) return - const cache = await URLResolverCache.get(descriptor.id) + await Promise.all(descriptorsWithIndex.map(async (descriptor, index) => + { + if (!descriptor) return + const cache = await LbryURLCache.get(descriptor.id) - // Cache can be null, if there is no lbry url yet - if (cache !== undefined) { - // Directly setting it to results - results[index] = cache + // Cache can be null, if there is no lbry url yet + if (cache !== undefined) + { + // Directly setting it to results + results[index] = cache - // We remove it so we dont ask it to API - descriptorsWithIndex.splice(index, 1) - } + // We remove it so we dont ask it to API + descriptorsWithIndex.splice(index, 1) + } })) - const descriptorsChunks = chunk(descriptorsWithIndex, QUERY_CHUNK_SIZE); - let progressCount = 0; + const descriptorsChunks = chunk(descriptorsWithIndex, QUERY_CHUNK_SIZE) + let progressCount = 0 await Promise.all(descriptorsChunks.map(async (descriptorChunk) => { - const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any; + const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any - const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() - const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] + const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() + const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] - const url = new URL(`https://${urlResolverSetting.hostname}`); + const url = new URL(`https://${urlResolverSetting.hostname}`) - function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) - { - for (const path of responsePath) + function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) { - switch (typeof path) - { - case 'string': - case 'number': - response = response[path] - break - default: - switch (path) - { - case Keys: - response = Object.keys(response) - break - case Values: - response = Object.values(response) - break - } - } - } - return response as T - } - - async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsWithIndex) - { - url.pathname = urlResolverFunction.pathname - - if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) - { - await Promise.all(descriptorsGroup.map(async (descriptor) => { - switch (null) + for (const path of responsePath) { - default: - if (!descriptor.id) break - url.searchParams.set(urlResolverFunction.paramName, descriptor.id) - - const apiResponse = await fetch(url.toString(), { cache: 'no-store' }); - if (!apiResponse.ok) { - // Some API might not respond with 200 if it can't find the url - if (apiResponse.status === 404) await URLResolverCache.put(null, descriptor.id) - break - } - - const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) - if (value) results[descriptor.index] = value - await URLResolverCache.put(value, descriptor.id) + switch (typeof path) + { + case 'string': + case 'number': + response = response[path] + break + default: + switch (path) + { + case Keys: + response = Object.keys(response) + break + case Values: + response = Object.values(response) + break + } + } } - progressCount++ - if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) - })) + return response as T } - else + + async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsWithIndex) { + url.pathname = urlResolverFunction.pathname - switch (null) - { - default: - url.searchParams - .set(urlResolverFunction.paramName, descriptorsGroup - .map((descriptor) => descriptor.id) - .filter((descriptorId) => descriptorId) - .join(urlResolverFunction.paramArraySeperator) - ) + if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) + { + await Promise.all(descriptorsGroup.map(async (descriptor) => + { + switch (null) + { + default: + if (!descriptor.id) break + url.searchParams.set(urlResolverFunction.paramName, descriptor.id) - const apiResponse = await fetch(url.toString(), { cache: 'no-store' }); - if (!apiResponse.ok) break - const values = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) + const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) + if (!apiResponse.ok) + { + // Some API might not respond with 200 if it can't find the url + if (apiResponse.status === 404) await LbryURLCache.put(null, descriptor.id) + break + } - await Promise.all(values.map(async (value, index) => { - const descriptor = descriptorsGroup[index] - if (value) results[descriptor.index] = value - await URLResolverCache.put(value, descriptor.id) - })) - } - progressCount += descriptorsGroup.length - if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) + const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) + if (value) results[descriptor.index] = value + await LbryURLCache.put(value, descriptor.id) + } + progressCount++ + if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) + })) + } + else + { + + switch (null) + { + default: + url.searchParams + .set(urlResolverFunction.paramName, descriptorsGroup + .map((descriptor) => descriptor.id) + .filter((descriptorId) => descriptorId) + .join(urlResolverFunction.paramArraySeperator) + ) + + const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) + if (!apiResponse.ok) break + const values = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) + + await Promise.all(values.map(async (value, index) => + { + const descriptor = descriptorsGroup[index] + if (value) results[descriptor.index] = value + await LbryURLCache.put(value, descriptor.id) + })) + } + progressCount += descriptorsGroup.length + if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) + } } - } - if (descriptorsGroupedByType['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel']) - if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video']) + if (descriptorsGroupedByType['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel']) + if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video']) + + })) - })); - return results - } - - return { readCsv, readJson, readOpml, resolveById } -})() +} \ No newline at end of file diff --git a/src/common/yt/urlCache.ts b/src/common/yt/urlCache.ts new file mode 100644 index 0000000..b598340 --- /dev/null +++ b/src/common/yt/urlCache.ts @@ -0,0 +1,50 @@ + +const openRequest = self.indexedDB?.open("yt-url-resolver-cache") + +if (openRequest) +{ + openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt")) + + // Delete Expired + openRequest.addEventListener('success', () => + { + const transaction = openRequest.result.transaction("store", "readwrite") + const range = IDBKeyRange.upperBound(new Date()) + + const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range) + expireAtCursorRequest.addEventListener('success', () => + { + const expireCursor = expireAtCursorRequest.result + if (!expireCursor) return + expireCursor.delete() + expireCursor.continue() + }) + }) +} +else console.warn(`IndexedDB not supported`) + +async function put(url: string | null, id: string): Promise +{ + return await new Promise((resolve, reject) => + { + const store = openRequest.result.transaction("store", "readwrite").objectStore("store") + if (!store) return resolve() + const request = store.put({ value: url, expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, id) + request.addEventListener('success', () => resolve()) + request.addEventListener('error', () => reject(request.error)) + }) +} +async function get(id: string): Promise +{ + return (await new Promise((resolve, reject) => + { + const store = openRequest.result.transaction("store", "readonly").objectStore("store") + if (!store) return resolve(null) + const request = store.get(id) + request.addEventListener('success', () => resolve(request.result)) + request.addEventListener('error', () => reject(request.error)) + }) as any)?.value +} + +export const LbryURLCache = { put, get } + diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index 6fb60c3..7ae21a5 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -1,7 +1,7 @@ -import { ExtensionSettings, getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatformName, targetPlatformSettings } from '../common/settings' -import type { UpdateContext } from '../scripts/tabOnUpdated' import { h, JSX, render } from 'preact' -import { YtIdResolverDescriptor, ytService } from '../common/yt' +import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatformName, targetPlatformSettings } from '../common/settings' +import { resolveById, YtIdResolverDescriptor } from '../common/yt' +import type { UpdateContext } from '../scripts/tabOnUpdated' const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)); @@ -177,7 +177,7 @@ window.addEventListener('load', async () => const videoId = url.searchParams.get('v') if (!videoId) return const descriptor: YtIdResolverDescriptor = { id: videoId, type: 'video' } - const lbryPathname = (await ytService.resolveById([descriptor]))[0] + const lbryPathname = (await resolveById([descriptor]))[0] if (!lbryPathname) return updateButton({ descriptor, lbryPathname, redirect: settings.redirect, targetPlatform: settings.targetPlatform }) } diff --git a/src/tools/YTtoLBRY.tsx b/src/tools/YTtoLBRY.tsx index ac38427..7ebbe01 100644 --- a/src/tools/YTtoLBRY.tsx +++ b/src/tools/YTtoLBRY.tsx @@ -1,7 +1,7 @@ import { h, render } from 'preact' import { useState } from 'preact/hooks' import { getExtensionSettingsAsync, targetPlatformSettings } from '../common/settings' -import { getFileContent, ytService } from '../common/yt' +import { getFileContent, getSubsFromCsv, getSubsFromJson, getSubsFromOpml, resolveById } from '../common/yt' import readme from './README.md' @@ -16,10 +16,10 @@ async function lbryChannelsFromFile(file: File) { const ext = file.name.split('.').pop()?.toLowerCase(); const ids = new Set(( - ext === 'xml' || ext == 'opml' ? ytService.readOpml : - ext === 'csv' ? ytService.readCsv : - ytService.readJson)(await getFileContent(file))) - const lbryUrls = await ytService.resolveById( + ext === 'xml' || ext == 'opml' ? getSubsFromOpml : + ext === 'csv' ? getSubsFromCsv : + getSubsFromJson)(await getFileContent(file))) + const lbryUrls = await resolveById( Array.from(ids).map(id => ({ id, type: 'channel' } as const)), (progress) => render(, document.getElementById('root')!)); const { targetPlatform: platform } = await getExtensionSettingsAsync(); From 4f8e807a65000b2322c2c62193384e4f541b2bb6 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:04:03 +0000 Subject: [PATCH 08/20] =?UTF-8?q?=F0=9F=8D=B1=20Refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/settings.ts | 91 ++++++++++++------ src/common/yt/index.ts | 33 ++++++- src/common/yt/urlCache.ts | 20 ++-- src/manifest.json | 2 +- src/scripts/background.ts | 22 +++++ src/scripts/tabOnUpdated.ts | 9 -- src/scripts/ytContent.tsx | 187 +++++++++++++----------------------- src/tools/YTtoLBRY.tsx | 4 +- 8 files changed, 198 insertions(+), 170 deletions(-) create mode 100644 src/scripts/background.ts delete mode 100644 src/scripts/tabOnUpdated.ts diff --git a/src/common/settings.ts b/src/common/settings.ts index de9065a..c69c832 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -1,50 +1,81 @@ -export interface ExtensionSettings { +import { JSX } from "preact" + +export interface ExtensionSettings +{ redirect: boolean targetPlatform: TargetPlatformName 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 { - return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any))); +export function getExtensionSettingsAsync(): Promise +{ + return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any))) } - -export type TargetPlatformName = 'madiator.com' | 'odysee' | 'app' -export interface TargetPlatformSettings { +export type TargetPlatformName = 'madiator.com' | 'odysee' | 'app' +export interface TargetPlatform +{ domainPrefix: string displayName: string theme: string + button: { + text: string + icon: string + style?: + { + icon?: JSX.CSSProperties + button?: JSX.CSSProperties + } + } } -export const targetPlatformSettings: Record = { - 'madiator.com': { - domainPrefix: 'https://madiator.com/', - displayName: 'Madiator.com', - theme: '#075656' +export const targetPlatformSettings: Record = { + 'madiator.com': { + domainPrefix: 'https://madiator.com/', + displayName: 'Madiator.com', + 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: { - domainPrefix: 'https://odysee.com/', - displayName: 'Odysee', - theme: '#1e013b' + odysee: { + domainPrefix: 'https://odysee.com/', + displayName: 'Odysee', + theme: '#1e013b', + button: { + text: 'Watch on Odysee', + icon: chrome.runtime.getURL('icons/lbry/odysee-logo.svg') + } }, - app: { - domainPrefix: 'lbry://', - displayName: 'LBRY App', - theme: '#075656' + app: { + domainPrefix: 'lbry://', + displayName: 'LBRY App', + theme: '#075656', + button: { + text: 'Watch on LBRY', + icon: chrome.runtime.getURL('icons/lbry/lbry-logo.svg') + } }, -}; +} -export const getTargetPlatfromSettingsEntiries = () => { - return Object.entries(targetPlatformSettings) as any as [Extract, TargetPlatformSettings][] +export const getTargetPlatfromSettingsEntiries = () => +{ + return Object.entries(targetPlatformSettings) as any as [Extract, TargetPlatform][] } export type SourcePlatfromName = 'youtube.com' | 'yewtu.be' -export interface SourcePlatfromSettings { +export interface SourcePlatfrom +{ hostnames: string[] htmlQueries: { mountButtonBefore: string, @@ -52,7 +83,7 @@ export interface SourcePlatfromSettings { } } -export const sourcePlatfromSettings: Record = { +export const sourcePlatfromSettings: Record = { "yewtu.be": { hostnames: ['yewtu.be'], htmlQueries: { @@ -69,7 +100,8 @@ export const sourcePlatfromSettings: Record = name: "LBRY Inc.", hostname: "api.odysee.com", functions: { - getChannelId : { + getChannelId: { pathname: "/yt/resolve", paramName: "channel_ids", paramArraySeperator: ',', @@ -139,6 +171,7 @@ export const ytUrlResolversSettings: Record = } } -export const getYtUrlResolversSettingsEntiries = () => { +export const getYtUrlResolversSettingsEntiries = () => +{ return Object.entries(ytUrlResolversSettings) as any as [Extract, YTUrlResolver][] } \ No newline at end of file diff --git a/src/common/yt/index.ts b/src/common/yt/index.ts index d7ca1da..31f0440 100644 --- a/src/common/yt/index.ts +++ b/src/common/yt/index.ts @@ -1,7 +1,7 @@ import chunk from 'lodash/chunk' import groupBy from 'lodash/groupBy' 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 QUERY_CHUNK_SIZE = 300 @@ -57,6 +57,29 @@ export function getChannelId(channelURL: string) 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 * @@ -113,7 +136,7 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres await Promise.all(descriptorsWithIndex.map(async (descriptor, index) => { 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 if (cache !== undefined) @@ -180,13 +203,13 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres if (!apiResponse.ok) { // 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 } const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) if (value) results[descriptor.index] = value - await LbryURLCache.put(value, descriptor.id) + await LbryPathnameCache.put(value, descriptor.id) } progressCount++ if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) @@ -213,7 +236,7 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres { const descriptor = descriptorsGroup[index] if (value) results[descriptor.index] = value - await LbryURLCache.put(value, descriptor.id) + await LbryPathnameCache.put(value, descriptor.id) })) } progressCount += descriptorsGroup.length diff --git a/src/common/yt/urlCache.ts b/src/common/yt/urlCache.ts index b598340..8b3b6f6 100644 --- a/src/common/yt/urlCache.ts +++ b/src/common/yt/urlCache.ts @@ -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")) // Delete Expired 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 expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range) @@ -23,22 +29,24 @@ if (openRequest) } else console.warn(`IndexedDB not supported`) + async function put(url: string | null, id: string): Promise { 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() const request = store.put({ value: url, expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, id) request.addEventListener('success', () => resolve()) request.addEventListener('error', () => reject(request.error)) }) } + async function get(id: string): Promise { 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) const request = store.get(id) request.addEventListener('success', () => resolve(request.result)) @@ -46,5 +54,5 @@ async function get(id: string): Promise }) as any)?.value } -export const LbryURLCache = { put, get } +export const LbryPathnameCache = { put, get } diff --git a/src/manifest.json b/src/manifest.json index 15df083..c34f56f 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -25,7 +25,7 @@ "background": { "scripts": [ "scripts/storageSetup.js", - "scripts/tabOnUpdated.js" + "scripts/background.js" ], "persistent": false }, diff --git a/src/scripts/background.ts b/src/scripts/background.ts new file mode 100644 index 0000000..a86ef0e --- /dev/null +++ b/src/scripts/background.ts @@ -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> = {} +async function lbryPathnameFromVideoId(videoId: string): Promise { + // 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; +}) \ No newline at end of file diff --git a/src/scripts/tabOnUpdated.ts b/src/scripts/tabOnUpdated.ts deleted file mode 100644 index 17e1bb5..0000000 --- a/src/scripts/tabOnUpdated.ts +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index 7ae21a5..d5bcffa 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -1,55 +1,30 @@ -import { h, JSX, render } from 'preact' -import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatformName, targetPlatformSettings } from '../common/settings' -import { resolveById, YtIdResolverDescriptor } from '../common/yt' -import type { UpdateContext } from '../scripts/tabOnUpdated' +import { h, render } from 'preact' +import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatform, targetPlatformSettings } from '../common/settings' +import { parseYouTubeURLTimeString } from '../common/yt' const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)); function pauseAllVideos() { document.querySelectorAll('video').forEach(v => v.pause()); } -interface ButtonSettings { - text: string - icon: string - style?: - { - icon?: JSX.CSSProperties - button?: JSX.CSSProperties - } -} - -const buttonSettings: Record = { - 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 +interface WatchOnLbryButtonParameters { - targetPlatform?: TargetPlatformName + targetPlatform?: TargetPlatform lbryPathname?: string time?: number } -export function WatchOnLbryButton({ targetPlatform = 'app', lbryPathname, time }: ButtonParameters = {}) { - if (!lbryPathname || !targetPlatform) return null; - const targetPlatformSetting = targetPlatformSettings[targetPlatform]; - const buttonSetting = buttonSettings[targetPlatform]; +interface Target +{ + platfrom: TargetPlatform + lbryPathname: string + time: number | null +} - const url = new URL(`${targetPlatformSetting.domainPrefix}${lbryPathname}`) - if (time) url.searchParams.append('t', time.toFixed(0)) +export 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 ; } -let mountPoint: HTMLDivElement | null = null +function updateButton(mountPoint: HTMLDivElement, target: Target | null): void { + if (!target) return render(, mountPoint) + render(, 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 */ -async function findButtonMountPoint(): Promise { +async function findButtonMountPoint(): Promise { let mountBefore: HTMLDivElement | null = null const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname) if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`) @@ -88,82 +83,24 @@ async function findButtonMountPoint(): Promise { div.style.display = 'flex'; div.style.alignItems = 'center' mountBefore.parentElement?.insertBefore(div, mountBefore) - mountPoint = div + + return div } -let videoElement: HTMLVideoElement | null = null; 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) - videoElement.addEventListener('timeupdate', () => updateButton(ctxCache)) -} - -/** 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(, 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(, 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()); + return videoElement } window.addEventListener('load', async () => { - // Listen History.pushState - { - const originalPushState = history.pushState - history.pushState = function(...params) { onPushState(); return originalPushState(...params) } - } - const settings = await getExtensionSettingsAsync() + const [buttonMountPoint, videoElement] = await Promise.all([findButtonMountPoint(), findVideoElement()]) // Listen Settings Change chrome.storage.onChanged.addListener(async (changes, areaName) => { @@ -171,24 +108,38 @@ window.addEventListener('load', async () => 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((resolve) => chrome.runtime.sendMessage({ videoId }, resolve)) + + let target: Target | null = null async function updateByURL(url: URL) { if (url.pathname !== '/watch') return + const videoId = url.searchParams.get('v') if (!videoId) return - const descriptor: YtIdResolverDescriptor = { id: videoId, type: 'video' } - const lbryPathname = (await resolveById([descriptor]))[0] + const lbryPathname = await requestLbryPathname(videoId) 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)) - - findButtonMountPoint().then(() => updateButton(ctxCache)) - findVideoElement().then(() => updateButton(ctxCache)) }) \ No newline at end of file diff --git a/src/tools/YTtoLBRY.tsx b/src/tools/YTtoLBRY.tsx index 7ebbe01..ab120a1 100644 --- a/src/tools/YTtoLBRY.tsx +++ b/src/tools/YTtoLBRY.tsx @@ -19,12 +19,12 @@ async function lbryChannelsFromFile(file: File) { ext === 'xml' || ext == 'opml' ? getSubsFromOpml : ext === 'csv' ? getSubsFromCsv : getSubsFromJson)(await getFileContent(file))) - const lbryUrls = await resolveById( + const lbryPathnames = await resolveById( Array.from(ids).map(id => ({ id, type: 'channel' } as const)), (progress) => render(, document.getElementById('root')!)); const { targetPlatform: platform } = await getExtensionSettingsAsync(); 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, progress: number }) { From 2b914369003c6af26c8bc012481c859fbf7f8fa5 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:11:58 +0000 Subject: [PATCH 09/20] =?UTF-8?q?=F0=9F=A5=A1=20Refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/yt/index.ts | 229 +++++++----------------------------- src/common/yt/urlResolve.ts | 141 ++++++++++++++++++++++ src/scripts/background.ts | 2 +- src/tools/YTtoLBRY.tsx | 3 +- 4 files changed, 188 insertions(+), 187 deletions(-) create mode 100644 src/common/yt/urlResolve.ts diff --git a/src/common/yt/index.ts b/src/common/yt/index.ts index 31f0440..58a7859 100644 --- a/src/common/yt/index.ts +++ b/src/common/yt/index.ts @@ -1,10 +1,3 @@ -import chunk from 'lodash/chunk' -import groupBy from 'lodash/groupBy' -import { getExtensionSettingsAsync, Keys, SingleValueAtATime, Values, YtUrlResolveFunction, YtUrlResolveResponsePath, ytUrlResolversSettings } from '../settings' -import { LbryPathnameCache } from './urlCache' - -// const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS -const QUERY_CHUNK_SIZE = 300 interface YtExportedJsonSubscription { @@ -19,11 +12,6 @@ interface YtExportedJsonSubscription } } -export interface YtIdResolverDescriptor -{ - id: string - type: 'channel' | 'video' -} /** * @param file to load @@ -44,6 +32,50 @@ export function getFileContent(file: File): Promise }) } + +/** + * Reads the array of YT channels from an OPML file + * + * @param opmlContents an opml file as as tring + * @returns the channel IDs + */ + export function getSubsFromOpml(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) + .map(url => getChannelId(url)) + .filter((url): url is string => !!url) // we don't want it if it's empty + } + + /** + * Reads an array of YT channel IDs from the YT subscriptions JSON file + * + * @param jsonContents a JSON file as a string + * @returns the channel IDs + */ + export function getSubsFromJson(jsonContents: string): string[] + { + const subscriptions: YtExportedJsonSubscription[] = 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 + */ + export function getSubsFromCsv(csvContent: string): string[] + { + const rows = csvContent.split('\n') + csvContent = '' + return rows.slice(1).map((row) => row.substring(0, row.indexOf(','))) + } + /** * Extracts the channelID from a YT URL. * @@ -77,177 +109,4 @@ export function parseYouTubeURLTimeString(timeString: string) total += t } return total.toString() -} - - -/** - * Reads the array of YT channels from an OPML file - * - * @param opmlContents an opml file as as tring - * @returns the channel IDs - */ -export function getSubsFromOpml(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) - .map(url => getChannelId(url)) - .filter((url): url is string => !!url) // we don't want it if it's empty -} - -/** - * Reads an array of YT channel IDs from the YT subscriptions JSON file - * - * @param jsonContents a JSON file as a string - * @returns the channel IDs - */ -export function getSubsFromJson(jsonContents: string): string[] -{ - const subscriptions: YtExportedJsonSubscription[] = 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 - */ -export function getSubsFromCsv(csvContent: string): string[] -{ - const rows = csvContent.split('\n') - csvContent = '' - return rows.slice(1).map((row) => row.substring(0, row.indexOf(','))) -} - -/** -* @param descriptorsWithIndex YT resource IDs to check -* @returns a promise with the list of channels that were found on lbry -*/ -export async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> -{ - const descriptorsWithIndex: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({ ...descriptor, index })) - descriptors = null as any - const results: (string | null)[] = [] - - await Promise.all(descriptorsWithIndex.map(async (descriptor, index) => - { - if (!descriptor) return - const cache = await LbryPathnameCache.get(descriptor.id) - - // Cache can be null, if there is no lbry url yet - if (cache !== undefined) - { - // Directly setting it to results - results[index] = cache - - // We remove it so we dont ask it to API - descriptorsWithIndex.splice(index, 1) - } - })) - - const descriptorsChunks = chunk(descriptorsWithIndex, QUERY_CHUNK_SIZE) - let progressCount = 0 - await Promise.all(descriptorsChunks.map(async (descriptorChunk) => - { - const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any - - const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() - const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] - - const url = new URL(`https://${urlResolverSetting.hostname}`) - - function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) - { - for (const path of responsePath) - { - switch (typeof path) - { - case 'string': - case 'number': - response = response[path] - break - default: - switch (path) - { - case Keys: - response = Object.keys(response) - break - case Values: - response = Object.values(response) - break - } - } - } - return response as T - } - - async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsWithIndex) - { - url.pathname = urlResolverFunction.pathname - - if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) - { - await Promise.all(descriptorsGroup.map(async (descriptor) => - { - switch (null) - { - default: - if (!descriptor.id) break - url.searchParams.set(urlResolverFunction.paramName, descriptor.id) - - const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) - if (!apiResponse.ok) - { - // Some API might not respond with 200 if it can't find the url - if (apiResponse.status === 404) await LbryPathnameCache.put(null, descriptor.id) - break - } - - const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) - if (value) results[descriptor.index] = value - await LbryPathnameCache.put(value, descriptor.id) - } - progressCount++ - if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) - })) - } - else - { - - switch (null) - { - default: - url.searchParams - .set(urlResolverFunction.paramName, descriptorsGroup - .map((descriptor) => descriptor.id) - .filter((descriptorId) => descriptorId) - .join(urlResolverFunction.paramArraySeperator) - ) - - const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) - if (!apiResponse.ok) break - const values = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) - - await Promise.all(values.map(async (value, index) => - { - const descriptor = descriptorsGroup[index] - if (value) results[descriptor.index] = value - await LbryPathnameCache.put(value, descriptor.id) - })) - } - progressCount += descriptorsGroup.length - if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) - } - } - - if (descriptorsGroupedByType['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel']) - if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video']) - - })) - - return results } \ No newline at end of file diff --git a/src/common/yt/urlResolve.ts b/src/common/yt/urlResolve.ts new file mode 100644 index 0000000..26f45da --- /dev/null +++ b/src/common/yt/urlResolve.ts @@ -0,0 +1,141 @@ +import { chunk, groupBy } from "lodash" +import { getExtensionSettingsAsync, Keys, SingleValueAtATime, Values, YtUrlResolveFunction, YtUrlResolveResponsePath, ytUrlResolversSettings } from "../settings" +import { LbryPathnameCache } from "./urlCache" + +// const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS +const QUERY_CHUNK_SIZE = 300 + +export interface YtIdResolverDescriptor +{ + id: string + type: 'channel' | 'video' +} + + /** + * @param descriptorsWithIndex YT resource IDs to check + * @returns a promise with the list of channels that were found on lbry + */ + export async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> + { + const descriptorsWithIndex: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({ ...descriptor, index })) + descriptors = null as any + const results: (string | null)[] = [] + + await Promise.all(descriptorsWithIndex.map(async (descriptor, index) => + { + if (!descriptor) return + const cache = await LbryPathnameCache.get(descriptor.id) + + // Cache can be null, if there is no lbry url yet + if (cache !== undefined) + { + // Directly setting it to results + results[index] = cache + + // We remove it so we dont ask it to API + descriptorsWithIndex.splice(index, 1) + } + })) + + const descriptorsChunks = chunk(descriptorsWithIndex, QUERY_CHUNK_SIZE) + let progressCount = 0 + await Promise.all(descriptorsChunks.map(async (descriptorChunk) => + { + const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any + + const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() + const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] + + const url = new URL(`https://${urlResolverSetting.hostname}`) + + function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) + { + for (const path of responsePath) + { + switch (typeof path) + { + case 'string': + case 'number': + response = response[path] + break + default: + switch (path) + { + case Keys: + response = Object.keys(response) + break + case Values: + response = Object.values(response) + break + } + } + } + return response as T + } + + async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsWithIndex) + { + url.pathname = urlResolverFunction.pathname + + if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) + { + await Promise.all(descriptorsGroup.map(async (descriptor) => + { + switch (null) + { + default: + if (!descriptor.id) break + url.searchParams.set(urlResolverFunction.paramName, descriptor.id) + + const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) + if (!apiResponse.ok) + { + // Some API might not respond with 200 if it can't find the url + if (apiResponse.status === 404) await LbryPathnameCache.put(null, descriptor.id) + break + } + + const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) + if (value) results[descriptor.index] = value + await LbryPathnameCache.put(value, descriptor.id) + } + progressCount++ + if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) + })) + } + else + { + + switch (null) + { + default: + url.searchParams + .set(urlResolverFunction.paramName, descriptorsGroup + .map((descriptor) => descriptor.id) + .filter((descriptorId) => descriptorId) + .join(urlResolverFunction.paramArraySeperator) + ) + + const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) + if (!apiResponse.ok) break + const values = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) + + await Promise.all(values.map(async (value, index) => + { + const descriptor = descriptorsGroup[index] + if (value) results[descriptor.index] = value + await LbryPathnameCache.put(value, descriptor.id) + })) + } + progressCount += descriptorsGroup.length + if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) + } + } + + if (descriptorsGroupedByType['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel']) + if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video']) + + })) + + return results + } \ No newline at end of file diff --git a/src/scripts/background.ts b/src/scripts/background.ts index a86ef0e..591c7e2 100644 --- a/src/scripts/background.ts +++ b/src/scripts/background.ts @@ -1,5 +1,5 @@ import { parseProtocolUrl } from '../common/lbry-url' -import { resolveById, YtIdResolverDescriptor } from '../common/yt' +import { resolveById, YtIdResolverDescriptor } from '../common/yt/urlResolve' async function resolveYT(descriptor: YtIdResolverDescriptor) { const lbryProtocolUrl: string | null = await resolveById([descriptor]).then(a => a[0]); const segments = parseProtocolUrl(lbryProtocolUrl || '', { encode: true }); diff --git a/src/tools/YTtoLBRY.tsx b/src/tools/YTtoLBRY.tsx index ab120a1..ca89dc0 100644 --- a/src/tools/YTtoLBRY.tsx +++ b/src/tools/YTtoLBRY.tsx @@ -1,7 +1,8 @@ import { h, render } from 'preact' import { useState } from 'preact/hooks' import { getExtensionSettingsAsync, targetPlatformSettings } from '../common/settings' -import { getFileContent, getSubsFromCsv, getSubsFromJson, getSubsFromOpml, resolveById } from '../common/yt' +import { getFileContent, getSubsFromCsv, getSubsFromJson, getSubsFromOpml } from '../common/yt' +import { resolveById } from '../common/yt/urlResolve' import readme from './README.md' From 610b47d1e41fd256a0dedf6b212f5824f5c22aee Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 22:03:31 +0000 Subject: [PATCH 10/20] =?UTF-8?q?=F0=9F=8D=98=20Refactor=20bugfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/yt/index.ts | 6 +-- src/scripts/background.ts | 4 +- src/scripts/ytContent.tsx | 92 ++++++++++++++++++++++----------------- 3 files changed, 57 insertions(+), 45 deletions(-) diff --git a/src/common/yt/index.ts b/src/common/yt/index.ts index 58a7859..30f0099 100644 --- a/src/common/yt/index.ts +++ b/src/common/yt/index.ts @@ -92,7 +92,7 @@ export function getChannelId(channelURL: string) export function parseYouTubeURLTimeString(timeString: string) { const signs = timeString.replace(/[0-9]/g, '') - if (signs.length === 0) return timeString + if (signs.length === 0) return 0 const numbers = timeString.replace(/[^0-9]/g, '-').split('-') let total = 0 for (let i = 0; i < signs.length; i++) @@ -104,9 +104,9 @@ export function parseYouTubeURLTimeString(timeString: string) case 'h': t *= 60 case 'm': t *= 60 case 's': break - default: return '0' + default: return 0 } total += t } - return total.toString() + return total } \ No newline at end of file diff --git a/src/scripts/background.ts b/src/scripts/background.ts index 591c7e2..95ad564 100644 --- a/src/scripts/background.ts +++ b/src/scripts/background.ts @@ -19,4 +19,6 @@ async function lbryPathnameFromVideoId(videoId: string): Promise chrome.runtime.onMessage.addListener(({ videoId }: { videoId: string }, sender, sendResponse) => { lbryPathnameFromVideoId(videoId).then((lbryPathname) => sendResponse(lbryPathname)) return true; -}) \ No newline at end of file +}) + +chrome.tabs.onUpdated.addListener((tabId, changeInfo) => changeInfo.url && chrome.tabs.sendMessage(tabId, { })); \ No newline at end of file diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index d5bcffa..f7b5493 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -2,9 +2,9 @@ import { h, render } from 'preact' import { getExtensionSettingsAsync, getSourcePlatfromSettingsFromHostname, TargetPlatform, targetPlatformSettings } from '../common/settings' import { parseYouTubeURLTimeString } from '../common/yt' -const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)); +const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)) -function pauseAllVideos() { document.querySelectorAll('video').forEach(v => v.pause()); } +function pauseAllVideos() { document.querySelectorAll('video').forEach(v => v.pause()) } interface WatchOnLbryButtonParameters { @@ -20,8 +20,9 @@ interface Target time: number | null } -export function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryButtonParameters) { - if (!lbryPathname || !targetPlatform) return null; +export 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)) @@ -43,56 +44,59 @@ export function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchO textDecoration: 'none', ...targetPlatform.button.style?.button, }}> - - {targetPlatform.button.text} - - ; + + {targetPlatform.button.text} + + } -function updateButton(mountPoint: HTMLDivElement, target: Target | null): void { +function updateButton(mountPoint: HTMLDivElement, target: Target | null): void +{ if (!target) return render(, mountPoint) - render(, mountPoint) + const time = target.time && target.time > 3 ? target.time : null + render(, mountPoint) } -function redirectTo({ lbryPathname, platfrom }: Target) +function redirectTo({ lbryPathname, platfrom, time }: 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 (time) url.searchParams.set('t', time.toFixed(0)) if (platfrom === targetPlatformSettings.app) { - pauseAllVideos(); - location.assign(url); + pauseAllVideos() + location.assign(url) return } - location.replace(url.toString()); + location.replace(url.toString()) } /** Returns a mount point for the button */ -async function findButtonMountPoint(): Promise { +async function findButtonMountPoint(): Promise +{ let mountBefore: HTMLDivElement | null = null const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname) if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`) - while (!(mountBefore = document.querySelector(sourcePlatform.htmlQueries.mountButtonBefore))) await sleep(200); + while (!(mountBefore = document.querySelector(sourcePlatform.htmlQueries.mountButtonBefore))) await sleep(200) - const div = document.createElement('div'); - div.style.display = 'flex'; + const div = document.createElement('div') + div.style.display = 'flex' div.style.alignItems = 'center' mountBefore.parentElement?.insertBefore(div, mountBefore) - + return div } -async function findVideoElement() { +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; + 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) return videoElement } @@ -100,19 +104,24 @@ async function findVideoElement() { window.addEventListener('load', async () => { const settings = await getExtensionSettingsAsync() - const [buttonMountPoint, videoElement] = await Promise.all([findButtonMountPoint(), findVideoElement()]) - - // Listen Settings Change - chrome.storage.onChanged.addListener(async (changes, areaName) => { - if (areaName !== 'local') return; - Object.assign(settings, changes) - }); + if (settings.redirect) return await updateByURL(new URL(location.href)) - // Listen History.pushState + const [buttonMountPoint, videoElement] = await Promise.all([findButtonMountPoint(), findVideoElement()]) + + // Listen Settings Change + chrome.storage.onChanged.addListener(async (changes, areaName) => { - const originalPushState = history.pushState - history.pushState = function(...params) { originalPushState(...params); afterPushState(); } - } + if (areaName !== 'local') return + Object.assign(settings, changes) + updateByURL(new URL(location.href)) + }) + + /* + * 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(onUrlChange) + // 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 @@ -127,16 +136,17 @@ window.addEventListener('load', async () => if (!videoId) return const lbryPathname = await requestLbryPathname(videoId) if (!lbryPathname) return - const time = videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null + const time = settings.redirect ? parseYouTubeURLTimeString(url.searchParams.get('t') ?? '0') : videoElement.currentTime target = { lbryPathname, platfrom: targetPlatformSettings[settings.targetPlatform], time } - if (settings.redirect) redirectTo(target) + if (settings.redirect) redirectTo(target) else updateButton(buttonMountPoint, target) } - videoElement.addEventListener('timeupdate', () => updateButton(buttonMountPoint, target)) + videoElement.addEventListener('timeupdate', () => target && updateButton(buttonMountPoint, Object.assign(target, { time: videoElement.currentTime }))) + videoElement.addEventListener('ended', () => target && updateButton(buttonMountPoint, Object.assign(target, { time: null }))) - async function afterPushState() + async function onUrlChange() { await updateByURL(new URL(location.href)) } From 30f077ba38ca4aeb6b8b636521d1228ca03c9041 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 23:01:04 +0000 Subject: [PATCH 11/20] =?UTF-8?q?=F0=9F=8D=99=20Refactor=20and=20bugfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/yt/index.ts | 4 +- src/common/yt/urlCache.ts | 3 - src/common/yt/urlResolve.ts | 237 ++++++++++++++++-------------------- src/scripts/ytContent.tsx | 20 +-- src/tools/YTtoLBRY.tsx | 2 - 5 files changed, 123 insertions(+), 143 deletions(-) diff --git a/src/common/yt/index.ts b/src/common/yt/index.ts index 30f0099..f7240de 100644 --- a/src/common/yt/index.ts +++ b/src/common/yt/index.ts @@ -92,7 +92,7 @@ export function getChannelId(channelURL: string) export function parseYouTubeURLTimeString(timeString: string) { const signs = timeString.replace(/[0-9]/g, '') - if (signs.length === 0) return 0 + if (signs.length === 0) return null const numbers = timeString.replace(/[^0-9]/g, '-').split('-') let total = 0 for (let i = 0; i < signs.length; i++) @@ -104,7 +104,7 @@ export function parseYouTubeURLTimeString(timeString: string) case 'h': t *= 60 case 'm': t *= 60 case 's': break - default: return 0 + default: return null } total += t } diff --git a/src/common/yt/urlCache.ts b/src/common/yt/urlCache.ts index 8b3b6f6..a68aef7 100644 --- a/src/common/yt/urlCache.ts +++ b/src/common/yt/urlCache.ts @@ -2,9 +2,6 @@ let db: IDBDatabase | null = null -// 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") diff --git a/src/common/yt/urlResolve.ts b/src/common/yt/urlResolve.ts index 26f45da..dd955e3 100644 --- a/src/common/yt/urlResolve.ts +++ b/src/common/yt/urlResolve.ts @@ -10,132 +10,111 @@ export interface YtIdResolverDescriptor id: string type: 'channel' | 'video' } - - /** - * @param descriptorsWithIndex YT resource IDs to check - * @returns a promise with the list of channels that were found on lbry - */ - export async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> - { - const descriptorsWithIndex: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({ ...descriptor, index })) - descriptors = null as any - const results: (string | null)[] = [] - - await Promise.all(descriptorsWithIndex.map(async (descriptor, index) => - { - if (!descriptor) return - const cache = await LbryPathnameCache.get(descriptor.id) - - // Cache can be null, if there is no lbry url yet - if (cache !== undefined) - { - // Directly setting it to results - results[index] = cache - - // We remove it so we dont ask it to API - descriptorsWithIndex.splice(index, 1) - } - })) - - const descriptorsChunks = chunk(descriptorsWithIndex, QUERY_CHUNK_SIZE) - let progressCount = 0 - await Promise.all(descriptorsChunks.map(async (descriptorChunk) => - { - const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any - - const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() - const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] - - const url = new URL(`https://${urlResolverSetting.hostname}`) - - function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) - { - for (const path of responsePath) - { - switch (typeof path) - { - case 'string': - case 'number': - response = response[path] - break - default: - switch (path) - { - case Keys: - response = Object.keys(response) - break - case Values: - response = Object.values(response) - break - } - } - } - return response as T - } - - async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsWithIndex) - { - url.pathname = urlResolverFunction.pathname - - if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) - { - await Promise.all(descriptorsGroup.map(async (descriptor) => - { - switch (null) - { - default: - if (!descriptor.id) break - url.searchParams.set(urlResolverFunction.paramName, descriptor.id) - - const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) - if (!apiResponse.ok) - { - // Some API might not respond with 200 if it can't find the url - if (apiResponse.status === 404) await LbryPathnameCache.put(null, descriptor.id) - break - } - - const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) - if (value) results[descriptor.index] = value - await LbryPathnameCache.put(value, descriptor.id) - } - progressCount++ - if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) - })) - } - else - { - - switch (null) - { - default: - url.searchParams - .set(urlResolverFunction.paramName, descriptorsGroup - .map((descriptor) => descriptor.id) - .filter((descriptorId) => descriptorId) - .join(urlResolverFunction.paramArraySeperator) - ) - - const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) - if (!apiResponse.ok) break - const values = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) - - await Promise.all(values.map(async (value, index) => - { - const descriptor = descriptorsGroup[index] - if (value) results[descriptor.index] = value - await LbryPathnameCache.put(value, descriptor.id) - })) - } - progressCount += descriptorsGroup.length - if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) - } - } - - if (descriptorsGroupedByType['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel']) - if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video']) - - })) - - return results - } \ No newline at end of file + +/** +* @param descriptorsWithIndex YT resource IDs to check +* @returns a promise with the list of channels that were found on lbry +*/ +export async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> +{ + let descriptorsPayload: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({ ...descriptor, index })) + descriptors = null as any + const results: (string | null)[] = []; + + + descriptorsPayload = (await Promise.all(descriptorsPayload.map(async (descriptor, index) => + { + if (!descriptor?.id) return + const cache = await LbryPathnameCache.get(descriptor.id) + + // Cache can be null, if there is no lbry url yet + if (cache !== undefined) + { + // Null values shouldn't be in the results + if (cache) results[index] = cache + return + } + + return descriptor + }))).filter((descriptor) => descriptor) as any + + const descriptorsPayloadChunks = chunk(descriptorsPayload, QUERY_CHUNK_SIZE) + let progressCount = 0 + await Promise.all(descriptorsPayloadChunks.map(async (descriptorChunk) => + { + const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any + + const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() + const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] + + const url = new URL(`https://${urlResolverSetting.hostname}`) + + function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) + { + for (const path of responsePath) + { + switch (typeof path) + { + case 'string': case 'number': response = response[path]; continue + } + switch (path) + { + case Keys: response = Object.keys(response); continue + case Values: response = Object.values(response); continue + } + } + return response as T + } + + async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsPayload) + { + url.pathname = urlResolverFunction.pathname + + if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) + { + await Promise.all(descriptorsGroup.map(async (descriptor) => + { + url.searchParams.set(urlResolverFunction.paramName, descriptor.id) + + const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) + if (apiResponse.ok) + { + const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) + if (value) results[descriptor.index] = value + await LbryPathnameCache.put(value, descriptor.id) + } + else if (apiResponse.status === 404) await LbryPathnameCache.put(null, descriptor.id) + + progressCount++ + if (progressCallback) progressCallback(progressCount / descriptorsPayload.length) + })) + } + else + { + url.searchParams.set(urlResolverFunction.paramName, descriptorsGroup + .map((descriptor) => descriptor.id) + .join(urlResolverFunction.paramArraySeperator)) + + const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) + if (apiResponse.ok) + { + const values = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) + await Promise.all(values.map(async (value, index) => + { + const descriptor = descriptorsGroup[index] + if (value) results[descriptor.index] = value + await LbryPathnameCache.put(value, descriptor.id) + })) + } + + progressCount += descriptorsGroup.length + if (progressCallback) progressCallback(progressCount / descriptorsPayload.length) + } + } + + if (descriptorsGroupedByType['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel']) + if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video']) + })) + if (progressCallback) progressCallback(1); + return results +} \ No newline at end of file diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index f7b5493..a0b80e0 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -54,8 +54,7 @@ export function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchO function updateButton(mountPoint: HTMLDivElement, target: Target | null): void { if (!target) return render(, mountPoint) - const time = target.time && target.time > 3 ? target.time : null - render(, mountPoint) + render(, mountPoint) } function redirectTo({ lbryPathname, platfrom, time }: Target) @@ -112,8 +111,8 @@ window.addEventListener('load', async () => chrome.storage.onChanged.addListener(async (changes, areaName) => { if (areaName !== 'local') return - Object.assign(settings, changes) - updateByURL(new URL(location.href)) + Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue]))) + await updateByURL(new URL(location.href)) }) /* @@ -127,6 +126,13 @@ window.addEventListener('load', async () => // 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((resolve) => chrome.runtime.sendMessage({ videoId }, resolve)) + function getVideoTime(url: URL) + { + return settings.redirect ? + (url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null) : + (videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null) + } + let target: Target | null = null async function updateByURL(url: URL) { @@ -136,15 +142,15 @@ window.addEventListener('load', async () => if (!videoId) return const lbryPathname = await requestLbryPathname(videoId) if (!lbryPathname) return - const time = settings.redirect ? parseYouTubeURLTimeString(url.searchParams.get('t') ?? '0') : videoElement.currentTime + const time = getVideoTime(url) target = { lbryPathname, platfrom: targetPlatformSettings[settings.targetPlatform], time } if (settings.redirect) redirectTo(target) else updateButton(buttonMountPoint, target) } - videoElement.addEventListener('timeupdate', () => target && updateButton(buttonMountPoint, Object.assign(target, { time: videoElement.currentTime }))) - videoElement.addEventListener('ended', () => target && updateButton(buttonMountPoint, Object.assign(target, { time: null }))) + videoElement.addEventListener('timeupdate', + () => target && updateButton(buttonMountPoint, Object.assign(target, { time: getVideoTime(new URL(location.href)) }))) async function onUrlChange() { diff --git a/src/tools/YTtoLBRY.tsx b/src/tools/YTtoLBRY.tsx index ca89dc0..b350bec 100644 --- a/src/tools/YTtoLBRY.tsx +++ b/src/tools/YTtoLBRY.tsx @@ -5,8 +5,6 @@ import { getFileContent, getSubsFromCsv, getSubsFromJson, getSubsFromOpml } from import { resolveById } from '../common/yt/urlResolve' import readme from './README.md' - - /** * Parses the subscription file and queries the API for lbry channels * From 86879183b2f599188c559117d02084776b64fadd Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Sun, 9 Jan 2022 23:17:20 +0000 Subject: [PATCH 12/20] =?UTF-8?q?=F0=9F=8D=B1=20Refactor=20bug=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/ytContent.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index a0b80e0..a5799ec 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -128,7 +128,7 @@ window.addEventListener('load', async () => function getVideoTime(url: URL) { - return settings.redirect ? + return settings.redirect ? (url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null) : (videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null) } @@ -139,17 +139,15 @@ window.addEventListener('load', async () => if (url.pathname !== '/watch') return const videoId = url.searchParams.get('v') - if (!videoId) return - const lbryPathname = await requestLbryPathname(videoId) - if (!lbryPathname) return - const time = getVideoTime(url) - target = { lbryPathname, platfrom: targetPlatformSettings[settings.targetPlatform], time } + const lbryPathname = videoId && await requestLbryPathname(videoId) + if (lbryPathname) target = { lbryPathname, platfrom: targetPlatformSettings[settings.targetPlatform], time: getVideoTime(url) } + else target = null - if (settings.redirect) redirectTo(target) + if (settings.redirect) target && redirectTo(target) else updateButton(buttonMountPoint, target) } - videoElement.addEventListener('timeupdate', + videoElement.addEventListener('timeupdate', () => target && updateButton(buttonMountPoint, Object.assign(target, { time: getVideoTime(new URL(location.href)) }))) async function onUrlChange() From 06524954e76dce255ee895761ea8a0b773f90d5e Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Mon, 10 Jan 2022 00:08:10 +0000 Subject: [PATCH 13/20] =?UTF-8?q?=F0=9F=A5=9E=20redirect=20bugfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/ytContent.tsx | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index a5799ec..2fc8ece 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -4,8 +4,6 @@ import { parseYouTubeURLTimeString } from '../common/yt' const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)) -function pauseAllVideos() { document.querySelectorAll('video').forEach(v => v.pause()) } - interface WatchOnLbryButtonParameters { targetPlatform?: TargetPlatform @@ -28,7 +26,7 @@ export function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchO if (time) url.searchParams.set('t', time.toFixed(0)) return
- , mountPoint) } -function redirectTo({ lbryPathname, platfrom, time }: Target) +async function redirectTo({ lbryPathname, platfrom, time }: Target) { const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`) @@ -65,9 +63,13 @@ function redirectTo({ lbryPathname, platfrom, time }: Target) if (platfrom === targetPlatformSettings.app) { - pauseAllVideos() - location.assign(url) - return + open(url, '_blank') + findVideoElement().then((videoElement) => { + videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) + videoElement.pause() + }) + if (window.history.length === 1) window.close(); + else window.history.back() } location.replace(url.toString()) } @@ -78,10 +80,12 @@ async function findButtonMountPoint(): Promise 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('#watch-on-yt-button-container') + if (exits) return exits; while (!(mountBefore = document.querySelector(sourcePlatform.htmlQueries.mountButtonBefore))) await sleep(200) - + const div = document.createElement('div') + div.id = 'watch-on-yt-button-container' div.style.display = 'flex' div.style.alignItems = 'center' mountBefore.parentElement?.insertBefore(div, mountBefore) @@ -103,7 +107,7 @@ async function findVideoElement() window.addEventListener('load', async () => { const settings = await getExtensionSettingsAsync() - if (settings.redirect) return await updateByURL(new URL(location.href)) + let target: Target | null = null const [buttonMountPoint, videoElement] = await Promise.all([findButtonMountPoint(), findVideoElement()]) @@ -119,12 +123,14 @@ window.addEventListener('load', async () => * 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(onUrlChange) - - // 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((resolve) => chrome.runtime.sendMessage({ videoId }, resolve)) + async function requestLbryPathname(videoId: string) + { + return await new Promise((resolve) => chrome.runtime.sendMessage({ videoId }, resolve)) + } function getVideoTime(url: URL) { @@ -133,9 +139,9 @@ window.addEventListener('load', async () => (videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null) } - let target: Target | null = null async function updateByURL(url: URL) { + console.log(url) if (url.pathname !== '/watch') return const videoId = url.searchParams.get('v') From 539693658677f9643c7dd62d53cd30fdd9e2e434 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Mon, 10 Jan 2022 00:09:15 +0000 Subject: [PATCH 14/20] =?UTF-8?q?=F0=9F=8D=A3=20typo=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/settings.ts | 6 +++--- src/scripts/ytContent.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/settings.ts b/src/common/settings.ts index c69c832..6ec75f3 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -73,8 +73,8 @@ export const getTargetPlatfromSettingsEntiries = () => -export type SourcePlatfromName = 'youtube.com' | 'yewtu.be' -export interface SourcePlatfrom +export type SourcePlatformName = 'youtube.com' | 'yewtu.be' +export interface SourcePlatform { hostnames: string[] htmlQueries: { @@ -83,7 +83,7 @@ export interface SourcePlatfrom } } -export const sourcePlatfromSettings: Record = { +export const sourcePlatfromSettings: Record = { "yewtu.be": { hostnames: ['yewtu.be'], htmlQueries: { diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index 2fc8ece..c08315a 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -126,7 +126,7 @@ window.addEventListener('load', async () => // Listen URL Change chrome.runtime.onMessage.addListener(onUrlChange) - // 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 + // 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 requestLbryPathname(videoId: string) { return await new Promise((resolve) => chrome.runtime.sendMessage({ videoId }, resolve)) From d32958852a228c263a15c43cbfefee4c5b678f63 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Mon, 10 Jan 2022 00:22:20 +0000 Subject: [PATCH 15/20] =?UTF-8?q?=F0=9F=8D=99=20bugfix=20redirect=20to=20l?= =?UTF-8?q?bryapp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/ytContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index c08315a..8650a2c 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -63,11 +63,12 @@ async function redirectTo({ lbryPathname, platfrom, time }: Target) if (platfrom === targetPlatformSettings.app) { - open(url, '_blank') findVideoElement().then((videoElement) => { videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) videoElement.pause() }) + if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true })) + open(url, '_blank') if (window.history.length === 1) window.close(); else window.history.back() } @@ -141,7 +142,6 @@ window.addEventListener('load', async () => async function updateByURL(url: URL) { - console.log(url) if (url.pathname !== '/watch') return const videoId = url.searchParams.get('v') From 5bcd33890d0909f7ebd4140d9204688f6e953566 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Mon, 10 Jan 2022 00:39:28 +0000 Subject: [PATCH 16/20] =?UTF-8?q?=F0=9F=8D=98=20made=20video=20pause=20on?= =?UTF-8?q?=20all=20redirects=20not=20only=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/ytContent.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index 8650a2c..12cdb2a 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -61,12 +61,13 @@ async function redirectTo({ lbryPathname, platfrom, time }: Target) if (time) url.searchParams.set('t', time.toFixed(0)) + findVideoElement().then((videoElement) => { + videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) + videoElement.pause() + }) + if (platfrom === targetPlatformSettings.app) { - findVideoElement().then((videoElement) => { - videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) - videoElement.pause() - }) if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true })) open(url, '_blank') if (window.history.length === 1) window.close(); From 8f75c676011c8630f5c27f0d70727608e003cd36 Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Mon, 10 Jan 2022 12:32:32 +0000 Subject: [PATCH 17/20] =?UTF-8?q?=F0=9F=A5=A1=20Refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/ytContent.tsx | 101 ++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index 12cdb2a..be7631c 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -18,7 +18,7 @@ interface Target time: number | null } -export function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryButtonParameters) +function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryButtonParameters) { if (!lbryPathname || !targetPlatform) return null @@ -61,7 +61,8 @@ async function redirectTo({ lbryPathname, platfrom, time }: Target) if (time) url.searchParams.set('t', time.toFixed(0)) - findVideoElement().then((videoElement) => { + findVideoElement().then((videoElement) => + { videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) videoElement.pause() }) @@ -70,7 +71,7 @@ async function redirectTo({ lbryPathname, platfrom, time }: Target) { if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true })) open(url, '_blank') - if (window.history.length === 1) window.close(); + if (window.history.length === 1) window.close() else window.history.back() } location.replace(url.toString()) @@ -79,15 +80,16 @@ async function redirectTo({ lbryPathname, platfrom, time }: Target) /** Returns a mount point for the button */ async function findButtonMountPoint(): Promise { + 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('#watch-on-yt-button-container') - if (exits) return exits; + 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 = 'watch-on-yt-button-container' + div.id = id div.style.display = 'flex' div.style.alignItems = 'center' mountBefore.parentElement?.insertBefore(div, mountBefore) @@ -100,25 +102,28 @@ 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 } -window.addEventListener('load', async () => +// 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 requestLbryPathname(videoId: string) +{ + return await new Promise((resolve) => chrome.runtime.sendMessage({ videoId }, resolve)) +} + +// Start +(async () => { const settings = await getExtensionSettingsAsync() - let target: Target | null = null - - const [buttonMountPoint, videoElement] = await Promise.all([findButtonMountPoint(), findVideoElement()]) + let updater: (() => Promise) // 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]))) - await updateByURL(new URL(location.href)) + if (changes.redirect) await onModeChange() }) /* @@ -126,41 +131,55 @@ window.addEventListener('load', async () => * history.pushState changes from a content script */ // Listen URL Change - chrome.runtime.onMessage.addListener(onUrlChange) + chrome.runtime.onMessage.addListener(() => updater()) - // 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 requestLbryPathname(videoId: string) + async function getTargetByURL(url: URL) { - return await new Promise((resolve) => chrome.runtime.sendMessage({ videoId }, resolve)) - } - - function getVideoTime(url: URL) - { - return settings.redirect ? - (url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null) : - (videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null) - } - - async function updateByURL(url: URL) - { - if (url.pathname !== '/watch') return + if (url.pathname !== '/watch') return null const videoId = url.searchParams.get('v') const lbryPathname = videoId && await requestLbryPathname(videoId) - if (lbryPathname) target = { lbryPathname, platfrom: targetPlatformSettings[settings.targetPlatform], time: getVideoTime(url) } - else target = null + const target: Target | null = lbryPathname ? { lbryPathname, platfrom: targetPlatformSettings[settings.targetPlatform], time: null } : null - if (settings.redirect) target && redirectTo(target) - else updateButton(buttonMountPoint, target) + return target } - videoElement.addEventListener('timeupdate', - () => target && updateButton(buttonMountPoint, Object.assign(target, { time: getVideoTime(new URL(location.href)) }))) - - async function onUrlChange() + let removeVideoTimeUpdateListener: (() => void) | null = null + async function onModeChange() { - await updateByURL(new URL(location.href)) + 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) + } + } + + await updater() } - await updateByURL(new URL(location.href)) -}) \ No newline at end of file + await onModeChange() +})() \ No newline at end of file From 2c75082af998842e7192071ab80a4a952336742c Mon Sep 17 00:00:00 2001 From: Shiba <44804845+DeepDoge@users.noreply.github.com> Date: Mon, 10 Jan 2022 12:36:29 +0000 Subject: [PATCH 18/20] =?UTF-8?q?=F0=9F=8D=A3=20Formatted=20Files,=20Organ?= =?UTF-8?q?ized=20Imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/components/ButtonRadio.tsx | 18 +++--- src/common/lbry-url.spec.ts | 18 +++--- src/common/lbry-url.ts | 66 +++++++++---------- src/common/settings.ts | 27 +++----- src/common/useSettings.ts | 42 ++++++------ src/common/yt/index.ts | 93 ++++++++++++--------------- src/common/yt/urlCache.ts | 21 ++---- src/common/yt/urlResolve.ts | 54 ++++++---------- src/global.d.ts | 4 +- src/manifest.json | 2 +- src/popup/popup.html | 2 +- src/popup/popup.tsx | 16 ++--- src/scripts/background.ts | 12 ++-- src/scripts/storageSetup.ts | 24 +++---- src/scripts/ytContent.tsx | 57 ++++++---------- src/tools/YTtoLBRY.html | 22 ++++--- src/tools/YTtoLBRY.tsx | 36 +++++------ 17 files changed, 228 insertions(+), 286 deletions(-) diff --git a/src/common/components/ButtonRadio.tsx b/src/common/components/ButtonRadio.tsx index c8aa9c7..b38ce41 100644 --- a/src/common/components/ButtonRadio.tsx +++ b/src/common/components/ButtonRadio.tsx @@ -1,7 +1,7 @@ -import { h } from 'preact'; -import classnames from 'classnames'; +import classnames from 'classnames' +import { h } from 'preact' +import './ButtonRadio.sass' -import './ButtonRadio.sass'; export interface SelectionOption { value: string @@ -9,13 +9,13 @@ export interface SelectionOption { } export interface ButtonRadioProps { - name?: string; - onChange(redirect: string): void; - value: T extends SelectionOption ? T['value'] : T; - options: T[]; + name?: string + onChange(redirect: string): void + value: T extends SelectionOption ? T['value'] : T + options: T[] } -const getAttr = (x: string | SelectionOption, key: keyof SelectionOption): string => typeof x === 'string' ? x : x[key]; +const getAttr = (x: string | SelectionOption, key: keyof SelectionOption): string => typeof x === 'string' ? x : x[key] export default function ButtonRadio({ name = 'buttonRadio', onChange, options, value }: ButtonRadioProps) { /** If it's a string, return the string, if it's a SelectionOption get the selection option property */ @@ -27,5 +27,5 @@ export default function ButtonRadio
)} - ; + } diff --git a/src/common/lbry-url.spec.ts b/src/common/lbry-url.spec.ts index 3c492b1..14d30f0 100644 --- a/src/common/lbry-url.spec.ts +++ b/src/common/lbry-url.spec.ts @@ -1,4 +1,4 @@ -import { appRedirectUrl, parseProtocolUrl } from './lbry-url'; +import { appRedirectUrl, parseProtocolUrl } from './lbry-url' describe('web url parsing', () => { const testCases: [string, string | undefined][] = [ @@ -9,21 +9,21 @@ describe('web url parsing', () => { ['https://lbry.tv/@test:c', 'lbry://@test:c'], ['https://lbry.tv/$/discover?t=foo%20bar', undefined], ['https://lbry.tv/$/signup?redirect=/@test1:0/foo-bar-2-baz-7:e#adasasddasdas123', undefined], - ]; + ] test.each(testCases)('redirect %s', (url, expected) => { - expect(appRedirectUrl(url)).toEqual(expected); - }); -}); + expect(appRedirectUrl(url)).toEqual(expected) + }) +}) describe('app url parsing', () => { const testCases: Array<[string, string[]]> = [ ['test', ['test']], ['@test', ['@test']], ['lbry://@test$1/stuff', ['@test$1', 'stuff']], - ]; + ] test.each(testCases)('redirect %s', (url, expected) => { - expect(parseProtocolUrl(url)).toEqual(expected); - }); -}); + expect(parseProtocolUrl(url)).toEqual(expected) + }) +}) diff --git a/src/common/lbry-url.ts b/src/common/lbry-url.ts index 0eede34..20873b4 100644 --- a/src/common/lbry-url.ts +++ b/src/common/lbry-url.ts @@ -8,14 +8,14 @@ interface UrlOptions { encode?: boolean } -const invalidNamesRegex = /[^=&#:$@%*?;\"/\\<>%{}|^~`\[\]\u0000-\u0020\uD800-\uDFFF\uFFFE-\uFFFF]+/.source; +const invalidNamesRegex = /[^=&#:$@%*?;\"/\\<>%{}|^~`\[\]\u0000-\u0020\uD800-\uDFFF\uFFFE-\uFFFF]+/.source /** Creates a named regex group */ -const named = (name: string, regex: string) => `(?<${name}>${regex})`; +const named = (name: string, regex: string) => `(?<${name}>${regex})` /** Creates a non-capturing group */ -const group = (regex: string) => `(?:${regex})`; +const group = (regex: string) => `(?:${regex})` /** Allows for one of the patterns */ -const oneOf = (...choices: string[]) => group(choices.join('|')); +const oneOf = (...choices: string[]) => group(choices.join('|')) /** Create an lbry url claim */ const claim = (name: string, prefix = '') => group( named(`${name}_name`, prefix + invalidNamesRegex) @@ -24,7 +24,7 @@ const claim = (name: string, prefix = '') => group( group('\\*' + named(`${name}_sequence`, '[1-9][0-9]*')), group('\\$' + named(`${name}_amount_order`, '[1-9][0-9]*')) ) + '?' -); +) /** Create an lbry url claim, but use the old pattern for claims */ const legacyClaim = (name: string, prefix = '') => group( @@ -33,34 +33,34 @@ const legacyClaim = (name: string, prefix = '') => group( group('#' + named(`${name}_claim_id`, '[0-9a-f]{1,40}')), group(':' + named(`${name}_sequence`, '[1-9][0-9]*')), group('\\$' + named(`${name}_amount_order`, '[1-9][0-9]*')) - ) + '?'); + ) + '?') -export const builder = { named, group, oneOf, claim, legacyClaim, invalidNamesRegex }; +export const builder = { named, group, oneOf, claim, legacyClaim, invalidNamesRegex } /** Creates a pattern to parse lbry protocol URLs. Unused, but I left it here. */ function createProtocolUrlRegex(legacy = false) { - const claim = legacy ? builder.legacyClaim : builder.claim; + const claim = legacy ? builder.legacyClaim : builder.claim return new RegExp('^' + named('scheme', 'lbry://') + '?' + oneOf( group(claim('channel_with_stream', '@') + '/' + claim('stream_in_channel')), claim('channel', '@'), claim('stream'), - ) + '$'); + ) + '$') } /** Creates a pattern to match lbry.tv style sites by their pathname */ function createWebUrlRegex(legacy = false) { - const claim = legacy ? builder.legacyClaim : builder.claim; + const claim = legacy ? builder.legacyClaim : builder.claim return new RegExp('^/' + oneOf( group(claim('channel_with_stream', '@') + '/' + claim('stream_in_channel')), claim('channel', '@'), claim('stream'), - ) + '$'); + ) + '$') } /** Pattern for lbry.tv style sites */ -export const URL_REGEX = createWebUrlRegex(); -export const PROTOCOL_URL_REGEX = createProtocolUrlRegex(); -const PROTOCOL_URL_REGEX_LEGACY = createProtocolUrlRegex(true); +export const URL_REGEX = createWebUrlRegex() +export const PROTOCOL_URL_REGEX = createProtocolUrlRegex() +const PROTOCOL_URL_REGEX_LEGACY = createProtocolUrlRegex(true) /** * Encapsulates a lbry url path segment. @@ -78,15 +78,15 @@ export class PathSegment { groups[`${segment}_claim_id`], parseInt(groups[`${segment}_sequence`]), parseInt(groups[`${segment}_amount_order`]) - ); + ) } /** Prints the segment */ toString() { - if (this.claimID) return `${this.name}:${this.claimID}`; - if (this.sequence) return `${this.name}*${this.sequence}`; - if (this.amountOrder) return `${this.name}$${this.amountOrder}`; - return this.name; + if (this.claimID) return `${this.name}:${this.claimID}` + if (this.sequence) return `${this.name}*${this.sequence}` + if (this.amountOrder) return `${this.name}$${this.amountOrder}` + return this.name } } @@ -98,18 +98,18 @@ export class PathSegment { * @returns an array of path segments; if invalid, will return an empty array */ function patternSegmenter(ptn: RegExp, url: string, options: UrlOptions = { encode: false }): string[] { - const match = url.match(ptn)?.groups; - if (!match) return []; + const match = url.match(ptn)?.groups + if (!match) return [] const segments = match['channel_name'] ? ['channel'] : match['channel_with_stream_name'] ? ['channel_with_stream', 'stream_in_channel'] - : match['stream_name'] ? ['stream'] - : null; + : match['stream_name'] ? ['stream'] + : null - if (!segments) throw new Error(`${url} matched the overall pattern, but could not determine type`); + if (!segments) throw new Error(`${url} matched the overall pattern, but could not determine type`) return segments.map(s => PathSegment.fromMatchGroup(s, match).toString()) - .map(s => options.encode ? encodeURIComponent(s) : s); + .map(s => options.encode ? encodeURIComponent(s) : s) } /** @@ -119,11 +119,11 @@ function patternSegmenter(ptn: RegExp, url: string, options: UrlOptions = { enco * @param options options for the redirect */ export function appRedirectUrl(url: string, options?: UrlOptions): string | undefined { - const segments = patternSegmenter(URL_REGEX, new URL(url).pathname, options); - if (segments.length === 0) return; - const path = segments.join('/'); + const segments = patternSegmenter(URL_REGEX, new URL(url).pathname, options) + if (segments.length === 0) return + const path = segments.join('/') - return `lbry://${path}`; + return `lbry://${path}` } /** @@ -134,9 +134,9 @@ export function appRedirectUrl(url: string, options?: UrlOptions): string | unde */ export function parseProtocolUrl(url: string, options: UrlOptions = { encode: false }): string[] { for (const ptn of [PROTOCOL_URL_REGEX, PROTOCOL_URL_REGEX_LEGACY]) { - const segments = patternSegmenter(ptn, url, options); - if (segments.length === 0) continue; - return segments; + const segments = patternSegmenter(ptn, url, options) + if (segments.length === 0) continue + return segments } - return []; + return [] } diff --git a/src/common/settings.ts b/src/common/settings.ts index 6ec75f3..b57e04b 100644 --- a/src/common/settings.ts +++ b/src/common/settings.ts @@ -1,7 +1,6 @@ import { JSX } from "preact" -export interface ExtensionSettings -{ +export interface ExtensionSettings { redirect: boolean targetPlatform: TargetPlatformName urlResolver: YTUrlResolverName @@ -9,15 +8,13 @@ export interface ExtensionSettings export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee', urlResolver: 'lbryInc' } -export function getExtensionSettingsAsync(): Promise -{ +export function getExtensionSettingsAsync(): Promise { return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any))) } export type TargetPlatformName = 'madiator.com' | 'odysee' | 'app' -export interface TargetPlatform -{ +export interface TargetPlatform { domainPrefix: string displayName: string theme: string @@ -66,16 +63,14 @@ export const targetPlatformSettings: Record }, } -export const getTargetPlatfromSettingsEntiries = () => -{ +export const getTargetPlatfromSettingsEntiries = () => { return Object.entries(targetPlatformSettings) as any as [Extract, TargetPlatform][] } export type SourcePlatformName = 'youtube.com' | 'yewtu.be' -export interface SourcePlatform -{ +export interface SourcePlatform { hostnames: string[] htmlQueries: { mountButtonBefore: string, @@ -100,8 +95,7 @@ export const sourcePlatfromSettings: Record } } -export function getSourcePlatfromSettingsFromHostname(hostname: string) -{ +export function getSourcePlatfromSettingsFromHostname(hostname: string) { const values = Object.values(sourcePlatfromSettings) for (const settings of values) if (settings.hostnames.includes(hostname)) return settings @@ -115,15 +109,13 @@ export const Keys = Symbol('keys') export const Values = Symbol('values') export const SingleValueAtATime = Symbol() export type YtUrlResolveResponsePath = (string | number | typeof Keys | typeof Values)[] -export interface YtUrlResolveFunction -{ +export interface YtUrlResolveFunction { pathname: string paramName: string paramArraySeperator: string | typeof SingleValueAtATime responsePath: YtUrlResolveResponsePath } -export interface YTUrlResolver -{ +export interface YTUrlResolver { name: string hostname: string functions: { @@ -171,7 +163,6 @@ export const ytUrlResolversSettings: Record = } } -export const getYtUrlResolversSettingsEntiries = () => -{ +export const getYtUrlResolversSettingsEntiries = () => { return Object.entries(ytUrlResolversSettings) as any as [Extract, YTUrlResolver][] } \ No newline at end of file diff --git a/src/common/useSettings.ts b/src/common/useSettings.ts index b1d9113..26e519d 100644 --- a/src/common/useSettings.ts +++ b/src/common/useSettings.ts @@ -1,5 +1,5 @@ -import { useReducer, useEffect } from 'preact/hooks'; -import { DEFAULT_SETTINGS } from './settings'; +import { useEffect, useReducer } from 'preact/hooks' +import { DEFAULT_SETTINGS } from './settings' /** * A hook to read the settings from local storage @@ -7,24 +7,24 @@ import { DEFAULT_SETTINGS } from './settings'; * @param initial the default value. Must have all relevant keys present and should not change */ export function useSettings(initial: T) { - const [state, dispatch] = useReducer((state, nstate: Partial) => ({ ...state, ...nstate }), initial); - // register change listeners, gets current values, and cleans up the listeners on unload - useEffect(() => { - const changeListener = (changes: Record, areaName: string) => { - if (areaName !== 'local') return; - const changeSet = Object.keys(changes) - .filter(k => Object.keys(initial).includes(k)) - .map(k => [k, changes[k].newValue]); - if (changeSet.length === 0) return; // no changes; no use dispatching - dispatch(Object.fromEntries(changeSet)); - }; - chrome.storage.onChanged.addListener(changeListener); - chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial)); - return () => chrome.storage.onChanged.removeListener(changeListener); - }, []); + const [state, dispatch] = useReducer((state, nstate: Partial) => ({ ...state, ...nstate }), initial) + // register change listeners, gets current values, and cleans up the listeners on unload + useEffect(() => { + const changeListener = (changes: Record, areaName: string) => { + if (areaName !== 'local') return + const changeSet = Object.keys(changes) + .filter(k => Object.keys(initial).includes(k)) + .map(k => [k, changes[k].newValue]) + if (changeSet.length === 0) return // no changes; no use dispatching + dispatch(Object.fromEntries(changeSet)) + } + chrome.storage.onChanged.addListener(changeListener) + chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial)) + return () => chrome.storage.onChanged.removeListener(changeListener) + }, []) - return state; - } + return state +} - /** A hook to read watch on lbry settings from local storage */ - export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS); +/** A hook to read watch on lbry settings from local storage */ +export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS) diff --git a/src/common/yt/index.ts b/src/common/yt/index.ts index f7240de..68184f5 100644 --- a/src/common/yt/index.ts +++ b/src/common/yt/index.ts @@ -1,6 +1,5 @@ -interface YtExportedJsonSubscription -{ +interface YtExportedJsonSubscription { id: string etag: string title: string @@ -17,14 +16,11 @@ interface YtExportedJsonSubscription * @param file to load * @returns a promise with the file as a string */ -export function getFileContent(file: File): Promise -{ - return new Promise((resolve, reject) => - { +export function getFileContent(file: File): Promise { + return new Promise((resolve, reject) => { const reader = new FileReader() reader.addEventListener('load', event => resolve(event.target?.result as string || '')) - reader.addEventListener('error', () => - { + reader.addEventListener('error', () => { reader.abort() reject(new DOMException(`Could not read ${file.name}`)) }) @@ -39,42 +35,39 @@ export function getFileContent(file: File): Promise * @param opmlContents an opml file as as tring * @returns the channel IDs */ - export function getSubsFromOpml(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) - .map(url => getChannelId(url)) - .filter((url): url is string => !!url) // we don't want it if it's empty - } - - /** - * Reads an array of YT channel IDs from the YT subscriptions JSON file - * - * @param jsonContents a JSON file as a string - * @returns the channel IDs - */ - export function getSubsFromJson(jsonContents: string): string[] - { - const subscriptions: YtExportedJsonSubscription[] = 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 - */ - export function getSubsFromCsv(csvContent: string): string[] - { - const rows = csvContent.split('\n') - csvContent = '' - return rows.slice(1).map((row) => row.substring(0, row.indexOf(','))) - } +export function getSubsFromOpml(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) + .map(url => getChannelId(url)) + .filter((url): url is string => !!url) // we don't want it if it's empty +} + +/** + * Reads an array of YT channel IDs from the YT subscriptions JSON file + * + * @param jsonContents a JSON file as a string + * @returns the channel IDs + */ +export function getSubsFromJson(jsonContents: string): string[] { + const subscriptions: YtExportedJsonSubscription[] = 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 + */ +export function getSubsFromCsv(csvContent: string): string[] { + const rows = csvContent.split('\n') + csvContent = '' + return rows.slice(1).map((row) => row.substring(0, row.indexOf(','))) +} /** * Extracts the channelID from a YT URL. @@ -83,23 +76,19 @@ export function getFileContent(file: File): Promise * * /feeds/videos.xml?channel_id=* * * /channel/* */ -export function getChannelId(channelURL: string) -{ +export function getChannelId(channelURL: string) { const match = channelURL.match(/channel\/([^\s?]*)/) return match ? match[1] : new URL(channelURL).searchParams.get('channel_id') } -export function parseYouTubeURLTimeString(timeString: string) -{ +export function parseYouTubeURLTimeString(timeString: string) { const signs = timeString.replace(/[0-9]/g, '') if (signs.length === 0) return null const numbers = timeString.replace(/[^0-9]/g, '-').split('-') let total = 0 - for (let i = 0; i < signs.length; i++) - { + for (let i = 0; i < signs.length; i++) { let t = parseInt(numbers[i]) - switch (signs[i]) - { + switch (signs[i]) { case 'd': t *= 24 case 'h': t *= 60 case 'm': t *= 60 diff --git a/src/common/yt/urlCache.ts b/src/common/yt/urlCache.ts index a68aef7..1a47c8d 100644 --- a/src/common/yt/urlCache.ts +++ b/src/common/yt/urlCache.ts @@ -2,21 +2,18 @@ let db: IDBDatabase | null = null -if (typeof self.indexedDB !== 'undefined') -{ +if (typeof self.indexedDB !== 'undefined') { const openRequest = indexedDB.open("yt-url-resolver-cache") openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt")) // Delete Expired - openRequest.addEventListener('success', () => - { + openRequest.addEventListener('success', () => { db = openRequest.result const transaction = db.transaction("store", "readwrite") const range = IDBKeyRange.upperBound(new Date()) const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range) - expireAtCursorRequest.addEventListener('success', () => - { + expireAtCursorRequest.addEventListener('success', () => { const expireCursor = expireAtCursorRequest.result if (!expireCursor) return expireCursor.delete() @@ -27,10 +24,8 @@ if (typeof self.indexedDB !== 'undefined') else console.warn(`IndexedDB not supported`) -async function put(url: string | null, id: string): Promise -{ - return await new Promise((resolve, reject) => - { +async function put(url: string | null, id: string): Promise { + return await new Promise((resolve, reject) => { const store = db?.transaction("store", "readwrite").objectStore("store") if (!store) return resolve() const request = store.put({ value: url, expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, id) @@ -39,10 +34,8 @@ async function put(url: string | null, id: string): Promise }) } -async function get(id: string): Promise -{ - return (await new Promise((resolve, reject) => - { +async function get(id: string): Promise { + return (await new Promise((resolve, reject) => { const store = db?.transaction("store", "readonly").objectStore("store") if (!store) return resolve(null) const request = store.get(id) diff --git a/src/common/yt/urlResolve.ts b/src/common/yt/urlResolve.ts index dd955e3..1e6c37e 100644 --- a/src/common/yt/urlResolve.ts +++ b/src/common/yt/urlResolve.ts @@ -5,8 +5,7 @@ import { LbryPathnameCache } from "./urlCache" // const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS const QUERY_CHUNK_SIZE = 300 -export interface YtIdResolverDescriptor -{ +export interface YtIdResolverDescriptor { id: string type: 'channel' | 'video' } @@ -15,33 +14,29 @@ export interface YtIdResolverDescriptor * @param descriptorsWithIndex YT resource IDs to check * @returns a promise with the list of channels that were found on lbry */ -export async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> -{ +export async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> { let descriptorsPayload: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({ ...descriptor, index })) descriptors = null as any - const results: (string | null)[] = []; + const results: (string | null)[] = [] - descriptorsPayload = (await Promise.all(descriptorsPayload.map(async (descriptor, index) => - { + descriptorsPayload = (await Promise.all(descriptorsPayload.map(async (descriptor, index) => { if (!descriptor?.id) return const cache = await LbryPathnameCache.get(descriptor.id) // Cache can be null, if there is no lbry url yet - if (cache !== undefined) - { + if (cache !== undefined) { // Null values shouldn't be in the results if (cache) results[index] = cache return } - + return descriptor }))).filter((descriptor) => descriptor) as any const descriptorsPayloadChunks = chunk(descriptorsPayload, QUERY_CHUNK_SIZE) let progressCount = 0 - await Promise.all(descriptorsPayloadChunks.map(async (descriptorChunk) => - { + await Promise.all(descriptorsPayloadChunks.map(async (descriptorChunk) => { const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() @@ -49,16 +44,12 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres const url = new URL(`https://${urlResolverSetting.hostname}`) - function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) - { - for (const path of responsePath) - { - switch (typeof path) - { + function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) { + for (const path of responsePath) { + switch (typeof path) { case 'string': case 'number': response = response[path]; continue } - switch (path) - { + switch (path) { case Keys: response = Object.keys(response); continue case Values: response = Object.values(response); continue } @@ -66,19 +57,15 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres return response as T } - async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsPayload) - { + async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsPayload) { url.pathname = urlResolverFunction.pathname - if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) - { - await Promise.all(descriptorsGroup.map(async (descriptor) => - { + if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) { + await Promise.all(descriptorsGroup.map(async (descriptor) => { url.searchParams.set(urlResolverFunction.paramName, descriptor.id) const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) - if (apiResponse.ok) - { + if (apiResponse.ok) { const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) if (value) results[descriptor.index] = value await LbryPathnameCache.put(value, descriptor.id) @@ -89,18 +76,15 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres if (progressCallback) progressCallback(progressCount / descriptorsPayload.length) })) } - else - { + else { url.searchParams.set(urlResolverFunction.paramName, descriptorsGroup .map((descriptor) => descriptor.id) .join(urlResolverFunction.paramArraySeperator)) const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) - if (apiResponse.ok) - { + if (apiResponse.ok) { const values = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) - await Promise.all(values.map(async (value, index) => - { + await Promise.all(values.map(async (value, index) => { const descriptor = descriptorsGroup[index] if (value) results[descriptor.index] = value await LbryPathnameCache.put(value, descriptor.id) @@ -115,6 +99,6 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres if (descriptorsGroupedByType['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel']) if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video']) })) - if (progressCallback) progressCallback(1); + if (progressCallback) progressCallback(1) return results } \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts index f7ba0eb..55d74f2 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,4 +1,4 @@ declare module '*.md' { - var _: string; - export default _; + var _: string + export default _ } diff --git a/src/manifest.json b/src/manifest.json index c34f56f..8b25bec 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -46,4 +46,4 @@ "128": "icons/wol/icon128.png" }, "manifest_version": 2 -} +} \ No newline at end of file diff --git a/src/popup/popup.html b/src/popup/popup.html index 9f432db..d419cc8 100644 --- a/src/popup/popup.html +++ b/src/popup/popup.html @@ -11,4 +11,4 @@ - + \ No newline at end of file diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx index e901b05..b804a72 100644 --- a/src/popup/popup.tsx +++ b/src/popup/popup.tsx @@ -1,21 +1,21 @@ import { h, render } from 'preact' import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio' -import { getTargetPlatfromSettingsEntiries, ExtensionSettings, TargetPlatformName, getYtUrlResolversSettingsEntiries, YTUrlResolverName } from '../common/settings' +import { ExtensionSettings, getTargetPlatfromSettingsEntiries, getYtUrlResolversSettingsEntiries, TargetPlatformName, YTUrlResolverName } from '../common/settings' import { useLbrySettings } from '../common/useSettings' import './popup.sass' /** Utilty to set a setting in the browser */ -const setSetting = (setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value }); +const setSetting = (setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value }) /** Gets all the options for redirect destinations as selection options */ const platformOptions: SelectionOption[] = getTargetPlatfromSettingsEntiries() - .map(([value, { displayName: display }]) => ({ value, display })); + .map(([value, { displayName: display }]) => ({ value, display })) - const ytUrlResolverOptions: SelectionOption[] = getYtUrlResolversSettingsEntiries() - .map(([value, { name: display }]) => ({ value, display })); +const ytUrlResolverOptions: SelectionOption[] = getYtUrlResolversSettingsEntiries() + .map(([value, { name: display }]) => ({ value, display })) function WatchOnLbryPopup() { - const { redirect, targetPlatform, urlResolver } = useLbrySettings(); + const { redirect, targetPlatform, urlResolver } = useLbrySettings() return ; + } -render(, document.getElementById('root')!); +render(, document.getElementById('root')!) diff --git a/src/scripts/background.ts b/src/scripts/background.ts index 95ad564..5e5abf4 100644 --- a/src/scripts/background.ts +++ b/src/scripts/background.ts @@ -1,10 +1,10 @@ import { parseProtocolUrl } from '../common/lbry-url' import { resolveById, YtIdResolverDescriptor } from '../common/yt/urlResolve' 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 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> = {} @@ -18,7 +18,7 @@ async function lbryPathnameFromVideoId(videoId: string): Promise chrome.runtime.onMessage.addListener(({ videoId }: { videoId: string }, sender, sendResponse) => { lbryPathnameFromVideoId(videoId).then((lbryPathname) => sendResponse(lbryPathname)) - return true; + return true }) -chrome.tabs.onUpdated.addListener((tabId, changeInfo) => changeInfo.url && chrome.tabs.sendMessage(tabId, { })); \ No newline at end of file +chrome.tabs.onUpdated.addListener((tabId, changeInfo) => changeInfo.url && chrome.tabs.sendMessage(tabId, {})) \ No newline at end of file diff --git a/src/scripts/storageSetup.ts b/src/scripts/storageSetup.ts index 7f90e83..ccfbcac 100644 --- a/src/scripts/storageSetup.ts +++ b/src/scripts/storageSetup.ts @@ -1,28 +1,28 @@ -import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync } from '../common/settings'; +import { DEFAULT_SETTINGS, ExtensionSettings, getExtensionSettingsAsync } from '../common/settings' /** Reset settings to default value and update the browser badge text */ async function initSettings() { - const settings = await getExtensionSettingsAsync(); + const settings = await getExtensionSettingsAsync() // get all the values that aren't set and use them as a change set const invalidEntries = (Object.entries(DEFAULT_SETTINGS) as Array<[keyof ExtensionSettings, ExtensionSettings[keyof ExtensionSettings]]>) - .filter(([k]) => settings[k] === null || settings[k] === undefined); + .filter(([k]) => settings[k] === null || settings[k] === undefined) // fix our local var and set it in storage for later if (invalidEntries.length > 0) { - const changeSet = Object.fromEntries(invalidEntries); - Object.assign(settings, changeSet); - chrome.storage.local.set(changeSet); + const changeSet = Object.fromEntries(invalidEntries) + Object.assign(settings, changeSet) + chrome.storage.local.set(changeSet) } - chrome.browserAction.setBadgeText({ text: settings.redirect ? 'ON' : 'OFF' }); + chrome.browserAction.setBadgeText({ text: settings.redirect ? 'ON' : 'OFF' }) } chrome.storage.onChanged.addListener((changes, areaName) => { - if (areaName !== 'local' || !changes.redirect) return; - chrome.browserAction.setBadgeText({ text: changes.redirect.newValue ? 'ON' : 'OFF' }); -}); + if (areaName !== 'local' || !changes.redirect) return + chrome.browserAction.setBadgeText({ text: changes.redirect.newValue ? 'ON' : 'OFF' }) +}) -chrome.runtime.onStartup.addListener(initSettings); -chrome.runtime.onInstalled.addListener(initSettings); +chrome.runtime.onStartup.addListener(initSettings) +chrome.runtime.onInstalled.addListener(initSettings) diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index be7631c..50ea6ac 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -4,22 +4,19 @@ import { parseYouTubeURLTimeString } from '../common/yt' const sleep = (t: number) => new Promise(resolve => setTimeout(resolve, t)) -interface WatchOnLbryButtonParameters -{ +interface WatchOnLbryButtonParameters { targetPlatform?: TargetPlatform lbryPathname?: string time?: number } -interface Target -{ +interface Target { platfrom: TargetPlatform lbryPathname: string time: number | null } -function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryButtonParameters) -{ +function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryButtonParameters) { if (!lbryPathname || !targetPlatform) return null const url = new URL(`${targetPlatform.domainPrefix}${lbryPathname}`) @@ -49,26 +46,22 @@ function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryBu } -function updateButton(mountPoint: HTMLDivElement, target: Target | null): void -{ +function updateButton(mountPoint: HTMLDivElement, target: Target | null): void { if (!target) return render(, mountPoint) render(, mountPoint) } -async function redirectTo({ lbryPathname, platfrom, time }: Target) -{ +async function redirectTo({ lbryPathname, platfrom, time }: Target) { const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`) if (time) url.searchParams.set('t', time.toFixed(0)) - findVideoElement().then((videoElement) => - { + findVideoElement().then((videoElement) => { videoElement.addEventListener('play', () => videoElement.pause(), { once: true }) videoElement.pause() }) - if (platfrom === targetPlatformSettings.app) - { + if (platfrom === targetPlatformSettings.app) { if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true })) open(url, '_blank') if (window.history.length === 1) window.close() @@ -78,8 +71,7 @@ async function redirectTo({ lbryPathname, platfrom, time }: Target) } /** Returns a mount point for the button */ -async function findButtonMountPoint(): Promise -{ +async function findButtonMountPoint(): Promise { const id = 'watch-on-lbry-button-container' let mountBefore: HTMLDivElement | null = null const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname) @@ -97,8 +89,7 @@ async function findButtonMountPoint(): Promise return div } -async function findVideoElement() -{ +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 @@ -107,20 +98,17 @@ async function findVideoElement() } // 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 requestLbryPathname(videoId: string) -{ +async function requestLbryPathname(videoId: string) { return await new Promise((resolve) => chrome.runtime.sendMessage({ videoId }, resolve)) } // Start -(async () => -{ +(async () => { const settings = await getExtensionSettingsAsync() let updater: (() => Promise) // Listen Settings Change - chrome.storage.onChanged.addListener(async (changes, areaName) => - { + 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() @@ -133,8 +121,7 @@ async function requestLbryPathname(videoId: string) // Listen URL Change chrome.runtime.onMessage.addListener(() => updater()) - async function getTargetByURL(url: URL) - { + async function getTargetByURL(url: URL) { if (url.pathname !== '/watch') return null const videoId = url.searchParams.get('v') @@ -145,32 +132,28 @@ async function requestLbryPathname(videoId: string) } let removeVideoTimeUpdateListener: (() => void) | null = null - async function onModeChange() - { + async function onModeChange() { let target: Target | null = null if (settings.redirect) - updater = async () => - { + 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 - { + 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 () => - { + + updater = async () => { const url = new URL(location.href) target = await getTargetByURL(url) if (target) target.time = getTime() diff --git a/src/tools/YTtoLBRY.html b/src/tools/YTtoLBRY.html index 1488422..3a62039 100644 --- a/src/tools/YTtoLBRY.html +++ b/src/tools/YTtoLBRY.html @@ -1,13 +1,15 @@ - - - Subscription Converter - - - - -
- - + + + Subscription Converter + + + + + +
+ + + \ No newline at end of file diff --git a/src/tools/YTtoLBRY.tsx b/src/tools/YTtoLBRY.tsx index b350bec..5faaa29 100644 --- a/src/tools/YTtoLBRY.tsx +++ b/src/tools/YTtoLBRY.tsx @@ -12,23 +12,23 @@ import readme from './README.md' * @returns a promise with the list of channels that were found on lbry */ async function lbryChannelsFromFile(file: File) { - const ext = file.name.split('.').pop()?.toLowerCase(); - + const ext = file.name.split('.').pop()?.toLowerCase() + const ids = new Set(( - ext === 'xml' || ext == 'opml' ? getSubsFromOpml : - ext === 'csv' ? getSubsFromCsv : - getSubsFromJson)(await getFileContent(file))) + ext === 'xml' || ext == 'opml' ? getSubsFromOpml : + ext === 'csv' ? getSubsFromCsv : + getSubsFromJson)(await getFileContent(file))) const lbryPathnames = await resolveById( - Array.from(ids).map(id => ({ id, type: 'channel' } as const)), - (progress) => render(, document.getElementById('root')!)); - const { targetPlatform: platform } = await getExtensionSettingsAsync(); - const urlPrefix = targetPlatformSettings[platform].domainPrefix; - return lbryPathnames.map(channel => urlPrefix + channel); + Array.from(ids).map(id => ({ id, type: 'channel' } as const)), + (progress) => render(, document.getElementById('root')!)) + const { targetPlatform: platform } = await getExtensionSettingsAsync() + const urlPrefix = targetPlatformSettings[platform].domainPrefix + return lbryPathnames.map(channel => urlPrefix + channel) } function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise | void, progress: number }) { - const [file, setFile] = useState(null as File | null); - const [isLoading, setLoading] = useState(false); + const [file, setFile] = useState(null as File | null) + const [isLoading, setLoading] = useState(false) return

Select YouTube Subscriptions

@@ -36,10 +36,10 @@ function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise< setFile(e.currentTarget.files?.length ? e.currentTarget.files[0] : null)} />