diff --git a/materialious/src/lib/components/settings/ExportImport.svelte b/materialious/src/lib/components/settings/ExportImport.svelte
index 3d9ed330..708d0e4e 100644
--- a/materialious/src/lib/components/settings/ExportImport.svelte
+++ b/materialious/src/lib/components/settings/ExportImport.svelte
@@ -1,6 +1,6 @@
@@ -36,3 +36,20 @@
/>
info {$_('layout.export.importSubSupported')}
+
+
+
+
+
+
+
diff --git a/materialious/src/lib/i18n/locales/en.json b/materialious/src/lib/i18n/locales/en.json
index c5589347..2246f6e9 100644
--- a/materialious/src/lib/i18n/locales/en.json
+++ b/materialious/src/lib/i18n/locales/en.json
@@ -276,7 +276,8 @@
"importSub": "Import subscriptions",
"importSubSupported": "YouTube CSV/OPML, Invidious OPML/JSON, Freetube database & NewPipe JSON supported",
"importing": "Importing subscriptions (this will take a moment)",
- "importingCompleted": "Subscriptions successfully imported"
+ "importingCompleted": "Subscriptions successfully imported",
+ "exportSub": "Export subscriptions"
},
"deArrow": {
"title": "DeArrow",
diff --git a/materialious/src/lib/importExport.ts b/materialious/src/lib/importExport.ts
index 2f51979d..a44d3241 100644
--- a/materialious/src/lib/importExport.ts
+++ b/materialious/src/lib/importExport.ts
@@ -1,7 +1,8 @@
import z from 'zod';
import type { Subscription } from './api/model';
-import { getChannel, postSubscribe } from './api';
+import { getChannel, getSubscriptions, postSubscribe } from './api';
import Papa from 'papaparse';
+import { downloadStringAsFile } from './misc';
const zInvidiousSubs = z.object({
subscriptions: z.array(z.string())
@@ -166,3 +167,61 @@ export async function importSubscriptionsFromFile(file: File) {
throw new Error('Unable to determine imported file type');
}
+
+export async function exportSubscriptionsAsFile(exportType: 'InvidiousJSON' | 'OPML') {
+ const subscriptions = await getSubscriptions();
+
+ let data: string | undefined;
+
+ if (exportType === 'InvidiousJSON') {
+ const subIds = [];
+
+ for (const sub of subscriptions) {
+ subIds.push(sub.authorId);
+ }
+
+ data = JSON.stringify({
+ subscriptions: subIds
+ });
+ } else {
+ const xmlDoc = document.implementation.createDocument('', '', null);
+
+ const opml = xmlDoc.createElement('opml');
+ opml.setAttribute('version', '1.1');
+
+ const body = xmlDoc.createElement('body');
+
+ const mainOutline = xmlDoc.createElement('outline');
+ mainOutline.setAttribute('text', 'YouTube Subscriptions');
+ mainOutline.setAttribute('title', 'YouTube Subscriptions');
+
+ body.appendChild(mainOutline);
+
+ for (const sub of subscriptions) {
+ const outline = xmlDoc.createElement('outline');
+ outline.setAttribute('text', sub.author);
+ outline.setAttribute('title', sub.author);
+ outline.setAttribute('type', 'rss');
+ outline.setAttribute(
+ 'xmlUrl',
+ `https://www.youtube.com/feeds/videos.xml?channel_id=${sub.authorId}`
+ );
+
+ body.appendChild(outline);
+ }
+
+ opml.appendChild(body);
+ xmlDoc.appendChild(opml);
+
+ const serializer = new XMLSerializer();
+
+ data = serializer.serializeToString(xmlDoc);
+ }
+
+ if (data) {
+ downloadStringAsFile(
+ data,
+ `materialious-export-${new Date().toDateString().replaceAll(' ', '')}.${exportType === 'InvidiousJSON' ? 'json' : 'opml'}`
+ );
+ }
+}
diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts
index 1fec17d6..9988a359 100644
--- a/materialious/src/lib/misc.ts
+++ b/materialious/src/lib/misc.ts
@@ -118,3 +118,17 @@ export function isUnrestrictedPlatform(): boolean {
export function isYTBackend(): boolean {
return get(backendInUseStore) === 'yt' && isUnrestrictedPlatform();
}
+
+export function downloadStringAsFile(content: string, filename: string) {
+ const blob = new Blob([content]);
+ const url = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}