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')}

+ +
+ +
+ + +
  • exportSubscriptionsAsFile('InvidiousJSON')}> + Invidious JSON +
  • +
  • exportSubscriptionsAsFile('OPML')}> + FreeTube/NewPipe/YouTube OPML +
  • +
    +
    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); +}