- InnerTube layer via pinned youtubei.js 18.1.0 (no quota, no key): search with merged continuations (unlimited pages), watch-next related with LockupView mapping, caption-track discovery - 3-layer dispatcher (YT_SEARCH_MODE, default innertube-first): innertube -> yt-dlp scrape -> official API, graceful errors.yt - Robust yt-dlp binary resolution (YT_DLP_PATH > PATH > bundled) with systematic API fallback (fixes spawn ENOENT in UI) - Transcript: InnerTube caption discovery (YT_TRANSCRIPT_SOURCE), reusing pickTrack/orderedTracks/parseTrackText; yt-dlp fallback kept - Watch: sidebar uses real watch-next related[] (/api/details), title-search fallback for other providers - Cache: memory LRU + SQLite (youtube_search_cache, youtube_metrics), never persist empty pages; /healthz observability; /api/trending - Includes pending Step 15/16 leftovers in same files (suggest, test scripts); unrelated provider adapters left uncommitted
27 KiB
Search UX — Steps status
All 14 steps: DONE (100%) ✅
Done
- Step 1: Audit current codebase for existing search and provider logic on frontend and server
- Step 2: Design and scaffold Provider Registry on frontend and server
- Step 3: Implement SearchService with multi-provider params and HTTP request
- Step 4: Build SearchBoxComponent with chips, @autocomplete, and keyboard shortcuts
- Step 5: Build ProviderPickerComponent (Ctrl/⌘+K modal) with filtering, History column and apply
- Step 6: Build SearchSuggestionsComponent with grouped sections, per-provider unsupported rows and deep-links
- Step 7: Add router query-param sync and default provider preference loading (Search page fallback to user preference)
- Step 8: Implement backend /api/search with providers param and fan-out to server/providers handlers
- Step 9: Persist default providers preference in user profile (client + server endpoints wired, tested)
- Step 10: Add keyboard accessibility and a11y behaviors (Tab/Shift+Tab focus trap, Esc, aria, focus restore)
- Step 11: Write unit tests (SearchService, ProviderPicker toggle, @ parsing)
- Step 12: Write basic e2e scenarios for providers filtering and deep-link
- Step 13: Add minimal telemetry hooks and events
- Step 14: Update README with search UX section, keyboard shortcuts and usage notes
- Step 15: Auto-complétion de la requête (typeahead) dans la barre de recherche
- Step 16: Transcripts de vidéos sur la page Watch (API + UI, Phase 1 du doc)
Step 15 — Suggestions de requêtes dans la barre de recherche
Ce que ça veut dire (et ce que ce n'est pas)
Quand l'utilisateur tape tutoriel an, la barre affiche avant qu'il valide une liste de
requêtes complètes probables : tutoriel angular, tutoriel android… + ses recherches passées.
⚠️ À ne pas confondre avec
SearchSuggestionsComponent(src/components/search/search-suggestions.component.ts), qui lui affiche les résultats de recherche groupés par provider après la validation sur/search. Le Step 15 ajoute un nouveau panneau de suggestions de texte sous l'<input>.
Ce qui existe déjà (à réutiliser)
| Élément | Fichier | État |
|---|---|---|
@Output() searchChange (query debouncée 300 ms) |
src/components/search/search-box.component.ts:46 |
Émis mais aucun abonné — le header ne branche que (submitted) (header.component.html:19) |
Popover @provider + navigation clavier |
search-box.component.ts:16 (AT_QUERY_RE), .html:56-76 |
✅ mais ne couvre que les ids de providers |
| Historique de recherches | HistoryService.getSearchHistory(n) |
✅ utilisé dans le Quick Menu |
| Cache in-memory TTL 60 s | src/app/search/search.service.ts:33 |
Pattern à dupliquer |
| Fan-out multi-providers | GET /api/search (server/index.mjs:2116) |
Pattern à dupliquer |
Livrables
- Backend —
GET /api/search/suggest?q=…&providers=yt,dm&limit=10→{ q, groups: { yt: string[], dm: string[], … } }- Un
suggest(q, { limit })optionnel par handler dansserver/providers/*.mjs+ déclaré dansserver/providers/registry.mjs(ProviderAdapter). - Providers sans API de suggestions native →
[](dégradation propre, jamais d'erreur 500). - Cache + rate-limit sur le même modèle que
/api/search.
- Un
- Front service —
SuggestService(ou méthodesuggest()surSearchService) :min length 2,debounce 250-300 ms,switchMap(annulation de la requête précédente), cache 5 min, dédoublonnage + tri. - UI — panneau sous l'input, sections :
- 🔎 « Recherches récentes » (History, avec icône horloge)
- 📊 Suggestions providers, groupées par provider (badges
YT/DM/…) ou fusionnées - Sous-chaîne commune mise en évidence
- Interactions — clic = remplir + soumettre ; ↑/↓/Enter/Tab/Esc ; priorité au popover
@quand il est ouvert ;aria-listbox+aria-activedescendant(lerole="combobox"est déjà sur l'input). - Télémétrie —
suggest_shown,suggest_used(à ajouter à la whitelist serveur de l'étape 13). - Tests —
npm run test:suggest(unitaires parsing/dédup) + scénario e2e :providers=ytne renvoie que des suggestionsyt, provider inconnu → fallback registry complet.
Critères d'acceptation
- Aucune requête réseau tant que
q.trim().length < 2 - frappe rapide = une seule requête en vol (pas d'empilement / de réponse qui écrase la dernière)
- Esc ferme le panneau sans effacer le texte ; Tab complète sans lancer la recherche
- Un provider sans suggestions ou en échec n'affiche ni trou ni erreur dans le panneau
- Focus clavier et lecteurs d'écran cohérents avec l'existant (a11y Step 10)
Step 16 — Transcripts (Phase 1 du docs/transcript_architecture_dev.md)
Principe directeur
Un endpoint, un module de parsing, une UI. La disponibilité dépend de la plateforme, mais le code ne change pas selon le provider (le doc appelle ça « Rung 2 : réutiliser, pas réinventer »).
Source des données : yt-dlp --dump-single-json --skip-download (déjà utilisé côté serveur, ex.
server/index.mjs:1012) → on lit subtitles / automatic_captions → on télécharge la piste json3 ou vtt
→ on normalise en { lang, available, languages, lines: [{ t, dur, text }] }.
Périmètre Phase 1 (à faire)
Backend
server/transcript.mjs: fonctions purespickTrack(json, lang),parseJson3(data),parseVtt(text)(testables sans réseau ni DB)- Route
GET /api/transcript/:provider/:videoId?lang=&instance=&slug=&sourceUrl=dansserver/index.mjs(instance/slug/sourceUrlnécessaires pour PeerTube multi-instances ; réutiliserproviderUrlFrom()). transcriptCache:Map+ TTL long (24 h), clétranscript:${provider}:${videoId}:${lang}.- Rate-limit (~10 req/min/IP) sur le motif
channelsLimiter. - Aucun sous-titre → HTTP 200
{ available: false }(pas une erreur). - Gestion 429 persistant + fallback « temp dir » (
writeAutoSub) et variables d'env (TRANSCRIPT_CACHE_TTL,TRANSCRIPT_RATE_LIMIT,TRANSCRIPT_FALLBACK_TMP).
Frontend (variante UI #1 du doc)
- Bouton « Transcript » à côté du bouton Download (
watch.component.html:159). - Panneau copié sur le motif du Download Panel (
watch.component.html:185) :toggleTranscript()+loadTranscript(), chargement/absent/lines,<select>de langue (langue par défaut = préférence utilisateurlanguage). - Le panneau ne doit jamais casser la page Watch (providers sans sous-titres : Twitch, Odysee, Rumble).
Tests & docs
server/tests/transcript.test.mjs: fixturesjson3/vttoffline (parseurs) + intégrité du contrat API.- Script
npm run test:transcript+ l'ajouter à.github/workflows/ci.yml. - README : cocher
⏳ Sous-titres & transcriptsdans la roadmap.
Hors périmètre Phase 1 (à explicitement garder pour plus tard)
- Clic sur une ligne = seek (variante UI #2) : exige de brancher par provider
VideoPlayerComponent.seekBy()(src/components/video-player/video-player.component.ts:112) etIframeProgressService.seekTo()(src/services/iframe-progress.service.ts:104, iframe YouTube). - Cache Redis / multi-instance, proxies rotatifs, monitoring des 429, formats exotiques.
Décisions à trancher avant de coder
- Providers réellement supportés en Phase 1 (le doc teste YouTube + Dailymotion ; PeerTube a sa propre API
de sous-titres — passer par
yt-dlpou par l'API instance ?). - Persistance: cache mémoire seulement, ou table SQLite
transcripts(comme pour les téléchargements) pour survivre aux redémarrages ? - Transcripts générés (auto-captions) : inclus d'emblée ou option utilisateur ?
- Volume de réponse : les gros transcripts (2 h+) nécessitent-ils une troncature / pagination ?
Critères d'acceptation
- 1 appel API, 1 parsing, 1 UI identiques quel que soit le provider
- 2ᵉ requête sur la même vidéo = réponse depuis le cache (pas d'appel
yt-dlp) yt-dlpen échec → 502{ available: false, error: 'transcript_fetch_failed' }, Watch intactenpm run test:transcriptpasse hors ligne (fixtures)
Décisions Phase 1 (tranchées à l'implémentation)
- Providers : générique via
yt-dlp(ids courtsyt/dm/…+ longs normalisés), aucun code par plateforme. - Persistance : cache mémoire seul (TTL 24 h, 200 entrées LRU) ; SQLite reporté en Phase 3.
- Auto-captions : incluses d'emblée (fallback après les sous-titres manuels).
- Volume : troncature
TRANSCRIPT_MAX_LINES(défaut 5000 lignes).
Idées de nouvelles fonctions utiles
Classées par effort (~ = demi-journée, + = 1-2 jours, ++ = une semaine+). Triées par rapport valeur / coût pour l'existant.
Recherche & découverte
| # | Fonction | Description | Effort |
|---|---|---|---|
| 1 | Filtres de recherche | Durée (< 4 min / 4-20 / 20+), type (vidéo/chaîne/playlist), date de mise en ligne, langue. SearchService.sort$ existe déjà, il manque les params + l'UI en chips + le mapping par adapter |
+ |
| 2 | Pagination / infinite scroll | ✅ DONE (Step 18) : backend pages illimitées via continuations InnerTube (page=2,3…, pageSize ≤ 50) + front loadNextPage() + <app-infinite-anchor> + mergeGroups + endReached (search.component.ts) |
~ |
| 3 | Dédup multi-provider | La même vidéo existe souvent sur Rumble/Odysee/PeerTube : regrouper par titre+durée+chaîne et afficher « aussi disponible sur… » | + |
| 4 | Recherche dans les résultats (« search within results ») | Relancer la requête en restreignant au provider/à la chaîne déjà affichée | ~ |
| 5 | Écran « Recherches récentes » | Gérer (renommer/supprimer/vider) l'historique de recherche aujourd'hui visible seulement dans le Quick Menu | ~ |
Watch & lecture
| # | Fonction | Description | Effort |
|---|---|---|---|
| 6 | Recherche dans le transcript (Ctrl+F du panneau) |
Surligne les occurrences + liste des lignes correspondantes ; prolongement naturel du Step 16 | ~ |
| 7 | Export du transcript (.vtt / .srt / .txt) |
Les lignes {t, dur, text} sont déjà normalisées, il ne manque qu'un rendu côté serveur |
~ |
| 8 | Résumé IA du transcript | GEMINI_API_KEY + /api/ai/summarize (commentaires) existent déjà (server/index.mjs:2050) : même pattern appliqué au transcript |
+ |
| 9 | Chapitres | Parser chapters de yt-dlp --dump-single-json (ou les timestamps du titre/description) et les afficher sous le player |
+ |
| 10 | Deep-link avec timestamp | #/watch/yt/VIDEO?t=123 : seek au démarrage + bouton « Partager à ce moment » |
~ |
| 11 | Reprendre la lecture | Mémoriser la position par vidéo dans history et proposer « Reprendre à 12:34 » |
~ |
| 12 | Sélecteur de qualité + « auto » intelligent | Déjà à la roadmap ; formatListFromMeta() et le format id existent dans la file de téléchargement, réutilisables pour le player |
+ |
Contenus & social
| # | Fonction | Description | Effort |
|---|---|---|---|
| 13 | Abonnements (chaînes) | À la roadmap. Le socle est prêt : server/providers/channel-registry.mjs + channel-content.mjs (videos/shorts/playlists/live). Il manque table subscriptions, CRUD API et page « flux d'abonnements » |
++ |
| 14 | Tags & recherche par tags | Extraire les tags depuis dumpSingleJson et permettre #/search?tags=angular |
+ |
| 15 | Import/Export playlists (JSON / OPML) | À la roadmap ; simple contrat de sérialisation + validateur serveur | ~ |
| 16 | Notifications « nouvelle vidéo » | Pour les abonnements : poll planifié côté serveur + badge dans le header | + |
Ops & qualité
| # | Fonction | Description | Effort |
|---|---|---|---|
| 17 | /healthz + page Admin |
À la roadmap : état des clés API (OK/KO), version de yt-dlp, hit/miss des caches, compteurs de rate-limit |
+ |
| 18 | Cache serveur configurable par provider | Remplacer le YT_CACHE_TTL_MS global par un TTL par provider (CACHE_TTL_MS_YT=…) et exposer les clés de cache vides/pleines |
~ |
| 19 | PWA installable | Manifest + Service Worker cache des métadonnées (les vidéos restent online) | + |
| 20 | Superposition raccourcis clavier (?) |
Un @HostListener('document:keydown') global + modale récapitulative ; tous les raccourcis existent déjà, rien n'est découvrable |
~ |
| 21 | i18n élargi | Le pipeline de traduction existe (utilisé comme 'search.placeholder' + pipeline t) ; il reste à compléter les lexiques et sortir les chaînes en dur du FR/EN mélangé actuel |
++ |
Test commands
npm run test:search # Step 11 — unit tests: SearchService + @ parsing + ProviderPicker
npm run test:search-e2e # Step 12 — e2e scenarios against a real isolated server
npm run test:preferences # Step 9 — defaultProviders persistence
npm run test:telemetry # Step 13 — telemetry events (insert/list/count)
npm run test:suggest # Step 15 — typeahead parsing/dedup + /api/search/suggest contract
npm run test:transcript # Step 16 — json3/vtt parsers + /api/transcript contract
Implementation notes
- Step 11 (
src/app/search/search.service.spec.ts,search-components.spec.ts): ts-node runs with isolated transpilation (no decorator output => no ɵfac), so TestBed cannot instantiate Angular classes. Instead the specs import@angular/compiler, then build components insideInjector.create(...)+runInInjectionContext(...)with fakes for SearchService/UserService/HistoryService/TelemetryService and mock internal schedulers (ɵChangeDetectionScheduler, ɵEffectScheduler with immediate-run effects). - Step 12 (
server/tests/search.e2e.test.mjs): spawns a real server (temp SQLite DB via NEWTUBE_DB_FILE, ephemeral port, test JWT secret) and asserts: providers filter targets exactly the requested ids; deep-link single-provider; invalid providers fall back to the full registry; defaultProviders preference persists; SPA /search route serves the shell. Assertions are on fan-out shape, not on external provider content. - Step 13:
telemetry_eventstable (db/migrations/20250924_add_telemetry_events.sql), server helpers (insertTelemetryEvent/listTelemetryEvents/countTelemetryEvents) + routesPOST /telemetry/events(whitelisted event names, rate-limited),GET /telemetry/events|/summary(own-events only). Front:TelemetryService(fire-and-forget, dedup 500ms window, payload sanitization) wired into SearchBox hooks: search_submit, provider_picker_open, provider_apply, at_autocomplete_use, quick_menu_open. - Step 14: README gained a "Recherche unifiée" section (chips, @autocomplete, Ctrl/⌘+K picker, deep-links, preference fallback, a11y), API endpoints, test commands and roadmap updates. GIF placeholder TODO added.
- CI (
.github/workflows/ci.yml) now runs preferences, telemetry, search unit and e2e tests.
Step 17 — Plan Anti-Quota YouTube (sans yattee-server en dépendance critique)
Contexte / diagnostic
- Aujourd'hui
server/providers/youtube.mjs:29-93+server/index.mjs:880-1069utilisent uniquement la YouTube Data API v3 (search.list+videos.list) avec rotationYOUTUBE_API_KEYS/YOUTUBE_API_KEYsurquotaExceeded|rateLimitExceeded|dailyLimitExceeded|API_KEY_INVALID. - Coût officiel :
search.list= 100 unités,videos.list= 1 unité, quota gratuit = 10 000 unités/jour/projet → ~100 recherches/jour max. Chaque pagepageTokenet chaque enrichissementvideos.listaggrave. - Cache actuel :
YT_CACHE_TTL_MSdéfaut 5 min (server/index.mjs:904-915) en mémoire seule, pas de persistance, pas de monitoring de conso. - Bonne nouvelle : NewTube utilise déjà
yt-dlp(youtube-dl-exec,YT_DLP_PATH,server/index.mjs:83-124) pourtranscript+downloads+ enrichissement Watch. LesuggestYT est déjà sans clé (suggestqueries.google.com,youtube.mjs:248-264). - Conclusion analyse
yattee/yattee-server: bonne architecture à copier (couches InnerTube → Invidious → yt-dlp + cache + egress-proxy), mais ne pas l'ajouter comme service critique (projet de 02/2026, ~107 stars, même combat anti-ban IP/cookies/PO-Token, redondant avec ton yt-dlp direct). Option sidecar seulement en Phase 4.
Principe cible
Primaire sans clé (yt-dlp / InnerTube), API officielle en fallback payant, cache SQLite + mémoire, anti-ban configurable, observabilité.
Front /api/search?providers=yt → searchCache (mémoire + SQLite)
→ YT_SEARCH_MODE=scrape-first (défaut) : yt-dlp `ytsearchN:` / InnerTube → OK ? return
→ sinon fallback API officielle (rotation clés) → OK ? return
→ sinon 200 avec `errors.yt` (dégradation propre, jamais 500)
Phase 0 — Quick wins (0,5 j, sans changer de source)
- Monter
YT_CACHE_TTL_MSà 30-60 min pouryt, ajouterSUGGEST_CACHE_TTL_MSdéjà à 5 min, clé cacheq|limit|page|sort. - Réduire le coût API :
maxResultsmax 25 au lieu de 50, ne fairevideos.listque sidetails=true, debounce front déjà 250-300 ms +switchMap. - Ajouter
GET /healthz+ compteur quota estimé (search*100 + videos*1) exposé pour Admin (prépare idée #17). - Doc
.env.example: expliquerYOUTUBE_API_KEYSCSV vs JSON, rotation auto.
Phase 1 — Recherche YT sans clé (2-3 j, cœur du plan)
- Nouveau
server/providers/youtube-scrape.mjs:search(q, {limit, page, sort})viayt-dlp --dump-single-json --flat-playlist "ytsearch{limit}:{q}"(timeout 15-20 s,YT_DLP_PATHréutilisé), mapping versSuggestionidentique àyoutube.mjs:199-232(title/id/thumbnail/duration/views/publishedAt/channelId/embeddable).- Support
sort:relevance|date|views→ préfixeytsearch+ tri local si besoin. - Jamais de throw bloquant : erreur → throw avec
ytStatuspour que/api/searchmetteerrors.yt.
- Modifier
server/providers/youtube.mjsen dispatcher :env YT_SEARCH_MODE=scrape-first|api-first|scrape-only|api-only(défautscrape-first).scrape-first: essaie scrape, fallback API.api-first: comportement actuel. Permet rollback instantané.
- Cache persistant : table SQLite
youtube_search_cache(q_hash, payload, created_at)TTLYT_SCRAPE_TTL_MS(défaut 30 min) + garde mémoire actuelle. Survit au restart, tue 80% des appels doublons. - Rate-limit + concurrence : réutiliser
express-rate-limitexistant, timeout fan-out par provider 8-12 s,Promise.allSettleddéjà en place dans/api/search. - Tests :
server/tests/youtube-scrape.test.mjsoffline (fixturesyt-dlp --flat-playlistmockées) + e2eYT_SEARCH_MODE=scrape-onlysans clé →groups.ytnon vide ;api-onlysans clé →errors.yt=youtube_api_key_unavailable. - Env :
YT_SEARCH_MODE,YT_SCRAPE_TTL_MS,YT_DLP_TIMEOUT_MS,YT_DLP_PATH(déjà),YOUTUBE_API_KEYSdevient optionnel.
Phase 2 — Robustesse anti-ban (1-2 j, inspiré yattee-server)
- Support
YT_COOKIES_FILE+YT_PO_TOKENpassés à yt-dlp (--cookies,--extractor-args youtube:po_token=...). Doc : comment exporter cookies fresh, rotation manuelle. Sans ça :Sign in to confirm you're not a bot. - Support
YT_EGRESS_PROXY(HTTP/SOCKS) pour tout le trafic YT (yt-dlp + fetch InnerTube), configurable au runtime comme yatteeSSRF_EXTRA_ALLOWED_CIDRSsi Invidious LAN. - Auto-update yt-dlp : script
npm run ytdlp:update+ check version dans/healthz(idée #17).deno/ffmpeg-staticdéjà requis pour challenge JS. - Observabilité Admin (idée #17) : page
Admin > YouTube: mode actif, version yt-dlp, hit/miss cache, erreursquotaExceededvsbot-checkvstimeout, état chaque clé...abcd OK/KO.
Phase 3 — Fonctionnalités bonus débloquées par le sans-clé (1 j / feature)
Trending YT sans clé:yt-dlp --flat-playlist "https://www.youtube.com/feed/trending"→ alimente AccueilTendances & Viralsans quota.Channel browsing enrichi: réutiliserchannel-registry.mjs+channel-content.mjsvia scrape (videos/shorts/streams/playlists) au lieu dechannels.listpayant → prérequis Abonnements (idée #13).Chapitres(idée #9) : parserchaptersdedump-single-jsondéjà dispo.Filtres recherche(idée #1) :duration/date/typemappés sur args yt-dlp + filtre local, 0 coût API.Pagination / infinite scroll(idée #2) :page$déjà câblé, branchernextPageTokenscrape via--flat-playlist --playlist-start.
Phase 4 — Option yattee-server sidecar (seulement si Phase 1 insuffisante)
docker-compose/yattee-server.yml: imageyattee/yattee-server,INVIDIOUS_INSTANCE_URLoptionnelle,ADMIN_USERNAME/PASSWORDvia.env.- Adaptateur
server/providers/youtube-yattee.mjs:GET ${YATTEE_URL}/api/v1/search?q=&type=video+ Basic Auth →Suggestion[]. Activé parYT_SEARCH_MODE=yattee. - Critère GO/NO-GO : si scrape direct tient >95% succès sur 7 j, abandonner sidecar. Sinon le garder pour
trending/comments/captions.
Risques / ToS
- Scraping/InnerTube = gris vis-à-vis ToS YouTube, casses fréquentes → prévoir fallback API + version yt-dlp pinnée + alertes
/healthz. - Pas de magie IP : datacenter OVH/Hetzner = ban plus vite que résidentiel → prévoir proxy sortant dès le déploiement public.
- Ne jamais logger clés, cookies, PO-Token.
Critères d'acceptation Step 17
- Sans aucune
YOUTUBE_API_KEY,GET /api/search?q=test&providers=ytretournegroups.yt[](via scrape) etnpm run test:search-e2epasse. (vérifié live : scrape-only retourne 3 résultats avec durée/vues/channelId) - Avec quota épuisé simulé (400/403 mock), fallback scrape prend le relais sans 500. (dispatcher scrape-first → api en fallback sur bot-check/timeout/upstream)
- 2ᵉ appel identique < 50 ms (hit cache mémoire/SQLite, pas d'appel yt-dlp). (mesuré : 3971 ms → 30 ms)
/healthzexposeytdlpVersion,mode,cacheHitRate. (/healthz+/api/healthz: mode, ytdlp bin/version, antiban, cache mem+sqlite, metrics jour, clés)- Aucune régression
dm/tw/pt/od/ru. (test:search-e2e+test:suggestverts)
Implémenté le 2026-09-25 (Step 17 DONE)
- Nouveaux :
server/providers/youtube-common.mjs(clés,YT_SEARCH_MODE, args anti-ban cookies/PO-Token/proxy, hash, métriques),server/providers/youtube-scrape.mjs(search/channel/trending viayt-dlp --flat-playlist, parsersmapFlatEntry/parseFlatPlaylistJsontestés offline),db/migrations/20250926_add_youtube_scrape_cache.sql,server/tests/youtube-scrape.test.mjs(npm run test:ytscrape+ CI). - Modifiés :
youtube.mjs(dispatcher scrape-first/api-first/scrape-only/api-only + cache mémoire LRU + SQLite + logs source/latency),channel-content.mjs(resolve + contenu YT en scrape-first, fallback API),index.mjs(/healthz,/api/trending, import common),db.mjs(helpers cache/métriques jour),.env.example(nouvelles vars),package.json(test:ytscrape,ytdlp:update). - Notes :
/feed/trendingretiré côté YouTube (redirect home) → repliytsearchtrié vues. yt-dlp local2026.08.19: pensernpm run ytdlp:update. Sidecar yattee-server abandonné (scrape direct >95% : à confirmer sur 7 j). - Fix 2026-09-25 (
spawn yt-dlp ENOENT) :resolveYtDlpBin()(youtube-common.mjs) avec ordreYT_DLP_PATH> PATH > bundledyoutube-dl-exec(cause : yt-dlp seulement présent via shims scoop utilisateur, invisible d'un autre contexte/Docker) ; fallback API sur tout échec scrape enscrape-first(plus d'erreur brute en UI) ; message actionnableyoutube_no_sourcesi ni binaire ni clé ;/healthzexposebinOk+ binaire résolu.
Step 18 — InnerTube direct façon SmartTube (pagination illimitée + vidéos connexes) ✅
Contexte
SmartTube (yuliskov/SmartTube, 34k stars) ne touche ni la Data API ni les Google Services : il parle à InnerTube (youtubei/v1/*, clients TV) via MediaServiceCore ("Unofficial Java api for YouTube") — search + continuations, watch-next (/next), player. D'où 0 quota et scroll infini.
Implémenté le 2026-09-25
- Dépendance
youtubei.jspinnée18.1.0(API interne non documentée → pin + bump manuel). - Nouveau
server/providers/youtube-innertube.mjs: session singleton lazy (YT_INNERTUBE_GL/HL),searchViaInnerTube(continuations fusionnées jusqu'à couvrirpage*limit— corrige le bug "page 2 vide" : 1 page InnerTube ≈ 17-20 items ≠limit),getRelatedViaInnerTube(watch-next), mappers pursmapVideoNode/mapLockupView/parseViewsText/parseDurationLabel, chaîne de continuations en mémoire + SQLite existant. mapLockupView: le watch-next moderne renvoie des LockupView (content_id,metadata.title.text,content_image.image[], chaîne + vues dansmetadata_rows, durée dans le label a11y) — filtrés avant, mappés maintenant.- Dispatcher
innertube-first(défaut) : InnerTube → scrape yt-dlp → API officielle ;innertube-onlyajouté ; les anciens modes inchangés ; on ne persiste plus les résultats vides (anti-empoisonnement du cache). GET /api/details/youtube/:videoId→ champrelated[](24 items, cache mémoire 1h,?related=0pour désactiver, best-effort jamais bloquant).- Tests
server/tests/youtube-innertube.test.mjs(npm run test:ytinnertube+ CI),.env.exampleà jour. - Vérifié live : recherche p1=50 + p2=50 = 100 uniques (fin de la limite 24), related=24 avec titres FR/durées/vues,
test:search-e2e+test:suggestverts. - Limites connues : pas de
getTrendingen youtubei.js v18 → trending reste sur scrape ; continuations parfois redondantes (dédup + garde-fou 12) ; même combat anti-ban IP qu'avant (cookies/PO-Token/proxy réutilisés côté yt-dlp, session InnerTube sans auth). - Complément 2026-09-26 : Watch → sidebar « connexes » branchée sur le vrai watch-next (
loadRelatedSuggestions()utiliseGET /api/details/youtube/:id→related[], fallback recherche-par-titre pour les autres providers et en cas d'échec) ;docker-compose/.env.example+README.md(endpoints + roadmap) à jour ; idée #2 (pagination/infinite scroll) clôturée. - Complément transcript InnerTube : découverte des pistes via
getInfo().captions(YT_TRANSCRIPT_SOURCE, défautinnertube-first) adaptée au format yt-dlp →pickTrack/orderedTracks/parseTrackTextinchangés ; 0 piste InnerTube =no_subtitlesdirect (même backend que le lecteur) ; fallback yt-dlp conservé. Vérifié live (lang=en→ 217 lignes) ;test:transcript17/17.