Started invidious import
This commit is contained in:
@@ -257,15 +257,15 @@ export async function amSubscribed(
|
||||
|
||||
export async function postSubscribe(
|
||||
authorId: string,
|
||||
fetchOptions: RequestInit = {},
|
||||
bypassYTBackend: boolean = false
|
||||
authorName: string | undefined = undefined,
|
||||
fetchOptions: RequestInit = {}
|
||||
) {
|
||||
if (isYTBackend() && !bypassYTBackend) {
|
||||
if (isYTBackend()) {
|
||||
if (isOwnBackend()?.internalAuth && get(rawMasterKeyStore)) {
|
||||
return postSubscribeBackend(authorId);
|
||||
return postSubscribeBackend(authorId, authorName);
|
||||
}
|
||||
|
||||
return postSubscribeYTjs(authorId);
|
||||
return postSubscribeYTjs(authorId, authorName);
|
||||
}
|
||||
|
||||
return postSubscribeInvidious(authorId, fetchOptions);
|
||||
|
||||
@@ -7,16 +7,14 @@
|
||||
engineFallbacksStore,
|
||||
engineMaxConcurrentChannelsStore,
|
||||
invidiousInstanceStore,
|
||||
backendInUseStore,
|
||||
rawMasterKeyStore
|
||||
backendInUseStore
|
||||
} from '$lib/store';
|
||||
import { useEngineFallback, type EngineFallback } from '$lib/api/misc';
|
||||
import { get } from 'svelte/store';
|
||||
import { getSubscriptions, postSubscribe } from '$lib/api';
|
||||
import { postSubscribeYTjs } from '$lib/api/youtubejs/subscriptions';
|
||||
import { getSubscriptions } from '$lib/api';
|
||||
import { addToast } from '../Toast.svelte';
|
||||
import { isOwnBackend } from '$lib/shared';
|
||||
import { postSubscribeBackend } from '$lib/api/backend/subscriptions';
|
||||
import { importSubscriptions } from '$lib/importExport';
|
||||
import { postSubscribeInvidious } from '$lib/api/invidious/subscribe';
|
||||
|
||||
const engineFallbacks: EngineFallback[] = [
|
||||
'Channel',
|
||||
@@ -56,20 +54,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
let subscribeFunction: (id: string, name: string) => Promise<void>;
|
||||
|
||||
if (isOwnBackend()?.internalAuth && $rawMasterKeyStore) {
|
||||
subscribeFunction = postSubscribeBackend;
|
||||
} else {
|
||||
subscribeFunction = postSubscribeYTjs;
|
||||
}
|
||||
|
||||
const subPromises: Promise<void>[] = [];
|
||||
importedSubs.forEach((sub) => {
|
||||
subPromises.push(subscribeFunction(sub.authorId, sub.author));
|
||||
});
|
||||
|
||||
await Promise.all(subPromises);
|
||||
await importSubscriptions(importedSubs);
|
||||
|
||||
addToast({
|
||||
data: {
|
||||
@@ -88,9 +73,9 @@
|
||||
});
|
||||
|
||||
const subPromises: Promise<void>[] = [];
|
||||
subs.forEach((sub) => {
|
||||
subPromises.push(postSubscribe(sub.authorId, {}, true));
|
||||
});
|
||||
for (const sub of subs) {
|
||||
subPromises.push(postSubscribeInvidious(sub.authorId));
|
||||
}
|
||||
|
||||
await Promise.all(subPromises);
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { _ } from '$lib/i18n';
|
||||
import { importSubscriptionsFromFile } from '$lib/importExport';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<button>
|
||||
<i>attach_file</i>
|
||||
<span>{$_('layout.export.importSub')}</span>
|
||||
</button>
|
||||
<input
|
||||
onchange={async (event: Event) => {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
if (files?.length === 0 || !files) return;
|
||||
|
||||
await importSubscriptionsFromFile(files[0]);
|
||||
}}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
<p><i>info</i> {$_('layout.export.importSubSupported')}</p>
|
||||
@@ -15,6 +15,7 @@
|
||||
import { Tabs } from 'melt/builders';
|
||||
import { mergeAttrs } from 'melt';
|
||||
import Filters from './Filters.svelte';
|
||||
import ExportImport from './ExportImport.svelte';
|
||||
|
||||
type TabCategories =
|
||||
| 'interface'
|
||||
@@ -25,7 +26,8 @@
|
||||
| 'about'
|
||||
| 'engine'
|
||||
| 'account'
|
||||
| 'filters';
|
||||
| 'filters'
|
||||
| 'export';
|
||||
|
||||
const tabCategories: Tabs<TabCategories> = new Tabs({
|
||||
value: 'interface',
|
||||
@@ -50,6 +52,12 @@
|
||||
icon: 'keyboard_double_arrow_down',
|
||||
component: DeArrow
|
||||
},
|
||||
{
|
||||
id: 'export',
|
||||
label: $_('layout.export.title'),
|
||||
icon: 'file_export',
|
||||
component: ExportImport
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
label: $_('layout.about'),
|
||||
|
||||
@@ -271,6 +271,11 @@
|
||||
"addConditional": "Add conditional",
|
||||
"addFilter": "Add filter"
|
||||
},
|
||||
"export": {
|
||||
"title": "Export/Import",
|
||||
"importSub": "Import subscriptions",
|
||||
"importSubSupported": "YouTube CSV/OPML, Invidious OPML/JSON, Freetube database & NewPipe JSON supported"
|
||||
},
|
||||
"deArrow": {
|
||||
"title": "DeArrow",
|
||||
"thumbnailInstanceUrl": "Thumbnail instance URL",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import z from 'zod';
|
||||
import type { Subscription } from './api/model';
|
||||
import { getChannel, postSubscribe } from './api';
|
||||
|
||||
const zInvidiousSubs = z.object({
|
||||
subscriptions: z.array(z.string())
|
||||
});
|
||||
|
||||
const zFreetubeSubs = z.object({
|
||||
subscriptions: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
thumbnail: z.string()
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
export async function importSubscriptions(subscriptions: Subscription[]) {
|
||||
const subPromises: Promise<void>[] = [];
|
||||
for (const sub of subscriptions) {
|
||||
subPromises.push(postSubscribe(sub.authorId, sub.author));
|
||||
}
|
||||
|
||||
await Promise.all(subPromises);
|
||||
}
|
||||
|
||||
export async function importSubscriptionsFromFile(file: File) {
|
||||
const fileContents = await file.text();
|
||||
|
||||
const subsToImport: Subscription[] = [];
|
||||
|
||||
if (fileContents.startsWith('<opml')) {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(fileContents, 'text/xml');
|
||||
|
||||
const opmlSubs = xmlDoc.getElementsByTagName('outline');
|
||||
|
||||
for (const sub of opmlSubs) {
|
||||
const channelUrl = sub.getAttribute('xmlUrl');
|
||||
const channelName = sub.getAttribute('text') || sub.getAttribute('title');
|
||||
|
||||
if (!channelUrl || !channelName) continue;
|
||||
|
||||
let channelUrlObj: URL | undefined;
|
||||
|
||||
try {
|
||||
channelUrlObj = new URL(channelUrl);
|
||||
} catch {
|
||||
// Handled outsidde of error
|
||||
}
|
||||
|
||||
if (!channelUrlObj) continue;
|
||||
|
||||
let channelId: string | null | undefined = channelUrlObj.searchParams.get('channel_id');
|
||||
if (!channelId) {
|
||||
channelId = channelUrlObj.pathname.split('/')[3];
|
||||
}
|
||||
|
||||
if (typeof channelId !== 'string') continue;
|
||||
|
||||
subsToImport.push({
|
||||
author: channelName,
|
||||
authorId: channelId
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let fileJson: Record<any, any> | undefined;
|
||||
try {
|
||||
fileJson = JSON.parse(fileContents);
|
||||
} catch {
|
||||
// Continue regardless
|
||||
}
|
||||
|
||||
if (fileJson) {
|
||||
const invidiousSubs = zInvidiousSubs.safeParse(fileJson);
|
||||
const freetubeSubs = zFreetubeSubs.safeParse(fileJson);
|
||||
|
||||
if (invidiousSubs.success) {
|
||||
for (const authorId of invidiousSubs.data.subscriptions) {
|
||||
try {
|
||||
const channel = await getChannel(authorId);
|
||||
subsToImport.push({
|
||||
authorId,
|
||||
author: channel.author
|
||||
});
|
||||
} catch {
|
||||
// Continue regardless
|
||||
}
|
||||
}
|
||||
} else if (freetubeSubs.success) {
|
||||
for (const sub of freetubeSubs.data.subscriptions) {
|
||||
subsToImport.push({
|
||||
author: sub.name,
|
||||
authorId: sub.id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subsToImport.length > 0) {
|
||||
await importSubscriptions(subsToImport);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Unable to determine imported file type');
|
||||
}
|
||||
Reference in New Issue
Block a user