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:
+3
-111
@@ -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 ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user