mirror of
https://github.com/LBRYFoundation/Watch-on-LBRY.git
synced 2025-08-23 17:47:26 +00:00
Merge pull request #43 from Aenigma/feature/takeout-json
Support Google Takeout JSON for Subscription Manager
This commit is contained in:
commit
a51be792ab
8 changed files with 24162 additions and 5180 deletions
29110
package-lock.json
generated
29110
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -17,7 +17,6 @@
|
|||
">1%",
|
||||
"not ie > 0"
|
||||
],
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@types/chrome": "0.0.124",
|
||||
|
@ -30,6 +29,7 @@
|
|||
"cross-env": "^7.0.2",
|
||||
"jest": "^26.5.3",
|
||||
"lodash": "^4.17.20",
|
||||
"marked": "^1.2.5",
|
||||
"node-forge": ">=0.10.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
|
|
|
@ -6,12 +6,24 @@ const LBRY_API_HOST = 'https://api.lbry.com';
|
|||
const QUERY_CHUNK_SIZE = 300;
|
||||
|
||||
interface YtResolverResponse {
|
||||
success: boolean
|
||||
error: object | null
|
||||
success: boolean;
|
||||
error: object | null;
|
||||
data: {
|
||||
videos?: Record<string, string>
|
||||
channels?: Record<string, string>
|
||||
}
|
||||
videos?: 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
|
||||
*
|
||||
* @param opmlContents an opml file as as tring
|
||||
* @returns the channel URLs
|
||||
* @returns the channel IDs
|
||||
*/
|
||||
readOpml(opmlContents: string): string[] {
|
||||
const opml = new DOMParser().parseFromString(opmlContents, 'application/xml');
|
||||
|
||||
return Array.from(opml.querySelectorAll('outline > outline'))
|
||||
.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
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -89,7 +115,7 @@ export const ytService = {
|
|||
video_ids: groups['video']?.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);
|
||||
}));
|
||||
|
||||
|
|
4
src/global.d.ts
vendored
Normal file
4
src/global.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module '*.md' {
|
||||
var _: string;
|
||||
export default _;
|
||||
}
|
6
src/tools/README.md
Normal file
6
src/tools/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Getting your subscription data
|
||||
|
||||
1. Go to https://takeout.google.com/settings/takeout
|
||||
2. Deselect everything except `YouTube and YouTube Music` and within that only select `subscriptions`
|
||||
3. Go through the process and create the export
|
||||
4. Once it's exported, open the archive and find `YouTube and YouTube Music/subscriptions/subscriptions.json` and upload it to the extension
|
|
@ -1,11 +1,20 @@
|
|||
.body {
|
||||
display: block;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
border: 3px;
|
||||
body {
|
||||
--color-text: whitesmoke;
|
||||
--color-backround: rgb(28, 31, 34);
|
||||
--color-card: #2a2e32;
|
||||
--color-primary: rgb(43, 187, 144);
|
||||
|
||||
background-color: var(--color-backround);
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
|
@ -19,53 +28,61 @@
|
|||
|
||||
}
|
||||
|
||||
.goButton {
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 15px 30px;
|
||||
color: teal;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 4px;
|
||||
text-decoration: none;
|
||||
font-size: 24px;
|
||||
border-radius: 5px;
|
||||
.YTtoLBRY {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-around;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.Conversion {
|
||||
margin: 1rem;
|
||||
width: 45em;
|
||||
}
|
||||
|
||||
.ConversionHelp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: transparent;
|
||||
font-weight: 400;
|
||||
padding: 4px 15px;
|
||||
margin-right: 25px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
transition: 0.2s;
|
||||
overflow: hidden;
|
||||
margin-top: 90px;
|
||||
}
|
||||
|
||||
.goButton:hover{
|
||||
color: #075656;
|
||||
background: teal;
|
||||
box-shadow: 0 0px 10px teal, 0 0px 40px teal, 0 0 80px teal;
|
||||
|
||||
.ConversionCard {
|
||||
box-shadow: 0 0 0 1px rgba(16,22,26,.1), 0 1px 1px rgba(16,22,26,.2), 0 2px 6px rgba(16,22,26,.2);
|
||||
border-radius: 5px;
|
||||
background-color: var(--color-card);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.1rem;
|
||||
margin: 15px auto;
|
||||
display: block;
|
||||
.btn {
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
border: 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
min-width: 30px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.selectYtSubscriptions{
|
||||
color: teal;
|
||||
font: 36px Tahoma, Helvetica, Arial, Sans-Serif;
|
||||
text-shadow: 0px 2px 3px rgb(0, 0, 0);
|
||||
text-align: center;
|
||||
margin: 90px;
|
||||
.btn.btn-primary {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.selectYtSubscriptions:hover{
|
||||
text-shadow: 0px 2px 3px rgb(0, 255, 255);
|
||||
transition: 0.2s;
|
||||
}
|
||||
.btn:disabled {
|
||||
background-color: rgba(200, 200, 200, .5);
|
||||
color: rgba(90, 90, 90, .6);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
.YTtoLBRY {
|
||||
flex-direction: column;
|
||||
}
|
||||
.Conversion {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,7 @@
|
|||
<script src="YTtoLBRY.tsx" charset="utf-8" defer></script>
|
||||
</head>
|
||||
|
||||
<body style="background-color:#474747;">
|
||||
<div class="container">
|
||||
<div id='root' />
|
||||
</div>
|
||||
<body>
|
||||
<div id="root" />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,47 +1,62 @@
|
|||
import { Fragment, h, render } from 'preact';
|
||||
import { Fragment, h, JSX, render } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
|
||||
import { getSettingsAsync, redirectDomains } from '../common/settings';
|
||||
import { YTDescriptor, getFileContent, ytService } from '../common/yt';
|
||||
import { getFileContent, ytService } from '../common/yt';
|
||||
|
||||
import readme from './README.md';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns a promise with the list of channels that were found on lbry
|
||||
*/
|
||||
async function lbryChannelsFromOpml(file: File): Promise<string[]> {
|
||||
const lbryUrls = await ytService.resolveById(...ytService.readOpml(await getFileContent(file))
|
||||
.map(url => ytService.getId(url))
|
||||
.filter((id): id is YTDescriptor => !!id));
|
||||
async function lbryChannelsFromFile(file: File) {
|
||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||
const content = await getFileContent(file);
|
||||
|
||||
const ids = new Set((ext === 'xml' || ext == 'opml' ? ytService.readOpml(content) : ytService.readJson(content)))
|
||||
const lbryUrls = await ytService.resolveById(...Array.from(ids).map(id => ({ id, type: 'channel' } as const)));
|
||||
const { redirect } = await getSettingsAsync('redirect');
|
||||
const urlPrefix = redirectDomains[redirect].prefix;
|
||||
return lbryUrls.map(channel => urlPrefix + channel);
|
||||
}
|
||||
|
||||
function YTtoLBRY() {
|
||||
function ConversionCard({ onSelect }: { onSelect(file: File): Promise<void> | void }) {
|
||||
const [file, setFile] = useState(null as File | null);
|
||||
const [lbryChannels, setLbryChannels] = useState([] as string[]);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
return <>
|
||||
<iframe width="100%" height="400px" allowFullScreen
|
||||
src="https://lbry.tv/$/embed/howtouseconverter/c9827448d6ac7a74ecdb972c5cdf9ddaf648a28e" />
|
||||
<div class="selectYtSubscriptions">Select Youtube Subscriptions</div>
|
||||
<hr />
|
||||
<input type="file" className="PickFile" onChange={e =>
|
||||
setFile(e.currentTarget.files?.length ? e.currentTarget.files[0] : null)} />
|
||||
<hr />
|
||||
<input type="button" value="Start Conversion!" class="goButton" disabled={!file || isLoading} onClick={async () => {
|
||||
|
||||
return <div className='ConversionCard'>
|
||||
<h2>Select YouTube Subscriptions</h2>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<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);
|
||||
setLbryChannels(await lbryChannelsFromOpml(file));
|
||||
await onSelect(file);
|
||||
setLoading(false);
|
||||
}} />
|
||||
<ul>
|
||||
{lbryChannels.map((x, i) => <li key={i} children={<a href={x} children={x} />} />)}
|
||||
</ul>
|
||||
</>;
|
||||
</div>
|
||||
}
|
||||
|
||||
function YTtoLBRY() {
|
||||
const [lbryChannels, setLbryChannels] = useState([] as string[]);
|
||||
|
||||
return <div className='YTtoLBRY'>
|
||||
<div className='Conversion'>
|
||||
<ConversionCard onSelect={async file => setLbryChannels(await lbryChannelsFromFile(file))} />
|
||||
<ul>
|
||||
{lbryChannels.map((x, i) => <li key={i} children={<a href={x} children={x} />} />)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className='ConversionHelp'>
|
||||
<iframe width='712px' height='400px' allowFullScreen
|
||||
src='https://lbry.tv/$/embed/howtouseconverter/c9827448d6ac7a74ecdb972c5cdf9ddaf648a28e' />
|
||||
<section dangerouslySetInnerHTML={{ __html: readme }} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
render(<YTtoLBRY />, document.getElementById('root')!);
|
||||
|
|
Loading…
Add table
Reference in a new issue