Files
NewTube/README.md
T
bruno f1f673040e
CI / build-and-test (push) Successful in 14m10s
feat(search): panneau de filtres unifié, clavier/focus du typeahead
Barre de recherche
- Panneau de filtres unifié (remplace le ProviderPicker) : sources, type
  (vidéos/shorts/direct/chaînes), période (heure/jour/semaine/mois/année),
  durée (<4 / 4-20 / >20 min) et tri. Pastilles + roving tabindex
  (←/→ dans un groupe, ↑/↓ entre groupes), focus trap, Échap, Ctrl/⌘+Maj+F,
  recherches récentes, « mémoriser par défaut », « Réinitialiser ».
- Pastilles des filtres actifs sous la barre (retrait en un clic) + compteur.
- Typeahead : un seul keydown (fin du double Enter), ↑/↓ bouclants,
  Home/End, PageUp/PageDown, Entrée valide la ligne surlignée (sinon
  recherche brute), Tab complète sans chercher, Échap ferme puis vide,
  ligne « Rechercher <q> », défilement de l'option active, aria à jour.
- Focus : le panneau se referme dès que le focus quitte la barre
  (focusout + relatedTarget + activeElement), pointerdown neutralisé sur les
  lignes pour garder le focus dans l'input (plus de scintillement).
- Opérateurs en clair : `linux live:`, `tuto today: long:` deviennent des
  filtres et sont retirés de la requête (autocomplétion après le `:`).

Filtres côté serveur
- Modèle partagé : src/app/search/filters.ts + server/search-filters.mjs.
- /api/search?type=&duration=&period=&sort= : InnerTube (upload_date, type,
  duration, features), Data API v3 (type, videoDuration, publishedAfter),
  yt-dlp --dateafter ; post-filtrage pour les providers sans filtre natif.
  Un champ manquant ne fait jamais disparaître un résultat.
- Filtres dans l'URL (partageables) + adapters + SearchService (clé de cache).
- Correction : la recherche Shorts renvoyait 0 (ShortsLockupView/GridShelfView
  non mappés, id/titre dans l'endpoint) -> mappage + shelf + vignette/vues.

Tests / docs
- 23 tests unitaires (clavier, focus, opérateurs, panneau de filtres),
  server/tests/search-filters.test.mjs (npm run test:filters) + CI,
  scénarios e2e des filtres, README/api/README/todo/MCP à jour.
2026-09-28 17:42:19 -04:00

344 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 🟣 | ✅ | ✅ | N/A | ✅ | ⏳ |
| PeerTube 🟢 (multi-instances) | ✅ | ✅ | ⏳ | ⏳ | ⏳ |
| Odysee 🟡 | ✅ | ✅ | ⏳ | ⏳ | ⏳ |
| Rumble 🟠 | ✅ | ✅ | ⏳ | ⏳ | ⏳ |
\* Playlists = intégration locale NewTube (création/gestion); la synchro native dépend de l’API publique de chaque fournisseur.
---
## 🔎 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 `<q>` » 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)
<!-- TODO: add docs/search-ux.gif (capture du panneau de filtres, du panneau de suggestions et du deep-link) -->
### 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
* 🕘 **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)
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 <registre>/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** : `GEMINI_API_KEY`, `YOUTUBE_API_KEY` ou `YOUTUBE_API_KEYS` (CSV), `VIMEO_ACCESS_TOKEN`, `TWITCH_CLIENT_ID`, `TWITCH_CLIENT_SECRET`
* `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
* ✅ Dist statique + API Node/Express
* ✅ Playlists **publiques/privées** (tests de visibilité)
* ✅ Barre de recherche **Liked videos** (filtrage serveur)
* ✅ PeerTube **multi-instances** (activer/désactiver, set active)
* ✅ Thème (système / dark / light / blue / black)
* ✅ 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)
* ✅ **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)
* ✅ Navigation par thèmes (Trending, Live, Gaming, News, Finance, Tech, Science, Health, Music, Podcasts, Movies/TV, Education, Travel, Food, DIY, Auto…)
* ⏳ **Abonnements** (routes + DB)
* ⏳ **Tags** & recherche par tags
* ⏳ **Page “Shorts”** unifiée (tous fournisseurs) + badges de durée
* ✅ **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)
* ⏳ **Import/Export** playlists (JSON / OPML-like)
* ✅ **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)
* ⏳ **PWA** (installable, offline cache des métadonnées)
* ⏳ **Chromecast / AirPlay**
* ⏳ Mode “TV”
* ⏳ **Observabilité** : healthcheck `/healthz`, métriques, page **Admin** (clés OK/KO, logs, versions)
* ✅ **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`
* ⏳ **Cache serveur** configurable (TTL par provider)
* ⏳ **Qualité vidéo** : sélecteur et “auto” intelligent
* ⏳ **Traduction UI** (i18n) élargie
* ⏳ **Theming avancé** (polices, densité, accents)
---
## 🔒 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
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/<provider>.mjs` qui exporte `default` avec:
- `id`, `label`
- `async search(q, { limit, page })` → `Promise<Suggestion[]>`
- `async search(q, { limit, page, sort, filters })` → `Promise<Suggestion[]>` (`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.