Support YouTube takeout JSON

This commit is contained in:
Kevin Raoofi 2020-11-11 14:21:34 -05:00
parent 08842c4ba8
commit bd5adf6652
2 changed files with 43 additions and 13 deletions

View file

@ -6,12 +6,24 @@ const LBRY_API_HOST = 'https://api.lbry.com';
const QUERY_CHUNK_SIZE = 300; const QUERY_CHUNK_SIZE = 300;
interface YtResolverResponse { interface YtResolverResponse {
success: boolean success: boolean;
error: object | null error: object | null;
data: { data: {
videos?: Record<string, string> videos?: Record<string, string>;
channels?: Record<string, string> channels?: Record<string, string>;
} };
}
interface YtSubscription {
id: string;
etag: string;
title: string;
snippet: {
description: string;
resourceId: {
channelId: string;
};
};
} }
/** /**
@ -41,15 +53,29 @@ export const ytService = {
* Reads the array of YT channels from an OPML file * Reads the array of YT channels from an OPML file
* *
* @param opmlContents an opml file as as tring * @param opmlContents an opml file as as tring
* @returns the channel URLs * @returns the channel IDs
*/ */
readOpml(opmlContents: string): string[] { readOpml(opmlContents: string): string[] {
const opml = new DOMParser().parseFromString(opmlContents, 'application/xml'); const opml = new DOMParser().parseFromString(opmlContents, 'application/xml');
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)
.map(url => ytService.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
*
* @param jsonContents a JSON file as a string
* @returns the channel IDs
*/
readJson(jsonContents: string): string[] {
const subscriptions: YtSubscription[] = JSON.parse(jsonContents);
return subscriptions.map(sub => sub.snippet.resourceId.channelId);
},
/** /**
* Extracts the channelID from a YT URL. * Extracts the channelID from a YT URL.
* *
@ -89,7 +115,7 @@ export const ytService = {
video_ids: groups['video']?.map(s => s.id).join(','), video_ids: groups['video']?.map(s => s.id).join(','),
channel_ids: groups['channel']?.map(s => s.id).join(','), channel_ids: groups['channel']?.map(s => s.id).join(','),
})); }));
return fetch(`${LBRY_API_HOST}/yt/resolve?${params}`, {cache: 'force-cache'}) return fetch(`${LBRY_API_HOST}/yt/resolve?${params}`, { cache: 'force-cache' })
.then(rsp => rsp.ok ? rsp.json() : null); .then(rsp => rsp.ok ? rsp.json() : null);
})); }));

View file

@ -5,16 +5,19 @@ import { getSettingsAsync, redirectDomains } from '../common/settings';
import { YTDescriptor, getFileContent, ytService } from '../common/yt'; import { YTDescriptor, getFileContent, ytService } from '../common/yt';
/** /**
* Parses OPML file and queries the API for lbry channels * Parses the subscription file and queries the API for lbry channels
* *
* @param file to read * @param file to read
* @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 lbryChannelsFromOpml(file: File): Promise<string[]> { async function lbryChannelsFromFile(file: File) {
const lbryUrls = await ytService.resolveById(...ytService.readOpml(await getFileContent(file)) const ext = file.name.split('.').pop()?.toLowerCase();
.map(url => ytService.getId(url)) const content = await getFileContent(file);
.filter((id): id is YTDescriptor => !!id));
const ids: YTDescriptor[] = (ext === 'xml' || ext == 'opml' ? ytService.readOpml(content) : ytService.readJson(content))
.map(id => ({ id, type: 'channel' }));
const lbryUrls = await ytService.resolveById(...ids);
const { redirect } = await getSettingsAsync('redirect'); const { redirect } = await getSettingsAsync('redirect');
const urlPrefix = redirectDomains[redirect].prefix; const urlPrefix = redirectDomains[redirect].prefix;
return lbryUrls.map(channel => urlPrefix + channel); return lbryUrls.map(channel => urlPrefix + channel);
@ -24,6 +27,7 @@ function YTtoLBRY() {
const [file, setFile] = useState(null as File | null); const [file, setFile] = useState(null as File | null);
const [lbryChannels, setLbryChannels] = useState([] as string[]); const [lbryChannels, setLbryChannels] = useState([] as string[]);
const [isLoading, setLoading] = useState(false); const [isLoading, setLoading] = useState(false);
return <> return <>
<iframe width="100%" height="400px" allowFullScreen <iframe width="100%" height="400px" allowFullScreen
src="https://lbry.tv/$/embed/howtouseconverter/c9827448d6ac7a74ecdb972c5cdf9ddaf648a28e" /> src="https://lbry.tv/$/embed/howtouseconverter/c9827448d6ac7a74ecdb972c5cdf9ddaf648a28e" />
@ -35,7 +39,7 @@ function YTtoLBRY() {
<input type="button" value="Start Conversion!" class="goButton" disabled={!file || isLoading} onClick={async () => { <input type="button" value="Start Conversion!" class="goButton" disabled={!file || isLoading} onClick={async () => {
if (!file) return; if (!file) return;
setLoading(true); setLoading(true);
setLbryChannels(await lbryChannelsFromOpml(file)); setLbryChannels(await lbryChannelsFromFile(file));
setLoading(false); setLoading(false);
}} /> }} />
<ul> <ul>