feat(youtube): InnerTube-first search, transcripts and watch-next related (Steps 15-18)
- 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
This commit is contained in:
@@ -17,6 +17,179 @@
|
||||
- [x] Step 12: Write basic e2e scenarios for providers filtering and deep-link
|
||||
- [x] Step 13: Add minimal telemetry hooks and events
|
||||
- [x] Step 14: Update README with search UX section, keyboard shortcuts and usage notes
|
||||
- [x] Step 15: Auto-complétion de la **requête** (typeahead) dans la barre de recherche
|
||||
- [x] 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
|
||||
|
||||
1. **Backend** — `GET /api/search/suggest?q=…&providers=yt,dm&limit=10`
|
||||
→ `{ q, groups: { yt: string[], dm: string[], … } }`
|
||||
- Un `suggest(q, { limit })` optionnel par handler dans `server/providers/*.mjs` + déclaré dans
|
||||
`server/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`.
|
||||
2. **Front service** — `SuggestService` (ou méthode `suggest()` sur `SearchService`) :
|
||||
`min length 2`, `debounce 250-300 ms`, `switchMap` (annulation de la requête précédente),
|
||||
cache 5 min, dédoublonnage + tri.
|
||||
3. **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
|
||||
4. **Interactions** — clic = remplir + soumettre ; ↑/↓/Enter/Tab/Esc ; **priorité au popover `@`**
|
||||
quand il est ouvert ; `aria-listbox` + `aria-activedescendant` (le `role="combobox"` est déjà sur l'input).
|
||||
5. **Télémétrie** — `suggest_shown`, `suggest_used` (à ajouter à la whitelist serveur de l'étape 13).
|
||||
6. **Tests** — `npm run test:suggest` (unitaires parsing/dédup) + scénario e2e : `providers=yt` ne renvoie
|
||||
que des suggestions `yt`, provider inconnu → fallback registry complet.
|
||||
|
||||
### Critères d'acceptation
|
||||
|
||||
- [x] Aucune requête réseau tant que `q.trim().length < 2`
|
||||
- [x] frappe rapide = une seule requête en vol (pas d'empilement / de réponse qui écrase la dernière)
|
||||
- [x] Esc ferme le panneau sans effacer le texte ; Tab complète sans lancer la recherche
|
||||
- [x] Un provider sans suggestions ou en échec n'affiche ni trou ni erreur dans le panneau
|
||||
- [x] 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**
|
||||
|
||||
- [x] `server/transcript.mjs` : fonctions **pures** `pickTrack(json, lang)`, `parseJson3(data)`, `parseVtt(text)`
|
||||
(testables sans réseau ni DB)
|
||||
- [x] Route `GET /api/transcript/:provider/:videoId?lang=&instance=&slug=&sourceUrl=` dans `server/index.mjs`
|
||||
(`instance/slug/sourceUrl` nécessaires pour PeerTube multi-instances ; réutiliser `providerUrlFrom()`).
|
||||
- [x] `transcriptCache` : `Map` + TTL long (24 h), clé `transcript:${provider}:${videoId}:${lang}`.
|
||||
- [x] Rate-limit (~10 req/min/IP) sur le motif `channelsLimiter`.
|
||||
- [x] Aucun sous-titre → **HTTP 200** `{ available: false }` (pas une erreur).
|
||||
- [x] 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)**
|
||||
|
||||
- [x] Bouton « Transcript » à côté du bouton Download (`watch.component.html:159`).
|
||||
- [x] 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 utilisateur `language`).
|
||||
- [x] Le panneau ne doit **jamais** casser la page Watch (providers sans sous-titres : Twitch, Odysee, Rumble).
|
||||
|
||||
**Tests & docs**
|
||||
|
||||
- [x] `server/tests/transcript.test.mjs` : fixtures `json3`/`vtt` offline (parseurs) + intégrité du contrat API.
|
||||
- [x] Script `npm run test:transcript` + l'ajouter à `.github/workflows/ci.yml`.
|
||||
- [x] README : cocher `⏳ Sous-titres & transcripts` dans 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`) et
|
||||
`IframeProgressService.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
|
||||
|
||||
1. **Providers réellement supportés** en Phase 1 (le doc teste YouTube + Dailymotion ; PeerTube a sa propre API
|
||||
de sous-titres — passer par `yt-dlp` ou par l'API instance ?).
|
||||
2. **Persistance**: cache mémoire seulement, ou table SQLite `transcripts` (comme pour les téléchargements)
|
||||
pour survivre aux redémarrages ?
|
||||
3. **Transcripts générés (auto-captions)** : inclus d'emblée ou option utilisateur ?
|
||||
4. **Volume de réponse** : les gros transcripts (2 h+) nécessitent-ils une troncature / pagination ?
|
||||
|
||||
### Critères d'acceptation
|
||||
|
||||
- [x] 1 appel API, 1 parsing, 1 UI identiques quel que soit le provider
|
||||
- [x] 2ᵉ requête sur la même vidéo = réponse depuis le cache (pas d'appel `yt-dlp`)
|
||||
- [x] `yt-dlp` en échec → 502 `{ available: false, error: 'transcript_fetch_failed' }`, Watch intacte
|
||||
- [x] `npm run test:transcript` passe hors ligne (fixtures)
|
||||
|
||||
### Décisions Phase 1 (tranchées à l'implémentation)
|
||||
|
||||
1. **Providers** : générique via `yt-dlp` (ids courts `yt/dm/…` + longs normalisés), aucun code par plateforme.
|
||||
2. **Persistance** : cache mémoire seul (TTL 24 h, 200 entrées LRU) ; SQLite reporté en Phase 3.
|
||||
3. **Auto-captions** : incluses d'emblée (fallback après les sous-titres manuels).
|
||||
4. **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
|
||||
```bash
|
||||
@@ -24,6 +197,8 @@ npm run test:search # Step 11 — unit tests: SearchService + @ parsing +
|
||||
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
|
||||
@@ -44,3 +219,110 @@ npm run test:telemetry # Step 13 — telemetry events (insert/list/count)
|
||||
- **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-1069` utilisent **uniquement la YouTube Data API v3** (`search.list` + `videos.list`) avec rotation `YOUTUBE_API_KEYS` / `YOUTUBE_API_KEY` sur `quotaExceeded|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 page `pageToken` et chaque enrichissement `videos.list` aggrave.
|
||||
- Cache actuel : `YT_CACHE_TTL_MS` dé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`) pour `transcript` + `downloads` + enrichissement Watch. Le `suggest` YT 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 pour `yt`, ajouter `SUGGEST_CACHE_TTL_MS` déjà à 5 min, clé cache `q|limit|page|sort`.
|
||||
- [ ] Réduire le coût API : `maxResults` max 25 au lieu de 50, ne faire `videos.list` que si `details=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` : expliquer `YOUTUBE_API_KEYS` CSV 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})` via `yt-dlp --dump-single-json --flat-playlist "ytsearch{limit}:{q}"` (timeout 15-20 s, `YT_DLP_PATH` réutilisé), mapping vers `Suggestion` identique à `youtube.mjs:199-232` (title/id/thumbnail/duration/views/publishedAt/channelId/embeddable).
|
||||
- Support `sort` : `relevance|date|views` → préfixe `ytsearch` + tri local si besoin.
|
||||
- Jamais de throw bloquant : erreur → throw avec `ytStatus` pour que `/api/search` mette `errors.yt`.
|
||||
- [ ] Modifier `server/providers/youtube.mjs` en dispatcher :
|
||||
- `env YT_SEARCH_MODE=scrape-first|api-first|scrape-only|api-only` (défaut `scrape-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)` TTL `YT_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-limit` existant, timeout fan-out par provider 8-12 s, `Promise.allSettled` déjà en place dans `/api/search`.
|
||||
- [ ] Tests : `server/tests/youtube-scrape.test.mjs` offline (fixtures `yt-dlp --flat-playlist` mockées) + e2e `YT_SEARCH_MODE=scrape-only` sans clé → `groups.yt` non vide ; `api-only` sans 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_KEYS` devient optionnel.
|
||||
|
||||
### Phase 2 — Robustesse anti-ban (1-2 j, inspiré yattee-server)
|
||||
|
||||
- [ ] Support `YT_COOKIES_FILE` + `YT_PO_TOKEN` passé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 yattee `SSRF_EXTRA_ALLOWED_CIDRS` si Invidious LAN.
|
||||
- [ ] Auto-update yt-dlp : script `npm run ytdlp:update` + check version dans `/healthz` (idée #17). `deno`/`ffmpeg-static` déjà requis pour challenge JS.
|
||||
- [ ] Observabilité Admin (idée #17) : page `Admin > YouTube` : mode actif, version yt-dlp, hit/miss cache, erreurs `quotaExceeded` vs `bot-check` vs `timeout`, é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 Accueil `Tendances & Viral` sans quota.
|
||||
- [ ] `Channel browsing enrichi` : réutiliser `channel-registry.mjs` + `channel-content.mjs` via scrape (`videos/shorts/streams/playlists`) au lieu de `channels.list` payant → prérequis Abonnements (idée #13).
|
||||
- [ ] `Chapitres` (idée #9) : parser `chapters` de `dump-single-json` déjà dispo.
|
||||
- [ ] `Filtres recherche` (idée #1) : `duration/date/type` mappés sur args yt-dlp + filtre local, 0 coût API.
|
||||
- [ ] `Pagination / infinite scroll` (idée #2) : `page$` déjà câblé, brancher `nextPageToken` scrape via `--flat-playlist --playlist-start`.
|
||||
|
||||
### Phase 4 — Option yattee-server sidecar (seulement si Phase 1 insuffisante)
|
||||
|
||||
- [ ] `docker-compose/yattee-server.yml` : image `yattee/yattee-server`, `INVIDIOUS_INSTANCE_URL` optionnelle, `ADMIN_USERNAME/PASSWORD` via `.env`.
|
||||
- [ ] Adaptateur `server/providers/youtube-yattee.mjs` : `GET ${YATTEE_URL}/api/v1/search?q=&type=video` + Basic Auth → `Suggestion[]`. Activé par `YT_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
|
||||
|
||||
- [x] Sans aucune `YOUTUBE_API_KEY`, `GET /api/search?q=test&providers=yt` retourne `groups.yt[]` (via scrape) et `npm run test:search-e2e` passe. (vérifié live : scrape-only retourne 3 résultats avec durée/vues/channelId)
|
||||
- [x] 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)
|
||||
- [x] 2ᵉ appel identique < 50 ms (hit cache mémoire/SQLite, pas d'appel yt-dlp). (mesuré : 3971 ms → 30 ms)
|
||||
- [x] `/healthz` expose `ytdlpVersion`, `mode`, `cacheHitRate`. (`/healthz` + `/api/healthz` : mode, ytdlp bin/version, antiban, cache mem+sqlite, metrics jour, clés)
|
||||
- [x] Aucune régression `dm/tw/pt/od/ru`. (`test:search-e2e` + `test:suggest` verts)
|
||||
|
||||
### 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 via `yt-dlp --flat-playlist`, parsers `mapFlatEntry`/`parseFlatPlaylistJson` testé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/trending` retiré côté YouTube (redirect home) → repli `ytsearch` trié vues. yt-dlp local `2026.08.19` : penser `npm 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 ordre `YT_DLP_PATH` > PATH > bundled `youtube-dl-exec` (cause : yt-dlp seulement présent via shims scoop utilisateur, invisible d'un autre contexte/Docker) ; fallback API sur **tout** échec scrape en `scrape-first` (plus d'erreur brute en UI) ; message actionnable `youtube_no_source` si ni binaire ni clé ; `/healthz` expose `binOk` + 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.js` **pinnée `18.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'à couvrir `page*limit` — corrige le bug "page 2 vide" : 1 page InnerTube ≈ 17-20 items ≠ `limit`), `getRelatedViaInnerTube` (watch-next), mappers purs `mapVideoNode`/`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 dans `metadata_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-only` ajouté ; les anciens modes inchangés ; on ne persiste plus les résultats vides (anti-empoisonnement du cache).
|
||||
- `GET /api/details/youtube/:videoId` → champ **`related[]`** (24 items, cache mémoire 1h, `?related=0` pour 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:suggest` verts.
|
||||
- Limites connues : pas de `getTrending` en 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()` utilise `GET /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éfaut `innertube-first`) adaptée au format yt-dlp → `pickTrack`/`orderedTracks`/`parseTrackText` inchangés ; 0 piste InnerTube = `no_subtitles` direct (même backend que le lecteur) ; fallback yt-dlp conservé. Vérifié live (`lang=en` → 217 lignes) ; `test:transcript` 17/17.
|
||||
|
||||
Reference in New Issue
Block a user