# 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, **astuces `daemon.json`** pour 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/playlists` et `/helix/playlists/{id}/videos` répondent tous **404** (contrôle `/helix/users` → 400 = route existante). * **Odysee N/A** — proxy `na-backend` en lecture seule (méthodes d’écriture : « authentication token missing ») ; **Rumble N/A** — aucune API publique. Notes sur les colonnes : * **Shorts** : le flux `/#/shorts` tourne 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 natif `rumble.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é par `channelId`). **Dailymotion** : lecture possible mais `live:false` cô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épond `503 { error: "rumble_cloudflare_challenge" }` (« bloqué temporairement ») au lieu d’un résultat vide : Rumble peut donc être temporairement absent selon le réseau — voir `server/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 (InnerTube `upload_date`/`type`/`duration`/`features`, Data API `type`/`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 `@yt` dans 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=live` relance la recherche filtrée — partageable * **Fallback préférence** : URL sans `providers` → préférence `defaultProviders` de l’utilisateur → provider actif * **Accessibilité** : focus trap dans le panneau, Esc pour fermer, aria-combobox + `aria-activedescendant` sur le champ, focus restauré à la fermeture * **Panneau de suggestions** : sous l’input, ligne « Rechercher `` » puis suggestions (recherches récentes 🕘 + groupes par provider `YT/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=0` pour désactiver) * `GET /api/trending?provider=yt&limit=…` — tendances YT sans clé * `GET /healthz` (alias `/api/healthz`) — mode YT, binaire yt-dlp `binOk`, cache, métriques quota/jour, clés * `GET /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 ```bash 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` — service `newtube` (image, ports, env, volumes, restart) * `.env.example` — modèle d’environnement * `maj.sh` — **pull** image + **restart** stack * `init.sh` — init des dossiers/volumes & `.env` * `docker/` * `Dockerfile` — build Node/Express (sert `dist/` + API) * `scripts/env-dump.sh` — génère `assets/config.js` depuis l’env (option NGINX) * `config/nginx.conf` — exemple (si variante NGINX) * `server/` * `index.mjs` — routes API, statiques `dist/`, downloads * `db.mjs` — SQLite + migrations légères * `tests/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é) ```bash 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](http://localhost:8080) (mappage `8080:4000`) ### Option B — Dev local ```bash 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`) ```bash chmod +x docker-compose/maj.sh ./docker-compose/maj.sh ``` Ce script fait : 1. `docker image pull /newtube-angular:latest` 2. `docker compose down` 3. `docker 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`. ```bash 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_SECRET` * `ACCESS_TTL_MIN`, `REFRESH_TTL_DAYS`, `REMEMBER_TTL_DAYS` * `YT_CACHE_TTL_MS` * **Clés API** (toutes **côté serveur**, aucune n'est envoyée au navigateur) : `GEMINI_API_KEY`, `YOUTUBE_API_KEY` ou `YOUTUBE_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éfaut `localhost:4200,4000,3000` — y ajouter l'URL publique de l'app pour des clients tiers), `METRICS_TOKEN` (si défini, `/metrics` exige `Authorization: Bearer …`), `DETAILS_RATE_LIMIT` (appels `/api/details` par minute, défaut 60) * `NEWTUBE_DB_FILE` (chemin SQLite alternatif) > Voir `docker-compose/.env.example` pour un point de départ. --- ## 🩺 Troubleshooting (rapide) * 🧾 **Certif/registre** : `x509… unknown authority` → configurez `daemon.json` (ci-dessus) puis `systemctl restart docker` * 🔌 **Port 8080 occupé** : changez le mappage dans `docker-compose.yml` * 🗄️ **Permissions volumes** : vérifiez que `DIR_NEWTUBE` existe et est accessible par Docker * 🧩 **Variables manquantes** : complétez `.env` (clés API, `JWT_SECRET`, etc.) * 🌐 **CORS en dev** : utilisez le `proxy.conf.json` et `npm 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-later` avec 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, états `hover:`/`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) ; marqueur `border-l-4` des titres de page sur l'accent du thème ; cartes vidéo harmonisées (`text-sm` / `font-semibold` / méta `text-xs` / rayon `rounded-lg`, pastille de durée sans `font-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érateurs `live:`/`today:`, deep-links `?providers=…&type=…&period=…`, préférence `defaultProviders` persisté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-groups` CRUD + affectation de membres), tables SQLite `subscriptions` / 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 `/#/shorts` sur les 6 fournisseurs (catalogue `/api/search?type=shorts` sinon repli recherche, dédup centralisée), durées : filtres côté serveur (YouTube `videoDuration=short`, Dailymotion `shorter_than`, PeerTube `durationMax`) + 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-motion` respecté) ; 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** — `resolve` de l'API LBRY (**~0,2 s** contre **~45 s** mesurées avec l'extracteur `lbry` de yt-dlp, qui finissait sur « No video formats found! ») : titre, description, vignette, durée et date arrivent enfin sur `/watch` ; `views` volontairement omis (LBRY n'expose aucun compteur pour un claim isolé). Au passage : **cache `/api/details` 6 h** (LRU 500, `DETAILS_CACHE_TTL_MS`) et `views`/`duration` ne 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être `DOWNLOAD_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-dlp `subtitles`/`automatic_captions`, parsing `json3`/`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.js` pinné, chaîne `innertube → scrape yt-dlp → API officielle` (`YT_SEARCH_MODE`, défaut `innertube-first`), **pagination illimitée** via continuations (scroll infini, `page=2,3…`), **vidéos connexes** watch-next dans `GET /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, binaire `binOk`, 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, items `type:'live'` + spectateurs), Rumble via la page SSR `rumble.com/browse/live` (route `/api/rumble/live`, items typés live ; onglet chaîne filtré par `channelId` — 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, format `newtube-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/metrics` rendus 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 : `healthz` servi 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` (`_` → `` → défaut, valeurs invalides ignorées, jamais de TTL nul) branché sur les caches **details** (`DETAILS_CACHE_TTL_MS_

