mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
🍣 Formatted Files, Organized Imports
This commit is contained in:
parent
8f75c67601
commit
2c75082af9
17 changed files with 228 additions and 286 deletions
|
@ -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 {
|
export interface SelectionOption {
|
||||||
value: string
|
value: string
|
||||||
|
@ -9,13 +9,13 @@ export interface SelectionOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ButtonRadioProps<T extends string | SelectionOption = string> {
|
export interface ButtonRadioProps<T extends string | SelectionOption = string> {
|
||||||
name?: string;
|
name?: string
|
||||||
onChange(redirect: string): void;
|
onChange(redirect: string): void
|
||||||
value: T extends SelectionOption ? T['value'] : T;
|
value: T extends SelectionOption ? T['value'] : T
|
||||||
options: 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<T extends string | SelectionOption = string>({ name = 'buttonRadio', onChange, options, value }: ButtonRadioProps<T>) {
|
export default function ButtonRadio<T extends string | SelectionOption = string>({ name = 'buttonRadio', onChange, options, value }: ButtonRadioProps<T>) {
|
||||||
/** If it's a string, return the string, if it's a SelectionOption get the selection option property */
|
/** 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<T extends string | SelectionOption = string>
|
||||||
<label>{display}</label>
|
<label>{display}</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>;
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { appRedirectUrl, parseProtocolUrl } from './lbry-url';
|
import { appRedirectUrl, parseProtocolUrl } from './lbry-url'
|
||||||
|
|
||||||
describe('web url parsing', () => {
|
describe('web url parsing', () => {
|
||||||
const testCases: [string, string | undefined][] = [
|
const testCases: [string, string | undefined][] = [
|
||||||
|
@ -9,21 +9,21 @@ describe('web url parsing', () => {
|
||||||
['https://lbry.tv/@test:c', 'lbry://@test:c'],
|
['https://lbry.tv/@test:c', 'lbry://@test:c'],
|
||||||
['https://lbry.tv/$/discover?t=foo%20bar', undefined],
|
['https://lbry.tv/$/discover?t=foo%20bar', undefined],
|
||||||
['https://lbry.tv/$/signup?redirect=/@test1:0/foo-bar-2-baz-7:e#adasasddasdas123', undefined],
|
['https://lbry.tv/$/signup?redirect=/@test1:0/foo-bar-2-baz-7:e#adasasddasdas123', undefined],
|
||||||
];
|
]
|
||||||
|
|
||||||
test.each(testCases)('redirect %s', (url, expected) => {
|
test.each(testCases)('redirect %s', (url, expected) => {
|
||||||
expect(appRedirectUrl(url)).toEqual(expected);
|
expect(appRedirectUrl(url)).toEqual(expected)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
describe('app url parsing', () => {
|
describe('app url parsing', () => {
|
||||||
const testCases: Array<[string, string[]]> = [
|
const testCases: Array<[string, string[]]> = [
|
||||||
['test', ['test']],
|
['test', ['test']],
|
||||||
['@test', ['@test']],
|
['@test', ['@test']],
|
||||||
['lbry://@test$1/stuff', ['@test$1', 'stuff']],
|
['lbry://@test$1/stuff', ['@test$1', 'stuff']],
|
||||||
];
|
]
|
||||||
|
|
||||||
test.each(testCases)('redirect %s', (url, expected) => {
|
test.each(testCases)('redirect %s', (url, expected) => {
|
||||||
expect(parseProtocolUrl(url)).toEqual(expected);
|
expect(parseProtocolUrl(url)).toEqual(expected)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
|
@ -8,14 +8,14 @@ interface UrlOptions {
|
||||||
encode?: boolean
|
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 */
|
/** 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 */
|
/** Creates a non-capturing group */
|
||||||
const group = (regex: string) => `(?:${regex})`;
|
const group = (regex: string) => `(?:${regex})`
|
||||||
/** Allows for one of the patterns */
|
/** 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 */
|
/** Create an lbry url claim */
|
||||||
const claim = (name: string, prefix = '') => group(
|
const claim = (name: string, prefix = '') => group(
|
||||||
named(`${name}_name`, prefix + invalidNamesRegex)
|
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}_sequence`, '[1-9][0-9]*')),
|
||||||
group('\\$' + named(`${name}_amount_order`, '[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 */
|
/** Create an lbry url claim, but use the old pattern for claims */
|
||||||
const legacyClaim = (name: string, prefix = '') => group(
|
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}_claim_id`, '[0-9a-f]{1,40}')),
|
||||||
group(':' + named(`${name}_sequence`, '[1-9][0-9]*')),
|
group(':' + named(`${name}_sequence`, '[1-9][0-9]*')),
|
||||||
group('\\$' + named(`${name}_amount_order`, '[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. */
|
/** Creates a pattern to parse lbry protocol URLs. Unused, but I left it here. */
|
||||||
function createProtocolUrlRegex(legacy = false) {
|
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(
|
return new RegExp('^' + named('scheme', 'lbry://') + '?' + oneOf(
|
||||||
group(claim('channel_with_stream', '@') + '/' + claim('stream_in_channel')),
|
group(claim('channel_with_stream', '@') + '/' + claim('stream_in_channel')),
|
||||||
claim('channel', '@'),
|
claim('channel', '@'),
|
||||||
claim('stream'),
|
claim('stream'),
|
||||||
) + '$');
|
) + '$')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates a pattern to match lbry.tv style sites by their pathname */
|
/** Creates a pattern to match lbry.tv style sites by their pathname */
|
||||||
function createWebUrlRegex(legacy = false) {
|
function createWebUrlRegex(legacy = false) {
|
||||||
const claim = legacy ? builder.legacyClaim : builder.claim;
|
const claim = legacy ? builder.legacyClaim : builder.claim
|
||||||
return new RegExp('^/' + oneOf(
|
return new RegExp('^/' + oneOf(
|
||||||
group(claim('channel_with_stream', '@') + '/' + claim('stream_in_channel')),
|
group(claim('channel_with_stream', '@') + '/' + claim('stream_in_channel')),
|
||||||
claim('channel', '@'),
|
claim('channel', '@'),
|
||||||
claim('stream'),
|
claim('stream'),
|
||||||
) + '$');
|
) + '$')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pattern for lbry.tv style sites */
|
/** Pattern for lbry.tv style sites */
|
||||||
export const URL_REGEX = createWebUrlRegex();
|
export const URL_REGEX = createWebUrlRegex()
|
||||||
export const PROTOCOL_URL_REGEX = createProtocolUrlRegex();
|
export const PROTOCOL_URL_REGEX = createProtocolUrlRegex()
|
||||||
const PROTOCOL_URL_REGEX_LEGACY = createProtocolUrlRegex(true);
|
const PROTOCOL_URL_REGEX_LEGACY = createProtocolUrlRegex(true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates a lbry url path segment.
|
* Encapsulates a lbry url path segment.
|
||||||
|
@ -78,15 +78,15 @@ export class PathSegment {
|
||||||
groups[`${segment}_claim_id`],
|
groups[`${segment}_claim_id`],
|
||||||
parseInt(groups[`${segment}_sequence`]),
|
parseInt(groups[`${segment}_sequence`]),
|
||||||
parseInt(groups[`${segment}_amount_order`])
|
parseInt(groups[`${segment}_amount_order`])
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prints the segment */
|
/** Prints the segment */
|
||||||
toString() {
|
toString() {
|
||||||
if (this.claimID) return `${this.name}:${this.claimID}`;
|
if (this.claimID) return `${this.name}:${this.claimID}`
|
||||||
if (this.sequence) return `${this.name}*${this.sequence}`;
|
if (this.sequence) return `${this.name}*${this.sequence}`
|
||||||
if (this.amountOrder) return `${this.name}$${this.amountOrder}`;
|
if (this.amountOrder) return `${this.name}$${this.amountOrder}`
|
||||||
return this.name;
|
return this.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,18 +98,18 @@ export class PathSegment {
|
||||||
* @returns an array of path segments; if invalid, will return an empty array
|
* @returns an array of path segments; if invalid, will return an empty array
|
||||||
*/
|
*/
|
||||||
function patternSegmenter(ptn: RegExp, url: string, options: UrlOptions = { encode: false }): string[] {
|
function patternSegmenter(ptn: RegExp, url: string, options: UrlOptions = { encode: false }): string[] {
|
||||||
const match = url.match(ptn)?.groups;
|
const match = url.match(ptn)?.groups
|
||||||
if (!match) return [];
|
if (!match) return []
|
||||||
|
|
||||||
const segments = match['channel_name'] ? ['channel']
|
const segments = match['channel_name'] ? ['channel']
|
||||||
: match['channel_with_stream_name'] ? ['channel_with_stream', 'stream_in_channel']
|
: match['channel_with_stream_name'] ? ['channel_with_stream', 'stream_in_channel']
|
||||||
: match['stream_name'] ? ['stream']
|
: match['stream_name'] ? ['stream']
|
||||||
: null;
|
: 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())
|
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
|
* @param options options for the redirect
|
||||||
*/
|
*/
|
||||||
export function appRedirectUrl(url: string, options?: UrlOptions): string | undefined {
|
export function appRedirectUrl(url: string, options?: UrlOptions): string | undefined {
|
||||||
const segments = patternSegmenter(URL_REGEX, new URL(url).pathname, options);
|
const segments = patternSegmenter(URL_REGEX, new URL(url).pathname, options)
|
||||||
if (segments.length === 0) return;
|
if (segments.length === 0) return
|
||||||
const path = segments.join('/');
|
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[] {
|
export function parseProtocolUrl(url: string, options: UrlOptions = { encode: false }): string[] {
|
||||||
for (const ptn of [PROTOCOL_URL_REGEX, PROTOCOL_URL_REGEX_LEGACY]) {
|
for (const ptn of [PROTOCOL_URL_REGEX, PROTOCOL_URL_REGEX_LEGACY]) {
|
||||||
const segments = patternSegmenter(ptn, url, options);
|
const segments = patternSegmenter(ptn, url, options)
|
||||||
if (segments.length === 0) continue;
|
if (segments.length === 0) continue
|
||||||
return segments;
|
return segments
|
||||||
}
|
}
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { JSX } from "preact"
|
import { JSX } from "preact"
|
||||||
|
|
||||||
export interface ExtensionSettings
|
export interface ExtensionSettings {
|
||||||
{
|
|
||||||
redirect: boolean
|
redirect: boolean
|
||||||
targetPlatform: TargetPlatformName
|
targetPlatform: TargetPlatformName
|
||||||
urlResolver: YTUrlResolverName
|
urlResolver: YTUrlResolverName
|
||||||
|
@ -9,15 +8,13 @@ export interface ExtensionSettings
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee', urlResolver: 'lbryInc' }
|
export const DEFAULT_SETTINGS: ExtensionSettings = { redirect: true, targetPlatform: 'odysee', urlResolver: 'lbryInc' }
|
||||||
|
|
||||||
export function getExtensionSettingsAsync(): Promise<ExtensionSettings>
|
export function getExtensionSettingsAsync(): Promise<ExtensionSettings> {
|
||||||
{
|
|
||||||
return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any)))
|
return new Promise(resolve => chrome.storage.local.get(o => resolve(o as any)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type TargetPlatformName = 'madiator.com' | 'odysee' | 'app'
|
export type TargetPlatformName = 'madiator.com' | 'odysee' | 'app'
|
||||||
export interface TargetPlatform
|
export interface TargetPlatform {
|
||||||
{
|
|
||||||
domainPrefix: string
|
domainPrefix: string
|
||||||
displayName: string
|
displayName: string
|
||||||
theme: string
|
theme: string
|
||||||
|
@ -66,16 +63,14 @@ export const targetPlatformSettings: Record<TargetPlatformName, TargetPlatform>
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTargetPlatfromSettingsEntiries = () =>
|
export const getTargetPlatfromSettingsEntiries = () => {
|
||||||
{
|
|
||||||
return Object.entries(targetPlatformSettings) as any as [Extract<keyof typeof targetPlatformSettings, string>, TargetPlatform][]
|
return Object.entries(targetPlatformSettings) as any as [Extract<keyof typeof targetPlatformSettings, string>, TargetPlatform][]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type SourcePlatformName = 'youtube.com' | 'yewtu.be'
|
export type SourcePlatformName = 'youtube.com' | 'yewtu.be'
|
||||||
export interface SourcePlatform
|
export interface SourcePlatform {
|
||||||
{
|
|
||||||
hostnames: string[]
|
hostnames: string[]
|
||||||
htmlQueries: {
|
htmlQueries: {
|
||||||
mountButtonBefore: string,
|
mountButtonBefore: string,
|
||||||
|
@ -100,8 +95,7 @@ export const sourcePlatfromSettings: Record<SourcePlatformName, SourcePlatform>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSourcePlatfromSettingsFromHostname(hostname: string)
|
export function getSourcePlatfromSettingsFromHostname(hostname: string) {
|
||||||
{
|
|
||||||
const values = Object.values(sourcePlatfromSettings)
|
const values = Object.values(sourcePlatfromSettings)
|
||||||
for (const settings of values)
|
for (const settings of values)
|
||||||
if (settings.hostnames.includes(hostname)) return settings
|
if (settings.hostnames.includes(hostname)) return settings
|
||||||
|
@ -115,15 +109,13 @@ export const Keys = Symbol('keys')
|
||||||
export const Values = Symbol('values')
|
export const Values = Symbol('values')
|
||||||
export const SingleValueAtATime = Symbol()
|
export const SingleValueAtATime = Symbol()
|
||||||
export type YtUrlResolveResponsePath = (string | number | typeof Keys | typeof Values)[]
|
export type YtUrlResolveResponsePath = (string | number | typeof Keys | typeof Values)[]
|
||||||
export interface YtUrlResolveFunction
|
export interface YtUrlResolveFunction {
|
||||||
{
|
|
||||||
pathname: string
|
pathname: string
|
||||||
paramName: string
|
paramName: string
|
||||||
paramArraySeperator: string | typeof SingleValueAtATime
|
paramArraySeperator: string | typeof SingleValueAtATime
|
||||||
responsePath: YtUrlResolveResponsePath
|
responsePath: YtUrlResolveResponsePath
|
||||||
}
|
}
|
||||||
export interface YTUrlResolver
|
export interface YTUrlResolver {
|
||||||
{
|
|
||||||
name: string
|
name: string
|
||||||
hostname: string
|
hostname: string
|
||||||
functions: {
|
functions: {
|
||||||
|
@ -171,7 +163,6 @@ export const ytUrlResolversSettings: Record<YTUrlResolverName, YTUrlResolver> =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getYtUrlResolversSettingsEntiries = () =>
|
export const getYtUrlResolversSettingsEntiries = () => {
|
||||||
{
|
|
||||||
return Object.entries(ytUrlResolversSettings) as any as [Extract<keyof typeof ytUrlResolversSettings, string>, YTUrlResolver][]
|
return Object.entries(ytUrlResolversSettings) as any as [Extract<keyof typeof ytUrlResolversSettings, string>, YTUrlResolver][]
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { useReducer, useEffect } from 'preact/hooks';
|
import { useEffect, useReducer } from 'preact/hooks'
|
||||||
import { DEFAULT_SETTINGS } from './settings';
|
import { DEFAULT_SETTINGS } from './settings'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook to read the settings from local storage
|
* 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
|
* @param initial the default value. Must have all relevant keys present and should not change
|
||||||
*/
|
*/
|
||||||
export function useSettings<T extends object>(initial: T) {
|
export function useSettings<T extends object>(initial: T) {
|
||||||
const [state, dispatch] = useReducer((state, nstate: Partial<T>) => ({ ...state, ...nstate }), initial);
|
const [state, dispatch] = useReducer((state, nstate: Partial<T>) => ({ ...state, ...nstate }), initial)
|
||||||
// register change listeners, gets current values, and cleans up the listeners on unload
|
// register change listeners, gets current values, and cleans up the listeners on unload
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const changeListener = (changes: Record<string, chrome.storage.StorageChange>, areaName: string) => {
|
const changeListener = (changes: Record<string, chrome.storage.StorageChange>, areaName: string) => {
|
||||||
if (areaName !== 'local') return;
|
if (areaName !== 'local') return
|
||||||
const changeSet = Object.keys(changes)
|
const changeSet = Object.keys(changes)
|
||||||
.filter(k => Object.keys(initial).includes(k))
|
.filter(k => Object.keys(initial).includes(k))
|
||||||
.map(k => [k, changes[k].newValue]);
|
.map(k => [k, changes[k].newValue])
|
||||||
if (changeSet.length === 0) return; // no changes; no use dispatching
|
if (changeSet.length === 0) return // no changes; no use dispatching
|
||||||
dispatch(Object.fromEntries(changeSet));
|
dispatch(Object.fromEntries(changeSet))
|
||||||
};
|
}
|
||||||
chrome.storage.onChanged.addListener(changeListener);
|
chrome.storage.onChanged.addListener(changeListener)
|
||||||
chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial<T>));
|
chrome.storage.local.get(Object.keys(initial), o => dispatch(o as Partial<T>))
|
||||||
return () => chrome.storage.onChanged.removeListener(changeListener);
|
return () => chrome.storage.onChanged.removeListener(changeListener)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return state;
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A hook to read watch on lbry settings from local storage */
|
/** A hook to read watch on lbry settings from local storage */
|
||||||
export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS);
|
export const useLbrySettings = () => useSettings(DEFAULT_SETTINGS)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
|
|
||||||
interface YtExportedJsonSubscription
|
interface YtExportedJsonSubscription {
|
||||||
{
|
|
||||||
id: string
|
id: string
|
||||||
etag: string
|
etag: string
|
||||||
title: string
|
title: string
|
||||||
|
@ -17,14 +16,11 @@ interface YtExportedJsonSubscription
|
||||||
* @param file to load
|
* @param file to load
|
||||||
* @returns a promise with the file as a string
|
* @returns a promise with the file as a string
|
||||||
*/
|
*/
|
||||||
export function getFileContent(file: File): Promise<string>
|
export function getFileContent(file: File): Promise<string> {
|
||||||
{
|
return new Promise((resolve, reject) => {
|
||||||
return new Promise((resolve, reject) =>
|
|
||||||
{
|
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.addEventListener('load', event => resolve(event.target?.result as string || ''))
|
reader.addEventListener('load', event => resolve(event.target?.result as string || ''))
|
||||||
reader.addEventListener('error', () =>
|
reader.addEventListener('error', () => {
|
||||||
{
|
|
||||||
reader.abort()
|
reader.abort()
|
||||||
reject(new DOMException(`Could not read ${file.name}`))
|
reject(new DOMException(`Could not read ${file.name}`))
|
||||||
})
|
})
|
||||||
|
@ -39,42 +35,39 @@ export function getFileContent(file: File): Promise<string>
|
||||||
* @param opmlContents an opml file as as tring
|
* @param opmlContents an opml file as as tring
|
||||||
* @returns the channel IDs
|
* @returns the channel IDs
|
||||||
*/
|
*/
|
||||||
export function getSubsFromOpml(opmlContents: string): string[]
|
export function getSubsFromOpml(opmlContents: string): string[] {
|
||||||
{
|
const opml = new DOMParser().parseFromString(opmlContents, 'application/xml')
|
||||||
const opml = new DOMParser().parseFromString(opmlContents, 'application/xml')
|
opmlContents = ''
|
||||||
opmlContents = ''
|
return Array.from(opml.querySelectorAll('outline > outline'))
|
||||||
return Array.from(opml.querySelectorAll('outline > outline'))
|
.map(outline => outline.getAttribute('xmlUrl'))
|
||||||
.map(outline => outline.getAttribute('xmlUrl'))
|
.filter((url): url is string => !!url)
|
||||||
.filter((url): url is string => !!url)
|
.map(url => getChannelId(url))
|
||||||
.map(url => getChannelId(url))
|
.filter((url): url is string => !!url) // we don't want it if it's empty
|
||||||
.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
|
* Reads an array of YT channel IDs from the YT subscriptions JSON file
|
||||||
*
|
*
|
||||||
* @param jsonContents a JSON file as a string
|
* @param jsonContents a JSON file as a string
|
||||||
* @returns the channel IDs
|
* @returns the channel IDs
|
||||||
*/
|
*/
|
||||||
export function getSubsFromJson(jsonContents: string): string[]
|
export function getSubsFromJson(jsonContents: string): string[] {
|
||||||
{
|
const subscriptions: YtExportedJsonSubscription[] = JSON.parse(jsonContents)
|
||||||
const subscriptions: YtExportedJsonSubscription[] = JSON.parse(jsonContents)
|
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
|
* Reads an array of YT channel IDs from the YT subscriptions CSV file
|
||||||
*
|
*
|
||||||
* @param csvContent a CSV file as a string
|
* @param csvContent a CSV file as a string
|
||||||
* @returns the channel IDs
|
* @returns the channel IDs
|
||||||
*/
|
*/
|
||||||
export function getSubsFromCsv(csvContent: string): string[]
|
export function getSubsFromCsv(csvContent: string): string[] {
|
||||||
{
|
const rows = csvContent.split('\n')
|
||||||
const rows = csvContent.split('\n')
|
csvContent = ''
|
||||||
csvContent = ''
|
return rows.slice(1).map((row) => row.substring(0, row.indexOf(',')))
|
||||||
return rows.slice(1).map((row) => row.substring(0, row.indexOf(',')))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the channelID from a YT URL.
|
* Extracts the channelID from a YT URL.
|
||||||
|
@ -83,23 +76,19 @@ export function getFileContent(file: File): Promise<string>
|
||||||
* * /feeds/videos.xml?channel_id=*
|
* * /feeds/videos.xml?channel_id=*
|
||||||
* * /channel/*
|
* * /channel/*
|
||||||
*/
|
*/
|
||||||
export function getChannelId(channelURL: string)
|
export function getChannelId(channelURL: string) {
|
||||||
{
|
|
||||||
const match = channelURL.match(/channel\/([^\s?]*)/)
|
const match = channelURL.match(/channel\/([^\s?]*)/)
|
||||||
return match ? match[1] : new URL(channelURL).searchParams.get('channel_id')
|
return match ? match[1] : new URL(channelURL).searchParams.get('channel_id')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseYouTubeURLTimeString(timeString: string)
|
export function parseYouTubeURLTimeString(timeString: string) {
|
||||||
{
|
|
||||||
const signs = timeString.replace(/[0-9]/g, '')
|
const signs = timeString.replace(/[0-9]/g, '')
|
||||||
if (signs.length === 0) return null
|
if (signs.length === 0) return null
|
||||||
const numbers = timeString.replace(/[^0-9]/g, '-').split('-')
|
const numbers = timeString.replace(/[^0-9]/g, '-').split('-')
|
||||||
let total = 0
|
let total = 0
|
||||||
for (let i = 0; i < signs.length; i++)
|
for (let i = 0; i < signs.length; i++) {
|
||||||
{
|
|
||||||
let t = parseInt(numbers[i])
|
let t = parseInt(numbers[i])
|
||||||
switch (signs[i])
|
switch (signs[i]) {
|
||||||
{
|
|
||||||
case 'd': t *= 24
|
case 'd': t *= 24
|
||||||
case 'h': t *= 60
|
case 'h': t *= 60
|
||||||
case 'm': t *= 60
|
case 'm': t *= 60
|
||||||
|
|
|
@ -2,21 +2,18 @@
|
||||||
|
|
||||||
let db: IDBDatabase | null = null
|
let db: IDBDatabase | null = null
|
||||||
|
|
||||||
if (typeof self.indexedDB !== 'undefined')
|
if (typeof self.indexedDB !== 'undefined') {
|
||||||
{
|
|
||||||
const openRequest = indexedDB.open("yt-url-resolver-cache")
|
const openRequest = indexedDB.open("yt-url-resolver-cache")
|
||||||
openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt"))
|
openRequest.addEventListener('upgradeneeded', () => openRequest.result.createObjectStore("store").createIndex("expireAt", "expireAt"))
|
||||||
|
|
||||||
// Delete Expired
|
// Delete Expired
|
||||||
openRequest.addEventListener('success', () =>
|
openRequest.addEventListener('success', () => {
|
||||||
{
|
|
||||||
db = openRequest.result
|
db = openRequest.result
|
||||||
const transaction = db.transaction("store", "readwrite")
|
const transaction = db.transaction("store", "readwrite")
|
||||||
const range = IDBKeyRange.upperBound(new Date())
|
const range = IDBKeyRange.upperBound(new Date())
|
||||||
|
|
||||||
const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range)
|
const expireAtCursorRequest = transaction.objectStore("store").index("expireAt").openCursor(range)
|
||||||
expireAtCursorRequest.addEventListener('success', () =>
|
expireAtCursorRequest.addEventListener('success', () => {
|
||||||
{
|
|
||||||
const expireCursor = expireAtCursorRequest.result
|
const expireCursor = expireAtCursorRequest.result
|
||||||
if (!expireCursor) return
|
if (!expireCursor) return
|
||||||
expireCursor.delete()
|
expireCursor.delete()
|
||||||
|
@ -27,10 +24,8 @@ if (typeof self.indexedDB !== 'undefined')
|
||||||
else console.warn(`IndexedDB not supported`)
|
else console.warn(`IndexedDB not supported`)
|
||||||
|
|
||||||
|
|
||||||
async function put(url: string | null, id: string): Promise<void>
|
async function put(url: string | null, id: string): Promise<void> {
|
||||||
{
|
return await new Promise((resolve, reject) => {
|
||||||
return await new Promise((resolve, reject) =>
|
|
||||||
{
|
|
||||||
const store = db?.transaction("store", "readwrite").objectStore("store")
|
const store = db?.transaction("store", "readwrite").objectStore("store")
|
||||||
if (!store) return resolve()
|
if (!store) return resolve()
|
||||||
const request = store.put({ value: url, expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, id)
|
const request = store.put({ value: url, expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000) }, id)
|
||||||
|
@ -39,10 +34,8 @@ async function put(url: string | null, id: string): Promise<void>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(id: string): Promise<string | null>
|
async function get(id: string): Promise<string | null> {
|
||||||
{
|
return (await new Promise((resolve, reject) => {
|
||||||
return (await new Promise((resolve, reject) =>
|
|
||||||
{
|
|
||||||
const store = db?.transaction("store", "readonly").objectStore("store")
|
const store = db?.transaction("store", "readonly").objectStore("store")
|
||||||
if (!store) return resolve(null)
|
if (!store) return resolve(null)
|
||||||
const request = store.get(id)
|
const request = store.get(id)
|
||||||
|
|
|
@ -5,8 +5,7 @@ import { LbryPathnameCache } from "./urlCache"
|
||||||
// const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS
|
// const LBRY_API_HOST = 'https://api.odysee.com'; MOVED TO SETTINGS
|
||||||
const QUERY_CHUNK_SIZE = 300
|
const QUERY_CHUNK_SIZE = 300
|
||||||
|
|
||||||
export interface YtIdResolverDescriptor
|
export interface YtIdResolverDescriptor {
|
||||||
{
|
|
||||||
id: string
|
id: string
|
||||||
type: 'channel' | 'video'
|
type: 'channel' | 'video'
|
||||||
}
|
}
|
||||||
|
@ -15,21 +14,18 @@ export interface YtIdResolverDescriptor
|
||||||
* @param descriptorsWithIndex YT resource IDs to check
|
* @param descriptorsWithIndex YT resource IDs to check
|
||||||
* @returns a promise with the list of channels that were found on lbry
|
* @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 }))
|
let descriptorsPayload: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({ ...descriptor, index }))
|
||||||
descriptors = null as any
|
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
|
if (!descriptor?.id) return
|
||||||
const cache = await LbryPathnameCache.get(descriptor.id)
|
const cache = await LbryPathnameCache.get(descriptor.id)
|
||||||
|
|
||||||
// Cache can be null, if there is no lbry url yet
|
// Cache can be null, if there is no lbry url yet
|
||||||
if (cache !== undefined)
|
if (cache !== undefined) {
|
||||||
{
|
|
||||||
// Null values shouldn't be in the results
|
// Null values shouldn't be in the results
|
||||||
if (cache) results[index] = cache
|
if (cache) results[index] = cache
|
||||||
return
|
return
|
||||||
|
@ -40,8 +36,7 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres
|
||||||
|
|
||||||
const descriptorsPayloadChunks = chunk(descriptorsPayload, QUERY_CHUNK_SIZE)
|
const descriptorsPayloadChunks = chunk(descriptorsPayload, QUERY_CHUNK_SIZE)
|
||||||
let progressCount = 0
|
let progressCount = 0
|
||||||
await Promise.all(descriptorsPayloadChunks.map(async (descriptorChunk) =>
|
await Promise.all(descriptorsPayloadChunks.map(async (descriptorChunk) => {
|
||||||
{
|
|
||||||
const descriptorsGroupedByType: Record<YtIdResolverDescriptor['type'], typeof descriptorsPayload | null> = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any
|
const descriptorsGroupedByType: Record<YtIdResolverDescriptor['type'], typeof descriptorsPayload | null> = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any
|
||||||
|
|
||||||
const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync()
|
const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync()
|
||||||
|
@ -49,16 +44,12 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres
|
||||||
|
|
||||||
const url = new URL(`https://${urlResolverSetting.hostname}`)
|
const url = new URL(`https://${urlResolverSetting.hostname}`)
|
||||||
|
|
||||||
function followResponsePath<T>(response: any, responsePath: YtUrlResolveResponsePath)
|
function followResponsePath<T>(response: any, responsePath: YtUrlResolveResponsePath) {
|
||||||
{
|
for (const path of responsePath) {
|
||||||
for (const path of responsePath)
|
switch (typeof path) {
|
||||||
{
|
|
||||||
switch (typeof path)
|
|
||||||
{
|
|
||||||
case 'string': case 'number': response = response[path]; continue
|
case 'string': case 'number': response = response[path]; continue
|
||||||
}
|
}
|
||||||
switch (path)
|
switch (path) {
|
||||||
{
|
|
||||||
case Keys: response = Object.keys(response); continue
|
case Keys: response = Object.keys(response); continue
|
||||||
case Values: response = Object.values(response); continue
|
case Values: response = Object.values(response); continue
|
||||||
}
|
}
|
||||||
|
@ -66,19 +57,15 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres
|
||||||
return response as T
|
return response as T
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsPayload)
|
async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsPayload) {
|
||||||
{
|
|
||||||
url.pathname = urlResolverFunction.pathname
|
url.pathname = urlResolverFunction.pathname
|
||||||
|
|
||||||
if (urlResolverFunction.paramArraySeperator === SingleValueAtATime)
|
if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) {
|
||||||
{
|
await Promise.all(descriptorsGroup.map(async (descriptor) => {
|
||||||
await Promise.all(descriptorsGroup.map(async (descriptor) =>
|
|
||||||
{
|
|
||||||
url.searchParams.set(urlResolverFunction.paramName, descriptor.id)
|
url.searchParams.set(urlResolverFunction.paramName, descriptor.id)
|
||||||
|
|
||||||
const apiResponse = await fetch(url.toString(), { cache: 'no-store' })
|
const apiResponse = await fetch(url.toString(), { cache: 'no-store' })
|
||||||
if (apiResponse.ok)
|
if (apiResponse.ok) {
|
||||||
{
|
|
||||||
const value = followResponsePath<string>(await apiResponse.json(), urlResolverFunction.responsePath)
|
const value = followResponsePath<string>(await apiResponse.json(), urlResolverFunction.responsePath)
|
||||||
if (value) results[descriptor.index] = value
|
if (value) results[descriptor.index] = value
|
||||||
await LbryPathnameCache.put(value, descriptor.id)
|
await LbryPathnameCache.put(value, descriptor.id)
|
||||||
|
@ -89,18 +76,15 @@ export async function resolveById(descriptors: YtIdResolverDescriptor[], progres
|
||||||
if (progressCallback) progressCallback(progressCount / descriptorsPayload.length)
|
if (progressCallback) progressCallback(progressCount / descriptorsPayload.length)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
{
|
|
||||||
url.searchParams.set(urlResolverFunction.paramName, descriptorsGroup
|
url.searchParams.set(urlResolverFunction.paramName, descriptorsGroup
|
||||||
.map((descriptor) => descriptor.id)
|
.map((descriptor) => descriptor.id)
|
||||||
.join(urlResolverFunction.paramArraySeperator))
|
.join(urlResolverFunction.paramArraySeperator))
|
||||||
|
|
||||||
const apiResponse = await fetch(url.toString(), { cache: 'no-store' })
|
const apiResponse = await fetch(url.toString(), { cache: 'no-store' })
|
||||||
if (apiResponse.ok)
|
if (apiResponse.ok) {
|
||||||
{
|
|
||||||
const values = followResponsePath<string[]>(await apiResponse.json(), urlResolverFunction.responsePath)
|
const values = followResponsePath<string[]>(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]
|
const descriptor = descriptorsGroup[index]
|
||||||
if (value) results[descriptor.index] = value
|
if (value) results[descriptor.index] = value
|
||||||
await LbryPathnameCache.put(value, descriptor.id)
|
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['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel'])
|
||||||
if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video'])
|
if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video'])
|
||||||
}))
|
}))
|
||||||
if (progressCallback) progressCallback(1);
|
if (progressCallback) progressCallback(1)
|
||||||
return results
|
return results
|
||||||
}
|
}
|
4
src/global.d.ts
vendored
4
src/global.d.ts
vendored
|
@ -1,4 +1,4 @@
|
||||||
declare module '*.md' {
|
declare module '*.md' {
|
||||||
var _: string;
|
var _: string
|
||||||
export default _;
|
export default _
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import { h, render } from 'preact'
|
import { h, render } from 'preact'
|
||||||
import ButtonRadio, { SelectionOption } from '../common/components/ButtonRadio'
|
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 { useLbrySettings } from '../common/useSettings'
|
||||||
import './popup.sass'
|
import './popup.sass'
|
||||||
|
|
||||||
/** Utilty to set a setting in the browser */
|
/** Utilty to set a setting in the browser */
|
||||||
const setSetting = <K extends keyof ExtensionSettings>(setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value });
|
const setSetting = <K extends keyof ExtensionSettings>(setting: K, value: ExtensionSettings[K]) => chrome.storage.local.set({ [setting]: value })
|
||||||
|
|
||||||
/** Gets all the options for redirect destinations as selection options */
|
/** Gets all the options for redirect destinations as selection options */
|
||||||
const platformOptions: SelectionOption[] = getTargetPlatfromSettingsEntiries()
|
const platformOptions: SelectionOption[] = getTargetPlatfromSettingsEntiries()
|
||||||
.map(([value, { displayName: display }]) => ({ value, display }));
|
.map(([value, { displayName: display }]) => ({ value, display }))
|
||||||
|
|
||||||
const ytUrlResolverOptions: SelectionOption[] = getYtUrlResolversSettingsEntiries()
|
const ytUrlResolverOptions: SelectionOption[] = getYtUrlResolversSettingsEntiries()
|
||||||
.map(([value, { name: display }]) => ({ value, display }));
|
.map(([value, { name: display }]) => ({ value, display }))
|
||||||
|
|
||||||
function WatchOnLbryPopup() {
|
function WatchOnLbryPopup() {
|
||||||
const { redirect, targetPlatform, urlResolver } = useLbrySettings();
|
const { redirect, targetPlatform, urlResolver } = useLbrySettings()
|
||||||
|
|
||||||
return <div className='container'>
|
return <div className='container'>
|
||||||
<section>
|
<section>
|
||||||
|
@ -39,7 +39,7 @@ function WatchOnLbryPopup() {
|
||||||
<button type='button' className='btn1 button is-primary'>Subscriptions Converter</button>
|
<button type='button' className='btn1 button is-primary'>Subscriptions Converter</button>
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
</div>;
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
render(<WatchOnLbryPopup />, document.getElementById('root')!);
|
render(<WatchOnLbryPopup />, document.getElementById('root')!)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { parseProtocolUrl } from '../common/lbry-url'
|
import { parseProtocolUrl } from '../common/lbry-url'
|
||||||
import { resolveById, YtIdResolverDescriptor } from '../common/yt/urlResolve'
|
import { resolveById, YtIdResolverDescriptor } from '../common/yt/urlResolve'
|
||||||
async function resolveYT(descriptor: YtIdResolverDescriptor) {
|
async function resolveYT(descriptor: YtIdResolverDescriptor) {
|
||||||
const lbryProtocolUrl: string | null = await resolveById([descriptor]).then(a => a[0]);
|
const lbryProtocolUrl: string | null = await resolveById([descriptor]).then(a => a[0])
|
||||||
const segments = parseProtocolUrl(lbryProtocolUrl || '', { encode: true });
|
const segments = parseProtocolUrl(lbryProtocolUrl || '', { encode: true })
|
||||||
if (segments.length === 0) return;
|
if (segments.length === 0) return
|
||||||
return segments.join('/');
|
return segments.join('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
const onGoingLbryPathnameRequest: Record<string, Promise<string | void>> = {}
|
const onGoingLbryPathnameRequest: Record<string, Promise<string | void>> = {}
|
||||||
|
@ -18,7 +18,7 @@ async function lbryPathnameFromVideoId(videoId: string): Promise<string | void>
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(({ videoId }: { videoId: string }, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener(({ videoId }: { videoId: string }, sender, sendResponse) => {
|
||||||
lbryPathnameFromVideoId(videoId).then((lbryPathname) => sendResponse(lbryPathname))
|
lbryPathnameFromVideoId(videoId).then((lbryPathname) => sendResponse(lbryPathname))
|
||||||
return true;
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => changeInfo.url && chrome.tabs.sendMessage(tabId, { }));
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => changeInfo.url && chrome.tabs.sendMessage(tabId, {}))
|
|
@ -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 */
|
/** Reset settings to default value and update the browser badge text */
|
||||||
async function initSettings() {
|
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
|
// 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]]>)
|
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
|
// fix our local var and set it in storage for later
|
||||||
if (invalidEntries.length > 0) {
|
if (invalidEntries.length > 0) {
|
||||||
const changeSet = Object.fromEntries(invalidEntries);
|
const changeSet = Object.fromEntries(invalidEntries)
|
||||||
Object.assign(settings, changeSet);
|
Object.assign(settings, changeSet)
|
||||||
chrome.storage.local.set(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) => {
|
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||||
if (areaName !== 'local' || !changes.redirect) return;
|
if (areaName !== 'local' || !changes.redirect) return
|
||||||
chrome.browserAction.setBadgeText({ text: changes.redirect.newValue ? 'ON' : 'OFF' });
|
chrome.browserAction.setBadgeText({ text: changes.redirect.newValue ? 'ON' : 'OFF' })
|
||||||
});
|
})
|
||||||
|
|
||||||
|
|
||||||
chrome.runtime.onStartup.addListener(initSettings);
|
chrome.runtime.onStartup.addListener(initSettings)
|
||||||
chrome.runtime.onInstalled.addListener(initSettings);
|
chrome.runtime.onInstalled.addListener(initSettings)
|
||||||
|
|
|
@ -4,22 +4,19 @@ 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))
|
||||||
|
|
||||||
interface WatchOnLbryButtonParameters
|
interface WatchOnLbryButtonParameters {
|
||||||
{
|
|
||||||
targetPlatform?: TargetPlatform
|
targetPlatform?: TargetPlatform
|
||||||
lbryPathname?: string
|
lbryPathname?: string
|
||||||
time?: number
|
time?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Target
|
interface Target {
|
||||||
{
|
|
||||||
platfrom: TargetPlatform
|
platfrom: TargetPlatform
|
||||||
lbryPathname: string
|
lbryPathname: string
|
||||||
time: number | null
|
time: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryButtonParameters)
|
function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryButtonParameters) {
|
||||||
{
|
|
||||||
if (!lbryPathname || !targetPlatform) return null
|
if (!lbryPathname || !targetPlatform) return null
|
||||||
|
|
||||||
const url = new URL(`${targetPlatform.domainPrefix}${lbryPathname}`)
|
const url = new URL(`${targetPlatform.domainPrefix}${lbryPathname}`)
|
||||||
|
@ -49,26 +46,22 @@ function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchOnLbryBu
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateButton(mountPoint: HTMLDivElement, target: Target | null): void
|
function updateButton(mountPoint: HTMLDivElement, target: Target | null): void {
|
||||||
{
|
|
||||||
if (!target) return render(<WatchOnLbryButton />, mountPoint)
|
if (!target) return render(<WatchOnLbryButton />, mountPoint)
|
||||||
render(<WatchOnLbryButton targetPlatform={target.platfrom} lbryPathname={target.lbryPathname} time={target.time ?? undefined} />, mountPoint)
|
render(<WatchOnLbryButton targetPlatform={target.platfrom} lbryPathname={target.lbryPathname} time={target.time ?? undefined} />, mountPoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function redirectTo({ lbryPathname, platfrom, time }: Target)
|
async function redirectTo({ lbryPathname, platfrom, time }: Target) {
|
||||||
{
|
|
||||||
const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`)
|
const url = new URL(`${platfrom.domainPrefix}${lbryPathname}`)
|
||||||
|
|
||||||
if (time) url.searchParams.set('t', time.toFixed(0))
|
if (time) url.searchParams.set('t', time.toFixed(0))
|
||||||
|
|
||||||
findVideoElement().then((videoElement) =>
|
findVideoElement().then((videoElement) => {
|
||||||
{
|
|
||||||
videoElement.addEventListener('play', () => videoElement.pause(), { once: true })
|
videoElement.addEventListener('play', () => videoElement.pause(), { once: true })
|
||||||
videoElement.pause()
|
videoElement.pause()
|
||||||
})
|
})
|
||||||
|
|
||||||
if (platfrom === targetPlatformSettings.app)
|
if (platfrom === targetPlatformSettings.app) {
|
||||||
{
|
|
||||||
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true }))
|
if (document.hidden) await new Promise((resolve) => document.addEventListener('visibilitychange', resolve, { once: true }))
|
||||||
open(url, '_blank')
|
open(url, '_blank')
|
||||||
if (window.history.length === 1) window.close()
|
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 */
|
/** Returns a mount point for the button */
|
||||||
async function findButtonMountPoint(): Promise<HTMLDivElement>
|
async function findButtonMountPoint(): Promise<HTMLDivElement> {
|
||||||
{
|
|
||||||
const id = 'watch-on-lbry-button-container'
|
const id = 'watch-on-lbry-button-container'
|
||||||
let mountBefore: HTMLDivElement | null = null
|
let mountBefore: HTMLDivElement | null = null
|
||||||
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
||||||
|
@ -97,8 +89,7 @@ async function findButtonMountPoint(): Promise<HTMLDivElement>
|
||||||
return div
|
return div
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findVideoElement()
|
async function findVideoElement() {
|
||||||
{
|
|
||||||
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
const sourcePlatform = getSourcePlatfromSettingsFromHostname(new URL(location.href).hostname)
|
||||||
if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`)
|
if (!sourcePlatform) throw new Error(`Unknown source of: ${location.href}`)
|
||||||
let videoElement: HTMLVideoElement | null = null
|
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
|
// 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<string | null>((resolve) => chrome.runtime.sendMessage({ videoId }, resolve))
|
return await new Promise<string | null>((resolve) => chrome.runtime.sendMessage({ videoId }, resolve))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
(async () =>
|
(async () => {
|
||||||
{
|
|
||||||
const settings = await getExtensionSettingsAsync()
|
const settings = await getExtensionSettingsAsync()
|
||||||
let updater: (() => Promise<void>)
|
let updater: (() => Promise<void>)
|
||||||
|
|
||||||
// Listen Settings Change
|
// Listen Settings Change
|
||||||
chrome.storage.onChanged.addListener(async (changes, areaName) =>
|
chrome.storage.onChanged.addListener(async (changes, areaName) => {
|
||||||
{
|
|
||||||
if (areaName !== 'local') return
|
if (areaName !== 'local') return
|
||||||
Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue])))
|
Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue])))
|
||||||
if (changes.redirect) await onModeChange()
|
if (changes.redirect) await onModeChange()
|
||||||
|
@ -133,8 +121,7 @@ async function requestLbryPathname(videoId: string)
|
||||||
// Listen URL Change
|
// Listen URL Change
|
||||||
chrome.runtime.onMessage.addListener(() => updater())
|
chrome.runtime.onMessage.addListener(() => updater())
|
||||||
|
|
||||||
async function getTargetByURL(url: URL)
|
async function getTargetByURL(url: URL) {
|
||||||
{
|
|
||||||
if (url.pathname !== '/watch') return null
|
if (url.pathname !== '/watch') return null
|
||||||
|
|
||||||
const videoId = url.searchParams.get('v')
|
const videoId = url.searchParams.get('v')
|
||||||
|
@ -145,20 +132,17 @@ async function requestLbryPathname(videoId: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
let removeVideoTimeUpdateListener: (() => void) | null = null
|
let removeVideoTimeUpdateListener: (() => void) | null = null
|
||||||
async function onModeChange()
|
async function onModeChange() {
|
||||||
{
|
|
||||||
let target: Target | null = null
|
let target: Target | null = null
|
||||||
if (settings.redirect)
|
if (settings.redirect)
|
||||||
updater = async () =>
|
updater = async () => {
|
||||||
{
|
|
||||||
const url = new URL(location.href)
|
const url = new URL(location.href)
|
||||||
target = await getTargetByURL(url)
|
target = await getTargetByURL(url)
|
||||||
if (!target) return
|
if (!target) return
|
||||||
target.time = url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null
|
target.time = url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null
|
||||||
redirectTo(target)
|
redirectTo(target)
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
{
|
|
||||||
const mountPoint = await findButtonMountPoint()
|
const mountPoint = await findButtonMountPoint()
|
||||||
const videoElement = await findVideoElement()
|
const videoElement = await findVideoElement()
|
||||||
|
|
||||||
|
@ -169,8 +153,7 @@ async function requestLbryPathname(videoId: string)
|
||||||
videoElement.addEventListener('timeupdate', onTimeUpdate)
|
videoElement.addEventListener('timeupdate', onTimeUpdate)
|
||||||
removeVideoTimeUpdateListener = () => videoElement.removeEventListener('timeupdate', onTimeUpdate)
|
removeVideoTimeUpdateListener = () => videoElement.removeEventListener('timeupdate', onTimeUpdate)
|
||||||
|
|
||||||
updater = async () =>
|
updater = async () => {
|
||||||
{
|
|
||||||
const url = new URL(location.href)
|
const url = new URL(location.href)
|
||||||
target = await getTargetByURL(url)
|
target = await getTargetByURL(url)
|
||||||
if (target) target.time = getTime()
|
if (target) target.time = getTime()
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" dir="ltr">
|
<html lang="en" dir="ltr">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Subscription Converter</title>
|
|
||||||
<link rel="stylesheet" href="YTtoLBRY.css" />
|
|
||||||
<script src="YTtoLBRY.tsx" charset="utf-8" defer></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<head>
|
||||||
<div id="root" />
|
<meta charset="utf-8">
|
||||||
</body>
|
<title>Subscription Converter</title>
|
||||||
|
<link rel="stylesheet" href="YTtoLBRY.css" />
|
||||||
|
<script src="YTtoLBRY.tsx" charset="utf-8" defer></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root" />
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -12,23 +12,23 @@ import readme from './README.md'
|
||||||
* @returns a promise with the list of channels that were found on lbry
|
* @returns a promise with the list of channels that were found on lbry
|
||||||
*/
|
*/
|
||||||
async function lbryChannelsFromFile(file: File) {
|
async function lbryChannelsFromFile(file: File) {
|
||||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
const ext = file.name.split('.').pop()?.toLowerCase()
|
||||||
|
|
||||||
const ids = new Set((
|
const ids = new Set((
|
||||||
ext === 'xml' || ext == 'opml' ? getSubsFromOpml :
|
ext === 'xml' || ext == 'opml' ? getSubsFromOpml :
|
||||||
ext === 'csv' ? getSubsFromCsv :
|
ext === 'csv' ? getSubsFromCsv :
|
||||||
getSubsFromJson)(await getFileContent(file)))
|
getSubsFromJson)(await getFileContent(file)))
|
||||||
const lbryPathnames = await resolveById(
|
const lbryPathnames = await resolveById(
|
||||||
Array.from(ids).map(id => ({ id, type: 'channel' } as const)),
|
Array.from(ids).map(id => ({ id, type: 'channel' } as const)),
|
||||||
(progress) => render(<YTtoLBRY progress={progress} />, document.getElementById('root')!));
|
(progress) => render(<YTtoLBRY progress={progress} />, document.getElementById('root')!))
|
||||||
const { targetPlatform: platform } = await getExtensionSettingsAsync();
|
const { targetPlatform: platform } = await getExtensionSettingsAsync()
|
||||||
const urlPrefix = targetPlatformSettings[platform].domainPrefix;
|
const urlPrefix = targetPlatformSettings[platform].domainPrefix
|
||||||
return lbryPathnames.map(channel => urlPrefix + channel);
|
return lbryPathnames.map(channel => urlPrefix + channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise<void> | void, progress: number }) {
|
function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise<void> | void, progress: number }) {
|
||||||
const [file, setFile] = useState(null as File | null);
|
const [file, setFile] = useState(null as File | null)
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false)
|
||||||
|
|
||||||
return <div className='ConversionCard'>
|
return <div className='ConversionCard'>
|
||||||
<h2>Select YouTube Subscriptions</h2>
|
<h2>Select YouTube Subscriptions</h2>
|
||||||
|
@ -36,10 +36,10 @@ function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise<
|
||||||
<input type='file' onChange={e => setFile(e.currentTarget.files?.length ? e.currentTarget.files[0] : null)} />
|
<input type='file' onChange={e => setFile(e.currentTarget.files?.length ? e.currentTarget.files[0] : null)} />
|
||||||
</div>
|
</div>
|
||||||
<button class='btn btn-primary' children='Start Conversion!' disabled={!file || isLoading} onClick={async () => {
|
<button class='btn btn-primary' children='Start Conversion!' disabled={!file || isLoading} onClick={async () => {
|
||||||
if (!file) return;
|
if (!file) return
|
||||||
setLoading(true);
|
setLoading(true)
|
||||||
await onSelect(file);
|
await onSelect(file)
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}} />
|
}} />
|
||||||
<div class="progress-text">
|
<div class="progress-text">
|
||||||
{progress > 0 ? `${(progress * 100).toFixed(1)}%` : ''}
|
{progress > 0 ? `${(progress * 100).toFixed(1)}%` : ''}
|
||||||
|
@ -48,7 +48,7 @@ function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise<
|
||||||
}
|
}
|
||||||
|
|
||||||
function YTtoLBRY({ progress }: { progress: number }) {
|
function YTtoLBRY({ progress }: { progress: number }) {
|
||||||
const [lbryChannels, setLbryChannels] = useState([] as string[]);
|
const [lbryChannels, setLbryChannels] = useState([] as string[])
|
||||||
|
|
||||||
return <div className='YTtoLBRY'>
|
return <div className='YTtoLBRY'>
|
||||||
<div className='Conversion'>
|
<div className='Conversion'>
|
||||||
|
@ -65,4 +65,4 @@ function YTtoLBRY({ progress }: { progress: number }) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
render(<YTtoLBRY progress={0} />, document.getElementById('root')!);
|
render(<YTtoLBRY progress={0} />, document.getElementById('root')!)
|
||||||
|
|
Loading…
Add table
Reference in a new issue