feat(sync): import Takeout HTML/CSV (historique, WL, abos) sans API

This commit is contained in:
2026-09-27 09:31:45 -04:00
parent 1ae70b5c41
commit 03ad46e91b
4 changed files with 310 additions and 13 deletions
+33
View File
@@ -357,6 +357,39 @@ r.post('/subscriptions', authMiddlewareCookieAware, subscriptionsLimiter, async
}
});
// Import en lot (ex. abonnements.csv de Takeout) : upsert direct, sans
// résolution externe (les IDs Takeout sont déjà fiables).
r.post('/subscriptions/batch', authMiddlewareCookieAware, subscriptionsLimiter, (req, res) => {
try {
const items = Array.isArray(req.body?.items) ? req.body.items : [];
if (!items.length) return res.status(400).json({ error: 'items_required' });
if (items.length > 1000) return res.status(400).json({ error: 'batch_too_large' });
const allowed = new Set(['youtube', 'twitch', 'dailymotion', 'peertube', 'odysee', 'rumble', 'yt', 'dm', 'tw', 'pt', 'od', 'ru']);
let imported = 0;
let skipped = 0;
for (const it of items) {
const provider = String(it?.provider || 'youtube').trim().toLowerCase();
const externalId = String(it?.externalId || '').trim().slice(0, 128);
if (!allowed.has(provider) || !externalId) { skipped++; continue; }
try {
const row = upsertChannelRow({
provider, externalId,
title: String(it?.title || externalId).slice(0, 200) || null,
handle: String(it?.handle || '').slice(0, 128) || null,
avatarUrl: String(it?.avatarUrl || '').slice(0, 500) || null,
url: String(it?.url || '').slice(0, 500) || null,
lastRefreshedAt: Date.now(),
});
subscribeChannel({ userId: req.user.id, provider, externalId, channelId: row?.id });
imported++;
} catch { skipped++; }
}
return res.json({ imported, skipped, total: items.length });
} catch {
return res.status(500).json({ error: 'subscriptions_batch_failed' });
}
});
r.delete('/subscriptions/:subscriptionId', authMiddlewareCookieAware, subscriptionsLimiter, (req, res) => {
try {
const info = unsubscribeChannel({ userId: req.user.id, subscriptionId: req.params.subscriptionId });