CI / build-and-test (push) Successful in 14m43s
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.
125 lines
10 KiB
Markdown
125 lines
10 KiB
Markdown
# 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é).
|