diff --git a/src/common/yt/index.ts b/src/common/yt/index.ts index 30f0099..f7240de 100644 --- a/src/common/yt/index.ts +++ b/src/common/yt/index.ts @@ -92,7 +92,7 @@ export function getChannelId(channelURL: string) export function parseYouTubeURLTimeString(timeString: string) { const signs = timeString.replace(/[0-9]/g, '') - if (signs.length === 0) return 0 + if (signs.length === 0) return null const numbers = timeString.replace(/[^0-9]/g, '-').split('-') let total = 0 for (let i = 0; i < signs.length; i++) @@ -104,7 +104,7 @@ export function parseYouTubeURLTimeString(timeString: string) case 'h': t *= 60 case 'm': t *= 60 case 's': break - default: return 0 + default: return null } total += t } diff --git a/src/common/yt/urlCache.ts b/src/common/yt/urlCache.ts index 8b3b6f6..a68aef7 100644 --- a/src/common/yt/urlCache.ts +++ b/src/common/yt/urlCache.ts @@ -2,9 +2,6 @@ let db: IDBDatabase | null = null -// Throw if its not in the background -if (chrome.extension.getBackgroundPage() !== self) throw new Error() - if (typeof self.indexedDB !== 'undefined') { const openRequest = indexedDB.open("yt-url-resolver-cache") diff --git a/src/common/yt/urlResolve.ts b/src/common/yt/urlResolve.ts index 26f45da..dd955e3 100644 --- a/src/common/yt/urlResolve.ts +++ b/src/common/yt/urlResolve.ts @@ -10,132 +10,111 @@ export interface YtIdResolverDescriptor id: string type: 'channel' | 'video' } - - /** - * @param descriptorsWithIndex YT resource IDs to check - * @returns a promise with the list of channels that were found on lbry - */ - export async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> - { - const descriptorsWithIndex: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({ ...descriptor, index })) - descriptors = null as any - const results: (string | null)[] = [] - - await Promise.all(descriptorsWithIndex.map(async (descriptor, index) => - { - if (!descriptor) return - const cache = await LbryPathnameCache.get(descriptor.id) - - // Cache can be null, if there is no lbry url yet - if (cache !== undefined) - { - // Directly setting it to results - results[index] = cache - - // We remove it so we dont ask it to API - descriptorsWithIndex.splice(index, 1) - } - })) - - const descriptorsChunks = chunk(descriptorsWithIndex, QUERY_CHUNK_SIZE) - let progressCount = 0 - await Promise.all(descriptorsChunks.map(async (descriptorChunk) => - { - const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any - - const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() - const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] - - const url = new URL(`https://${urlResolverSetting.hostname}`) - - function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) - { - for (const path of responsePath) - { - switch (typeof path) - { - case 'string': - case 'number': - response = response[path] - break - default: - switch (path) - { - case Keys: - response = Object.keys(response) - break - case Values: - response = Object.values(response) - break - } - } - } - return response as T - } - - async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsWithIndex) - { - url.pathname = urlResolverFunction.pathname - - if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) - { - await Promise.all(descriptorsGroup.map(async (descriptor) => - { - switch (null) - { - default: - if (!descriptor.id) break - url.searchParams.set(urlResolverFunction.paramName, descriptor.id) - - const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) - if (!apiResponse.ok) - { - // Some API might not respond with 200 if it can't find the url - if (apiResponse.status === 404) await LbryPathnameCache.put(null, descriptor.id) - break - } - - const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) - if (value) results[descriptor.index] = value - await LbryPathnameCache.put(value, descriptor.id) - } - progressCount++ - if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) - })) - } - else - { - - switch (null) - { - default: - url.searchParams - .set(urlResolverFunction.paramName, descriptorsGroup - .map((descriptor) => descriptor.id) - .filter((descriptorId) => descriptorId) - .join(urlResolverFunction.paramArraySeperator) - ) - - const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) - if (!apiResponse.ok) break - const values = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) - - await Promise.all(values.map(async (value, index) => - { - const descriptor = descriptorsGroup[index] - if (value) results[descriptor.index] = value - await LbryPathnameCache.put(value, descriptor.id) - })) - } - progressCount += descriptorsGroup.length - if (progressCallback) progressCallback(progressCount / descriptorsWithIndex.length) - } - } - - if (descriptorsGroupedByType['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel']) - if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video']) - - })) - - return results - } \ No newline at end of file + +/** +* @param descriptorsWithIndex YT resource IDs to check +* @returns a promise with the list of channels that were found on lbry +*/ +export async function resolveById(descriptors: YtIdResolverDescriptor[], progressCallback?: (progress: number) => void): Promise<(string | null)[]> +{ + let descriptorsPayload: (YtIdResolverDescriptor & { index: number })[] = descriptors.map((descriptor, index) => ({ ...descriptor, index })) + descriptors = null as any + const results: (string | null)[] = []; + + + descriptorsPayload = (await Promise.all(descriptorsPayload.map(async (descriptor, index) => + { + if (!descriptor?.id) return + const cache = await LbryPathnameCache.get(descriptor.id) + + // Cache can be null, if there is no lbry url yet + if (cache !== undefined) + { + // Null values shouldn't be in the results + if (cache) results[index] = cache + return + } + + return descriptor + }))).filter((descriptor) => descriptor) as any + + const descriptorsPayloadChunks = chunk(descriptorsPayload, QUERY_CHUNK_SIZE) + let progressCount = 0 + await Promise.all(descriptorsPayloadChunks.map(async (descriptorChunk) => + { + const descriptorsGroupedByType: Record = groupBy(descriptorChunk, (descriptor) => descriptor.type) as any + + const { urlResolver: urlResolverSettingName } = await getExtensionSettingsAsync() + const urlResolverSetting = ytUrlResolversSettings[urlResolverSettingName] + + const url = new URL(`https://${urlResolverSetting.hostname}`) + + function followResponsePath(response: any, responsePath: YtUrlResolveResponsePath) + { + for (const path of responsePath) + { + switch (typeof path) + { + case 'string': case 'number': response = response[path]; continue + } + switch (path) + { + case Keys: response = Object.keys(response); continue + case Values: response = Object.values(response); continue + } + } + return response as T + } + + async function requestGroup(urlResolverFunction: YtUrlResolveFunction, descriptorsGroup: typeof descriptorsPayload) + { + url.pathname = urlResolverFunction.pathname + + if (urlResolverFunction.paramArraySeperator === SingleValueAtATime) + { + await Promise.all(descriptorsGroup.map(async (descriptor) => + { + url.searchParams.set(urlResolverFunction.paramName, descriptor.id) + + const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) + if (apiResponse.ok) + { + const value = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) + if (value) results[descriptor.index] = value + await LbryPathnameCache.put(value, descriptor.id) + } + else if (apiResponse.status === 404) await LbryPathnameCache.put(null, descriptor.id) + + progressCount++ + if (progressCallback) progressCallback(progressCount / descriptorsPayload.length) + })) + } + else + { + url.searchParams.set(urlResolverFunction.paramName, descriptorsGroup + .map((descriptor) => descriptor.id) + .join(urlResolverFunction.paramArraySeperator)) + + const apiResponse = await fetch(url.toString(), { cache: 'no-store' }) + if (apiResponse.ok) + { + const values = followResponsePath(await apiResponse.json(), urlResolverFunction.responsePath) + await Promise.all(values.map(async (value, index) => + { + const descriptor = descriptorsGroup[index] + if (value) results[descriptor.index] = value + await LbryPathnameCache.put(value, descriptor.id) + })) + } + + progressCount += descriptorsGroup.length + if (progressCallback) progressCallback(progressCount / descriptorsPayload.length) + } + } + + if (descriptorsGroupedByType['channel']) await requestGroup(urlResolverSetting.functions.getChannelId, descriptorsGroupedByType['channel']) + if (descriptorsGroupedByType['video']) await requestGroup(urlResolverSetting.functions.getVideoId, descriptorsGroupedByType['video']) + })) + if (progressCallback) progressCallback(1); + return results +} \ No newline at end of file diff --git a/src/scripts/ytContent.tsx b/src/scripts/ytContent.tsx index f7b5493..a0b80e0 100644 --- a/src/scripts/ytContent.tsx +++ b/src/scripts/ytContent.tsx @@ -54,8 +54,7 @@ export function WatchOnLbryButton({ targetPlatform, lbryPathname, time }: WatchO function updateButton(mountPoint: HTMLDivElement, target: Target | null): void { if (!target) return render(, mountPoint) - const time = target.time && target.time > 3 ? target.time : null - render(, mountPoint) + render(, mountPoint) } function redirectTo({ lbryPathname, platfrom, time }: Target) @@ -112,8 +111,8 @@ window.addEventListener('load', async () => chrome.storage.onChanged.addListener(async (changes, areaName) => { if (areaName !== 'local') return - Object.assign(settings, changes) - updateByURL(new URL(location.href)) + Object.assign(settings, Object.fromEntries(Object.entries(changes).map(([key, change]) => [key, change.newValue]))) + await updateByURL(new URL(location.href)) }) /* @@ -127,6 +126,13 @@ window.addEventListener('load', async () => // We should get this from background, so the caching works and we don't get erros in the future if yt decides to impliment CORS const requestLbryPathname = async (videoId: string) => await new Promise((resolve) => chrome.runtime.sendMessage({ videoId }, resolve)) + function getVideoTime(url: URL) + { + return settings.redirect ? + (url.searchParams.has('t') ? parseYouTubeURLTimeString(url.searchParams.get('t')!) : null) : + (videoElement.currentTime > 3 && videoElement.currentTime < videoElement.duration - 1 ? videoElement.currentTime : null) + } + let target: Target | null = null async function updateByURL(url: URL) { @@ -136,15 +142,15 @@ window.addEventListener('load', async () => if (!videoId) return const lbryPathname = await requestLbryPathname(videoId) if (!lbryPathname) return - const time = settings.redirect ? parseYouTubeURLTimeString(url.searchParams.get('t') ?? '0') : videoElement.currentTime + const time = getVideoTime(url) target = { lbryPathname, platfrom: targetPlatformSettings[settings.targetPlatform], time } if (settings.redirect) redirectTo(target) else updateButton(buttonMountPoint, target) } - videoElement.addEventListener('timeupdate', () => target && updateButton(buttonMountPoint, Object.assign(target, { time: videoElement.currentTime }))) - videoElement.addEventListener('ended', () => target && updateButton(buttonMountPoint, Object.assign(target, { time: null }))) + videoElement.addEventListener('timeupdate', + () => target && updateButton(buttonMountPoint, Object.assign(target, { time: getVideoTime(new URL(location.href)) }))) async function onUrlChange() { diff --git a/src/tools/YTtoLBRY.tsx b/src/tools/YTtoLBRY.tsx index ca89dc0..b350bec 100644 --- a/src/tools/YTtoLBRY.tsx +++ b/src/tools/YTtoLBRY.tsx @@ -5,8 +5,6 @@ import { getFileContent, getSubsFromCsv, getSubsFromJson, getSubsFromOpml } from import { resolveById } from '../common/yt/urlResolve' import readme from './README.md' - - /** * Parses the subscription file and queries the API for lbry channels *