`) et **transcripts** (`TRANSCRIPT_CACHE_TTL_

`) en plus de la recherche (`SEARCH_CACHE_TTL_MS_

`). 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.json` dans 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é par `docker/deploy-img.sh`). Clés i18n déclarées des deux côtés (FR + EN) * ✅ **Observabilité** — `GET /healthz` (mode `YT_SEARCH_MODE`, binaire yt-dlp, cache, quota du jour, clés masquées — testé par `test:api`), `GET /api/providers/metrics` et la **page Admin** qui les rend, **journal JSON structuré en prod** (une ligne par requête : `ts`, `reqId`, `method`, `route`, `status`, `ms`, adossé au header `X-Request-Id` renvoyé sur chaque réponse) et **`GET /metrics`** — exposition Prometheus sans dépendance (`http_requests_total` par route et code, somme/nombre de durées, démarrage + mémoire du processus ; `METRICS_TOKEN` verrouille l'accès si défini) * ✅ **API production-ready (P0 + P1)** — **plus aucun secret servi au navigateur** : `/assets/config.local.js` est généré par le serveur **avant** les montages statiques (il l'emporte donc sur le fichier local) et ne contient plus `YOUTUBE_API_KEY(S)`, le fichier local n'est plus copié dans l'image Docker ni embarqué dans `dist`, et les appels YouTube passent par `/api/yt` uniquement (clé, rotation, quota et cache côté serveur — suppression des appels directs à googleapis et des gardes « pas de clé ⇒ écran vide ») ; **CORS piloté par `API_ALLOWED_ORIGINS`** (CSV) + méthode `PATCH` (requis par `/user/preferences`) ; **clés d'API longue durée** : `GET/POST /api/keys` + `DELETE /api/keys/:id`, jeton `ntk_…` affiché une seule fois, stocké en SHA-256 avec préfixe affichable, header `X-API-Key` accepté par les **deux** middlewares d'auth (toutes les routes protégées deviennent scriptables), `last_used_at` renseigné à chaque usage ; **`X-Request-Id`** en réponse sur chaque requête ; **`/api/details` rate-limité** (`DETAILS_RATE_LIMIT`, 60/min, réponse JSON — `/api/transcript` avait déjà le sien) ; **version unique `package.json`** (menu du compte + `info.version` de 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écroissant `1080p → 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) : 1. **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. 2. **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`. 3. **SoundCloud** — recherche via yt-dlp, lecture via le widget iframe officiel ; audio seul → carte avec pastille « audio », **hors flux Shorts**. 4. **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) : 1. **IDs & URLs** — id court dans `src/app/shared/providers/provider-ids.ts` + couleur du badge fournisseur ; brancher `providerUrlFrom()` (`server/index.mjs`) — jamais d'URL provider écrite à la main ; transmettre tels quels `instance` / `slug` / `sourceUrl`. 2. **Métadonnées** — privilégier l'API native du provider quand elle existe (modèle Odysee : `resolve` LBRY, ~0,2 s), sinon tuyau yt-dlp `youtubedl(url, {dumpSingleJson, skipDownload})` qui alimente gratuitement `/api/details/:provider/:videoId` et `/api/download/:provider/:videoId/formats`. 3. **Recherche** — `server/providers/

.mjs` = **un seul cœur** fetch + parse ; enregistrer `search()` dans `registry.mjs` (fan-out), les chaînes dans `channel-registry.mjs` ; rate-limit + cache dans un routeur fin dédié si besoin (modèle `server/rumble.mjs`). Ne jamais créer un second parseur. 4. **Capacités & robustesse** — déclarer `suggest / live / channelMeta / channelContent` dans le bloc capacités de `registry.mjs` ; feature-flag `FF_` automatique via `feature-flags.mjs` ; dégradation **toujours** `200 + 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. 5. **Shorts** — requêtes dédiées dans `SHORTS_QUERIES` + filtre de durée (serveur si l'API le supporte, client sinon), branche catalogue `type=shorts` ; tout chemin d'alimentation du flux passe par `dedup()` (sinon feed vide en silence) ; cadre vidéo avec attribut `data-video-provider` (contour 1 px). 6. **UI** — cartes, `/watch` et `/shorts` réutilisent les composants existants, **aucun composant par fournisseur** ; texte FR hardcodé sur `/watch` et `/shorts` (pas de clés i18n), ailleurs `| t: 'repli FR'` déclaré des **deux côtés** de `src/services/i18n.service.ts` (dictionnaire EN et dictionnaire FR). 7. **Tests** — `server/tests/

