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] =?UTF-8?q?=F0=9F=8D=A3=20Formatted=20Files,=20Organized?= =?UTF-8?q?=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
@@ -39,7 +39,7 @@ function WatchOnLbryPopup() {
-
; + } -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)} />