🍣 Formatted Files, Organized Imports

This commit is contained in:
Shiba 2022-01-10 12:36:29 +00:00
parent 8f75c67601
commit 2c75082af9
17 changed files with 228 additions and 286 deletions

View file

@ -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<T extends string | SelectionOption = string> {
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<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 */
@ -27,5 +27,5 @@ export default function ButtonRadio<T extends string | SelectionOption = string>
<label>{display}</label>
</div>
)}
</div>;
</div>
}

View file

@ -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)
})
})

View file

@ -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 []
}

View file

@ -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<ExtensionSettings>
{
export function getExtensionSettingsAsync(): Promise<ExtensionSettings> {
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<TargetPlatformName, TargetPlatform>
},
}
export const getTargetPlatfromSettingsEntiries = () =>
{
export const getTargetPlatfromSettingsEntiries = () => {
return Object.entries(targetPlatformSettings) as any as [Extract<keyof typeof targetPlatformSettings, string>, 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<SourcePlatformName, SourcePlatform>
}
}
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<YTUrlResolverName, YTUrlResolver> =
}
}
export const getYtUrlResolversSettingsEntiries = () =>
{
export const getYtUrlResolversSettingsEntiries = () => {
return Object.entries(ytUrlResolversSettings) as any as [Extract<keyof typeof ytUrlResolversSettings, string>, YTUrlResolver][]
}

View file

@ -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<T extends object>(initial: T) {
const [state, dispatch] = useReducer((state, nstate: Partial<T>) => ({ ...state, ...nstate }), initial);
// register change listeners, gets current values, and cleans up the listeners on unload
useEffect(() => {
const changeListener = (changes: Record<string, chrome.storage.StorageChange>, 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<T>));
return () => chrome.storage.onChanged.removeListener(changeListener);
}, []);
const [state, dispatch] = useReducer((state, nstate: Partial<T>) => ({ ...state, ...nstate }), initial)
// register change listeners, gets current values, and cleans up the listeners on unload
useEffect(() => {
const changeListener = (changes: Record<string, chrome.storage.StorageChange>, 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<T>))
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)

View file

@ -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<string>
{
return new Promise((resolve, reject) =>
{
export function getFileContent(file: File): Promise<string> {
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<string>
* @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<string>
* * /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

View file

@ -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<void>
{
return await new Promise((resolve, reject) =>
{
async function put(url: string | null, id: string): Promise<void> {
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<void>
})
}
async function get(id: string): Promise<string | null>
{
return (await new Promise((resolve, reject) =>
{
async function get(id: string): Promise<string | null> {
return (await new Promise((resolve, reject) => {
const store = db?.transaction("store", "readonly").objectStore("store")
if (!store) return resolve(null)
const request = store.get(id)

View file

@ -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<YtIdResolverDescriptor['type'], typeof descriptorsPayload | null> = 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<T>(response: any, responsePath: YtUrlResolveResponsePath)
{
for (const path of responsePath)
{
switch (typeof path)
{
function followResponsePath<T>(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<string>(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<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]
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
}

4
src/global.d.ts vendored
View file

@ -1,4 +1,4 @@
declare module '*.md' {
var _: string;
export default _;
var _: string
export default _
}

View file

@ -46,4 +46,4 @@
"128": "icons/wol/icon128.png"
},
"manifest_version": 2
}
}

View file

@ -11,4 +11,4 @@
</div>
</body>
</html>
</html>

View file

@ -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 = <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 */
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 <div className='container'>
<section>
@ -39,7 +39,7 @@ function WatchOnLbryPopup() {
<button type='button' className='btn1 button is-primary'>Subscriptions Converter</button>
</a>
</section>
</div>;
</div>
}
render(<WatchOnLbryPopup />, document.getElementById('root')!);
render(<WatchOnLbryPopup />, document.getElementById('root')!)

View file

@ -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<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) => {
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, {}))

View file

@ -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)

View file

@ -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
</div>
}
function updateButton(mountPoint: HTMLDivElement, target: Target | null): void
{
function updateButton(mountPoint: HTMLDivElement, target: Target | null): void {
if (!target) return render(<WatchOnLbryButton />, 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}`)
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<HTMLDivElement>
{
async function findButtonMountPoint(): Promise<HTMLDivElement> {
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<HTMLDivElement>
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<string | null>((resolve) => chrome.runtime.sendMessage({ videoId }, resolve))
}
// Start
(async () =>
{
(async () => {
const settings = await getExtensionSettingsAsync()
let updater: (() => Promise<void>)
// 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()

View file

@ -1,13 +1,15 @@
<!DOCTYPE html>
<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>
<div id="root" />
</body>
</html>
<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>
<div id="root" />
</body>
</html>

View file

@ -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(<YTtoLBRY progress={progress} />, 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(<YTtoLBRY progress={progress} />, 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> | 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 <div className='ConversionCard'>
<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)} />
</div>
<button class='btn btn-primary' children='Start Conversion!' disabled={!file || isLoading} onClick={async () => {
if (!file) return;
setLoading(true);
await onSelect(file);
setLoading(false);
if (!file) return
setLoading(true)
await onSelect(file)
setLoading(false)
}} />
<div class="progress-text">
{progress > 0 ? `${(progress * 100).toFixed(1)}%` : ''}
@ -48,7 +48,7 @@ function ConversionCard({ onSelect, progress }: { onSelect(file: File): Promise<
}
function YTtoLBRY({ progress }: { progress: number }) {
const [lbryChannels, setLbryChannels] = useState([] as string[]);
const [lbryChannels, setLbryChannels] = useState([] as string[])
return <div className='YTtoLBRY'>
<div className='Conversion'>
@ -65,4 +65,4 @@ function YTtoLBRY({ progress }: { progress: number }) {
</div>
}
render(<YTtoLBRY progress={0} />, document.getElementById('root')!);
render(<YTtoLBRY progress={0} />, document.getElementById('root')!)