fix(history): recherche et filtres fonctionnels sur les 3 onglets
CI / build-and-test (push) Successful in 14m0s
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:
+58
-21
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user