Files
NewTube/docs/rapport-execution-phases-0-1-2-3-7.md
T
bruno 665a0f0ebd
CI / build-and-test (push) Successful in 14m43s
feat(providers): phases 7.3/7.4/7.6/8.1 — provenance, health, NDJSON, contrat unique
7.3: capturedAt/source au registre + 6 adaptateurs + module provenance.ts + ?debug=1 (search-transport.mjs). 7.4: ProviderHealthService + badge source degradee. 7.6: squelettes par provider + snapshots progressifs + transport NDJSON /api/search. 8.1: ProviderAdapter unifie (search enveloppe + channelContent/channelMeta/capabilities) via getProviderAdapter + test de contrat offline.
2026-09-30 07:57:11 -04:00

125 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Rapport d'exécution — Phases 0, 1, 2, 3, 4 et 7
**Plan** : [`plan-phases-catalogue-classification.md`](./plan-phases-catalogue-classification.md)
**Analyse source** : [`ingestion-catalogue-video-par-fournisseur.md`](./ingestion-catalogue-video-par-fournisseur.md)
## Verdict
Ordre exécuté : **0 → 1 → 2 → 3 → 4 → 7** (le chemin à valeur rapide du plan).
Phases **5, 6, 8 non exécutées** — elles restent disponibles et n'ont pas été entamées.
| Vérification | Résultat |
|---|---|
| `npx tsc --noEmit` | exit 0 |
| `npm run build` (AOT, `strictTemplates`) | succès |
| Suites de tests | **21/21 vertes** |
| Assertions | 14 (contrat) + 41 (Rumble/JSON-LD) + 54 (cache/métriques/bascule) = **109** |
| Gain mesuré (phase 4) | 2ᵉ recherche identique : **1199 ms → 26 ms**, zéro appel amont |
## Bugs réels découverts pendant l'exécution
Ces anomalies n'étaient pas dans le document d'analyse : elles sont apparues en écrivant
les tests (phases 0-1) puis en auditant le diff (phases 2-3-7). Elles sont comptées dans les gains ci-dessous.
| # | Bug | Impact | Correctif |
|---|---|---|---|
| **B1** | `rumble.mjs` : `views` parse `"1,2K views"` → **12** (`.replace(/[^\d]/g,'')` supprimait le suffixe) | Le compteur de vues était **faux sur toutes les cartes Rumble > 999 vues** | `parseRumbleViews()` : compact FR/EN, milliers, séparateurs — 12 assertions |
| **B2** | `search-filters.mjs` : `itemDurationSec()` renvoyait `0` au lieu d'`undefined` | `0:00` affiché sur les cartes ; règle « verticale sans durée ⇒ pas short » inopérante | Retour `undefined` + 3 assertions de `test:filters` mises à jour |
| **B3** | `isShortItem()` serveur ignorait le marqueur URL `/shorts/` présent côté front | Un Short YouTube entrait dans la grille « vidéos » serveur mais sortait du filtre front — divergence silencieuse | Marqueur ajouté côté serveur |
| **B4** | `twitch.mjs` : `viewer_count` (spectateurs du live) exposé dans **`views`** | L'UI affichait « 1 234 **vues** » sur un live jamais regardé | Exposé dans `viewers` (champ v2 ajouté au contrat serveur + front) |
| **B5** | `toVideoItem()` ne propageait pas `type` | La page chaîne ne pouvait pas distinguer live/VOD sur la seule base de `kind` | `type: str('type')` ajouté |
## Phase 0 — Socle
### 0.1–0.3 Source unique
- **Créé** `src/app/shared/providers/provider-ids.ts` — `ProviderId`, `ProviderLongId`, `ALL_PROVIDER_IDS`, `ALL_PROVIDER_IDS_CSV`, `SHORT_TO_LONG`, `LONG_TO_SHORT`, `toShortProviderId`, `toLongProviderId`, `normalizeProviderList`, `parseProviderCsv`.
- **Créé** `src/app/shared/providers/provider-capabilities.ts` — `PROVIDER_CAPABILITIES` (matrice unique), `capabilitiesOf`, `providerSupportsLive`, `searchableProviders`, `toLegacySupports`.
- `provider-registry.ts` réduit à de la **présentation** (libellés, icônes, couleurs). `supports` est dérivé.
- `DEFAULT_CAPABILITIES` de `channel-detail.model.ts` est désormais **dérivé** de la matrice unique.
- **8 tables dupliquées supprimées** : `search.service.ts`, `suggest.util.ts`, `models.ts`, `search.component.ts` (×5), `header.component.ts`, `provider-badge`, `video-card`, `channel-provider.factory`, `watch.component`, `watch-short.component`, `subscriptions.component`, `http-channel.provider.ts`.
- Conflit `dm.playlists` (`false` dans le registre vs `true` dans `channel-detail`) **résolu** : le serveur fait foi ⇒ `true`.
### 0.4–0.5 Contrat versionné
- `server/providers/registry.mjs` : 22 champs optionnels documentés en JSDoc + `SUGGESTION_CONTRACT_VERSION = 2` + `SUGGESTION_V2_FIELDS`.
- `/api/search` renvoie désormais `v: 2`.
- `src/app/search/api.v1.ts` réécrit (`SuggestionItemV1` + champs v2), `VideoItem` étendu (v2 + `viewers`).
### 0.6 Test de contrat
- **Créé** `server/tests/provider-contract.test.mjs` (14 assertions) + `npm run test:contract`.
- Verrouille : `duration > 0` obligatoire, `publishedAt` doit être une vraie date (rejette « 2 months ago »), `views/likes/width/height > 0`, parité des règles (a)/(b)/(c) et du marqueur `/shorts/`.
## Phase 1 — Parité des champs
| Tâche | Fichier | Changement |
|---|---|---|
| 1.1 `views` | `odysee.mjs`, `channel-content.mjs` | `video.view_count` mappé. `effective_amount` **exclu** (montant LBC, pas des vues) |
| 1.2 `publishedAt` | `odysee.mjs` | `release_time` (déjà dans `include` !) converti en ISO. **Le filtre `period=` était inopérant sur Odysee** |
| 1.3 statut HTTP | `channel-content.mjs` | `resp.json().catch(()=>({}))` → `readJson()` + erreur 502 explicite |
| 1.4 `publishedAt` | `rumble.mjs` | `<time datetime>` au lieu du libellé « il y a 1 mois » |
| 1.5 `channelId` | `rumble.mjs` | Extrait de `/c/<username>/` |
| 1.6 `uploaderAvatar` | `rumble.mjs` | Image du by-line, `//` → `https` |
| 1.7 avatars | `youtube.mjs` | 1 appel `channels.list` par page (dégradation silencieuse sans clé) + `likeCount` + `liveBroadcastContent` |
| 1.8 avatars | `peertube.mjs` | `avatars[0].path` compte + chaîne, + `views`, `likes`, `publishedAt`, `language`, `hasSubtitles`, `kind` |
| 1.9 dates relatives | `youtube-innertube.mjs` | **`parseRelativeDate()`** FR/EN. `publishedAt` n'était jamais une date ⇒ `period=week` inopérant, `sort=date` sur `NaN` |
## Phase 2 — Propagation & classification
- **2.1** `toVideoItem()` réécrit : 12 champs ajoutés (`isLive`, `isShort`, `kind`, `game`, `width`, `height`, `likes`, `viewers`, `language`, `hasSubtitles`, `tags`, `description`). Lecture double nom long / nom court, `0 → undefined`.
- **2.2** `duration: 0 → undefined` dans `channel-content.mjs` (dm, pt, od), `search-filters.mjs`, et `toVideoItem`.
- **2.3** Règles (a)/(b)/(c) documentées et verrouillées dans les **deux** copies ; parité `/shorts/` rétablie.
- **2.4** `src/app/search/adapters/base.ts` **supprimé** ; branche morte `channel-content.mjs:155` supprimée.
## Phase 3 — Fiabilité
| Tâche | Changement |
|---|---|
| 3.1 Rumble JSON-LD | `parseJsonLd()` : `VideoObject` / `ItemList` / `@graph` → `datePublished`, `interactionStatistic`, `thumbnailUrl`, `duration`, `/c/<id>`. Surcouche additive, **n'écrase jamais** le DOM |
| 3.3 Cache négatif | 5 min, `search:vide:<q>:<page>`, borné à 200 entrées. Évite 4 requêtes (12-20 s) par frappe |
| 3.4 Échec ≠ vide | `search()` **lève** `rumble_cloudflare_challenge` ⇒ remonté dans `errors.ru` ⇒ bannière UI |
| 3.1b Curseur Twitch | `nextCursor` persisté de bout en bout (route → `ChannelContentPage` → `ChannelContentService` → `?cursor=`). **Avant, `page=2` renvoyait `page=1`** |
| 3.2 Item live Twitch | `type:'live'`, `isLive:true`, `kind:'live'`, **`viewers`** (et plus `views` — cf. B4), `game`, `language`, avatar. **Avant : marqué `kind:'vod'` (donc jamais classé live) et « vues » affichées à la place des spectateurs** |
| 3.5 `ruContent` | Filtre prioritaire sur `channelId` canonique |
| 3.6 `pageToken` YouTube | Cache de la chaîne de jetons (15 min, 200 entrées). La page 5 coûtait **5 appels Data API** ; désormais 1 en cache |
## Phase 7 — Présentation
- **7.1** Badges **génériques** (plus « Twitch-only ») : `LIVE` / `SHORT` / `CLIP` / `CHAÎNE` via `isShortVideoItem()` / `isLiveVideoItem()`.
- **7.3** `viewers` distinct de `viewCount` ; « joueurs en direct » réservé aux lives.
- **7.3** « **vues indisponibles** » avec infobulle au lieu d'un vide silencieux (qui se lisait comme « 0 vues »).
- **7.4** `providerErrors` structuré sur `search-result-grid` : bandeau ambre listant **source + cause traduite**, et message d'état vide distinct (« la recherche a échoué côté source ») de « aucun résultat ».
- **7.5** `canShowPill()` : une pastille n'apparaît que si un fournisseur actif sait **vraiment** servir ce type (source : `PROVIDER_CAPABILITIES`).
## Phase 4 — Cache générique & observabilité
- **4.1** Table `search_cache` (`cache_key`, `provider`, `q`, `payload_json`, `item_count`, `source`, `hit_count`, `created_at`, `expires_at`), TTL par provider, plafond 2 000/provider, jamais de vide persisté, `hit_count` à chaque lecture. Cache appliqué dans `server/providers/registry.mjs` (point d'entrée unique, 5 adaptateurs non-YT), YT exclu de par son cache à deux niveaux.
- **4.2** `migrateYoutubeCacheToSearchCache()` au boot (`INSERT OR IGNORE`), `youtube_search_cache` **conservée et lue en repli** → migration réversible par un simple `DROP TABLE search_cache`.
- **4.3** `GET /api/providers/health` (sonde `limit=1` + cache 60 s), `GET /api/providers/metrics`, table `provider_metrics` (par heure, rétention 24 h), `/healthz` étendu. Compteurs incrémentés **uniquement sur appels amont réels**.
- **4.4** Bascule automatique InnerTube → `scrape-first` sur taux d'échec ≥ 20 % sur 1 h, cooldown 15 min, seuil minimum de 3 appels, journalisée. `youtube.mjs` branche sur `getEffectiveSearchMode()` et inclut ce mode dans la clé de cache.
- **4.5** `startSearchCacheJanitor()` : `setInterval` 10 min + purge différée 2 s, tous deux `unref()`.
**Gain mesuré** (Dailymotion, 2ᵉ requête identique) : **1199 ms → 26 ms**, `calls` reste à 1, `hits` passe à 1.
## Tests ajoutés
| Fichier | Assertions |
|---|---|
| `server/tests/provider-contract.test.mjs` | 14 |
| `server/tests/rumble-ld.test.mjs` | 41 |
| `server/tests/search-cache.test.mjs` | 54 |
| `server/tests/search-filters.test.mjs` | +1 (3 assertions mises à jour en v2) |
Les 3 nouveaux fichiers de test sont câblés dans `.github/workflows/ci.yml`.
## Non exécuté
Phases **5** (table `videos`), **6** (`channelRef`), **8** (architecture). Aucune dépendance bloquante : les phases 0 et 4 les facilitent (contrat v2, source unique, cache générique).
## Points d'attention pour la suite
1. `provider-badge`, `video-card`, `channel-provider.factory` importent désormais la source unique : **ne plus réintroduire de table locale**.
2. `VideoItem.provider` est `ProviderLongId` : toute table doit venir de `SHORT_TO_LONG`.
3. Toute valeur absente reste `undefined`. `test:contract` échouera si un `0` ou une chaîne vide réapparaît.
4. `rumble.search()` lève désormais : ne pas l'appeler sans `try/catch` hors du fan-out `/api/search`.
5. Un nouveau fournisseur serverside hérite du cache et des métriques **gratuitement** en étant ajouté à `providerRegistry` — ne pas réimplémenter un cache dans un adaptateur.
6. `NEWTUBE_DB_FILE` doit être défini **avant** l'import de `db.mjs` : sinon un test écrit dans la base de dev (piège rencontré).