package.json est la source unique de version (menu du compte + info.version OpenAPI) ; le registre incrémente le patch à chaque déploiement, donc le tag de cette livraison sera 1.0.60. Bumper package.json AVANT docker/deploy-img.sh maintient les trois surfaces cohérentes.
NewTube — Votre hub vidéo multi-plateformes 🎬🌐
Agrégez, explorez et regardez des vidéos depuis
- YouTube 🔴
- Dailymotion 🔵
- Twitch 🟣
- PeerTube 🟢 (multi-instances)
- Odysee 🟡
- Rumble 🟠
✨ Pourquoi NewTube ? (Objectifs clés)
- 🔎 Recherche unifiée & tendances : un seul champ de recherche, des sections par fournisseur, filtres cohérents.
- 🧭 Navigation claire : thèmes (Trending, Live, Gaming, News…), onglets Shorts, pages Playlists/History/Liked.
- 👥 Multi-utilisateur : playlists publiques/privées, préférences, région/qualité par défaut.
- ⚙️ Prod-ready : API Node/Express, SQLite intégré, rate-limit, logs, tests ciblés.
- 📦 Ops friendly : image Docker, script
maj.sh, variables d’env, astucesdaemon.jsonpour registres HTTP.
🧩 Stack
- 🅰️ Angular 20 (standalone), RxJS, TailwindCSS
- 🟩 Node/Express (sert
dist/+ API) - 🐳 Docker/Compose (déploiement)
- 💾 SQLite (par défaut) – simple, portable
🎥 Fournisseurs supportés
| Plateforme | Recherche | Lecture | Shorts | Live | Playlists* |
|---|---|---|---|---|---|
| YouTube 🔴 | ✅ | ✅ | ✅ | ✅ | ✅ |
| Dailymotion 🔵 | ✅ | ✅ | ✅ | ✅ | ⏳ |
| Twitch 🟣 | ✅ | ✅ | Clips ✅ | ✅ | N/A |
| PeerTube 🟢 (multi-instances) | ✅ | ✅ | ✅ | ✅ | ⏳ |
| Odysee 🟡 | ✅ | ✅ | ✅ | N/A | N/A |
| Rumble 🟠 | ✅ | ✅ | ✅ | ✅ | N/A |
* Playlists = intégration locale NewTube (création/gestion) ; la synchro native (ajout/suppression côté fournisseur) exige l’API d’écriture du fournisseur :
- YouTube ✅ (Data API) ; Dailymotion / PeerTube ⏳ : APIs d’écriture existantes mais coûteuses (inscription d’app OAuth pour l’un, JWT par instance pour l’autre).
- Twitch N/A — API playlists supprimée : probe 2026-10 avec token app valide,
GET/POST /helix/playlistset/helix/playlists/{id}/videosrépondent tous 404 (contrôle/helix/users→ 400 = route existante). - Odysee N/A — proxy
na-backenden lecture seule (méthodes d’écriture : « authentication token missing ») ; Rumble N/A — aucune API publique.
Notes sur les colonnes :
-
Shorts : le flux
/#/shortstourne sur les 6 fournisseurs — catalogue/api/search?type=shorts(lot affiché s’il compte ≥ 3 résultats), sinon repli recherche avec requêtes dédiées (SHORTS_QUERIES). Filtre de durée côté serveur : YouTube (videoDuration=short), Dailymotion (shorter_than), PeerTube (durationMax) ; filtre client pour Odysee et Rumble ; Rumble : flux natifrumble.com/shorts; Twitch : pas de format vertical natif, le flux sert les clips. -
Live : sources câblées — YouTube (
search.list eventType=live), Twitch (top streams Helix), PeerTube (API?isLive=true: thème « En direct » + onglet Live de la chaîne), Rumble (rumble.com/browse/live: thème + onglet chaîne filtré parchannelId). Dailymotion : lecture possible maislive:falsecôté capacités (le thème y affiche « Ce fournisseur ne propose pas de directs »). Odysee N/A — aucune API publique de lives (proxy na-backend sous jeton, probe 2026-10). -
Rumble 🟠 : scraping maison derrière Cloudflare (fetch navigateur + helper
curl_cffi). Si le réseau du serveur est challengé, l’API répond503 { error: "rumble_cloudflare_challenge" }(« bloqué temporairement ») au lieu d’un résultat vide : Rumble peut donc être temporairement absent selon le réseau — voirserver/providers/rumble.mjs.
🔎 Recherche unifiée (multi-providers)
Un seul champ, tous les fournisseurs — avec filtres, raccourcis et deep-links :
- Panneau de filtres : bouton Filtres (ou Ctrl/⌘+Maj+F) — un seul écran pour toutes les dimensions : sources (multi), type de contenu (vidéos / shorts-clips / en direct / chaînes), période (dernière heure, aujourd’hui, semaine, mois, année), durée (courte < 4 min, moyenne 4-20 min, longue > 20 min) et tri. Pastilles, roving tabindex (←/→ dans un groupe, ↑/↓ entre groupes, Entrée pour choisir, Échap pour fermer), focus trap, « Réinitialiser », « Mémoriser comme sources par défaut » et les recherches récentes en raccourci
- Filtres appliqués côté serveur :
?type=…&duration=…&period=…&sort=…voyagent dans l’URL et dans/api/search— YouTube filtre nativement (InnerTubeupload_date/type/duration/features, Data APItype/videoDuration/publishedAfter, yt-dlp--dateafter), les autres providers sont affinés en post-traitement (server/search-filters.mjs) - Opérateurs en clair :
linux live:,tuto today: long:,concert shorts:… tapés dans la barre sont convertis en filtres et retirés de la requête (autocomplétion après le:) - Autocomplete
@: tapez@ytdans le champ pour cocher/décocher une source (↑/↓/Entrée, Échap pour fermer) — raccourcis Alt+1..6 - Deep-links :
/#/search?q=…&providers=yt,ru&period=week&type=liverelance la recherche filtrée — partageable - Fallback préférence : URL sans
providers→ préférencedefaultProvidersde l’utilisateur → provider actif - Accessibilité : focus trap dans le panneau, Esc pour fermer, aria-combobox +
aria-activedescendantsur le champ, focus restauré à la fermeture - Panneau de suggestions : sous l’input, ligne « Rechercher
<q>» puis suggestions (recherches récentes 🕘 + groupes par providerYT/DM/…, sous-chaîne surlignée) —GET /api/search/suggest?q=…&providers=…&limit=…(min 2 caractères, debounce 250 ms, cache 5 min, dégradation[]par provider) ; ↑/↓ (bouclants),Home/End,PageUp/PageDown,Entrée(valide la ligne surlignée, sinon lance la recherche),Tab(complète sans chercher),Échap(ferme puis vide), priorité au popover@ - Focus : le panneau se referme dès que le focus quitte la barre (
focusout+relatedTarget, clic neutralisé sur les lignes pour garder le focus dans l’input, pas de scintillement au re-clic)
Endpoints API concernés
GET /api/search?q=…&providers=yt,dm&type=live&duration=short&period=week&sort=date— fan-out parallèle, réponse groupée par provider (page/pageSize/sort+filters; YT sans quota via InnerTube + continuations)GET /api/search/suggest?q=…&providers=yt,dm&limit=10— typeahead{ q, groups: { yt: string[], … } }(cache 5 min, rate-limit)GET /api/details/youtube/:videoId— métadonnées +related[](watch-next InnerTube,?related=0pour désactiver)GET /api/trending?provider=yt&limit=…— tendances YT sans cléGET /healthz(alias/api/healthz) — mode YT, binaire yt-dlpbinOk, cache, métriques quota/jour, clésGET /api/transcript/:provider/:videoId?lang=&instance=&slug=&sourceUrl=— transcript{ lang, available, languages, lines: [{ t, dur, text }] }(cache 24 h, rate-limit 10/min ; absent → 200{ available: false }, échec → 502 ; YouTube : découverte des pistes via InnerTube,YT_TRANSCRIPT_SOURCE)GET/PATCH /api/user/preferences—defaultProviders(tableau JSON, sanitizé serveur)POST /api/telemetry/events— événements UX anonymes (whitelist :search_submit,provider_picker_open,provider_apply,at_autocomplete_use,quick_menu_open,filter_panel_open,filter_apply,suggest_shown,suggest_used)
Tests
npm run test:search # unitaires SearchService + clavier/focus du panneau de suggestions + panneau de filtres
npm run test:search-e2e # scénarios e2e (serveur réel isolé)
npm run test:filters # filtres de recherche : normalisation, bornes, mapping providers (offline)
npm run test:suggest # typeahead : parsing/dédup + contrat /api/search/suggest
npm run test:transcript # transcripts : parseurs json3/vtt + contrat /api/transcript
npm run test:preferences # persistance defaultProviders
npm run test:telemetry # télémétrie minimale
🖥️ Fonctionnalités (vue d’ensemble)
- 🧭 Accueil “Tendances & Viral” par fournisseurs
- 🧩 Thèmes : Trending, Live, Gaming, Sports, News, Finance, Tech, Science, Health, Music, Podcasts, Movies/TV, Education, Travel, Food, DIY, Auto…
- 🎯 Shorts : affichage dédié (séparé des vidéos longues)
- ❤️ Liked videos avec recherche serveur
- ⏰ À regarder plus tard (Watch later) : bouton horloge à côté du cœur sur toutes les cartes (accueil, thèmes, recherche), liste Vous → À regarder plus tard (
/#/library/watch-later), API/api/user/watch-later - 🕘 History (recherches + visionnage)
- 📚 Playlists publiques/privées (CRUD, compteurs, dates MAJ)
- 🔐 Auth légère (JWT), rate-limit API
- ⚙️ Préférences : langue, thème (système / dark / light / blue / black), région, qualité par défaut
- 🧩 PeerTube multi-instances : activer/désactiver, choisir l’instance active
🗂️ Structure du repo
-
docker-compose/docker-compose.yml— servicenewtube(image, ports, env, volumes, restart).env.example— modèle d’environnementmaj.sh— pull image + restart stackinit.sh— init des dossiers/volumes &.env
-
docker/Dockerfile— build Node/Express (sertdist/+ API)scripts/env-dump.sh— génèreassets/config.jsdepuis l’env (option NGINX)config/nginx.conf— exemple (si variante NGINX)
-
server/index.mjs— routes API, statiquesdist/, downloadsdb.mjs— SQLite + migrations légèrestests/playlist_visibility.test.mjs— tests playlists
-
db/—schema.sql+migrations/ -
assets/—config.local.example.js -
src/,app/,public/— front Angular -
package.json— scripts (dev,build,api,api:watch,test:playlists)
🧱 Prérequis
- 🐧 Linux (recommandé) / macOS / Windows (WSL2 ok)
- 🐳 Docker Engine ≥ 24, Compose v2
- 🟩 Node.js ≥ 20 (dev local)
- 🔐 Accès au registre d’images (ex.
docker-registry.dev.home:5000)
🚀 Installation
Option A — Docker (recommandé)
cp docker-compose/.env.example docker-compose/.env
# Éditez docker-compose/.env (hostnames, clés API, secrets…)
cd docker-compose
docker compose up -d
Application : http://localhost:8080 (mappage 8080:4000)
Option B — Dev local
npm install
cp assets/config.local.example.js assets/config.local.js # (optionnel — valeurs SANS SECRET uniquement)
npm run dev # front + api dev proxy
# API seule :
npm run api
npm run api:watch
# Build prod :
npm run build
🔁 Déploiement & mises à jour (maj.sh)
chmod +x docker-compose/maj.sh
./docker-compose/maj.sh
Ce script fait :
docker image pull <registre>/newtube-angular:latestdocker compose downdocker compose up -d
Vérifications : docker compose ps, docker logs newtube --tail=200, curl -I http://localhost:8080
🧰 Astuce Ops — Docker /etc/docker/daemon.json (registres HTTP/insecure)
À configurer sur la machine qui exécute
docker compose/maj.sh.
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json >/dev/null <<'JSON'
{
"insecure-registries": ["docker-registry.dev.home:5000"],
"registry-mirrors": [],
"debug": false
}
JSON
# debian linux
sudo systemctl daemon-reload
sudo systemctl restart docker
# Alpine linux
sudo rc-service docker restart
sudo rc-update add docker
docker info | grep -i registry
Ajoutez au besoin log-driver, log-opts, default-address-pools, etc.
🔐 Variables d’environnement (exemples)
Service / compose
NGINX_HOSTNAME,DIR_NEWTUBE,TZ
App / serveur
PORT(4000),NODE_ENV,JWT_SECRETACCESS_TTL_MIN,REFRESH_TTL_DAYS,REMEMBER_TTL_DAYSYT_CACHE_TTL_MS- Clés API (toutes côté serveur, aucune n'est envoyée au navigateur) :
GEMINI_API_KEY,YOUTUBE_API_KEYouYOUTUBE_API_KEYS(CSV),VIMEO_ACCESS_TOKEN,RUMBLE_API_KEY,TWITCH_CLIENT_ID,TWITCH_CLIENT_SECRET - API ouverte aux clients :
API_ALLOWED_ORIGINS(CSV des origines autorisées en CORS, défautlocalhost:4200,4000,3000— y ajouter l'URL publique de l'app pour des clients tiers),METRICS_TOKEN(si défini,/metricsexigeAuthorization: Bearer …),DETAILS_RATE_LIMIT(appels/api/detailspar minute, défaut 60) NEWTUBE_DB_FILE(chemin SQLite alternatif)
Voir
docker-compose/.env.examplepour un point de départ.
🩺 Troubleshooting (rapide)
- 🧾 Certif/registre :
x509… unknown authority→ configurezdaemon.json(ci-dessus) puissystemctl restart docker - 🔌 Port 8080 occupé : changez le mappage dans
docker-compose.yml - 🗄️ Permissions volumes : vérifiez que
DIR_NEWTUBEexiste et est accessible par Docker - 🧩 Variables manquantes : complétez
.env(clés API,JWT_SECRET, etc.) - 🌐 CORS en dev : utilisez le
proxy.conf.jsonetnpm run dev
🗺️ Roadmap
Légende : ✅ livré (câblé de bout en bout) · 🟡 partiellement livré (détail sous chaque item) · ⏳ pas encore démarré
✅ Livré
- ✅ Dist statique + API Node/Express
- ✅ Playlists publiques/privées (tests de visibilité)
- ✅ Barre de recherche Liked videos (filtrage serveur)
- ✅ Liste « À regarder plus tard » (watch later) — même stockage « tags » que les likes (aucune migration), bouton horloge à côté du cœur sur toutes les cartes, entrée « Vous » de la barre latérale, page
/#/library/watch-lateravec recherche serveur - ✅ PeerTube multi-instances (activer/désactiver, set active)
- ✅ Thème (système / dark / light / blue / black)
- ✅ Harmonisation visuelle — tous les utilitaires neutres (
slate/zinc/gray, fonds, textes, bordures, anneaux, placeholders, étatshover:/disabled:) sont mappés sur les variables du thème (75 jetons, 36 fichiers) : barre de recherche, menus, panneaux et cartes suivent enfin le thème appliqué ; panneau ⓘ des Shorts re-colorié dans son périmètre (le chrome au-dessus de la vidéo reste blanc) ; marqueurborder-l-4des titres de page sur l'accent du thème ; cartes vidéo harmonisées (text-sm/font-semibold/ métatext-xs/ rayonrounded-lg, pastille de durée sansfont-mono) ; contour fournisseur 1 px à la couleur de la pastille autour de chaque cadre vidéo (grilles, recherche, channel, watch — lecteur et « à suivre » —, shorts, historique, aimés, playlist) - ✅ Préférences (langue, thème, région, qualité par défaut)
- ✅ Auth légère (JWT), rate-limit API
- ✅ Recherche unifiée multi-providers — panneau de filtres (sources / type / période / durée / tri, Ctrl/⌘+Maj+F), autocomplete
@+ opérateurslive:/today:, deep-links?providers=…&type=…&period=…, préférencedefaultProviderspersistée, filtres serveur, télémétrie minimale, a11y (focus trap, Esc, clavier complet sur le panneau de suggestions) - ✅ Filtres & tri unifiés — barre partagée [Filtres] [Tri] sur la page de recherche ET sur les pages thèmes (pastilles de type, ligne Sources Toutes / Aucune, période / durée / langue, filtres actifs en pastilles) ; bouton « filtres rapides » supprimé, tout est sous [Filtres]. Tri : Pertinence, Date de mise en ligne, Vues, Note (likes), Durée (les deux derniers réordonnent côté client)
- ✅ Navigation par thèmes (Trending, Live, Gaming, News, Finance, Tech, Science, Health, Music, Podcasts, Movies/TV, Education, Travel, Food, DIY, Auto…)
- ✅ Abonnements (routes + DB) —
GET/POST/DELETE /api/subscriptions(+POST /api/subscriptions/batch), groupes façon PocketTube (/api/subscription-groupsCRUD + affectation de membres), tables SQLitesubscriptions/ groupes, page/#/library/subscriptions, bouton « S'abonner » partagé (subscribe-button) + entrée barre latérale ; alimentés par l'import OAuth (YouTube/Twitch). Tests :npm run test:subscriptions - ✅ Page « Shorts » unifiée (tous fournisseurs) — flux immersif
/#/shortssur les 6 fournisseurs (catalogue/api/search?type=shortssinon repli recherche, dédup centralisée), durées : filtres côté serveur (YouTubevideoDuration=short, Dailymotionshorter_than, PeerTubedurationMax) + filtre client ailleurs, pastille de durée sur les cartes de la recherche filtrée et durée affichée dans le panneau ⓘ, clips Twitch, mode « Pour toi » - ✅ UI/UX des Shorts — le rail d'actions quitte les bords (les flèches latérales sont supprimées) et s'ancre au bord droit de la vidéo : informations, flèches ↑/↓, j'aime, enregistrer (liste « À regarder plus tard »), s'abonner (pastille = avatar), commentaires, partager, lecture auto (interrupteur persisté, respecté par le minuteur ET la fin de vidéo YouTube) ; panneau ⓘ (titre, vues, engagement, chaîne, description et vues demandées à la source à l'ouverture via
/api/details— squelette de chargement, échec silencieux —, renvoi vers la page complète, aucune source de commentaires dans l'app) en colonne à droite au bureau et en tiroir plein écran sur mobile, la vidéo rétrécissant sans jamais être masquée ; transition glissée d'un short à l'autre (le cadre suit le doigt, résistance aux extrémités, ressort sous 25 % de la hauteur puis éjection/emboîtement,prefers-reduced-motionrespecté) ; FAB menu conservé (déplaçable, au-dessus de la vidéo). Le tap sur un contrôle du rail ne déclenche plus play/pause - ✅ Métadonnées Odysee sans yt-dlp —
resolvede l'API LBRY (~0,2 s contre ~45 s mesurées avec l'extracteurlbryde yt-dlp, qui finissait sur « No video formats found! ») : titre, description, vignette, durée et date arrivent enfin sur/watch;viewsvolontairement omis (LBRY n'expose aucun compteur pour un claim isolé). Au passage : cache/api/details6 h (LRU 500,DETAILS_CACHE_TTL_MS) etviews/durationne sont plus émis quand ils valent 0 (un 0 écrasait la valeur du flux : « 0 vue » sur les fournisseurs muets). Tests :npm run test:odysee - ✅ Téléchargements intégrés — file d'attente persistée en SQLite (survit aux redémarrages API), jobs par utilisateur (répertoires isolés, ownership sur status/fichier/cancel), quota de stockage configurable (
DOWNLOAD_STORAGE_QUOTA_BYTES, fenêtreDOWNLOAD_QUOTA_WINDOW_MS), reprise des jobs échoués/interrrompus (bouton Réessayer), page Bibliothèque > Téléchargements (filtres par état, progression live, quota), nettoyage auto des fichiers orphelins au boot. Variables :DOWNLOAD_MAX_CONCURRENT(2),DOWNLOAD_STORAGE_QUOTA_BYTES(5 GiB),DOWNLOAD_QUOTA_WINDOW_MS(30 j),DOWNLOAD_PROVIDERS(peertube,odysee) - ✅ Sous-titres & transcripts —
GET /api/transcript/:provider/:videoId(yt-dlpsubtitles/automatic_captions, parsingjson3/vtt, cache 24 h, rate-limit), panneau Transcript sur la page Watch (sélecteur de langue, dégradation propre si indisponible) - ✅ YouTube sans quota (InnerTube façon SmartTube) —
youtubei.jspinné, chaîneinnertube → scrape yt-dlp → API officielle(YT_SEARCH_MODE, défautinnertube-first), pagination illimitée via continuations (scroll infini,page=2,3…), vidéos connexes watch-next dansGET /api/details/youtube/:videoId→ sidebar Watch,GET /api/trending?provider=yt, cache mémoire + SQLite, anti-ban (YT_COOKIES_FILE,YT_PO_TOKEN,YT_EGRESS_PROXY), observabilité/healthz(mode, binairebinOk, cache, quota jour, clés) - ✅ OAuth (Google/Twitch) pour import favoris/abonnements —
/library/import: abonnements + likes YouTube (lecture seule), follows Twitch → abonnements, preview + import,GET /api/oauth/status|connections|preview,POST /api/oauth/:provider/import - ✅ Live sur PeerTube & Rumble — thème « En direct » + onglet Live de la chaîne : PeerTube via l'API native
?isLive=true(recherche sepiasearch + vidéos de chaîne, itemstype:'live'+ spectateurs), Rumble via la page SSRrumble.com/browse/live(route/api/rumble/live, items typés live ; onglet chaîne filtré parchannelId— plafond : page 1 bornée à 50 directs). Odysee : N/A (aucune API publique de lives, probe 2026-10). Tests :npm run test:live - ✅ Import/Export playlists (JSON) —
GET /playlists/export(dump complet + items, formatnewtube-playlists-v1),POST /playlists/import(création des listes manquantes, dédoublonnage par titre au réimport, compteurs détaillés ; bornes 200 listes / 5 000 vidéos par liste) ; boutons [Exporter] [Importer] sur/#/library/playlists. - ✅ Page Administration (
/#/admin, entrée « Administration » de la section Informations dans la barre latérale) —/healthz+/api/providers/metricsrendus lisibles : statut API, mode YouTube, version et santé yt-dlp, clés actives / bannies (+ suffixes), anti-ban (cookies / PO token / proxy), cache de recherche par provider, compteurs 1 h par provider, quota YouTube du jour. Au passage :healthzservi aussi sous/proxy/api/healthz(le chemin que le front utilise en prod). Journaux et exposition Prometheus livrés — voir Observabilité. - ✅ Cache TTL par provider —
server/env-ttl.mjs(<BASE>_<PROVIDER>→<BASE>→ défaut, valeurs invalides ignorées, jamais de TTL nul) branché sur les caches details (DETAILS_CACHE_TTL_MS_<P>) et transcripts (TRANSCRIPT_CACHE_TTL_<P>) en plus de la recherche (SEARCH_CACHE_TTL_MS_<P>). Le cache suggest reste global par conception : sa clé agrégée couvre plusieurs providers d'un coup, un TTL par provider demanderait de scinder le cache. Tests :npm run test:cache - ✅ Menu du compte refondu (bouton avatar, haut à droite du header) — panneau sombre à lignes « icône + titre gras + sous-titre gris » : Thème (thème réellement appliqué en sous-titre, pastilles dépliées au clic), Administration (
/#/admin), API (/proxy/api/openapi.jsondans un nouvel onglet), Préférences (/#/account/preferences), Guide d'utilisation (/#/info/utilisation), À propos (version, tagline, lien code source), Sessions (/#/account/sessions), Déconnexion ; pied de page Version = version officielle de l'application (src/app/version.ts, alignée sur le tag semver publié pardocker/deploy-img.sh). Clés i18n déclarées des deux côtés (FR + EN) - ✅ Observabilité —
GET /healthz(modeYT_SEARCH_MODE, binaire yt-dlp, cache, quota du jour, clés masquées — testé partest:api),GET /api/providers/metricset la page Admin qui les rend, journal JSON structuré en prod (une ligne par requête :ts,reqId,method,route,status,ms, adossé au headerX-Request-Idrenvoyé sur chaque réponse) etGET /metrics— exposition Prometheus sans dépendance (http_requests_totalpar route et code, somme/nombre de durées, démarrage + mémoire du processus ;METRICS_TOKENverrouille l'accès si défini) - ✅ API production-ready (P0 + P1) — plus aucun secret servi au navigateur :
/assets/config.local.jsest généré par le serveur avant les montages statiques (il l'emporte donc sur le fichier local) et ne contient plusYOUTUBE_API_KEY(S), le fichier local n'est plus copié dans l'image Docker ni embarqué dansdist, et les appels YouTube passent par/api/ytuniquement (clé, rotation, quota et cache côté serveur — suppression des appels directs à googleapis et des gardes « pas de clé ⇒ écran vide ») ; CORS piloté parAPI_ALLOWED_ORIGINS(CSV) + méthodePATCH(requis par/user/preferences) ; clés d'API longue durée :GET/POST /api/keys+DELETE /api/keys/:id, jetonntk_…affiché une seule fois, stocké en SHA-256 avec préfixe affichable, headerX-API-Keyaccepté par les deux middlewares d'auth (toutes les routes protégées deviennent scriptables),last_used_atrenseigné à chaque usage ;X-Request-Iden réponse sur chaque requête ;/api/detailsrate-limité (DETAILS_RATE_LIMIT, 60/min, réponse JSON —/api/transcriptavait déjà le sien) ; version uniquepackage.json(menu du compte +info.versionde l'OpenAPI). Tests :npm run test:api
🟡 Partiellement livré
- 🟡 Tags — ✅ tables
tags/video_tags(stockage des listes « Aimés » et « À regarder plus tard », recherche serveur dans ces listes) ; ⏳ tags libres créés par l'utilisateur sur n'importe quelle vidéo + recherche de vidéos par tag - 🟡 Qualité vidéo — ✅ sélecteur de qualité sur
/#/watch(formats yt-dlp, tri décroissant1080p → 144p, reprise de la lecture sur le flux choisi) ; ⏳ mode « auto » intelligent (choix automatique du flux selon la bande passante) - 🟡 Traduction UI (i18n) — ✅ socle FR/EN (
src/services/i18n.service.ts, chaque clé déclarée des deux côtés) couvrant barre de recherche, barre de filtres, pages thèmes ; ⏳ couverture élargie aux écrans encore en texte FR hardcodé (/watch,/shorts, bibliothèque)
⏳ Pas encore démarré
- ⏳ PWA (installable, offline cache des métadonnées)
- ⏳ Chromecast / AirPlay
- ⏳ Mode « TV »
- ⏳ Theming avancé (polices, densité, accents)
- ⏳ Playlists natives Dailymotion & PeerTube (les 3 autres fournisseurs sont en N/A — voir la matrice : API Twitch supprimée, Odysee/Rumble sans API d'écriture) : inscription d'app OAuth Dailymotion, JWT par instance PeerTube
- ⏳ Nouveaux fournisseurs — plan détaillé ci-dessous
🆕 Nouveaux fournisseurs — plan d'implémentation
Candidats (par rapport / effort ; endpoints et quotas à re-valider au démarrage de chacun) :
- Kick — API JSON publique (sans quota clé à confirmer), modèle identique à Twitch (live / vod / clip) : recherche + métadonnées via l'API, lecture via l'embed officiel, feature-flag
FF_KICK. Priorité haute : seul candidat avec un vrai live concurrent de Twitch. - Vimeo — recherche et détails via l'API Data (clé de développeur), formats/téléchargement via yt-dlp (extracteur Vimeo mature), lecture via l'embed officiel
player.vimeo.com. - SoundCloud — recherche via yt-dlp, lecture via le widget iframe officiel ; audio seul → carte avec pastille « audio », hors flux Shorts.
- TikTok — recherche via yt-dlp + embed officiel ; risque élevé (anti-bot de la même classe que Cloudflare/Rumble) : ne commencer qu'après les 1-3, avec contrat d'erreur strict (
503= challenge,404= introuvable prouvé — jamais l'inverse).
Chemin d'implémentation standard (identique pour chaque fournisseur, c'est celui des 6 existants ; la procédure détaillée côté recherche/filtres est aussi dans « Ajouter un provider en 5 minutes » en fin de README) :
- IDs & URLs — id court dans
src/app/shared/providers/provider-ids.ts+ couleur du badge fournisseur ; brancherproviderUrlFrom()(server/index.mjs) — jamais d'URL provider écrite à la main ; transmettre tels quelsinstance/slug/sourceUrl. - Métadonnées — privilégier l'API native du provider quand elle existe (modèle Odysee :
resolveLBRY, ~0,2 s), sinon tuyau yt-dlpyoutubedl(url, {dumpSingleJson, skipDownload})qui alimente gratuitement/api/details/:provider/:videoIdet/api/download/:provider/:videoId/formats. - Recherche —
server/providers/<p>.mjs= un seul cœur fetch + parse ; enregistrersearch()dansregistry.mjs(fan-out), les chaînes danschannel-registry.mjs; rate-limit + cache dans un routeur fin dédié si besoin (modèleserver/rumble.mjs). Ne jamais créer un second parseur. - Capacités & robustesse — déclarer
suggest / live / channelMeta / channelContentdans le bloc capacités deregistry.mjs; feature-flagFF_<ID>automatique viafeature-flags.mjs; dégradation toujours200 + available: false+ état « pas de données » dans l'UI, jamais une erreur ; contrat d'erreur net :404= introuvable prouvé (HTML reçu),503= blocage/rate-limit. - Shorts — requêtes dédiées dans
SHORTS_QUERIES+ filtre de durée (serveur si l'API le supporte, client sinon), branche cataloguetype=shorts; tout chemin d'alimentation du flux passe pardedup()(sinon feed vide en silence) ; cadre vidéo avec attributdata-video-provider(contour 1 px). - UI — cartes,
/watchet/shortsréutilisent les composants existants, aucun composant par fournisseur ; texte FR hardcodé sur/watchet/shorts(pas de clés i18n), ailleurs| t: 'repli FR'déclaré des deux côtés desrc/services/i18n.service.ts(dictionnaire EN et dictionnaire FR). - Tests —
server/tests/<p>*.test.mjshors ligne (fixtures HTML/JSON, zéro spawn yt-dlp, zéro appel réseau) + scripttest:<p>danspackage.json; passer aussitest:contractettest:shapes. - Livraison — tests verts avant commit ; matrice fournisseurs + ce roadmap mis à jour (✅ uniquement une fois câblé de bout en bout) ; vérif navigateur desktop 1280×900 ET mobile 390×844 (dont
/watchet/shorts, FAB menu visible sur tous les états) ; instance locale rebuildée, puis image proddocker/build-img.ps1+ déploiementdocker/deploy-img.ps1.
🔒 Notes de sécurité
- ❌ Ne jamais versionner de secrets réels
- 🔑 Utiliser
.envhors VCS + restrictions par referer côté fournisseurs - 🚧 Limiter l’exposition publique de l’API, activer rate-limit (déjà en place)
- 🧪 Ajouter des tests e2e/units pour les zones critiques (auth, playlists, downloads)
🤝 Contribuer
Issues & PR bienvenues ! Proposez des connecteurs, des règles de parsing plus robustes ou des idées d’UX. Astuce : ouvrez une PR “Draft” tôt pour discussion/feedback.
📜 Licence
MIT (voir LICENSE)
💡 Tip produit : gardez l’UX simple — Thèmes fixes sous le header, Shorts séparés, CTA clairs (“View All”, “Add to playlist”), et utilisez les états vides (empty states) sur History/Liked/Playlists pour guider l’utilisateur.
🧩 Ajouter un provider en 5 minutes
Complète le plan général « 🆕 Nouveaux fournisseurs » du roadmap : cette section détaille le côté recherche / filtres / deep-link, le plan du roadmap couvre IDs, URLs, métadonnées, shorts, capacités et livraison.
- Front — Registry
- Éditez
src/app/core/providers/provider-registry.tset ajoutez une entréeProviderSpecavec:id(ex."yt"),displayName,shortLabel,icon,colorClasssupports(search/shorts/live/playlists)buildSearchUrl(q)(facultatif côté front)
- Back — Handler
- Créez un fichier
server/providers/<provider>.mjsqui exportedefaultavec:id,labelasync search(q, { limit, page })→Promise<Suggestion[]>
async search(q, { limit, page, sort, filters })→Promise<Suggestion[]>(filters={ type, duration, period, sort }normalisé parserver/search-filters.mjs; ignoré par défaut, affiné en post‑traitement si besoin).- Enregistrez-le dans
server/providers/registry.mjspour être éligible au fan‑out/api/search.
- API — Recherche multi‑providers
- L’endpoint
GET /api/search?q=...&providers=yt,dm,ru&limit=...valide les providers et déclenche les recherches en parallèle (timeouts), puis retourne:
{
"q": "documentary",
"providers": ["yt","dm","ru"],
"groups": {
"yt": [{ "title": "...", "id": "...", "duration": 192, "isShort": false }],
"dm": [],
"ru": [{ "title": "...", "id": "...", "isShort": true }]
}
}
- UI — Panneau de filtres
- Le
SearchBoxComponent(standalone) rend le champ + le bouton Filtres (SearchFilterPanelComponent: sources, type, période, durée, tri, recherches récentes) et les pastilles des filtres actifs. - Le composant émet
(submitted)avec{ q, providers, filters }et(filtersChange)avec les filtres seuls ;SearchService(RxJS state) portefilters$et l’envoie à chaque adapter. - Le modèle de filtres est partagé :
src/app/search/filters.ts(front) etserver/search-filters.mjs(API) implémentent les mêmes règles.
- Deep‑link
- Les URLs du type
?q=…&providers=yt,rurelancent la même recherche. Assurez-vous de propager le paramètreproviderslors des navigations.
- Tests
- Unit (
npm run test:search) : parsing@yt, clavier/focus du panneau de suggestions, opérateurslive:, brouillon + apply du panneau de filtres, composition et fan-out deSearchService.npm run test:filters` : normalisation/bornes/post-filtrage/mapping providers (offline). - e2e (
npm run test:search-e2e) :providers=yt,dm→ réponse ciblée sur [yt,dm]; deep-linkproviders=pt; fallback registry complet; préférence persistée; route SPA.
Astuce: l’ajout d’un provider ne nécessite pas de modifier les composants — il suffit d’ajouter une entrée dans le registry front + un handler API.