# 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` | `