feat(library): liste « À regarder plus tard » (watch later)
CI / build-and-test (push) Successful in 14m55s

Nouvelle liste dans la section Vous de la barre latérale, avec un bouton
horloge a cote du coeur sur toutes les cartes (accueil, themes, provider-
themes, recherche).

- db : les 4 fonctions de listes (like/unlike/isLiked/list) prennent un
  paramètre `tag` — la table `tags` sert déjà de mot-clés génériques, donc
  zéro migration.
- api : les 4 routes likes sont générées par registerVideoTagRoutes(prefix,
  tagName) ; /api/user/watch-later = même fabrique, même enrichissement
  yt-dlp, même middleware cookie-aware. Entrée OpenAPI ajoutée.
- front : LikesService prend un VideoTag, like-button un @Input() list
  (icône horloge, ambre quand enregistré, libellés a11y dédiés) — un seul
  composant pour les deux boutons, pas de copie.
- cartes de recherche : la grille partagée n'avait AUCUN bouton ; cœur +
  horloge ajoutés, masqués sur les lives et cartes chaîne (coin bas-gauche
  déjà occupé par le compteur de spectateurs / pas des vidéos).
- route /library/watch-later = LikedComponent via data.list (titre, icône,
  placeholder et état vide pilotés par la route), clés nav.watchLater FR/EN.

Tests : npm run test:api 41/41 (nouveau test watch-later : ajout, statut,
indépendance vis-à-vis des likes, liste, retrait, 400, 401) + 21 autres
suites vertes. Build OK, instance locale 4200 rebuildée. Sondage Playwright
desktop+mobile : 12/14 (2 faux positifs du sondage : accordéon replié sur
une route hors /library, 3×401 console pendant la phase déconnectée).
This commit is contained in:
2026-10-01 14:13:43 -04:00
parent 88a2d21133
commit 735255b7d8
16 changed files with 246 additions and 114 deletions
+24 -21
View File
@@ -903,8 +903,11 @@ function ensureTag(userId, name) {
return id;
}
export function likeVideo({ userId, provider, videoId, title, thumbnail }) {
const tagId = ensureTag(userId, 'like');
// Les listes « par tag » (like, watch-later) partagent ces 4 fonctions :
// `tag` est le nom de ligne dans `tags` — la table sert déjà de mot-clés
// génériques, donc une nouvelle liste ne demande aucune migration.
export function likeVideo({ userId, provider, videoId, title, thumbnail, tag = 'like' }) {
const tagId = ensureTag(userId, tag);
// Upsert-like behavior; ignore if exists
db.prepare(`INSERT OR IGNORE INTO video_tags (user_id, provider, video_id, tag_id, created_at) VALUES (?, ?, ?, ?, ?)`)
.run(userId, provider, videoId, tagId, nowIso());
@@ -922,25 +925,25 @@ export function likeVideo({ userId, provider, videoId, title, thumbnail }) {
return { provider, video_id: videoId };
}
export function unlikeVideo({ userId, provider, videoId }) {
const tag = db.prepare(`SELECT id FROM tags WHERE user_id = ? AND name = ?`).get(userId, 'like');
if (!tag) return { removed: false };
export function unlikeVideo({ userId, provider, videoId, tag = 'like' }) {
const t = db.prepare(`SELECT id FROM tags WHERE user_id = ? AND name = ?`).get(userId, tag);
if (!t) return { removed: false };
const info = db.prepare(`DELETE FROM video_tags WHERE user_id = ? AND provider = ? AND video_id = ? AND tag_id = ?`)
.run(userId, provider, videoId, tag.id);
.run(userId, provider, videoId, t.id);
return { removed: (info.changes || 0) > 0 };
}
export function isVideoLiked({ userId, provider, videoId }) {
const tag = db.prepare(`SELECT id FROM tags WHERE user_id = ? AND name = ?`).get(userId, 'like');
if (!tag) return false;
export function isVideoLiked({ userId, provider, videoId, tag = 'like' }) {
const t = db.prepare(`SELECT id FROM tags WHERE user_id = ? AND name = ?`).get(userId, tag);
if (!t) return false;
const row = db.prepare(`SELECT 1 FROM video_tags WHERE user_id = ? AND provider = ? AND video_id = ? AND tag_id = ?`)
.get(userId, provider, videoId, tag.id);
.get(userId, provider, videoId, t.id);
return Boolean(row);
}
export function listLikedVideos({ userId, limit = 100, q }) {
export function listLikedVideos({ userId, limit = 100, q, tag = 'like' }) {
try {
console.log(`[listLikedVideos] Récupération des vidéos aimées pour l'utilisateur ${userId}`);
console.log(`[listLikedVideos] Récupération des vidéos "${tag}" pour l'utilisateur ${userId}`);
// Vérifier que la table tags existe
const tableExists = db.prepare(`
@@ -953,18 +956,18 @@ export function listLikedVideos({ userId, limit = 100, q }) {
return [];
}
// Récupérer le tag 'like' de l'utilisateur
const tag = db.prepare(`
// Récupérer le tag de la liste demandee
const tagRow = db.prepare(`
SELECT id FROM tags
WHERE user_id = ? AND name = ?
`).get(userId, 'like');
`).get(userId, tag);
if (!tag) {
console.log(`[listLikedVideos] Aucun tag 'like' trouvé pour l'utilisateur ${userId}`);
if (!tagRow) {
console.log(`[listLikedVideos] Aucun tag "${tag}" trouvé pour l'utilisateur ${userId}`);
return [];
}
console.log(`[listLikedVideos] Tag ID: ${tag.id}`);
console.log(`[listLikedVideos] Tag ID: ${tagRow.id}`);
// Vérifier que la table video_tags existe
const videoTagsExists = db.prepare(`
@@ -1032,11 +1035,11 @@ export function listLikedVideos({ userId, limit = 100, q }) {
// La requete est devenue longue (normalisation provider x 3, sous-requetes
// de repli) : la logger en entier poluait la sortie a chaque appel.
console.log(`[listLikedVideos] ${hasQ ? 'recherche' : 'liste'} pour ${userId} (tag ${tag.id})`);
console.log(`[listLikedVideos] ${hasQ ? 'recherche' : 'liste'} pour ${userId} (tag ${tagRow.id})`);
const rows = hasQ
? db.prepare(query).all(userId, tag.id, like, like, like, limit)
: db.prepare(query).all(userId, tag.id, limit);
? db.prepare(query).all(userId, tagRow.id, like, like, like, limit)
: db.prepare(query).all(userId, tagRow.id, limit);
console.log(`[listLikedVideos] ${rows.length} vidéos trouvées`);
return rows;