350 lines
20 KiB
Markdown
350 lines
20 KiB
Markdown
# 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.
|
||
|
||
* **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 `<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
|
||
* ⏰ **À 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)
|
||
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)
|
||
* ✅ 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)
|
||
* ✅ 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)
|
||
* ✅ **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)
|
||
* ⏳ **Tags** & recherche par tags
|
||
* ⏳ **Page “Shorts”** unifiée (tous fournisseurs) + badges de durée
|
||
* ✅ **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, 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, commentaires → Watch) 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
|
||
* ✅ **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.
|