fix(history): recherche et filtres fonctionnels sur les 3 onglets
CI / build-and-test (push) Successful in 14m0s

- distinctUntilChanged() sur Subject<void> avalait toutes les frappes
  apres la premiere : barre de recherche morte sur les 3 onglets.
- Filtre provider recherches : ids courts stockes (yt, dm) vs filtre
  long (youtube) -> zero resultat. Match court+long cote serveur.
- LIKE echappes (%, _, \\) sur q des 3 onglets.
- Watch/transcripts : provider normalise (court+long), lang
  insensible a la casse avec prefixe (fr -> fr, fr-ca).
- Ecriture watch canonique (nom long). Test test:history.
This commit is contained in:
2026-09-27 21:34:40 -04:00
parent a57bc0f7ae
commit 71278eb0b1
5 changed files with 174 additions and 26 deletions
+58 -21
View File
@@ -399,6 +399,29 @@ export function cryptoRandomUUID() {
export default db;
// -------------------- Search History --------------------
// Ids courts (front : yt, dm, tw, pt, od, ru) <-> noms longs (API/Watch :
// youtube, dailymotion, twitch, peertube, odysee, rumble). Les filtres
// d'historique acceptent les deux formes ; le stockage garde la forme reçue.
const HISTORY_PROVIDER_ALIASES = {
youtube: ['youtube', 'yt'],
dailymotion: ['dailymotion', 'dm'],
twitch: ['twitch', 'tw'],
peertube: ['peertube', 'pt'],
odysee: ['odysee', 'od'],
rumble: ['rumble', 'ru'],
};
const HISTORY_SHORT_TO_LONG = { yt: 'youtube', dm: 'dailymotion', tw: 'twitch', pt: 'peertube', od: 'odysee', ru: 'rumble' };
/** Normalise un provider (court ou long, toute casse) vers le nom long, ou null. */
export function normalizeHistoryProvider(raw) {
const v = String(raw || '').trim().toLowerCase();
if (!v) return null;
if (HISTORY_PROVIDER_ALIASES[v]) return v;
return HISTORY_SHORT_TO_LONG[v] || null;
}
/** Échappe les jokers LIKE (`%`, `_`, `\`) d'une saisie utilisateur. */
export function escapeLikePattern(raw) {
return String(raw ?? '').replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
}
export function insertSearchHistory({ userId, query, filters }) {
const id = cryptoRandomId();
const created_at = nowIso();
@@ -426,14 +449,18 @@ export function listSearchHistory({ userId, limit = 50, before, q, provider }) {
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 ?)`);
const like = `%${escapeLikePattern(q.trim())}%`;
conds.push(`(query LIKE ? ESCAPE '\\' OR COALESCE(filters_json,'') LIKE ? ESCAPE '\\')`);
params.push(like, like);
}
if (provider && String(provider).trim()) {
const likeP = `%${String(provider).trim()}%`;
conds.push(`COALESCE(filters_json,'') LIKE ?`);
params.push(likeP);
const normProvider = normalizeHistoryProvider(provider);
if (normProvider) {
// filters_json stocke indifféremment ids courts (["yt","dm"]) et/ou nom
// long ("provider":"youtube") : on matche les deux formes (forme courte
// entre guillemets pour éviter les faux positifs de sous-chaînes).
const [long, short] = HISTORY_PROVIDER_ALIASES[normProvider];
conds.push(`(COALESCE(filters_json,'') LIKE ? ESCAPE '\\' OR COALESCE(filters_json,'') LIKE ? ESCAPE '\\')`);
params.push(`%${escapeLikePattern(long)}%`, `%\"${short}\"%`);
}
const where = `WHERE ${conds.join(' AND ')}`;
return db.prepare(`SELECT * FROM search_history ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, lim);
@@ -451,6 +478,8 @@ export function deleteAllSearchHistory(userId) {
export function upsertWatchHistory({ userId, provider, videoId, title, thumbnail, watchedAt, progressSeconds = 0, durationSeconds = 0, lastPositionSeconds }) {
const now = nowIso();
const watched_at = watchedAt || now;
// Stockage canonique (nom long) : les filtres matchent court + long.
const normProvider = normalizeHistoryProvider(provider) || String(provider || '').trim().toLowerCase();
// Insert or update on unique (user_id, provider, video_id)
db.prepare(`INSERT INTO watch_history (id, user_id, provider, video_id, title, thumbnail, watched_at, progress_seconds, duration_seconds, last_position_seconds, last_watched_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -461,10 +490,10 @@ export function upsertWatchHistory({ userId, provider, videoId, title, thumbnail
duration_seconds=MAX(excluded.duration_seconds, watch_history.duration_seconds),
last_position_seconds=COALESCE(excluded.last_position_seconds, watch_history.last_position_seconds),
last_watched_at=excluded.last_watched_at`).run(
cryptoRandomId(), userId, provider, videoId, title || null, thumbnail || null, watched_at, progressSeconds, durationSeconds, (typeof lastPositionSeconds === 'number' ? lastPositionSeconds : null), now
cryptoRandomId(), userId, normProvider, videoId, title || null, thumbnail || null, watched_at, progressSeconds, durationSeconds, (typeof lastPositionSeconds === 'number' ? lastPositionSeconds : null), now
);
// Return the row id
const row = db.prepare(`SELECT * FROM watch_history WHERE user_id = ? AND provider = ? AND video_id = ?`).get(userId, provider, videoId);
const row = db.prepare(`SELECT * FROM watch_history WHERE user_id = ? AND provider = ? AND video_id = ?`).get(userId, normProvider, videoId);
return row;
}
@@ -484,13 +513,16 @@ export function listWatchHistory({ userId, limit = 50, before, q, provider }) {
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());
const normProvider = normalizeHistoryProvider(provider);
if (normProvider) {
// Lignes anciennes : forme courte possible -> matche les deux formes.
const [long, short] = HISTORY_PROVIDER_ALIASES[normProvider];
conds.push(`(provider = ? OR provider = ?)`);
params.push(long, short);
}
if (typeof q === 'string' && q.trim().length > 0) {
const like = `%${q.trim()}%`;
conds.push(`(COALESCE(title,'') LIKE ? OR provider LIKE ? OR video_id LIKE ?)`);
const like = `%${escapeLikePattern(q.trim())}%`;
conds.push(`(COALESCE(title,'') LIKE ? ESCAPE '\\' OR provider LIKE ? ESCAPE '\\' OR video_id LIKE ? ESCAPE '\\')`);
params.push(like, like, like);
}
const where = `WHERE ${conds.join(' AND ')}`;
@@ -616,17 +648,21 @@ export function listTranscriptHistory({ userId, limit = 50, before, q, provider,
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());
const normProvider = normalizeHistoryProvider(provider);
if (normProvider) {
const [long, short] = HISTORY_PROVIDER_ALIASES[normProvider];
conds.push(`(provider = ? OR provider = ?)`);
params.push(long, short);
}
if (lang && String(lang).trim()) {
conds.push('lang = ?');
params.push(String(lang).trim().slice(0, 12).toLowerCase());
// `fr` matche `fr` et `fr-ca` ; casse indifférente (stockage minuscule).
const primary = String(lang).trim().split('-')[0].toLowerCase();
conds.push(`(lang = ? OR lang LIKE ? ESCAPE '\\')`);
params.push(primary, `${escapeLikePattern(primary)}-%`);
}
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 ?)`);
const like = `%${escapeLikePattern(q.trim())}%`;
conds.push(`(COALESCE(title,'') LIKE ? ESCAPE '\\' OR video_id LIKE ? ESCAPE '\\' OR lang LIKE ? ESCAPE '\\' OR COALESCE(lines_json,'') LIKE ? ESCAPE '\\')`);
params.push(like, like, like, like);
}
const where = `WHERE ${conds.join(' AND ')}`;
@@ -642,7 +678,8 @@ export function deleteTranscriptHistoryById(userId, id) {
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());
const norm = normalizeHistoryProvider(provider) || String(provider).trim().toLowerCase();
db.prepare(`DELETE FROM transcript_history WHERE user_id = ? AND provider = ?`).run(userId, norm);
return;
}
db.prepare(`DELETE FROM transcript_history WHERE user_id = ?`).run(userId);