*.test.mjs` hors ligne (fixtures HTML/JSON, zéro spawn yt-dlp, zéro appel réseau) + script `test:

` dans `package.json` ; passer aussi `test:contract` et `test:shapes`. 8. **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 `/watch` et `/shorts`, FAB menu visible sur tous les états) ; instance locale rebuildée, puis image prod `docker/build-img.ps1` + déploiement `docker/deploy-img.ps1`. --- ## 🔒 Notes de sécurité * ❌ Ne **jamais** versionner de secrets réels * 🔑 Utiliser `.env` hors 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](#️-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. 1) Front — Registry - Éditez `src/app/core/providers/provider-registry.ts` et ajoutez une entrée `ProviderSpec` avec: - `id` (ex. `"yt"`), `displayName`, `shortLabel`, `icon`, `colorClass` - `supports` (search/shorts/live/playlists) - `buildSearchUrl(q)` (facultatif côté front) 2) Back — Handler - Créez un fichier `server/providers/.mjs` qui exporte `default` avec: - `id`, `label` - `async search(q, { limit, page })` → `Promise` - `async search(q, { limit, page, sort, filters })` → `Promise` (`filters` = `{ type, duration, period, sort }` normalisé par `server/search-filters.mjs` ; ignoré par défaut, affiné en post‑traitement si besoin). - Enregistrez-le dans `server/providers/registry.mjs` pour être éligible au fan‑out `/api/search`. 3) 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: ```json { "q": "documentary", "providers": ["yt","dm","ru"], "groups": { "yt": [{ "title": "...", "id": "...", "duration": 192, "isShort": false }], "dm": [], "ru": [{ "title": "...", "id": "...", "isShort": true }] } } ``` 4) 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) porte `filters$` et l’envoie à chaque adapter. - Le modèle de filtres est partagé : `src/app/search/filters.ts` (front) et `server/search-filters.mjs` (API) implémentent les mêmes règles. 5) Deep‑link - Les URLs du type `?q=…&providers=yt,ru` relancent la même recherche. Assurez-vous de propager le paramètre `providers` lors des navigations. 6) Tests - Unit (`npm run test:search`) : parsing `@yt`, clavier/focus du panneau de suggestions, opérateurs `live:`, brouillon + apply du panneau de filtres, composition et fan-out de `SearchService. `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-link `providers=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.