# Plan de phases — Amélioration du catalogue & de la classification vidéo (6 fournisseurs) **Version :** 1.0 **Date :** 2026-09-29 **Statut :** Phases 0, 1, 2, 3, 4 et 7 **exécutées** (ordre `0 → 1 → 2 → 3 → 4 → 7`). Phases 5, 6, 8 à faire. **Bilan d'exécution :** [`rapport-execution-phases-0-1-2-3-7.md`](./rapport-execution-phases-0-1-2-3-7.md) — inclut 5 bugs hors-analyse découverts par les tests. **Documents amont :** [`ingestion-catalogue-video-par-fournisseur.md`](./ingestion-catalogue-video-par-fournisseur.md) (analyse de référence) et l'analyse d'optimisation qui en découle. --- ## Table des matières 0. [Principes directeurs](#0-principes-directeurs) 1. [Vue d'ensemble des phases](#1-vue-densemble-des-phases) 2. [Phase 0 — Socle : une seule source de vérité](#2-phase-0--socle--une-seule-source-de-vérité) 3. [Phase 1 — Parité de champs (quick wins)](#3-phase-1--parité-de-champs-quick-wins) 4. [Phase 2 — Propagation front & classification](#4-phase-2--propagation-front--classification) 5. [Phase 3 — Fiabilité des sources fragiles](#5-phase-3--fiabilité-des-sources-fragiles) 6. [Phase 4 — Cache générique & observabilité](#6-phase-4--cache-générique--observabilité) 7. [Phase 5 — Table `videos` & fin de la dénormalisation](#7-phase-5--table-videos--fin-de-la-dénormalisation) 8. [Phase 6 — Identité de chaîne normalisée](#8-phase-6--identité-de-chaîne-normalisée) 9. [Phase 7 — Présentation](#9-phase-7--présentation) 10. [Phase 8 — Architecture de fond](#10-phase-8--architecture-de-fond) 11. [Matrice de traçabilité anomalie → phase](#11-matrice-de-traçabilité-anomalie--phase) 12. [Jalons & charge](#12-jalons--charge) 13. [Risques, rollback & Feature flags](#13-risques-rollback--feature-flags) 14. [Annexe — Commandes de test](#14-annexe--commandes-de-test) --- ## 0. Principes directeurs 1. **Ne rien casser au collecting.** Toute phase qui change le contrat `Suggestion` passe par `v: 2` (Phase 0) et reste rétrocompatible : les adaptateurs front ignorent les champs inconnus, le serveur tolère l'absence des nouveaux champs. 2. **Un filtre ne doit jamais vider un résultat pour cause de métadonnée absente.** Règle déjà écrite dans `search-filters.mjs:9-11`, à préserver dans toutes les phases de classification. 3. **Aucune donnée inventée.** Si un fournisseur ne donne pas `publishedAt`, on ne le devine pas — on expose `null` et l'UI affiche « — ». 4. **Chaque phase est livrable seule.** Pas de « phase 2 dépend de la 5 ». Le seul couplage fort est Phase 0 → toutes les autres (source de vérité unique). 5. **Test d'abord.** Chaque tâche porte un critère d'acceptation exécutable (`npm run test:*`), pas une description narrative. --- ## 1. Vue d'ensemble des phases | Phase | Thème | Anomalies traitées | Effort | Risque | Dépend de | |---|---|---|---|---|---| | **0** | Socle : registry unique front, contrat `v2`, tests de contrat | #8, #9, #12 | ~1,5 j | Faible | — | | **1** | Parité de champs : 9 champs disponibles mais jetés | #3 (+ §1.1 analyse) | ~1 j | **Très faible** | — | | **2** | `toVideoItem` complet + règles de classification | #7 + §4 analyse | ~2 j | Faible | — | | **3** | Fiabilité : Rumble JSON-LD, curseur Twitch, parallélisation | #1, #2, #4, #5, #6 | ~3 j | Moyen | 1 | | **4** | Cache générique `search_cache` + `/api/providers/*` | #15, #14 | ~2,5 j | Faible | 0 | | **5** | Table `videos` légère + refactor des lectures | #13 (+ §2.2 analyse) | ~3 j | Moyen | 1, 2 | | **6** | `channelRef` normalisé (scheme + value) | #9 | ~2 j | Moyen | 0 | | **7** | Présentation : badges, états vides, grille, bannière | §3 analyse | ~3 j | Faible | 1, 2 | | **8** | Architecture : plugin provider, `v2` strict, doc générée | #10, #11, §5 analyse | ++ (2 sem.) | Élevé | 0-7 | **Ordre recommandé d'exécution** : 0 → 1 → 2 → 3 → 7 (chemin de valeur court) puis 4 → 5 → 6 → 8. --- ## 2. Phase 0 — Socle : une seule source de vérité **Objectif** : supprimer les duplications de conventions qui rendent chaque gain ultérieur risqué (provider IDs, capacités, classification). ### 2.1 Tâches - [x] **0.1** Créer `src/app/shared/providers/provider-ids.ts` — exporte `PROVIDER_IDS` (short), `SHORT_TO_LONG`, `LONG_TO_SHORT`, `ALL_PROVIDER_IDS` (dérivé, plus de liste en dur). - [x] **0.2** Créer `src/app/shared/providers/provider-capabilities.ts` — **source unique** de la matrice de capacités ; réconcilie `provider-registry.ts:13-38` et `channel-detail.model.ts:42-49` (actuellement en contradiction sur `dm.playlists`). Règle de réconciliation : **la capacité vaut ce que le serveur sait réellement servir** (`channel-content.mjs` fait foi), donc `dm.playlists = true`. - [x] **0.3** Faire importer `ALL_PROVIDER_IDS` par : - `src/app/search/search.service.ts:93` (liste `'all'` codée en dur) - `src/app/components/search/search.component.ts` (4 occurrences : `:121, :123, :872`) - `src/app/core/providers/provider-registry.ts` (construit les entrées depuis les tables) - `provider-badge.component.ts:61-68`, `search.component.ts:133-140`, `video-card.component.ts:108-115` (tables dupliquées) - [x] **0.4** Ajouter le champ `v: 2` au JSDoc `Suggestion` (`server/providers/registry.mjs:5-16`) et l'exposer dans la réponse `/api/search` (`server/index.mjs:3120`). `v` absent = 1 (comportement actuel). - [x] **0.5** Introduire les champs optionnels du contrat v2 **sans les remplir** (déclarés pour verrouiller les noms) : `uploaderAvatar`, `viewCountRaw`, `language`, `hasSubtitles`, `capturedAt`, `source` (upstream qui a produit l'item). - [x] **0.6** Créer `server/tests/provider-contract.test.mjs` : pour chaque provider, un `suggest()` mocké doit produire des objets dont **tous les champs obligatoires** (`title`, `id`, `url`, `thumbnail`, `type`) sont présents et typés, et dont `duration` est un `number ≥ 0` ou absent. Test **offline** (fixtures JSON figées par provider). - [x] **0.7** Ajouter `npm run test:contract` et le câbler dans `.github/workflows/ci.yml`. ### 2.2 Critères d'acceptation - [x] `grep -rn "'yt','dm','tw','pt','od','ru'" src/` ne retourne plus que la définition dans `provider-ids.ts`. - [x] `providerSupportsLive()` et `HttpChannelProvider.supports()` lisent **la même** table de capacités ; `dm.playlists` est `true` partout. - [x] `npm run test:contract` passe ; aucun snapshot de réponse n'est modifié. ### 2.3 Rollback Réversible par simple revert : aucune donnée n'est migrée. --- ## 3. Phase 1 — Parité de champs (quick wins) **Objectif** : capter l'information **déjà présente dans la réponse amont** et jetée par le mapping. Le meilleur rapport gain/risque du plan. ### 3.1 Tableau des tâches | # | Fournisseur | Champ à récupérer | Source amont | Fichier | Difficulté | |---|---|---|---|---|---| | **1.1** | Odysee | `views` | `value.video.view_count` / `meta.effective_amount` — ou ajouter `video` aux `include` de Lighthouse | `server/providers/odysee.mjs:21,48-60` | Trivial | | **1.2** | Odysee | `publishedAt` | `release_time` (déjà dans les `include` !) | `server/providers/odysee.mjs:21,48-60` | Trivial | | **1.3** | Odysee | vérification du statut HTTP | `readJson()` contourné : `resp.json().catch(()=>({}))` | `server/providers/channel-content.mjs:349` | Trivial | | **1.4** | Rumble | `publishedAt` | `