Module split: library export/import → server/library.js

• setupLibraryRoutes(app) — registers GET /api/library/export and
                              POST /api/library/import

The library module reads through history.js helpers (getHistoryDir,
loadMeta, saveMeta) and reads/writes subscriptions.json directly.
Subscriptions integration is via raw fs because (a) the library merge
logic is library-specific (skip-if-already-exists semantics), and (b)
the subscriptions module hasn't been extracted yet — the only thing
the import path needs is to merge dedupe-by-URL into the file.

server/index.js: 2079 → 1971 lines.

Smoke tested: server boots; /api/license-status, /api/health respond;
/api/library/export still returns 402 license_required for unlicensed
(unchanged Pro-gate behavior). 69 unit tests still pass.
This commit is contained in:
Keysat
2026-05-09 10:39:09 -05:00
parent a09ad9c429
commit c06ffbbdf4
2 changed files with 163 additions and 111 deletions
+3 -111
View File
@@ -54,6 +54,7 @@ import {
setupHistoryRoutes,
getHistoryDir,
} from "./history.js";
import { setupLibraryRoutes } from "./library.js";
const execFileAsync = promisify(execFile);
const app = express();
@@ -157,118 +158,9 @@ app.post("/api/update-ytdlp", async (req, res) => {
setupCookieRoutes(app);
// ── Library export/import ────────────────────────────────────────────────
// ── Library export/import ──── moved to ./library.js ─────────
setupLibraryRoutes(app);
app.get("/api/library/export", async (req, res) => {
try {
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") continue;
try {
const raw = await fs.readFile(path.join(historyDir, file), "utf-8");
const id = file.replace(".json", "");
sessions[id] = JSON.parse(raw);
} catch {}
}
// Load subscriptions
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 });
}
});
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" });
}
let imported = 0;
let skipped = 0;
// Import sessions
for (const [id, session] of Object.entries(data.sessions)) {
const filePath = path.join(historyDir, `${id}.json`);
// Skip if session already exists (don't overwrite)
try {
await fs.access(filePath);
skipped++;
continue;
} catch {}
await fs.writeFile(filePath, JSON.stringify(session));
imported++;
}
// Merge meta (add imported sessions to uncategorized if not already placed)
if (data.meta) {
const existingMeta = await loadMeta();
const allExistingIds = new Set([
...existingMeta.uncategorized,
...existingMeta.folders.flatMap(f => f.items),
]);
// Import folders that don't exist
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));
}
}
}
// Add any uncategorized items that aren't already placed
if (data.meta.uncategorized) {
for (const id of data.meta.uncategorized) {
if (!allExistingIds.has(id)) {
existingMeta.uncategorized.unshift(id);
}
}
}
await saveMeta(existingMeta);
}
// Import subscriptions (merge, don't duplicate)
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));
let subsAdded = 0;
for (const sub of data.subscriptions) {
if (!existingUrls.has(sub.url)) {
existingSubs.push(sub);
subsAdded++;
}
}
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 });
}
});
// ── Subscriptions ─────────────────────────────────────────────────────────