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.
51 KiB
Saisie et catalogage des informations vidéo par fournisseur — NewTube
Version : 1.0 Date : 2026-09-29 Statut : Document de référence technique (lecture de code) Périmètre : comment chacun des 6 fournisseurs supportés est saisi (source amont, endpoint, auth, parsing) puis catalogué (modèle normalisé, classification, persistance SQLite).
Ce document est un compte rendu de lecture du code, pas une spécification normative. Chaque affirmation renvoie à un fichier et une ligne.
Table des matières
- Vue d'ensemble du pipeline
- Le contrat commun
Suggestion - Saisie — source amont par fournisseur
- Catalogue — champs réellement capturés
- Catalogue du contenu de chaîne
- Métadonnées de chaîne
- Classification Vidéo / Short / Live
- Normalisation front
- Persistance SQLite
- Caches
- Configuration par fournisseur
- Points d'attention et écarts constatés
- Annexe — Référence des fichiers
1. Vue d'ensemble du pipeline
Tous les fournisseurs convergent vers un contrat unique, puis passent par trois étapes de normalisation successives.
┌─ FRONT ────────────────────────────────────────────────────────────────┐
│ SearchBox → SearchService (RxJS) │
│ state : q$ providers$ page$ pageSize$ sort$ filters$ │
│ src/app/search/search.service.ts:27-40 │
└──────────────────────────────┬────────────────────────────────────────┘
│ GET /api/search?q=…&providers=yt,dm,…
│ &type=&duration=&period=&sort=
▼
┌─ SERVEUR ────────────────────────────────────────────────────────────┐
│ server/index.mjs:3120 (validation + normalisation filtres) │
│ server/search-filters.mjs:34-42 parseSearchFilters() │
│ │
│ fan-out parallèle → server/providers/registry.mjs:32-39 │
│ yt → providers/youtube.mjs (3 niveaux de source) │
│ dm → providers/dailymotion.mjs (REST Graph) │
│ tw → providers/twitch.mjs (Helix, token app) │
│ pt → providers/peertube.mjs (SepiaSearch) │
│ od → providers/odysee.mjs (Lighthouse / LBRY) │
│ ru → providers/rumble.mjs (scraping HTML + CF) │
│ │
│ post-traitement commun → search-filters.mjs:278-288 │
│ applySearchFilters(providerId, items, filters) │
└──────────────────────────────┬────────────────────────────────────────┘
│ { q, providers, groups: {yt:[], dm:[]…},
│ page, pageSize, sort, filters, errors }
▼
┌─ FRONT ────────────────────────────────────────────────────────────────┐
│ 6 adaptateurs src/app/search/adapters/{yt,dm,tw,pt,od,ru}.ts │
│ → modèle unique `VideoItem` (src/app/shared/models/video-item.model.ts)
│ → classification (src/app/shared/utils/video-kind.ts) │
│ → rendu (search-result-grid → video-card) │
│ │
│ Écritures SQLite déclenchées par le front : │
│ history.service.ts → /user/history/watch │
│ likes.service.ts → /user/likes │
│ → watch_history, playlist_items, video_tags, download_jobs │
└───────────────────────────────────────────────────────────────────────┘
Trois chemins d'ingestion cohabitent :
| Chemin | Fournisseurs | Point d'entrée |
|---|---|---|
| Recherche unifiée (fan-out) | les 6 | server/providers/registry.mjs |
| Contenu de chaîne (onglets vidéos/shorts/playlists/live) | les 6 | server/providers/channel-content.mjs:378-389 |
| Transcripts / détails / téléchargements | tous, via yt-dlp |
server/index.mjs:1271-1295 (providerUrlFrom) |
2. Le contrat commun Suggestion
Défini en JSDoc dans server/providers/registry.mjs:5-16. C'est la forme de sortie unique de tous les adaptateurs serveur.
| Champ | Type | Rôle |
|---|---|---|
title |
string |
obligatoire |
id |
string |
identifiant canonique provider |
url |
string? |
URL de lecture |
thumbnail |
string? |
vignette |
uploaderName |
string? |
nom de la chaîne / auteur |
type |
string? |
video | live | channel | short |
duration |
number? |
secondes |
isShort |
boolean? |
flag natif fournisseur |
width / height |
number? |
px — signal d'orientation |
views |
number? |
(enrichi hors contrat JSDoc) |
publishedAt / uploadedDate |
string? |
ISO 8601 |
channelId / channelExternalId / channelHandle / channelUrl |
identité chaîne | |
uploaderAvatar |
string? |
avatar |
kind |
string? |
vod | clip (Twitch) |
isLive |
boolean? |
en cours de diffusion |
game |
string? |
catégorie Twitch |
Doctrine de sûreté appliquée partout (search-filters.mjs:9-11) : un filtre inconnu est ignoré (jamais d'erreur 4xx), et un champ métadonnée absent ne fait jamais disparaître un résultat.
3. Saisie — source amont par fournisseur
3.1 Tableau récapitulatif
| Fournisseur | Source d'ingestion | Endpoint(s) amont | Auth | Pagination | Normalisation / parsing |
|---|---|---|---|---|---|
YouTube yt |
Chaîne de 3 niveaux : InnerTube (youtubei.js) → scrape yt-dlp → Data API v3. Dispatcher selon YT_SEARCH_MODE, défaut innertube-first (youtube.mjs:236-330) |
• InnerTube youtubei/v1/search• binaire yt-dlp --dump-single-json --flat-playlist• googleapis.com/youtube/v3/search + /videos |
Aucune pour InnerTube/scrape. Clés API YOUTUBE_API_KEY(S) avec rotation sur échec quota (youtube-common.mjs:9-40) |
Continuations InnerTube illimitées ; pageToken Data API ; --playlist-start/--playlist-end yt-dlp |
mapVideoNode() / mapLockupView() — 10 renderers YT (youtube-innertube.mjs:92-215) ; parseViewsText() (« 1,2 M ») ; parseDurationLabel() (labels a11y) ; parseISODurationToSeconds() PT#H#M#S ; mapFlatEntry() yt-dlp (youtube-scrape.mjs:14-39) |
Dailymotion dm |
API Graph publique (REST) | GET https://api.dailymotion.com/videos?search=&fields=…dailymotion.mjs:15-24 |
Aucune | page + limit (plafond 100) |
Mapping à plat (owner.screenname, owner.avatar_80_url) ; created_time epoch → ISO ; duration/views_total → Number() ; dimensions conservées (width, height) |
Twitch tw |
Helix — recherche mixte façon annuaire : chaînes + lives + VODs + clips | /helix/search/channels, /helix/search/categories, /helix/streams, /helix/videos, /helix/clipstwitch.mjs:302-567 |
App Access Token client_credentials, cache process-wide, 3 retries + refresh automatique sur 401 (twitch.mjs:30-107) |
Curseur Helix (pagination.cursor) ; requêtes multi-mots-clés fusionnées et dédupliquées (twitchKeywords) |
parseTwitchDurationToSeconds() (« 2h13m5s ») ; gabarits de vignettes {width}x{height} et %{width} ; 4 mappeurs dédiés mapChannel/mapStream/mapVod/mapClip (twitch.mjs:137-225) ; budgets par section |
PeerTube pt |
SepiaSearch (index fédéré des instances) | GET https://sepiasearch.org/api/v1/search/videos?search=&count=&start=peertube.mjs:16-23 |
Aucune | Offset start + count (plafond 50) |
uuid → id, name → title ; vignette absolutisée (//, chemin relatif) ; duration déjà en secondes ; dimensions = plus grande surface de files[] (signal d'orientation, lecture défensive) |
Odysee od |
Lighthouse (index LBRY) | GET https://lighthouse.odysee.tv/search?s=&size=&from=&include=…&mediaType=videoodysee.mjs:17-25 |
Aucune | Offset from + size (plafond 50) |
claimId → id ; construction du segment nom:claimId ; vignette reconstruite via thumbnails.odycdn.com/optimize/… ; durée en cascade duration → video.duration ; dims video.width/height |
Rumble ru |
Scraping HTML derrière Cloudflare — 2 tentatives | ① GET rumble.com/search/video?q=② GET rumble.com/search/all?search-videos=1&q=rumble.mjs:257-277 |
Aucune. Contournement CF : headers Chrome complets + cookie jar __cf_bm partagé (TTL 25 min), puis repli python3 + curl_cffi (rumble_fetch.py, impersonation Chrome) |
page HTML |
cheerio sur li.video-listing-entry ; parseDurationToSeconds() robuste (ISO 8601, h m s, H:MM:SS, heuristique ms > 100 000, garde-fou anti-datetime) ; id depuis data-id ou slug d'URL ; params de tracking (?e9s=, ?sci=) supprimés (rumble.mjs:184-238) |
3.2 Détail YouTube — la chaîne de fallback
server/providers/youtube.mjs:236-330 implémente 6 modes pilotés par YT_SEARCH_MODE
(youtube-common.mjs:42-48) :
| Mode | Ordre d'essai |
|---|---|
innertube-first (défaut) |
InnerTube → scrape → API |
scrape-first |
scrape → API |
api-first |
API → scrape |
innertube-only / scrape-only / api-only |
source unique |
- Chaque couche ne fait jamais échouer la recherche à elle seule : un échec déclenche le repli et incrémente
ytMetrics.fallbacks. - Si aucune source n'est disponible → erreur
503avec le codeyoutube_no_source(youtube.mjs:320-324). - Le dispatcher logue systématiquement la source gagnante, le mode et la latence (
youtube.mjs:234).
Anti-ban yt-dlp (youtube-common.mjs:130-147) : --cookies (si YT_COOKIES_FILE existe), --extractor-args youtube:po_token=…, --proxy (si YT_EGRESS_PROXY). Les valeurs ne sont jamais loguées.
Rotation des clés YouTube (youtube-common.mjs:32-40) : une clé n'est abandonnée que sur échec clé (400 API_KEY_INVALID / clé expirée, ou 403 quota/rateLimit). Les métriques de quota sont estimées : search × 100 unités, videos × 1 (youtube.mjs:44-47).
3.3 Détail Twitch — recherche multi-sections
twitch.mjs:328-567, dans l'ordre :
- Chaînes —
/search/channelssur la requête complète puis chaque mot-clé (Helix ne matche pas les longues phrases :twitch.mjs:229-247). Pagination par curseur. Repli :/streamstop si Helix a tout filtré. - Catégorie —
/search/categoriespour les thèmes génériques (sports,music) ; matching exact prioritaire. - Lives —
/streamsenrichis paruser_login(viewer_count, game_name, started_at) + top streams de la catégorie ; dédupliqués. - VODs —
/videos?type=archivepar broadcaster (6 max) + pargame_idsi catégorie trouvée.sortmappé :views→views,date→time, sinontrending. - Clips —
/clipspar broadcaster (fenêtre 30 jours) + pargame_id. - Assemblage façon annuaire avec budgets par section pour ne pas noyer les lives (
twitch.mjs:537-543), tri optionnel, déduplication partype:id. - Filet de sécurité : si la page 1 est vide alors que Twitch est configuré → top streams (
twitch.mjs:555-561).
3.4 Détail Rumble — stratégie anti-Cloudflare
rumble.mjs:121-133 (fetchHtml) :
1. Node fetch + headers Chrome + cookie jar __cf_bm → si 200 et pas de challenge
2. python3 + curl_cffi (rumble_fetch.py, impersonation Chrome)
3. sinon → []
Détection de challenge : /Just a moment|challenge-platform|cf-chl/i sur les 4 000 premiers caractères. La dégradation est silencieuse — le fan-out /api/search renvoie simplement un groupe vide pour ru.
4. Catalogue — champs réellement capturés
Section générée par
npm run doc:providersdepuisserver/tests/fixtures/provider-suggestions.json(gel du 2026-09-30, requêtetutorial). Ne pas éditer à la main.
Legende : x = émis dans le gel, · = absent (donnée inconnue, donc undefined côté front).
| Champ | Type | YouTube | Dailymotion | Twitch | PeerTube | Odysee | Rumble |
|---|---|---|---|---|---|---|---|
duration (durée) |
secondes | x | x | ? | x | x | ? |
views (vues) |
nombre | x | x | ? | x | · | ? |
likes (likes) |
nombre | · | · | ? | x | · | ? |
publishedAt (publication) |
date ISO | · | · | ? | x | · | ? |
thumbnail (vignette) |
URL | x | x | ? | x | x | ? |
uploaderName (chaîne) |
texte | x | x | ? | x | x | ? |
channelRef (identité chaîne) |
scheme + value | x | x | ? | x | x | ? |
type (type) |
video / live / short | x | x | ? | x | x | ? |
kind (kind) |
vod / live / clip / channel | · | · | ? | x | · | ? |
isLive (direct) |
booléen | · | · | ? | · | · | ? |
width (largeur) |
px | · | x | ? | · | · | ? |
height (hauteur) |
px | · | x | ? | · | · | ? |
language (langue) |
code | · | · | ? | x | · | ? |
Couverture du gel :
- YouTube : 3 item(s) vérifié(s).
- Dailymotion : 3 item(s) vérifié(s).
- Twitch : non couvert — aucun résultat au moment du gel — couverture non testée pour ce provider.
- PeerTube : 3 item(s) vérifié(s).
- Odysee : 3 item(s) vérifié(s).
- Rumble : non couvert — erreur au gel : rumble_unavailable.
(? = provider sans fixture au gel : la matrice ne prétend rien sur lui.)
Matrice de couverture : ✅ capturé · ⚠️ partiel / dégradé · ❌ non capturé malgré la disponibilité en amont.
| Champ catalogué | YouTube | Dailymotion | Twitch | PeerTube | Odysee | Rumble |
|---|---|---|---|---|---|---|
id (canonique) |
✅ videoId / UC… |
✅ x8… |
⚠️ login (live) ou id (vod/clip) |
✅ uuid |
✅ claimId |
✅ data-id ou slug URL |
title |
✅ | ✅ | ✅ | ✅ name |
✅ | ✅ |
url de lecture |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ canonicalisée |
thumbnail |
✅ meilleure dispo | ✅ 720→480→360→url | ✅ gabarit 640×360 (+70×70 avatar) | ✅ absolutisée | ✅ via proxy optimize | ✅ //→https: |
duration |
✅ ISO / label a11y / yt-dlp | ✅ | ✅ parsé 2h13m5s (vod+clip) / ❌ live |
✅ (secondes) | ✅ cascade | ✅ multi-format |
views |
✅ statistics.viewCount ou texte |
✅ views_total |
✅ view_count / viewer_count |
✅ | ❌ jeté | ✅ texte gratté |
publishedAt |
✅ | ✅ epoch → ISO | ✅ created_at/started_at |
✅ | ❌ jeté | ❌ jamais |
uploaderName |
✅ channelTitle |
✅ owner.screenname |
✅ display_name |
✅ account.displayName |
✅ channel |
✅ sélecteur .ellipsis-1 |
channelExternalId |
✅ UC… |
✅ owner.id |
✅ user_login |
✅ instance|channel |
✅ claim LBRY | ❌ jamais |
uploaderAvatar |
❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
width / height |
❌ | ✅ | ❌ | ✅ (files[]) |
✅ | ❌ |
type |
✅ | video |
✅ live/channel/video |
video |
video |
video |
kind (vod/clip) |
❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
isLive |
✅ (InnerTube) | ❌ | ✅ | ❌ | ❌ | ❌ |
isShort (flag natif) |
✅ | ❌ | clips uniquement | ❌ | ❌ | ❌ |
game |
❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
Modèle cible front : src/app/shared/models/video-item.model.ts:3-26.
Champs obligatoires : id, provider, title, thumbnailUrl. Le reste est optionnel car Dailymotion, PeerTube et Odysee ne renvoient pas toutes les métadonnées en recherche.
⚠️ Derive d'identifiants :
VideoItem.providerutilise les noms longs (youtube,dailymotion…) alors que le registry et les adaptateurs utilisent les ids courts (yt,dm…). La table de conversion est dupliquée dans 4 endroits (http-channel.provider.ts:16-23,provider-badge.component.ts:61-68,search.component.ts:133-140,video-card.component.ts:108-115).
5. Catalogue du contenu de chaîne
Route : GET /api/channels/:provider/:externalId/content?type=&page=&limit=&sort=&q=
Dispatcher : server/providers/channel-content.mjs:378-389.
type ∈ videos | shorts | playlists | live · sort ∈ recent | popular · limit plafonné à 50 (server/index.mjs:315-334).
5.1 Matrice de couverture
| Type de contenu | YouTube | Dailymotion | Twitch | PeerTube | Odysee | Rumble |
|---|---|---|---|---|---|---|
| vidéos | ✅ yt-dlp /videos + search.list:143-215 |
✅ /user/{u}/videos:219-248 |
✅ /videos?type=archive:273-303 |
✅ /video-channels/{c}/videos:307-335 |
✅ claim_search JSON-RPC:338-359 |
⚠️ recherche globale + filtre client sur uploaderName+url:363-376 |
| shorts | ✅ onglet /shorts + videoDuration=short + filtre ≤ 70 s (:203-213) |
❌ vide (:231) |
❌ vide (:293) |
❌ vide (:324) |
❌ vide (:339) |
❌ vide (:364) |
| playlists | ✅ playlists.list + onglet /playlists:170-193 |
✅ /user/{u}/playlists:222-230 |
❌ vide | ✅ /video-playlists?sort=-updatedAt:312-323 |
❌ vide | ❌ vide |
| live | ✅ onglet /streams + eventType=live:203 |
❌ vide | ⚠️ /streams?user_login&first=1nextPage:null codé en dur:281-292 |
❌ vide | ❌ vide | ❌ vide |
Les absences sont des return { items: [], nextPage: null } explicites, en miroir des flags de capacités front (src/app/shared/models/channel-detail.model.ts:42-49).
5.2 Mapping par fournisseur (contenu de chaîne)
| Fournisseur | Pagination | Points notables du mapping |
|---|---|---|
| YouTube | pageToken Data API mis en cache process-local et vers-l'avant uniquement (channel-content.mjs:122-141) ; onglets yt-dlp en parallèle |
type:'video' systématique (y compris pour le live) ; channelId/channelExternalId = UC… résolu ; résolution d'id chaîne en 4 étapes : regex UC → resolveChannelIdViaScrape → channels?forHandle= → search?type=channel (:62-89) ; tri popular refait en mémoire |
| Dailymotion | page + has_more |
duration et views forcés à 0 (pas undefined) ; q envoyé et re-filtré côté serveur ; pas de channelUrl/channelHandle |
| Twitch | curseur Helix lu puis jeté → page=2 renvoie page=1 (:302-303) |
item live taggé kind:'vod' et views=viewer_count ; duration: undefined (Helix /videos ne la fournit pas) ; vignette de live avec expansion .replace() inline (première occurrence seulement) |
| PeerTube | offset start + count |
externalId au format composite "instance|channel" ; channelId = l'externalId complet ; thumbnail = https:// + thumbnailPath ; q envoyé en amont |
| Odysee | page/page_size + total_pages ; total jamais retourné |
claim = @ + externalId ; channelId = externalId sans la normalisation @ (incohérence interne) ; views et slug disponibles mais non catalogués pour les vues ; statut HTTP non vérifié (:349 contourne readJson) |
| Rumble | page toujours forcée à 1 → nextPage:null |
aucun appel direct : délègue à searchRegistry.ru.search(q, { limit: 50, page: 1 }) puis filtre client ; sort non déstructuré → ignoré ; reposant sur uploaderName/url puisque les résultats Rumble ne portent pas de channelId |
6. Métadonnées de chaîne
server/providers/channel-registry.mjs — un fetchChannelById par fournisseur, fusionné dans le registry de recherche (registry.mjs:41-46). Persisté dans la table channels avec TTL 6 h.
| Fournisseur | Endpoint | Champs récupérés |
|---|---|---|
| YouTube | youtube/v3/channels?part=snippet,statistics,brandingSettings |
title, customUrl → @handle, avatarUrl, subscribers, verified (badges), URL custom |
| Dailymotion | api.dailymotion.com/user/{id} |
screenname, @username, avatar_720_url, followers_total, verified |
| Twitch | helix/users?id= ou ?login= (selon format de l'id) |
display_name, @login, profile_image_url (avatar), view_count |
| PeerTube | {instance}/api/v1/video-channels/{channel} |
displayName, handle @name@host, avatar via avatar.path, followersCount, ownerAccount.verified |
| Odysee | JSON-RPC resolve via api.na-backend.odysee.com/api/v1/proxy |
title, short_url, thumbnail.url, effective_amount (abonnés) |
| Rumble | scraping rumble.com/{handle} |
<title> (nettoyé du suffixe « on Rumble »), og:image |
Note : aucune de ces 6 sources ne renvoie de banner ni de description de chaîne — le modèle ChannelDetail les déclare mais les valorise à null (channel-detail.model.ts:51-62).
7. Classification Vidéo / Short / Live
Les règles sont dupliquées à l'identique côté serveur et front :
- Serveur :
server/search-filters.mjs:182-224(isShortItem) - Front :
src/app/shared/utils/video-kind.ts:64-102(isShortVideo)
7.1 Algorithme de détection Short (ordre strict)
| Ordre | Règle | Seuils / constantes |
|---|---|---|
| 1 | kind === 'clip' → toucourt short |
les clips Twitch sont ≤ 60 s par construction ; une durée > 75 s sur un clip est du bruit de métadonnées |
| 2 | Flag natif : isShort === true ou type === 'short' ou URL contenant /shorts/ → short, annulé si durée connue > 75 s |
SHORT_MAX_SECONDS = 75 |
| 3 | L'orientation fait autorité dès que width et height sont connus : verticale = h > w && w/h ≤ 0.8. Verticale + durée connue ≤ 90 s → short. Verticale sans durée connue → pas short. Horizontale ou carrée → jamais short (même à 50 s) |
VERTICAL_SHORT_MAX_SECONDS = 90VERTICAL_MAX_RATIO = 0.8 |
| 4 | Repli durée seule, uniquement si l'orientation est inconnue : ≤ 70 s pour YouTube, ≤ 75 s sinon. Durée inconnue → pas short | YOUTUBE_SHORT_MAX_SECONDS = 70SHORT_MAX_SECONDS = 75 |
L'ancien code excluait les titres contenant « short », ce qui excluait les vrais Shorts titlés
#shorts. Cette condition a été supprimée (search-filters.mjs:213-218).
7.2 Live et chaîne
| Fonction | Règle | Emplacement |
|---|---|---|
isLiveItem |
isLive === true ou type ∈ {live, stream, channel} |
search-filters.mjs:148-152 |
isChannelItem |
type === 'channel' et isLive !== true |
search-filters.mjs:226-228 |
classifyVideo |
short ⇒ short ; sinon live ⇒ live ; sinon video (short prioritaire sur live) |
video-kind.ts:111-115 |
7.3 Post-filtrage par fournisseur
search-filters.mjs:99-106 déclare les dimensions affinables en post-traitement :
| Provider | Dimensions post-filtrables | Raison documentée |
|---|---|---|
yt |
period, duration, type | tout est natif, le post-filtrage est un filet de sécurité |
dm |
period, duration, type | — |
tw |
period, type | chaînes sans durée exploitable ; live seul type fiable (les VOD portent type:'video') |
pt |
period, duration, type | — |
od |
period, type | pas de durée fiable en recherche |
ru |
period, duration, type | — |
Exception : le filtre type=channel n'est appliqué qu'à YouTube — ailleurs il viderait le résultat au lieu de le restreindre (search-filters.mjs:285-286).
7.4 Traduction des filtres vers le natif
| Cible | Mapping |
|---|---|
InnerTube (innertubeSearchFilters, :299-315) |
period → upload_date ; shorts → type:'shorts' ; live → features:['live'] (pas un SearchType) ; duration → UNDER_THREE_MINS / THREE_TO_TWENTY_MINS / OVER_TWENTY_MINS |
Data API v3 (apiSearchParams, :321-328) |
duration → videoDuration ; period → publishedAfter ; shorts/live inexistants → post-filtrage |
yt-dlp (youtube-scrape.mjs) |
seule la période est native (--dateafter) |
| PeerTube / Odysee / Rumble / Dailymotion / Twitch | aucun filtre natif → 100 % post-traitement |
8. Normalisation front
8.1 Adaptateurs de recherche
Un adaptateur par fournisseur dans src/app/search/adapters/. Tous appellent GET /api/search avec providers=<id court>, puis mappent Suggestion → VideoItem.
| Adaptateur | Renommages clés | Enrichissements spécifiques | Lignes |
|---|---|---|---|
yt.ts |
uploaderName→channelName, duration→durationSec, thumbnail→thumbnailUrl |
viewCount accepte views ou viewCount ; channelExternalId: channelExternalId || channelId |
:21-39 |
dm.ts |
idem | viewCount/publishedAt forcements undefined ; seul adaptateur à mapper channelAvatarUrl (uploaderAvatar) |
:22-37 |
tw.ts |
idem | dérive login ; filtre les lignes sans id ni vignette ; channel = embed ?channel=<login> (jamais pour VOD/clip) ; kind, game, type, isLive préservés |
:24-55 |
pt.ts |
idem | reconstruit channelExternalId = ${hostname(url)}|${channelId} (try/catch silencieux) |
:23-27 |
od.ts |
idem | extrait le slug de l'URL si domaine odysee.com ; channelExternalId = uploaderName (nom LBRY) |
:21-40 |
ru.ts |
idem | le plus simple : aucun channelExternalId, aucun isLive |
:23-34 |
withFilterParams (filter-params.ts:9-17) n'ajoute que les filtres non par défaut, pour que les URLs et le cache serveur restent partagés.
src/app/search/adapters/base.tsest du code mort :HttpAdaptern'est étendu par aucun adaptateur et utilise une formeSearchItemobsolète.
8.2 Adaptateur de contenu de chaîne
src/app/core/providers/channel/http-channel.provider.ts — une implémentation générique pour les 6 fournisseurs.
toVideoItem() (:25-40) porte la chaîne de repli la plus riche du code :
id ← raw.id
provider ← LONG_PROVIDER[provider] (court → long)
title ← raw.title
thumbnailUrl ← raw.thumbnail || raw.thumbnailUrl
durationSec ← raw.duration (number) || raw.durationSec
channelName ← raw.uploaderName || raw.channelName
channelExternalId ← raw.channelExternalId || raw.channelId || fallbackChannelId
channelAvatarUrl ← raw.uploaderAvatar || raw.channelAvatarUrl
viewCount ← raw.views (number) || raw.viewCount
publishedAt ← raw.publishedAt || raw.uploadedDate
slug, channel ← raw.slug, raw.channel
⚠️ Elle ne propage pas type, isLive, isShort, kind, width, height — les grilles de chaîne retombent donc sur la règle 4 (durée seule) de isShortVideo, alors que les résultats de recherche profitent de la logique d'orientation complète.
ChannelProviderFactory (channel-provider.factory.ts:29-50) mémoïse une instance par fournisseur ; repli par défaut sur YouTube.
8.3 Assemblage pour l'affichage
src/components/search/search.component.ts :
- Fusion par page (
:423-436) —mergeGroups()déduplique par provider surString(id ‖ videoId ‖ url);endReachedquand un lot n'apporte rien de nouveau. - Entrelacement par provider (
:763-794) — en trirelevance, un round-robin sur l'ordre des sources plutôt que des blocs, choisi car stable en cas d'ajout incrémental de l'infinite scroll. - Puits de erreurs (
:805-824) — un provider en échec produit un bandeau ambre « Résultats partiels », jamais une erreur bloquante. - Cartes —
search-result-grid.component.ts(grille 1/2/3/4 colonnes,trackById) →video-card.component.ts(badges provider, pastilles LIVE/CLIP/CHAÎNE, durée, vues, jeu, date, identité chaîne + bouton d'abonnement).
9. Persistance SQLite
Il n'existe pas de table
videos. Chaque métadonnée est dénormalisée dans des tables par fonctionnalité, toutes clés par le couple(provider, video_id).
Fichier : db/newtube.db (surchargeable via NEWTUBE_DB_FILE). Schéma : db/schema.sql + db/migrations/*.sql, appliqués au boot par server/db.mjs:47-89 (table migrations de suivi).
9.1 Inventaire des tables vidéo
| Table | Colonnes vidéo | Écriture (route → helper) | Providers |
|---|---|---|---|
watch_history |
provider, video_id, title, thumbnail, progress_seconds, duration_seconds, last_position_seconds |
POST /user/history/watch → upsertWatchHistory (db.mjs:478) |
tous |
playlist_items |
provider, video_id, title, thumbnail, position |
POST /playlists/:id/videos → addPlaylistVideo (db.mjs:926) — titre/vignette complétés par yt-dlp --dump-single-json si absents (index.mjs:3850-3862) |
tous |
video_tags (+ tags) |
likes modélisés comme un tag nommé like — aucun titre/vignette |
POST /user/likes → likeVideo (db.mjs:698) ; listing réhydraté par LEFT JOIN watch_history (db.mjs:771-789) |
tous |
transcript_history |
lines_json, languages_json, lang, line_count, char_count |
POST /user/history/transcripts → upsertTranscriptHistory (db.mjs:593) ; plafonds 2 000 lignes, 2 000 car./ligne, 32 langues |
yt, dm, pt seulement — index.mjs:3319 exclut twitch, odysee, rumble |
download_jobs |
url résolue, format_id, file_name, file_ext, file_size, file_path, state, progress, audio_only |
POST /download/:p/:videoId → insertDownloadJob (db.mjs:981) ; au boot, les jobs queued/running/merging passent à interrupted (db.mjs:1040) |
tous (DOWNLOAD_PROVIDERS) |
channels |
title, handle, avatar_url, url, subs_count, verified, last_refreshed_at — seule vraie table-catalogue |
ensureChannelFresh (db.mjs:1123), TTL 6 h, upsert même en cas d'échec (ligne stub) |
tous |
youtube_search_cache |
payload_json (Suggestion[]), source (innertube/scrape/api), expires_at |
setCachedYoutubeSearch (db.mjs:1399) |
yt uniquement |
youtube_metrics |
scrape_calls, api_calls, quota_units par jour |
incYoutubeMetrics (db.mjs:1417) |
yt |
search_history |
query, filters_json |
POST /user/history/search → insertSearchHistory (db.mjs:425) |
tous |
Tables non-vidéo mais liées : users, user_preferences (dont default_providers), sessions, login_audit, playlists, playlist_metrics, subscription_groups, oauth_connections (avec les seules colonnes provider-spécifiques du schéma : yt_channel_id, yt_page_id), telemetry_events.
9.2 Conventions d'identité
| Élément | Convention |
|---|---|
| Provider en base | nom long (youtube, dailymotion…) via normalizeHistoryProvider (db.mjs:405-418) |
| Provider en cache / préférences | id court (yt, dm…) — KNOWN_PROVIDER_IDS (db.mjs:168) |
| Clé vidéo | toujours composite (user_id, provider, video_id) ou (playlist_id, provider, video_id) |
| Timestamps | ISO-8601 texte partout sauf download_jobs, youtube_search_cache, oauth_connections (epoch ms entier) |
Identité de chaîne : channelRef (phase 6)
| Provider | Identifiant de chaîne | channelRef.scheme |
|---|---|---|
| YouTube | UC… |
yt-uc |
| Dailymotion | id numérique d'utilisateur (owner.id) |
dm-user |
| Twitch | login |
tw-login |
| PeerTube | instance|channel (instance déduite de l'URL vidéo) |
pt-composite |
| Odysee | claim LBRY, un seul @ en tête |
od-claim |
| Rumble | slug /c/<slug> |
ru-slug |
Source unique : server/providers/channel-ref.mjs (serveur) et
src/app/shared/providers/channel-ref.ts (front), parité vérifiée par
npm run test:channelref. channelRef est redondant avec
channelExternalId — c'est une transition non cassante, pas un remplacement :
le champ legacy reste lu partout, channelRef en priorité quand il est présent.
Deux règles méritent note :
- PeerTube : l'instance n'est pas optionnelle. Sans elle,
fetchPeerTubeChannelproduiraithttps://<channel>, une URL fausse — donc pas dechannelRefplutôt qu'un composite bancal. - Odysee : le claim LBRY arrivait avec et sans
@selon le chemin (claim_search,short_url, base). La forme canonique porte le@, et les deux usages (paramètre deresolve, slug d'URL) passent parodyseeClaimToResolveArg()/odyseeClaimToSlug().
9.3 Cache YouTube (2 niveaux)
| Niveau | Emplacement | Caractéristiques |
|---|---|---|
| L1 — mémoire | youtube.mjs:94-107 |
LRU, YT_SCRAPE_MEM_MAX (300) ; lecture avec rafraîchissement de récence ; réinjecté depuis SQLite |
| L2 — SQLite | youtube_search_cache |
clé yt|sha256(q|perPage|page|sort|mode|filtersCacheKey) tronquée à 32 car. ; TTL YT_SCRAPE_TTL_MS (30 min) ; plafond 2 000 lignes (purge par expires_at DESC) ; jamais de résultat vide persisté (youtube.mjs:213-217) ; expiration paresseuse à la lecture — pruneYoutubeCache existe mais n'est jamais appelé |
La signature des filtres entre dans la clé : deux recherches identiques avec des filtres différents ne partagent jamais leur cache (youtube.mjs:197-199).
Le cache InnerTube est memoire seule (youtube-innertube.mjs:280-286), clé it|<hash>.
9.4 Ce qui n'est pas persisté
/api/details/*, /api/transcript/* (hors transcript_history déclenché par le front), /api/search/suggest, /api/trending, /api/download/*/formats, /api/yt/*, /oauth/google/watchlater, /oauth/google/yt-history — caches mémoire de processus uniquement.
9.5 Catalogue videos (phase 5)
title / thumbnail étaient dupliqués dans watch_history, playlist_items et les tables de tags, et les tags n'en portaient pas. Conséquence concrète : un like posé depuis l'UI sans titre disponible n'écrivait nulle part (le front n'a pas toujours la fiche), et listLikedVideos — qui lisait title via LEFT JOIN watch_history — renvoyait une ligne vide, invisible ou sans titre.
La table videos (PRIMARY KEY (provider, video_id), provider en nom long) centralise ces métadonnées. Elle est alimentée par les trois points d'écriture existants :
| Point d'écriture | Effet |
|---|---|
upsertWatchHistory |
observation réelle d'une vidéo |
likeVideo |
comble le trou : appelé même sans titre ni vignette |
addPlaylistVideo |
ajout à une playlist = observation |
upsertVideoRow()est best-effort : entrée invalide →false, jamais d'exception, jamais d'échec de l'écriture fonctionnelle.- Chaque champ est écrit par
COALESCE: un appel sans titre n'efface pas les métadonnées déjà connues. 0, les valeurs négatives et les chaînes vides ne sont jamais persistés (NULL) — cohérent avec la règle « métadonnée absente =undefined, jamais 0 ».captured_atest rafraîchi à chaque ré-observation (indicateur de fraîcheur),created_atconserve la première observation.- Lecture :
videosen source primaire, repliwatch_historypuisplaylist_items. Le repli playlist est une sous-requête corrélée —UNIQUE(playlist_id, provider, video_id)autorise la même vidéo dans N playlists, une jointure aurait dupliqué les likes. video_tags.providerest stocké tel quel parlikeVideo(forme courte ou longue selon le front) : les JOIN normalisent donc les deux côtés.watch_historyetplaylist_itemsconservent leurs colonnestitle/thumbnail(déréplication = phase suivante) : le repli de lecture en dépend encore.
10. Caches
| Cache | Emplacement | Clé | TTL |
|---|---|---|---|
| Résultats de recherche (front) | search.service.ts:39-40, 99-117 |
pid|q|page|sort|type.duration.period.sort |
60 s, par provider, mémoire |
| Suggestions (front) | suggest.service.ts:27-28 |
sortedProviders|q|limit |
5 min, purge globale à 200 entrées |
| Suggestions (serveur) | index.mjs:3190-3213 |
suggest:{ids}:{q}:{limit} |
5 min, LRU 500, rate-limit 60/min |
| Contenu de chaîne | channel-content.service.ts:27-41 |
provider::channelId::type::sort::q |
5 min ; « frais » exige items.length > 0 |
| Métadonnées de chaîne | channels.service.ts:16-20 |
provider::externalId |
6 h, servi périmé en cas d'erreur |
| Recherche YouTube (serveur) | youtube.mjs:94-224 |
cf. §9.3 | 30 min |
| Transcript | index.mjs:3283-3305 |
provider:videoId:lang |
24 h, rate-limit 10/min |
| Vidéos connexes YT | index.mjs:1717-1723 |
related:{videoId} |
mémoire |
| Formats de téléchargement | index.mjs:1115 |
— | mémoire |
| Préférences d'affichage | search.component.ts:31-33 |
newtube:search.hiddenProviders etc. |
localStorage |
11. Configuration par fournisseur
| Variable | Fournisseur(s) | Défaut | Effet |
|---|---|---|---|
YT_SEARCH_MODE |
YouTube | innertube-first |
ordre d'essai des 3 sources |
YOUTUBE_API_KEY / YOUTUBE_API_KEYS |
YouTube | — | Data API v3 ; CSV ou tableau JSON ; rotation automatique |
YT_DLP_PATH |
YouTube | yt-dlp[.exe] sur le PATH |
résolution du binaire : YT_DLP_PATH → PATH → binaire bundled youtube-dl-exec |
YT_SCRAPE_TTL_MS |
YouTube | 30 min | TTL du cache de recherche |
YT_SCRAPE_MEM_MAX |
YouTube | 300 | taille du LRU mémoire |
YT_DLP_TIMEOUT_MS |
YouTube | 20 s | timeout du binaire |
YT_COOKIES_FILE / YT_PO_TOKEN / YT_EGRESS_PROXY |
YouTube | — | anti-ban yt-dlp (jamais logués) |
YT_INNERTUBE_GL / YT_INNERTUBE_HL |
YouTube | FR / fr |
localisation de la session InnerTube |
TWITCH_CLIENT_ID / TWITCH_CLIENT_SECRET |
Twitch | — | App Access Token ; requis, sinon le fournisseur renvoie [] |
CHANNEL_CONTENT_TIMEOUT_MS |
tous | 9 s | timeout des appels contenu de chaîne |
CHANNEL_FETCH_TIMEOUT_MS |
tous | 6 s | timeout des appels métadonnées de chaîne |
CHANNEL_TTL_MS |
tous | 6 h | fraîcheur de la table channels |
DOWNLOAD_PROVIDERS |
tous | les 6 | restreint les téléchargements (index.mjs:1258) |
SUPPORTED_DL_LANGS / DEFAULT_DL_LANGS |
tous | fr,en |
langues de transcript autorisées |
NEWTUBE_DB_FILE |
tous | db/newtube.db |
chemin SQLite alternatif |
Instances PeerTube : gérées uniquement côté front (src/services/instance.service.ts:115-124, défauts video.manu.quebec, peerate.fr, mytube.pyramix.ca, persistées en localStorage). La recherche serveur est figée sur sepiasearch.org ; le multi-instances n'a d'effet que sur la lecture et les téléchargements.
12. Points d'attention et écarts constatés
| # | Anomalie | Impact | Emplacement |
|---|---|---|---|
| 1 | Rumble n'a aucune API — tout dépend du scraping HTML ; en cas de challenge Cloudflare persistant le fournisseur renvoie [] silencieusement |
Groupe de résultats vide, sans message | rumble.mjs:121-133, 279 |
| 2 | Rumble sans identité de chaîne : channelId, channelExternalId, publishedAt jamais renseignés |
La page chaîne filtre sur uploaderName/url et nextPage est toujours null |
rumble.mjs:224-235, channel-content.mjs:370-374 |
| 3 | Odysee jette views et publishedAt alors qu'ils sont disponibles en amont ; statut HTTP jamais vérifié |
Cartes sans compteurs ; une erreur 4xx/5xx passe pour un résultat vide | odysee.mjs:48-60, channel-content.mjs:349 |
| 3b | Odysee (anomalie 3) : le mapping est correct, la SOURCE ne livre pas les données. Vérifié en direct (30/09/2026) : claim_search ne renvoie ni release_time ni l'objet video — donc ni view_count — même quand ils sont demandés dans include. Le gel de la phase 8.4 le confirme (views/publishedAt absents pour od) |
views/publishedAt restent absents sur la recherche Odysee ; le filtre period= n'y est pas inopérant par erreur de code mais par absence de donnée. Remplir ces champs demande un autre endpoint (résolution par claim), pas un correctif de mapping |
odysee.mjs:50-69 |
| 4 | Pagination Twitch (page chaîne) cassée : le curseur Helix est lu puis jeté → page=2 renvoie page=1 |
Infinite scroll dupliqué | channel-content.mjs:302-303 |
| 5 | Item live Twitch (page chaîne) mal taggé : kind:'vod', type:'video', views=viewer_count, isLive jamais posé |
isLiveItem() ne le classera pas comme live |
channel-content.mjs:286-290 |
| 6 | pageToken YouTube en cache process-local, vers-l'avant uniquement |
Changer limit/sort/q entre deux pages, un redémarrage ou un scale horizontal tronquent à la page 1 |
channel-content.mjs:122-141 |
| 7 | toVideoItem perd type/isLive/isShort/kind/width/height |
Les grilles de chaîne retombent sur la règle « durée seule » : une verticale de 60 s y est bien short, une horizontale de 50 s aussi (faux positif) | http-channel.provider.ts:25-40 |
| 8 | Dérive de capacités : provider-registry.ts:19 déclare dm.playlists = false, channel-detail.model.ts:44 déclare true |
Incohérence d'affichage selon le code appelant | — |
| 9 | 3 conventions d'identité de chaîne divergentes : UC… (yt), "instance|channel" (pt), externalId sans @ (od) |
Toute jointure future multi-provider sera fragile | channel-content.mjs:116-117, 331, 353 |
| 10 | Branche morte dans channel-content.mjs:155 (retour identique ligne 157) |
Sans effet, mais signale un refactor incomplet | — |
| 11 | Code mort front : src/app/search/adapters/base.ts |
Maintenance inutile | — |
| 12 | Liste de providers 'all' codée en dur dans search.service.ts:93 et 4× dans search.component.ts |
L'ajout d'un provider exige de toucher ces endroits, contredisant la promesse « 5 minutes » du README | — |
| 13 | video_tags sans titre/vignette |
Un like sans ligne watch_history s'affiche avec un titre vide |
db.mjs:771-789 |
| 14 | pruneYoutubeCache jamais appelé |
Les entrées expirées ne sont supprimées qu'à la lecture de la même clé | db.mjs:1413 |
| 15 | Brèche sur le quota YouTube : youtube_search_cache ne stocke que YT, aucun cache persistant pour les 5 autres fournisseurs |
Chaque recherche Dailymotion/PeerTube/Odysee/Rumble/Twitch refait un appel amont | db/schema.sql |
| 16 | Compteurs PeerTube perdus : le serveur émettait viewCount, le front lisait views (et viewCount: undefined en dur dans l'adaptateur) |
Les vues PeerTube n'affichaient jamais. Corrigé en phase 8.4 : views est désormais canonique, viewCount reste en alias |
peertube.mjs:67-72, adapters/pt.ts:36 |
| 17 | language PeerTube : objet au lieu de chaîne — l'API renvoie {id,label}, l'ancienne garde cherchait .code et laissait passer l'objet entier |
Le front recevait {id:null,label:'Unknown'} là où le contrat déclare string. Corrigé en phase 8.4 (label volontairement exclu : « Unknown » n'est pas un code de langue) |
peertube.mjs:71-79 |
13. Annexe — Référence des fichiers
Suite : le plan d'exécution des corrections (parité de champs, classification, cache, table
videos) se trouve dansplan-phases-catalogue-classification.md.État d'avancement : les phases 0, 1, 2, 3, 4 et 7 sont implémentées et testées — voir
rapport-execution-phases-0-1-2-3-7.md.
Backend
| Fichier | Rôle |
|---|---|
server/index.mjs |
routes API, fan-out /api/search, providerUrlFrom(), téléchargements, transcripts |
server/db.mjs |
schéma, migrations, tous les helpers d'écriture SQL |
server/search-filters.mjs |
modèle de filtres partagé, classification Short/Live, mapping natif |
server/transcript.mjs |
transcription multi-provider via yt-dlp + InnerTube |
server/providers/registry.mjs |
contrat Suggestion, registre des 6 adaptateurs, cache générique, pose de channelRef |
server/providers/channel-ref.mjs |
source unique des schemes d'identité de chaîne + normalisations Odysee / PeerTube (phase 6) |
server/providers/feature-flags.mjs |
FF_<PROVIDER> : extinction d'un provider sans redéploiement (phase 8.3) |
server/providers/youtube.mjs |
dispatcher 3 niveaux, cache 2 niveaux, métriques |
server/providers/youtube-common.mjs |
clés API, modes, résolution yt-dlp, anti-ban, métriques |
server/providers/youtube-innertube.mjs |
InnerTube (search, continuations, watch-next, caption tracks) |
server/providers/youtube-scrape.mjs |
scraping yt-dlp, flat-playlist, onglets de chaîne |
server/providers/dailymotion.mjs |
adaptateur REST Dailymotion |
server/providers/twitch.mjs |
adaptateur Helix (token, 4 mappeurs, recherche multi-sections) |
server/providers/peertube.mjs |
adaptateur SepiaSearch |
server/providers/odysee.mjs |
adaptateur Lighthouse |
server/providers/rumble.mjs + rumble_fetch.py |
scraping HTML + contournement Cloudflare |
server/providers/channel-content.mjs |
contenu de chaîne (4 types × 6 providers) |
server/providers/channel-registry.mjs |
métadonnées de chaîne (6 fetchers) |
Frontend
| Fichier | Rôle |
|---|---|
src/app/core/providers/provider-registry.ts |
specs providers (id, libellé, icône, couleur, capacités) |
src/app/shared/models/video-item.model.ts |
modèle VideoItem |
src/app/shared/models/channel-detail.model.ts |
ChannelMeta, ChannelDetail, DEFAULT_CAPABILITIES |
src/app/shared/utils/video-kind.ts |
classification Short/Live/Vidéo (miroir serveur) |
src/app/shared/providers/channel-ref.ts |
identité de chaîne normalisée, channelRefFrom() / channelUrlFromRef() (phase 6) |
src/app/shared/utils/section-policy.ts |
politique d'acceptation par section + isPlayable() |
src/app/search/search.service.ts |
fan-out RxJS, timeout 8 s/provider, 1 retry, cache 60 s |
src/app/search/filters.ts |
modèle de filtres front + opérateurs (live:, today:, long:) |
src/app/search/search-contract.ts |
lecture stricte du contrat v: 2 + extraction d'erreur par provider (phase 8.2) |
src/app/search/adapters/*.ts |
6 adaptateurs HTTP → VideoItem |
src/app/core/providers/channel/* |
adaptateurs de contenu de chaîne |
src/services/channel-content.service.ts |
cache 5 min + infinite scroll avec dédup |
src/services/youtube-api.service.ts |
couche legacy : mappers détaillés par provider, normalisation durées/vignettes |
Base de données
| Fichier | Rôle |
|---|---|
db/schema.sql |
15 tables de base |
db/migrations/20240915_add_thumbnail_to_watch_history.sql |
watch_history.thumbnail |
db/migrations/20250923_add_subscriptions_tables.sql |
channels, subscriptions |
db/migrations/20250924_add_default_providers.sql |
user_preferences.default_providers |
db/migrations/20250924_add_download_jobs.sql |
file de téléchargement persistante |
db/migrations/20250924_add_telemetry_events.sql |
télémétrie UX |
db/migrations/20250926_add_download_languages.sql |
user_preferences.download_languages |
db/migrations/20250926_add_youtube_scrape_cache.sql |
youtube_search_cache, youtube_metrics |
db/migrations/20260926_add_subscription_groups.sql |
groupes d'abonnements |
db/migrations/20260926_add_transcript_history.sql |
transcript_history |
db/migrations/20260927_add_oauth_connections.sql |
oauth_connections |
db/migrations/20260927_add_oauth_yt_channel.sql |
colonnes yt_channel_id, yt_page_id |
db/migrations/20260927_backfill_youtube_thumbnails.sql |
backfill vignettes YT depuis video_id |
db/migrations/20260930_add_videos_table.sql |
videos (catalogue partagé) + backfill depuis watch_history / playlist_items (cf. §9.5) |
Tests associés
| Test | Couverture |
|---|---|
server/tests/search-filters.test.mjs (npm run test:filters) |
normalisation, bornes, mapping providers, classification Short — offline |
server/tests/youtube-innertube.test.mjs |
mapVideoNode, mapLockupView, parsing |
server/tests/youtube-scrape.test.mjs |
mapFlatEntry, parseFlatPlaylistJson, classification d'erreurs |
server/tests/search.e2e.test.mjs (npm run test:search-e2e) |
fan-out réel, deep-links, préférences |
src/app/shared/utils/video-kind.spec.ts (npm run test:kind) |
classification Short front |
server/tests/transcript.test.mjs (npm run test:transcript) |
parseurs json3/vtt + contrat API |
server/tests/provider-contract.test.mjs (npm run test:contract) |
contrat Suggestion v2 des 6 fournisseurs — offline |
server/tests/rumble-ld.test.mjs (npm run test:rumble) |
parseRumbleViews() (« 1,2 K »), JSON-LD — offline |
server/tests/search-cache.test.mjs (npm run test:cache) |
cache générique, TTL, plafond, provider_metrics, bascule InnerTube (61 assertions) — offline |
server/tests/videos_catalog.test.mjs (npm run test:videos) |
catalogue videos, upsert non destructif, trou des likes, replis, backfill (56 assertions) — offline |
server/tests/channel_ref.test.mjs (npm run test:channelref) |
channelRef : reconversion des 3 conventions historiques, parité stricte avec le front d'avant, parité des schemes serveur ↔ front, aucune valeur inventée (12 assertions) — offline |
server/db.mjsrefuse d'ouvrirdb/newtube.dblorsqu'il est chargé depuis un*.test.mjssansNEWTUBE_DB_FILE: une faute de frappe sur le nom de variable avait déjà écrit des fixtures de test dans la base de développement. Chaque test doit pointer vers un fichier temporaire avant l'import.
Synthèse en une phrase
NewTube saisit les métadonnées vidéo par six chemins/upstream très hétérogènes — API REST structurée (Dailymotion, PeerTube via SepiaSearch), API authentifiée multi-sections (Twitch Helix), protocole interne sans clé avec 3 niveaux de repli (YouTube InnerTube/scrape/Data API), index JSON-RPC (Odysee), scraping HTML avec contournement anti-bot (Rumble) — puis les catalogue en les ramenant à un contrat unique (Suggestion), en les re-normalisant en VideoItem côté front, en les classant Vidéo/Short/Live par règles déterministes dupliquées serveur/front, et en ne persistant que les fragments liés à l'utilisateur (historique, playlists, likes, téléchargements) plus un catalogue videos partagé (phase 5) qui porte chaque métadonnée une seule fois, alimenté par les trois points d'écriture existants.
Document généré le 2026-09-29 à partir de la lecture du code source du dépôt NewTube.