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

10 KiB
Raw Blame History

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.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é).