From 201fe979b8d719fd1f05a1ea86bc7ad920450730 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sun, 27 Sep 2026 12:16:09 -0400 Subject: [PATCH] =?UTF-8?q?feat(history):=20miniatures=20des=20vid=C3=A9os?= =?UTF-8?q?=20import=C3=A9es=20(i.ytimg=20d=C3=A9terministe)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/migrations/20260927_backfill_youtube_thumbnails.sql | 7 +++++++ server/index.mjs | 7 ++++++- src/components/account/history/history.component.html | 2 +- src/components/account/history/history.component.ts | 10 ++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 db/migrations/20260927_backfill_youtube_thumbnails.sql diff --git a/db/migrations/20260927_backfill_youtube_thumbnails.sql b/db/migrations/20260927_backfill_youtube_thumbnails.sql new file mode 100644 index 0000000..b5bced6 --- /dev/null +++ b/db/migrations/20260927_backfill_youtube_thumbnails.sql @@ -0,0 +1,7 @@ +-- Backfill des miniatures YouTube manquantes (imports Takeout sans vignette). +-- Les miniatures YouTube sont déterministes : i.ytimg.com/vi//hqdefault.jpg +UPDATE watch_history +SET thumbnail = 'https://i.ytimg.com/vi/' || video_id || '/hqdefault.jpg' +WHERE provider = 'youtube' + AND (thumbnail IS NULL OR thumbnail = '') + AND length(video_id) = 11; diff --git a/server/index.mjs b/server/index.mjs index 9435dca..3738ecd 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -893,13 +893,18 @@ r.post('/user/history/takeout', authMiddleware, oauthLimiter, (req, res) => { const videoId = String(it?.videoId || '').trim().slice(0, 64); if (!/^[A-Za-z0-9_-]{6,64}$/.test(videoId)) { skipped++; continue; } const title = String(it?.title || '').slice(0, 300) || videoId; + // Miniature YouTube déterministe quand Takeout n'en fournit pas. + let thumbnail = String(it?.thumbnail || '').slice(0, 500); + if (!thumbnail && /^[A-Za-z0-9_-]{11}$/.test(videoId)) { + thumbnail = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; + } let watchedAt = new Date().toISOString(); if (it?.watchedAt) { const d = new Date(String(it.watchedAt)); if (!Number.isNaN(d.getTime())) watchedAt = d.toISOString(); } try { - upsertWatchHistory({ userId: req.user.id, provider: 'youtube', videoId, title, watchedAt }); + upsertWatchHistory({ userId: req.user.id, provider: 'youtube', videoId, title, thumbnail, watchedAt }); imported++; } catch { skipped++; } } diff --git a/src/components/account/history/history.component.html b/src/components/account/history/history.component.html index db87fab..2d62d75 100644 --- a/src/components/account/history/history.component.html +++ b/src/components/account/history/history.component.html @@ -193,7 +193,7 @@
- @if (w.thumbnail) { } + @if (thumbOf(w)) { }
diff --git a/src/components/account/history/history.component.ts b/src/components/account/history/history.component.ts index e88bc5d..5296d06 100644 --- a/src/components/account/history/history.component.ts +++ b/src/components/account/history/history.component.ts @@ -311,6 +311,16 @@ export class HistoryComponent { }); // ============ Helpers affichage (conservés) ============ + /** Miniature : stockée, sinon déterministe YouTube (imports Takeout sans vignette). */ + thumbOf(w: WatchHistoryItem): string { + if (w?.thumbnail) return w.thumbnail; + const id = String(w?.video_id || ''); + if ((w?.provider === 'youtube' || w?.provider === 'yt') && /^[A-Za-z0-9_-]{11}$/.test(id)) { + return `https://i.ytimg.com/vi/${id}/hqdefault.jpg`; + } + return ''; + } + formatDate(dateString: string | Date): string { if (!dateString) return ''; const date = new Date(dateString);