// Library export/import — bulk JSON dump + restore of sessions, folder // meta, and subscriptions. Only routes; no module state. Pulls history // data from history.js helpers; reads/writes the subscriptions file // directly because library import predates the subscriptions module // having a public 'merge' helper (and the merge logic is library- // specific anyway). import fs from "fs/promises"; import path from "path"; import express from "express"; import { getHistoryDir, loadMeta, saveMeta } from "./history.js"; // ── Routes ────────────────────────────────────────────────────────────────── // Both routes are gated by the Pro 'library' entitlement (the gate runs // upstream in license-middleware.js). They assume initHistory() has // already been called. export function setupLibraryRoutes(app) { // Bulk export everything: meta, sessions, subscriptions. app.get("/api/library/export", async (req, res) => { try { const historyDir = getHistoryDir(); const meta = await loadMeta(); const files = await fs.readdir(historyDir); const sessions = {}; for (const file of files) { if ( !file.endsWith(".json") || file === "_meta.json" || file === "subscriptions.json" || file === "auto-queue.json" || file === "skip-list.json" || file === "seen-list.json" ) continue; try { const raw = await fs.readFile(path.join(historyDir, file), "utf-8"); const id = file.replace(".json", ""); sessions[id] = JSON.parse(raw); } catch {} } let subscriptions = []; try { subscriptions = JSON.parse( await fs.readFile(path.join(historyDir, "subscriptions.json"), "utf-8") ).subscriptions || []; } catch {} const exportData = { version: 1, exportedAt: new Date().toISOString(), meta, sessions, subscriptions, }; res.setHeader("Content-Type", "application/json"); res.setHeader( "Content-Disposition", 'attachment; filename="recap-library.json"' ); res.json(exportData); } catch (err) { res.status(500).json({ error: err.message }); } }); // Bulk import: skip sessions that already exist (don't overwrite), // merge meta folders, add net-new subscriptions. app.post( "/api/library/import", express.json({ limit: "200mb" }), async (req, res) => { try { const data = req.body; if (!data || !data.sessions) { return res .status(400) .json({ error: "Invalid library file — missing sessions data" }); } const historyDir = getHistoryDir(); let imported = 0; let skipped = 0; // Sessions — skip if already present. for (const [id, session] of Object.entries(data.sessions)) { const filePath = path.join(historyDir, `${id}.json`); try { await fs.access(filePath); skipped++; continue; } catch {} await fs.writeFile(filePath, JSON.stringify(session)); imported++; } // Meta — merge folders + add new uncategorized at the top. if (data.meta) { const existingMeta = await loadMeta(); const allExistingIds = new Set([ ...existingMeta.uncategorized, ...existingMeta.folders.flatMap(f => f.items), ]); if (data.meta.folders) { for (const folder of data.meta.folders) { const existingFolder = existingMeta.folders.find( f => f.id === folder.id ); if (!existingFolder) { existingMeta.folders.push(folder); folder.items.forEach(id => allExistingIds.add(id)); } } } if (data.meta.uncategorized) { for (const id of data.meta.uncategorized) { if (!allExistingIds.has(id)) { existingMeta.uncategorized.unshift(id); } } } await saveMeta(existingMeta); } // Subscriptions — merge, dedupe by URL. if (data.subscriptions && data.subscriptions.length > 0) { let existingSubs = []; try { existingSubs = JSON.parse( await fs.readFile( path.join(historyDir, "subscriptions.json"), "utf-8" ) ).subscriptions || []; } catch {} const existingUrls = new Set(existingSubs.map(s => s.url)); for (const sub of data.subscriptions) { if (!existingUrls.has(sub.url)) { existingSubs.push(sub); } } await fs.writeFile( path.join(historyDir, "subscriptions.json"), JSON.stringify({ subscriptions: existingSubs }) ); } res.json({ ok: true, imported, skipped, total: Object.keys(data.sessions).length, }); } catch (err) { res.status(500).json({ error: err.message }); } } ); }