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.
10 KiB
Rapport d'exécution — Phases 0, 1, 2, 3, 4 et 7
Plan : plan-phases-catalogue-classification.md
Analyse source : 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.tsréduit à de la présentation (libellés, icônes, couleurs).supportsest dérivé.DEFAULT_CAPABILITIESdechannel-detail.model.tsest 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(falsedans le registre vstruedanschannel-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/searchrenvoie désormaisv: 2.src/app/search/api.v1.tsréé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 > 0obligatoire,publishedAtdoit ê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 → undefineddanschannel-content.mjs(dm, pt, od),search-filters.mjs, ettoVideoItem. - 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.tssupprimé ; branche mortechannel-content.mjs:155supprimé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ÎNEviaisShortVideoItem()/isLiveVideoItem(). - 7.3
viewersdistinct deviewCount; « 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
providerErrorsstructuré sursearch-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é dansserver/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_cacheconservée et lue en repli → migration réversible par un simpleDROP TABLE search_cache. - 4.3
GET /api/providers/health(sondelimit=1+ cache 60 s),GET /api/providers/metrics, tableprovider_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-firstsur taux d'échec ≥ 20 % sur 1 h, cooldown 15 min, seuil minimum de 3 appels, journalisée.youtube.mjsbranche surgetEffectiveSearchMode()et inclut ce mode dans la clé de cache. - 4.5
startSearchCacheJanitor():setInterval10 min + purge différée 2 s, tous deuxunref().
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
provider-badge,video-card,channel-provider.factoryimportent désormais la source unique : ne plus réintroduire de table locale.VideoItem.providerestProviderLongId: toute table doit venir deSHORT_TO_LONG.- Toute valeur absente reste
undefined.test:contractéchouera si un0ou une chaîne vide réapparaît. rumble.search()lève désormais : ne pas l'appeler sanstry/catchhors du fan-out/api/search.- 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. NEWTUBE_DB_FILEdoit être défini avant l'import dedb.mjs: sinon un test écrit dans la base de dev (piège rencontré).