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
+172 -65
View File
@@ -392,40 +392,23 @@ export function insertSearchHistory({ userId, query, filters }) {
return { id, created_at };
}
export function listSearchHistory({ userId, limit = 50, before, q }) {
const hasBefore = Boolean(before);
const hasQ = typeof q === 'string' && q.trim().length > 0;
const like = `%${(q || '').trim()}%`;
if (hasBefore && hasQ) {
return db.prepare(`
SELECT * FROM search_history
WHERE user_id = ? AND created_at < ? AND (query LIKE ? OR COALESCE(filters_json,'') LIKE ?)
ORDER BY created_at DESC
LIMIT ?
`).all(userId, before, like, like, limit);
export function listSearchHistory({ userId, limit = 50, before, q, provider }) {
const lim = Math.max(1, Math.min(200, Number(limit) || 50));
const conds = ['user_id = ?'];
const params = [userId];
if (before) { conds.push('created_at < ?'); params.push(String(before)); }
if (typeof q === 'string' && q.trim().length > 0) {
const like = `%${q.trim()}%`;
conds.push(`(query LIKE ? OR COALESCE(filters_json,'') LIKE ?)`);
params.push(like, like);
}
if (hasBefore) {
return db.prepare(`
SELECT * FROM search_history
WHERE user_id = ? AND created_at < ?
ORDER BY created_at DESC
LIMIT ?
`).all(userId, before, limit);
if (provider && String(provider).trim()) {
const likeP = `%${String(provider).trim()}%`;
conds.push(`COALESCE(filters_json,'') LIKE ?`);
params.push(likeP);
}
if (hasQ) {
return db.prepare(`
SELECT * FROM search_history
WHERE user_id = ? AND (query LIKE ? OR COALESCE(filters_json,'') LIKE ?)
ORDER BY created_at DESC
LIMIT ?
`).all(userId, like, like, limit);
}
return db.prepare(`
SELECT * FROM search_history
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ?
`).all(userId, limit);
const where = `WHERE ${conds.join(' AND ')}`;
return db.prepare(`SELECT * FROM search_history ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, lim);
}
export function deleteSearchHistoryById(userId, id) {
@@ -468,41 +451,22 @@ export function updateWatchHistoryById(id, { progressSeconds, lastPositionSecond
return db.prepare(`SELECT * FROM watch_history WHERE id = ?`).get(id);
}
export function listWatchHistory({ userId, limit = 50, before, q }) {
const hasBefore = Boolean(before);
const hasQ = typeof q === 'string' && q.trim().length > 0;
const like = `%${(q || '').trim()}%`;
if (hasBefore && hasQ) {
return db.prepare(`
SELECT * FROM watch_history
WHERE user_id = ? AND watched_at < ?
AND (COALESCE(title,'') LIKE ? OR provider LIKE ? OR video_id LIKE ?)
ORDER BY watched_at DESC
LIMIT ?
`).all(userId, before, like, like, like, limit);
export function listWatchHistory({ userId, limit = 50, before, q, provider }) {
const lim = Math.max(1, Math.min(200, Number(limit) || 50));
const conds = ['user_id = ?'];
const params = [userId];
if (before) { conds.push('watched_at < ?'); params.push(String(before)); }
if (provider && String(provider).trim()) {
conds.push('provider = ?');
params.push(String(provider).trim().toLowerCase());
}
if (hasBefore) {
return db.prepare(`
SELECT * FROM watch_history
WHERE user_id = ? AND watched_at < ?
ORDER BY watched_at DESC
LIMIT ?
`).all(userId, before, limit);
if (typeof q === 'string' && q.trim().length > 0) {
const like = `%${q.trim()}%`;
conds.push(`(COALESCE(title,'') LIKE ? OR provider LIKE ? OR video_id LIKE ?)`);
params.push(like, like, like);
}
if (hasQ) {
return db.prepare(`
SELECT * FROM watch_history
WHERE user_id = ? AND (COALESCE(title,'') LIKE ? OR provider LIKE ? OR video_id LIKE ?)
ORDER BY watched_at DESC
LIMIT ?
`).all(userId, like, like, like, limit);
}
return db.prepare(`
SELECT * FROM watch_history
WHERE user_id = ?
ORDER BY watched_at DESC
LIMIT ?
`).all(userId, limit);
const where = `WHERE ${conds.join(' AND ')}`;
return db.prepare(`SELECT * FROM watch_history ${where} ORDER BY watched_at DESC LIMIT ?`).all(...params, lim);
}
export function deleteWatchHistoryById(userId, id) {
@@ -513,6 +477,149 @@ export function deleteAllWatchHistory(userId) {
db.prepare(`DELETE FROM watch_history WHERE user_id = ?`).run(userId);
}
// -------------------- Transcript History --------------------
// Conserve les transcripts générés pour les rejouer sans régénération.
function ensureTranscriptHistoryTable() {
try {
db.exec(`CREATE TABLE IF NOT EXISTS transcript_history (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider TEXT NOT NULL,
video_id TEXT NOT NULL,
title TEXT,
thumbnail TEXT,
lang TEXT NOT NULL,
languages_json TEXT,
lines_json TEXT NOT NULL DEFAULT '[]',
line_count INTEGER NOT NULL DEFAULT 0,
char_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);`);
db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS uq_transcript_history_user_video_lang
ON transcript_history(user_id, provider, video_id, lang);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_transcript_history_user_time
ON transcript_history(user_id, updated_at DESC);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_transcript_history_user_provider
ON transcript_history(user_id, provider);`);
} catch {}
}
ensureTranscriptHistoryTable();
function transcriptRowToApi(row) {
if (!row) return null;
let lines = [];
let languages = [];
try { lines = JSON.parse(row.lines_json || '[]'); if (!Array.isArray(lines)) lines = []; } catch { lines = []; }
try { languages = JSON.parse(row.languages_json || '[]'); if (!Array.isArray(languages)) languages = []; } catch { languages = []; }
const preview = lines.slice(0, 3).map((l) => String(l?.text || '')).join(' ').slice(0, 280);
return {
id: row.id,
provider: row.provider,
video_id: row.video_id,
title: row.title,
thumbnail: row.thumbnail,
lang: row.lang,
languages,
line_count: row.line_count,
char_count: row.char_count,
preview,
created_at: row.created_at,
updated_at: row.updated_at,
lines,
};
}
export function upsertTranscriptHistory({ userId, provider, videoId, title, thumbnail, lang, languages, lines }) {
ensureTranscriptHistoryTable();
if (!userId || !provider || !videoId || !lang) return null;
const cleanLines = Array.isArray(lines) ? lines.slice(0, 2000).map((l) => ({
t: Number(l?.t) || 0,
dur: Number(l?.dur) || 0,
text: String(l?.text || '').slice(0, 2000),
})) : [];
if (!cleanLines.length) return null;
const now = nowIso();
const normLang = String(lang).trim().slice(0, 12).toLowerCase();
const normProvider = String(provider).trim().slice(0, 32).toLowerCase();
const normVideoId = String(videoId).trim().slice(0, 256);
const languages_json = JSON.stringify(Array.isArray(languages) ? languages.slice(0, 32) : []);
const lines_json = JSON.stringify(cleanLines);
const line_count = cleanLines.length;
const char_count = cleanLines.reduce((n, l) => n + String(l.text || '').length, 0);
const existing = db.prepare(
`SELECT * FROM transcript_history WHERE user_id = ? AND provider = ? AND video_id = ? AND lang = ?`
).get(userId, normProvider, normVideoId, normLang);
if (existing) {
db.prepare(`UPDATE transcript_history SET title = COALESCE(?, title), thumbnail = COALESCE(?, thumbnail),
languages_json = ?, lines_json = ?, line_count = ?, char_count = ?, updated_at = ? WHERE id = ?`)
.run(title || null, thumbnail || null, languages_json, lines_json, line_count, char_count, now, existing.id);
return transcriptRowToApi(db.prepare(`SELECT * FROM transcript_history WHERE id = ?`).get(existing.id));
}
const id = cryptoRandomId();
db.prepare(`INSERT INTO transcript_history (id, user_id, provider, video_id, title, thumbnail, lang, languages_json, lines_json, line_count, char_count, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
.run(id, userId, normProvider, normVideoId, title || null, thumbnail || null, normLang, languages_json, lines_json, line_count, char_count, now, now);
return transcriptRowToApi(db.prepare(`SELECT * FROM transcript_history WHERE id = ?`).get(id));
}
export function getTranscriptHistoryItem({ userId, provider, videoId, lang, id }) {
ensureTranscriptHistoryTable();
if (id) {
return transcriptRowToApi(db.prepare(`SELECT * FROM transcript_history WHERE id = ? AND user_id = ?`).get(id, userId));
}
if (!provider || !videoId) return null;
const normProvider = String(provider).trim().toLowerCase();
const normVideoId = String(videoId).trim();
if (lang) {
const normLang = String(lang).trim().slice(0, 12).toLowerCase();
return transcriptRowToApi(db.prepare(
`SELECT * FROM transcript_history WHERE user_id = ? AND provider = ? AND video_id = ? AND lang = ?`
).get(userId, normProvider, normVideoId, normLang));
}
return transcriptRowToApi(db.prepare(
`SELECT * FROM transcript_history WHERE user_id = ? AND provider = ? AND video_id = ? ORDER BY updated_at DESC LIMIT 1`
).get(userId, normProvider, normVideoId));
}
export function listTranscriptHistory({ userId, limit = 50, before, q, provider, lang }) {
ensureTranscriptHistoryTable();
const lim = Math.max(1, Math.min(200, Number(limit) || 50));
const conds = ['user_id = ?'];
const params = [userId];
if (before) { conds.push('updated_at < ?'); params.push(String(before)); }
if (provider && String(provider).trim()) {
conds.push('provider = ?');
params.push(String(provider).trim().toLowerCase());
}
if (lang && String(lang).trim()) {
conds.push('lang = ?');
params.push(String(lang).trim().slice(0, 12).toLowerCase());
}
if (typeof q === 'string' && q.trim().length > 0) {
const like = `%${q.trim()}%`;
conds.push(`(COALESCE(title,'') LIKE ? OR video_id LIKE ? OR lang LIKE ? OR COALESCE(lines_json,'') LIKE ?)`);
params.push(like, like, like, like);
}
const where = `WHERE ${conds.join(' AND ')}`;
const rows = db.prepare(`SELECT * FROM transcript_history ${where} ORDER BY updated_at DESC LIMIT ?`).all(...params, lim);
return rows.map(transcriptRowToApi);
}
export function deleteTranscriptHistoryById(userId, id) {
ensureTranscriptHistoryTable();
db.prepare(`DELETE FROM transcript_history WHERE id = ? AND user_id = ?`).run(id, userId);
}
export function deleteAllTranscriptHistory(userId, provider) {
ensureTranscriptHistoryTable();
if (provider && String(provider).trim()) {
db.prepare(`DELETE FROM transcript_history WHERE user_id = ? AND provider = ?`).run(userId, String(provider).trim().toLowerCase());
return;
}
db.prepare(`DELETE FROM transcript_history WHERE user_id = ?`).run(userId);
}
// -------------------- Likes (via tags/video_tags) --------------------
function ensureTag(userId, name) {
const tag = db.prepare(`SELECT id FROM tags WHERE user_id = ? AND name = ?`).get(userId, name);