feat(history): refonte page historique en tabs + transcripts conserves et rejoues sans regeneration

This commit is contained in:
2026-09-26 12:59:01 -04:00
parent 9b4fffd3c8
commit 55c02ac52f
8 changed files with 1113 additions and 735 deletions
+75 -2
View File
@@ -83,6 +83,11 @@ import {
sumCompletedDownloadBytes,
SUPPORTED_DOWNLOAD_LANGUAGES as SUPPORTED_DL_LANGS,
DEFAULT_DOWNLOAD_LANGUAGES as DEFAULT_DL_LANGS,
upsertTranscriptHistory,
getTranscriptHistoryItem,
listTranscriptHistory,
deleteTranscriptHistoryById,
deleteAllTranscriptHistory,
} from './db.mjs';
import { getChannelAdapter, setTwitchTokenProvider } from './providers/channel-registry.mjs';
import { fetchChannelContent } from './providers/channel-content.mjs';
@@ -1976,7 +1981,8 @@ r.get('/user/history/search', authMiddleware, (req, res) => {
const limit = Math.min(200, Number(req.query.limit || 50));
const before = req.query.before ? String(req.query.before) : undefined;
const q = typeof req.query.q === 'string' ? req.query.q : undefined;
const rows = listSearchHistory({ userId: req.user.id, limit, before, q });
const provider = typeof req.query.provider === 'string' ? req.query.provider : undefined;
const rows = listSearchHistory({ userId: req.user.id, limit, before, q, provider });
return res.json(rows);
});
@@ -2003,7 +2009,8 @@ r.get('/user/history/watch', authMiddleware, (req, res) => {
const limit = Math.min(200, Number(req.query.limit || 50));
const before = req.query.before ? String(req.query.before) : undefined;
const q = typeof req.query.q === 'string' ? req.query.q : undefined;
const rows = listWatchHistory({ userId: req.user.id, limit, before, q });
const provider = typeof req.query.provider === 'string' ? req.query.provider : undefined;
const rows = listWatchHistory({ userId: req.user.id, limit, before, q, provider });
return res.json(rows);
});
@@ -2032,6 +2039,72 @@ r.delete('/user/history/watch', authMiddleware, (req, res) => {
return res.status(204).end();
});
// --- History: Transcripts (conservés, rejoués sans régénération) ---
r.post('/user/history/transcripts', authMiddleware, (req, res) => {
const { provider, videoId, title, thumbnail, lang, languages, lines } = req.body || {};
if (!provider || !videoId || !lang) {
return res.status(400).json({ error: 'provider, videoId and lang are required' });
}
if (!Array.isArray(lines) || !lines.length) {
return res.status(400).json({ error: 'lines required' });
}
try {
const row = upsertTranscriptHistory({
userId: req.user.id, provider, videoId, title, thumbnail, lang, languages, lines,
});
if (!row) return res.status(400).json({ error: 'invalid_transcript' });
return res.status(201).json(row);
} catch (e) {
return res.status(500).json({ error: 'save_failed', details: String(e?.message || e) });
}
});
r.get('/user/history/transcripts', authMiddleware, (req, res) => {
const limit = Math.min(200, Number(req.query.limit || 50));
const before = req.query.before ? String(req.query.before) : undefined;
const q = typeof req.query.q === 'string' ? req.query.q : undefined;
const provider = typeof req.query.provider === 'string' ? req.query.provider : undefined;
const lang = typeof req.query.lang === 'string' ? req.query.lang : undefined;
try {
const rows = listTranscriptHistory({ userId: req.user.id, limit, before, q, provider, lang });
return res.json(rows);
} catch (e) {
return res.status(500).json({ error: 'list_failed', details: String(e?.message || e) });
}
});
r.get('/user/history/transcripts/:provider/:videoId', authMiddleware, (req, res) => {
const { provider, videoId } = req.params;
const lang = typeof req.query.lang === 'string' ? req.query.lang : undefined;
try {
const row = getTranscriptHistoryItem({ userId: req.user.id, provider, videoId, lang });
if (!row) return res.status(404).json({ available: false, error: 'not_found' });
return res.json({ available: true, fromHistory: true, ...row });
} catch (e) {
return res.status(500).json({ available: false, error: 'lookup_failed' });
}
});
r.delete('/user/history/transcripts/:id', authMiddleware, (req, res) => {
try {
deleteTranscriptHistoryById(req.user.id, req.params.id);
return res.status(204).end();
} catch (e) {
return res.status(500).json({ error: 'delete_failed', details: String(e?.message || e) });
}
});
r.delete('/user/history/transcripts', authMiddleware, (req, res) => {
if (String(req.query.all || '') !== '1') return res.status(400).json({ error: 'set all=1' });
const provider = typeof req.query.provider === 'string' && req.query.provider ? String(req.query.provider) : undefined;
try {
deleteAllTranscriptHistory(req.user.id, provider);
return res.status(204).end();
} catch (e) {
return res.status(500).json({ error: 'delete_failed', details: String(e?.message || e) });
}
});
// --- Likes ---
// NOTE : cookie-aware (et non Bearer seul) : le front s'authentifie via
// cookies httpOnly + Bearer en mémoire (perdu au F5) ; exiger le Bearer