152 Commits
Author SHA1 Message Date
bruno bb2cd2f780 feat(shorts): préchauffage en arrière-plan du flux « Pour toi »
CI / build-and-test (push) Successful in 15m4s
Cliquer sur « Pour toi » lançait un fan-out d'environ 22 appels (4 sujets ×
5 fournisseurs + Twitch) et attendait le plus lent : 4 à 8 s mesuré, 13 s à
froid.

- Dès la première vidéo affichée, le lot est préparé EN ARRIÈRE-PLAN
  (`warmForYou`) : mêmes tuyaux (`topics$()` puis `forYouFeed$()`), aucune
  écriture de signal — l'écran ne bouge pas. Réchauffage périodique toutes
  les 5 min (onglet visible), lot réputé frais pendant 5 min.
- `toggleForYou` consomme le lot : prêt → application immédiate ; en cours →
  le flux partagé (shareReplay) est rejoint au lieu de déclencher un second
  fan-out ; périmé ou absent → chemin réseau comme avant.
- Refactor : `loadForYou` = `topics$()` + `fetchForYou`, le fan-out devient
  `forYouFeed$()` (pur, retourne le lot) et un seul `applyForYouFeed()` écrit
  les signaux, `seenIds` compris.
- Journal `[forYou]` en console : préchauffage lancé, lot préparé, branche
  prise au clic — pour diagnostiquer un clic lent sans rejouer la scénario.

Mesures (Playwright, localhost:4200) : clic avec lot prêt 34-74 ms
(4,3 à 7,6 s avant), clic pendant la préparation 1,3-1,8 s avec UN seul
fan-out, UI intacte pendant la préparation. Harnais :
$LOCALAPPDATA/Temp/pw/foryou-warm-check.mjs (7 checks).
2026-10-03 11:42:12 -04:00
bruno 80ac48da20 fix(serveur): index.html ET index.css jamais mis en cache
CI / build-and-test (push) Successful in 15m0s
/index.css n'est pas fingerprinté (index.html le référence en dur) mais
héritait de `maxAge: '1h'` : après un redéploiement F5 continuait de servir
l'ANCIEN CSS jusqu'à une heure — correctif présent sur le serveur, invisible
au navigateur (« je ne vois toujours pas la ligne fournisseur »). Il rejoint
index.html dans le `setHeaders` no-cache (ETag → 304, coût nul).

Test : `index.html et index.css : jamais mis en cache` dans
server/tests/api_coverage.test.mjs (29 pass / 0 fail).
2026-10-03 10:19:51 -04:00
bruno 64821a8538 fix(shorts): tiroir ⓘ sous le header, coins arrondis, ligne fournisseur visible
CI / build-and-test (push) Successful in 15m1s
- Mobile : le panneau d'informations démarre à `var(--header-offset)` (bas du
  header fixe z-50) au lieu de `top: 0` — ses 56 premiers px passaient
  DERRIÈRE le header (bouton Fermer compris). Même motif que le drawer latéral
  de app.component.html ; `md:static` neutralise ce top au bureau.
- Le cadre vidéo est arrondi partout (`rounded-2xl`, plus `rounded-none` sous
  768 px).
- La ligne de couleur du fournisseur était invisible sur /shorts : l'iframe
  positionnée peint par-dessus l'outline du cadre. Redessinée en overlay
  `.shorts-frame[data-video-provider]::after` (bord 1 px `var(--prov)`,
  `border-radius: inherit`, z-index 5 sous le chrome, pointer-events: none).

Vérifié au navigateur (harnais Playwright local) en mobile 390x844 ET desktop
1280x900 : gap tiroir/header, coins, couleur calculée du ::after, et avant/après
sur l'instance de test locale.
2026-10-03 09:56:22 -04:00
bruno f1f6138eb9 feat(docs): documentation web des API (Swagger UI) liée depuis le menu
CI / build-and-test (push) Successful in 15m9s
Le lien « API » du menu ouvre /api/docs (et /proxy/api/docs) : Swagger UI
auto-hébergé (swagger-ui-dist, aucun CDN, thème sombre), spécification chargée
depuis `${base}/openapi.json`.

/openapi.json devient exhaustif :
- inventaire généré depuis l'arbre Express (collectApiRoutes) : `:param` →
  `{param}`, `*` → `{path}`, miroirs /proxy/* et métadonnées écartés, préfixe
  /api retiré (le document porte les serveurs /api et /proxy/api) ;
- `app.all()` réduit à GET — ses 35 méthodes HTTP (acl, propfind, m-search…)
  entraient dans le document et figaient Swagger au bout de 2 groupes ;
- authentification marquée automatiquement (middlewares de route ET de préfixe,
  ex. `r.use('/download', …)`), route publique laissée publique ;
- tags + descriptions de groupe, résumés FR pour les routes courantes ;
- 3 schémas d'auth : bearerAuth (JWT), apiKeyAuth (X-API-Key), cookieAuth ;
- description : méthodes d'auth avec exemples, codes d'erreur et seuils de
  débit, section « Serveur MCP » avec snippet `mcpServers` et la liste des
  outils LUE dans mcp/server.mjs au démarrage (aucune liste à maintenir).

Dockerfile : `mcp/` copié dans l'image (cette lecture doit exister en prod).
package.json → 1.0.61 (tag de cette livraison).

Tests : api_coverage +3 cas (page /docs + assets, exhaustivité/méthodes/tags/
sécurité, 3 schémas + outils MCP dans la description) → 51/51 verts.
Vérifs instance locale : lien menu = /proxy/api/docs, 114 opérations rendues
sur 12 groupes, bouton Authorize + section MCP visibles, zéro erreur console.
2026-10-03 00:33:46 -04:00
bruno 0f11b43e0e chore(version): 1.0.60 — alignement sur le tag que publiera cette livraison
CI / build-and-test (push) Successful in 15m7s
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.
2026-10-02 23:29:41 -04:00
bruno 41a28dfccc feat(api): production-ready P0/P1 — secrets hors navigateur, clés d'API, métriques
CI / build-and-test (push) Successful in 14m57s
P0
- Aucun secret servi au navigateur : /assets/config.local.js est généré par le
  serveur AVANT les montages statiques (il l'emporte 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 (angular ignore config.local.js) ;
  youtube-api.service n'appelle plus googleapis directement (fetchYouTube =
  /api/yt -> /proxy/api/yt), gardes « pas de clé => écran vide », rotation et
  carte de bans côté client supprimées ; readiness YouTube via /healthz
  (youtube.keys.count) ; messages d'erreur orientés configuration serveur.
- CORS : origines pilotées par API_ALLOWED_ORIGINS (CSV), méthode PATCH
  ajoutée (requis par /user/preferences), header X-API-Key accepté.
- Clés d'API longue durée : table api_keys (empreinte SHA-256 + préfixe
  affichable), routes GET/POST /api/keys et DELETE /api/keys/:id, jeton
  ntk_… affiché une seule fois, last_used_at à chaque usage ; X-API-Key
  accepté par authMiddleware ET authMiddlewareCookieAware.

P1
- X-Request-Id renvoyé sur chaque réponse + journal JSON structuré en prod
  (ts, reqId, method, route, status, ms).
- GET /metrics : exposition Prometheus sans dépendance (http_requests_total
  par route/code, somme+nombre de durées, uptime/mémoire ; METRICS_TOKEN
  verrouille l'accès si défini).
- Rate-limit sur /api/details (DETAILS_RATE_LIMIT, 60/min, réponse JSON) —
  /transcript avait déjà le sien.
- Version unique package.json : menu du compte, info.version de l'OpenAPI
  (+ schéma apiKeyAuth et chemins /keys / /metrics documentés).

Tests : api_coverage +7 cas « production-ready » (sentinel de fuite de clé,
X-Request-Id, /metrics, CORS PATCH, contrat/version, cycle complet des clés
d'API), suggest, transcript, flags, filters, kind — verts. Build OK.
Vérifs instance locale : config servie sans secret, /api/yt 200 avec la clé
serveur, /metrics alimenté, menu 1.0.59 et 40 cartes rendues sans clé côté
client.
2026-10-02 23:24:50 -04:00
bruno 6add43e730 feat(ui): menu du compte refondu (icône + titre + sous-titre) et version officielle
CI / build-and-test (push) Successful in 15m4s
Refonte du panneau du bouton avatar (haut à droite du header) : lignes
« icône + titre gras + sous-titre gris » pour Thème (thème appliqué, pastilles
dépliées au clic), Administration, API (openapi.json), Préférences, Guide
d'utilisation, À propos (version + tagline + code source) et Sessions ;
Déconnexion isolée après séparateur et pied de page « Version ».

Ajout de src/app/version.ts (APP_VERSION, alignée sur le tag semver du
registre : 1.0.59 pour cette livraison) affichée dans le menu.

Clés i18n FR + EN ajoutées des deux côtés, ligne README « Menu du compte
refondu » dans le roadmap.
2026-10-02 21:34:05 -04:00
bruno 326455adfe feat: lives PeerTube/Rumble, page Admin, TTL par provider, import/export playlists
CI / build-and-test (push) Successful in 14m56s
#1 Live
- PeerTube: API native ?isLive=true (recherche sepiasearch + videos de chaine),
  mapPeertubeItem exporte la regle live (type/kind/isLive, viewers, jamais de
  duree), caps pt live:true, themes « En direct » branches pt/ru
- Rumble: mode 'live' de scrapeRumbleList (rumble.com/browse/live, SSR), items
  type:live + extraction channelId (/c/<slug>), route GET /api/rumble/live,
  onglet chaine live filtre par channelId (plafond page 1 = 50 directs)
- Odysee: N/A prouve (proxy na-backend sous jeton, probe 2026-10), caps inchangees
- Tests: npm run test:live (25 assertions), assertion adapter_contract mise a
  jour (seuls tw/pt/ru declarent live)

#2 Playlists natives
- Twitch: NON FAISABLE — probe avec token app valide: GET/POST /helix/playlists
  et /helix/playlists/{id}/videos -> 404 (controle /helix/users -> 400).
  Matrice README: Twitch/Odysee/Rumble en N/A avec preuves

#3 Quick wins
- Page Administration /#/admin (entree sidebar Informations + i18n nav.admin
  FR/EN): healthz + providers/metrics rendus (statut, yt-dlp, clés actives/
  bannies, anti-ban, cache par provider, compteurs 1h, quota du jour) ;
  healthz sert aussi sous /proxy/api/healthz
- TTL par provider: server/env-ttl.mjs (<BASE>_<P> -> <BASE> -> defaut, jamais
  nul), branche sur les caches details et transcripts ; suggest reste global
  (cle agregee multi-providers) ; tests npm run test:cache
- Import/Export playlists JSON: GET /playlists/export (newtube-playlists-v1,
  avant /playlists/:id) + POST /playlists/import (dedoublonnage par titre),
  boutons [Exporter] [Importer] sur la page listes de lecture

README: matrice fournisseurs (Live pt/ru OK, od N/A, playlists N/A prouves),
roadmap deplace en ✅ (Live, import/export, Admin, TTL) + 🟡 mis a jour.
2026-10-02 20:25:08 -04:00
bruno 5f1bb78c3b docs(readme): roadmap restructuré (livré / partiel / à faire) + plan des nouveaux fournisseurs
CI / build-and-test (push) Successful in 15m0s
- Légende ✅/🟡/⏳ et section « Pas encore démarré » regroupant les vrais restes
- Abonnements et page Shorts unifiée passés en ✅ (vérifiés câblés: routes /api/subscriptions + groupes, flux /shorts 6 fournisseurs)
- Partiellement livré: tags, observabilité (healthz+metrics faits, Admin non), cache TTL, qualité vidéo, i18n
- Matrice fournisseurs: Shorts ✅ pour Dailymotion/PeerTube/Odysee/Rumble, Twitch = Clips, notes de lecture (filtres durée par provider, rumble natif)
- Nouvelle section candidats Kick/Vimeo/SoundCloud/TikTok + chemin d'implémentation standard en 8 étapes
- Dédoublonnage du doublon « Recherche unifiée », cross-référence avec « Ajouter un provider en 5 minutes »
2026-10-02 17:25:27 -04:00
bruno f99004f459 fix(ui): le texte blanc au-dessus d'une image/vidéo reste blanc
CI / build-and-test (push) Successful in 15m6s
text-white est thémé globalement (sinon mot-marque et boutons disparaissent
en thème clair) : les pastilles de durée, les badges LIVE/SHORT/CLIP et le
chrome Shorts en prenaient aussi — noir sur noir en thème clair. Exemption
scopée aux cadres [data-video-provider] et à .shorts-stage ; le panneau ⓘ
garde son scope plus spécifique (reste thémé).

Re-vérifié : 245 assertions UI (4 thèmes, desktop + mobile) + 13 couleurs de
contour au vert.
2026-10-02 16:37:11 -04:00
bruno d1acd1b5b5 style(ui): harmonisation des couleurs par thème + contour fournisseur 1 px
CI / build-and-test (push) Successful in 15m1s
Balayage de la couche d'overrides de index.css : les 75 jetons neutres
slate/zinc/gray qui n'étaient pas couverts (36 fichiers) sont mappés sur les
variables du thème — fonds, textes, bordures, anneaux, divide, placeholders,
dégradés + variants hover/focus/focus-within/disabled/group-hover, alpha via
color-mix. Barre de recherche, menus, panneaux, cartes et résultats de
recherche suivent donc le thème appliqué (dark/light/blue/black) au lieu de la
palette slate figée. Volontairement exclus : bg-white/text-black (boutons
inversés), pastilles « inversées » conditionnelles au thème clair, chrome
au-dessus des vidéos, rouges sémantiques.

Panneau d'informations ⓘ des Shorts re-colorié dans son propre scope
(fond/bordures/textes/pastilles d'action → panel/text/muted), les mêmes tokens
restant blancs ailleurs (rail, méta). Marqueur border-l-4 des titres de page
sur --accent.

Cartes vidéo harmonisées : titre text-sm font-semibold leading-snug
(résultats de recherche, chaîne, thèmes), méta text-xs, rayon rounded-lg sur les
cadres, pastille de durée sans font-mono, titre « Abonnements » avec le
marqueur d'accent.

Contour fournisseur 1 px autour de chaque cadre vidéo, à la couleur du premier
stop du dégradé de la pastille (table unique en CSS, ids longs + courts) :
résultats de recherche, grille chaîne, pages thèmes, watch (lecteur + « à
suivre »), shorts, historique, aimés/à regarder plus tard, détail de playlist.

Tests verts (search, filters, kind, section, highlight, shorts-catalog,
api 41/41) ; vérif UI Playwright desktop 1280x900 + mobile 390x844 sur 4
thèmes : 250 assertions + 13 couleurs de contour au vert.
2026-10-02 16:30:28 -04:00
bruno fe4a0f7f31 docs(readme): le panneau ⓘ des Shorts enrichit description/vues à la demande
CI / build-and-test (push) Successful in 14m58s
2026-10-02 15:01:40 -04:00
bruno ba7085c321 feat(shorts): le panneau ⓘ demande la description et les vues à la source
CI / build-and-test (push) Canceled after 0s
Le flux Shorts ne porte pas de description (et pas toujours de vues) : le
panneau affichait « le fournisseur ne transmet pas de description » alors que
`/api/details` la connaît. Il l'interroge maintenant à l'OUVERTURE, une fois
par vidéo (chargement squelette, échec silencieux + retentative au clic
suivant), et le serveur sert le tout depuis son cache 6 h.

- `detailsEnriched()` + `detailsLoading()` : description et vues réelles,
  `panelDescription()`/`panelMeta()` préfèrent le flux puis complètent.
- Paramètres par fournisseur réutilisés (`instance` PeerTube via
  `peerTubeHostOf`, `slug` Odysee via `odyseeSlugOf`) : mêmes règles que le
  reste du composant, pas de second jeu de conventions.
- Rafraîchi si le panneau reste ouvert pendant un changement de short.
2026-10-02 14:55:39 -04:00
bruno 7aea8e2c7b feat(shorts): bouton « Enregistrer » (à regarder plus tard) + panneau ⓘ honnête
CI / build-and-test (push) Canceled after 0s
- Rail + menu FAB : « Enregistrer » branché sur la liste réelle
  « À regarder plus tard » (LikesService, tag `watch-later`, page
  /library/watch-later) — état hydraté par vidéo comme J'aime, désactivé
  hors connexion (la liste est serveur). C'est le bouton « Enregistrer » de la
  description du rail, en vrai plutôt qu'en décoration.
- Panneau ⓘ : plus de promesse de commentaires — l'app n'en a aucune source
  (le champ du modèle est un tableau vide sur /watch) ; le bloc renvoie
  explicitement vers la page complète (description, téléchargements, transcript).
2026-10-02 14:48:58 -04:00
bruno c45e04af42 perf(details): Odysee par l'API LBRY + cache des métadonnées (45 s → 0,2 s)
Mesuré avant : `/api/details/odysee/…` = 44,7 s puis « No video formats
found! » — la page Watch restait une minute sans titre, vues, description ni
vignette. L'extracteur `lbry` de yt-dlp n'est pas utilisable ici.

- Nouvelle branche Odysee dans /api/details : `resolve` de l'API LBRY
  (`server/providers/odysee.mjs` → `resolveOdyseeVideo`) = 229 ms mesurés,
  titre/description/vignette/durée/date/canal + avatar. Claim introuvable →
  404 `odysee_claim_not_found` (le client garde les données du flux), panne →
  502 `odysee_details_failed`. Jamais de repli yt-dlp (45 s pour échouer).
- `views` volontairement OMIS pour Odysee : ni LBRY ni lighthouse n'exposent
  de compteur pour un claim isolé (l'`effective_amount` est un montant LBC).
- Cache mémoire de /api/details : 6 h, LRU 500 (`DETAILS_CACHE_TTL_MS`,
  `DETAILS_CACHE_MAX_ENTRIES`) — métadonnées immuables, chaque /watch
  relançait un dump complet.
- `views`/`duration` ne sont plus émis quand ils valent 0 : un 0 écrasait la
  valeur du flux côté client (merge sur `typeof === 'number'`), d'où les
  « 0 vue » sur les fournisseurs muets — même doctrine que
  `parseRumbleViews` (« jamais de compteur à 0 »).
- Mapping pur `odyseeClaimToVideo` testé hors ligne : 21 assertions,
  `npm run test:odysee`.
2026-10-02 14:48:53 -04:00
bruno 6f6d7ddde1 docs(readme): roadmap — UI/UX des Shorts (rail d'actions, panneau d'infos, transition glissée)
CI / build-and-test (push) Canceled after 0s
2026-10-02 13:44:53 -04:00
bruno 99e01c0679 feat(shorts): UI/UX — rail d'actions à droite (flèches ↑/↓), panneau d'infos, glissement magnétique
CI / build-and-test (push) Canceled after 8s
- Supprime les flèches Préc./Suiv. aux bords gauche/droite (troubles
  d'affichage récurrents) : la navigation passe au rail vertical ancré au
  bord DROIT de la vidéo, centré au bureau et ancré au-dessus de la méta
  sur mobile, avec des flèches ↑/↓ plus grosses (44 px).
- Même rail : informations, j'aime, s'abonner (pastille = avatar), commentaires,
  partager, lecture auto. Toujours monté (comme le FAB), désactivé sans vidéo.
- Bouton ⓘ : panneau d'informations (titre, vues, engagement, chaîne,
  description, commentaires → Watch). Colonne à droite au bureau (la vidéo
  rétrécit, jamais masquée : --shorts-panel-w retiré du calcul de taille),
  tiroir plein écran sur mobile. Échap ferme.
- Transition de short : le doigt fait glisser le cadre (chrome solidaire),
  résistance aux extrémités, seuil de bascule 25 % de la hauteur, ressort
  sous le seuil, éjection + emboîtement au-delà (Web Animations, respecte
  prefers-reduced-motion). Aucun état de lecture modifié.
- Lecture auto : interrupteur réel (rail + menu), persisté, respecté par le
  minuteur et par la fin de vidéo YouTube.
- Tap sur un contrôle du rail = plus de play/pause parasite (garde
  isChromeTarget dans onTouchStart/onTouchEnd).
- FAB menu conservé (haut droit, déplaçable) ; son touchmove ne déplace plus
  le cadre en même temps.
2026-10-02 13:37:50 -04:00
bruno 6b32ede302 fix(shorts): flèches aux bords gauche/droite, toujours montées, visibles
CI / build-and-test (push) Successful in 14m55s
Retour utilisateur : les flèches Préc./Suiv. n'étaient toujours pas
visibles et la position attendue est précisée — une à GAUCHE et une à
DROITE de la vidéo, centrées verticalement.

- Flèches remises aux bords gauche/droite du cadre (top-1/2,
  -translate-y-1/2) avec contraste renforcé : ring-2 white/50, fond
  black/55 — lisibles sur toute image.
- Sorties de @if (current()) comme le menu : TOUJOURS montées, donc
  présentes même sur l'état de flux mort (désactivées) — on ne cherche
  plus où naviguer.
- Chaque changement de vidéo réveille les contrôles 2,5 s (poke sous
  matchMedia hover : sur tactile le chrome reste visible en permanence,
  un poke y aurait armé le minuteur et masqué le chrome — régression
  détectée par la recette mobile puis corrigée).
- Recette Playwright desktop+mobile : 17/17 (bords à 6 px, centrage
  vertical ±1 px, fondu repos 0.18, plein au mouvement, mobile plein,
  pas de chevauchement 2D avec la flèche retour, flèches + menu
  présents sur feed mort). Build + test:section + test:shorts-catalog
  verts avant commit.
2026-10-02 12:34:13 -04:00
bruno e44b545e01 feat(shorts): flèches en ligne avec le menu + fondu des contrôles au repos
CI / build-and-test (push) Successful in 14m54s
Demandes (page /shorts) : retrouver les flèches Préc./Suiv. là où se
trouve le menu, et des contrôles presque invisibles au repos pour
laisser la vidéo respirer (ton pro).

- Flèches déplacées du milieu des côtés vers la rangée HAUTE du cadre,
  centrées : même niveau que le FAB menu, sans chevaucher la flèche
  retour mobile (gauche) ni le FAB (droite). Le toast « Lien copié »
  passe en z-30 pour rester lisible sous la rangée.
- Fondu au repos : nouvelles règles index.css — flèches (.shorts-ctrl)
  et FAB à 0.18 d'opacité quand le chrome est au repos (2,5 s sans
  souris), pleins au moindre mouvement (chromeVisible) ou au survol
  direct (:hover), jamais atténués quand le panneau est ouvert
  (pm-fab-open). Sur tactile, le chrome ne se masque jamais (pas de
  mousemove : comportement voulu, mobile inchangé).
- Le minuteur d'immobilité est armé au chargement (pokeChrome sous
  matchMedia('(hover: hover)')) : sans un premier mousemove,
  chromeVisible restait true et le fondu ne s'armait jamais.
- mouseenter sur l'iframe : en entrant sur la vidéo, le document
  parent ne reçoit pas les mouvements internes à l'iframe — l'entrée
  réveille les contrôles.

Recette Playwright desktop+mobile, 14/14 : position haute centrée,
absence de chevauchements, repos 0.18 (flèches + FAB), retour plein
après mouvement réel, mobile plein en permanence. Build + test:section
verts avant commit.
2026-10-02 11:28:03 -04:00
bruno 3ab61821b2 fix(shorts): flux vide + menu absent + flèches invisibles (Rumble)
CI / build-and-test (push) Successful in 14m55s
Trois symptômes, deux racines — diagnostiqués au navigateur (Playwright
desktop + mobile) et hors navigateur (vraies réponses API + fonctions
front réelles importées via ts-node) :

1. Double dedup() dans handleShortsResponse : show() re-dédupliquait une
   liste déjà dédupliquée par son appelant → le second passage marquait
   tout comme « déjà vu » et posait items=[] en silence → écran
   « Aucun short à afficher » sans erreur ni requête suivante, dès que
   le catalogue type=shorts rendait moins de 3 items (aléa de la
   requête tirée au sort). Racine des écrans morts, présente depuis le
   commit 983c6cb.
2. Menu FAB et flèches Préc./Suiv. vivaient dans @if (current()) :
   absents de TOUS les états vides, donc impossibles à utiliser
   exactement quand le flux meurt (impossible de changer de
   fournisseur). Le menu est désormais toujours monté (z-30 au-dessus
   des placeholders), et la flèche désactivée passe en opacity 0.4
   (au 1er item, Préc. était quasi invisible à 0.3).
3. Format : Rumble n'expose AUCUNE dimension — vérifié : vignettes og:image
   toutes en crop fixe 16:9 (9/9 échantillons, 1280x720 et 480x270) et
   aucun width/height dans les pages vidéo → un filtre d'orientation
   par vignette est impossible, la classification reste durée-seule.
   En revanche le flux natif rumble.com/shorts (les vrais shorts
   officiels) devient la voie primaire : loadFeed va droit au chemin
   historique au lieu du catalogue qui ne rendait que des brives 16:9.

Tests verts avant commit : npm run build, test:section,
test:shorts-catalog, test:filters.
2026-10-02 10:26:58 -04:00
bruno 4e44d85717 fix(rumble): le lien de nav /videos n'est plus pris pour une vidéo
CI / build-and-test (push) Canceled after 0s
Le scan générique acceptait tout href /^\/v/ dont le regex d'id
matchait : `/videos` donnait `videoId: "videos"` — une fausse carte en
tête du flux /shorts (pré-validée puis retirée, mais visible le temps
du résolve). Probe live : 32/32 vrais ids rumble portent au moins un
chiffre, seul /videos n'en a pas. Garde « >=1 chiffre » dans l'id,
plafond documenté (un id sans chiffre ferait sauter sa carte).

Effet de bord heureux : la branche /video/123 (isLegacy) était morte
depuis toujours — le regex capturait « video » avant elle — elle
redevient accessible.

Tests : lien /videos ajouté au fixture du flux, 64 assertions vertes.
2026-10-02 08:57:41 -04:00
bruno 7fc1e1096d fix(rumble): un HTTP 404 net n'est pas un blocage Cloudflare
CI / build-and-test (push) Canceled after 0s
Probe live depuis le conteneur : rumble.com renvoie 404 (vraie page,
non-challenge) pour un id inexistant. fetchHtml les classait en échec
total, d'où deux régressions : 503 sur /video (les vidéos mortes ne
sont plus retirées des Shorts) et armement du cooldown 60 s pour toute
la maison au premier id bogus.

- fetchHtml retourne toute réponse nette non-challenge ; le statut est
  jugé par les appelants (tous dans le même fichier).
- rumbleFailureForStatus() exporté : 403 -> 503
  rumble_cloudflare_challenge (bandeau identique à la recherche),
  404/410 -> 404 rumble_not_found, 5xx -> 503 rumble_upstream_error.
  /video, /browse et /shorts passent par ce mapping.
- handler.search ne parse plus que du HTTP 200 : les pages 404 de
  Rumble embarquent des cartes de recommandation qui seraient
  autrement prises pour des résultats de recherche.
- Tests : 7 assertions sur le mapping (sans réseau), 64 au total.
2026-10-02 08:49:44 -04:00
bruno e48d76be57 refactor(rumble): un seul cœur de scraping, routeur fin, contrat 503 CF
CI / build-and-test (push) Successful in 14m57s
- server/providers/rumble.mjs devient l'unique implémentation : fetch
  (cookie jar __cf_bm + curl_cffi + cooldown global 60 s après un échec
  total), parsing liste/vidéo avec surcouche JSON-LD partagée, vues via
  parseRumbleViews partout (« 1,2K » n'est plus lu en 12 sur /api/rumble).
- server/rumble.mjs = couche HTTP mince : limiter 20/min, cache 60 s,
  5 routes qui délèguent au cœur ; /search passe par le registre de
  providers (cache SQLite, negative cache et channelRef partagés avec
  /api/search).
- Contrat d'erreur : un blocage Cloudflare est un 503
  rumble_cloudflare_challenge, plus jamais un 404 (qui faisait retirer
  les vidéos du flux Shorts lors d'un challenge) ; page reçue sans
  identité = 404 rumble_video_not_found.
- Code mort supprimé : scrapeRumbleVideo d'index.mjs (191 l., 3e
  implémentation jamais appelée), rumbleLimiter 10/min jamais monté,
  import cheerio devenu inutile. Net -238 lignes.
- Tests : parseur générique (vues, durées, ids /shorts/), contrat 503
  sans réseau via le cooldown, durées absentes -> undefined.
- Docs : API_MCP_GUIDE §5.5 (routes, rate-limit réel, erreurs) + note
  README « Rumble peut être temporairement absent selon le réseau ».
2026-10-02 08:36:48 -04:00
bruno 4d1d44f4ee feat(shorts): flèches Préc./Suivant SUR la vidéo + embed Rumble en 16:9
CI / build-and-test (push) Canceled after 0s
- Flèches : elles vivaient EN DEHORS du cadre (et se fadaient avec le
  chrome auto-hide). Elles sont désormais posées dans le cadre, à mi-hauteur
  sur les bords, toujours visibles (classe `shorts-chrome` retirée), sur
  mobile comme sur bureau, et le stopPropagation protège le geste tactile.
- Rumble : la plateforme est16:9 mais n'expose ni largeur ni hauteur →
  l'embed tombait dans un cadre 9:16 (étiré / réduit). `isWideEmbed()`
  (pur + testé) décide de la boîte : dimensions connues => orientation
  réelle, sinon repli « rumble = paysage » ; la CSS `.embed-wide` rend une
  iframe 16:9 centrée (434x244 dans un cadre 434x771, mesuré). Les vidéos
  verticales (dimensions connues) gardent le cadre pleine hauteur.
- État vide : un fournisseur sans résultat laissait un cadre NOIR muet
  (ni squelette, ni message) — il affiche désormais « Aucun short à afficher
  pour ce fournisseur » + Réessayer.

Tests : test:shorts-catalog (3 cas isWideEmbed), test:kind, test:section,
test:search verts ; build ng OK ; sondage Playwright local : flèches dans
le cadre + navigation suivant/précédent réelle, boîte embed-wide 16:9
centrée, short vertical inchangé (iframe pleine hauteur), état vide affiché.
2026-10-01 23:37:10 -04:00
bruno 474de0570c fix(shorts): embed PeerTube sur l'instance d'origine + FAB glissable + layout bureau
CI / build-and-test (push) Canceled after 0s
- PeerTube : l'adaptateur pt.ts reconstruisait l'item SANS `url` et le pont
  catalogue ne conservait l'URL que pour Odysee → /shorts reconstruisait
  l'embed sur l'instance ACTIVE → « Sorry this video does not exist » sur
  toute vidéo fédérée. `url` est désormais miroitée dans VideoItem (contrat
  v2), propagée par PtAdapter et gardée par catalogShortToVideo :
  peerTubeHostOf() dérive enfin le host d'origine. Vérifié de bout en bout :
  embed tube.blueben.net → 200 + vrai titre, cas de test dédié dans
  shorts-catalog.spec.
- FAB du menu : déplaçable au pointeur (seuil 6 px, position en % du cadre,
  persistée en localStorage). Le clic qui suit un glissement n'ouvre pas le
  menu, et le panneau s'ouvre TOUJOURS vers l'intérieur du cadre (classes
  pm-opens-left / pm-opens-right / pm-opens-up posées par applyAnchor) :
  sinon overflow:hidden le rognait dès qu'on déplaçait le bouton.
- Bureau : --shorts-chrome 9rem → 4rem (la vidéo passe de 691 à 771 px de
  haut en 1280x900, 389 → 434 de large) et menu compact au-delà de 768 px
  (FAB 36 → 30 px, panneau 233 → 202 px). Mobile inchangé (36 / 636 px).

Tests : test:shorts-catalog (nouveau cas PeerTube), test:kind,
test:section, test:search verts ; build ng OK ; sondage Playwright 19/19
sur l'instance locale (layout, glissement + restauration, embed PeerTube
200 sans erreur).
2026-10-01 23:09:28 -04:00
bruno 5f43fa1e59 fix(filtres): rail Sources sans libellé, Toutes/Aucune en icônes
CI / build-and-test (push) Successful in 14m54s
- le mot « Sources » quitte le panneau Filtres (le groupe reste repéré par
  aria-label sur chaque bascule) ;
- « Toutes » → double coche, « Aucune » → cercle barré : libellés déplacés
  dans aria-label/title pour le clavier et les lecteurs d'écran.

Vérif Playwright sur l'instance locale : plus de libellé « Sources », les
deux boutons en svg + aria-label, Aucune → bandeau « Aucune source
sélectionnée » + grille vide, réactivation d'une source → 48 résultats,
tests test:search verts, build ng OK.
2026-10-01 21:28:06 -04:00
bruno bfbcb5b49d fix(themes): espace sous la barre [Filtres] [Tri] avant la grille
CI / build-and-test (push) Canceled after 0s
La barre partagée avait perdu la marge basse des blocs qu'elle a remplacés :
sur /p/:provider/t/:theme la 1re carte collait aux boutons (0 px, l'ancien
panneau portait mb-8), et /t/:theme ne gardait que mt-6.

- les deux pages thèmes enveloppent la barre dans un div mb-8 ;
- /t/:theme : le conteneur de sections perd son mt-6 (compensé par mb-8),
  plus aucun double espace.

Mesuré au navigateur (Playwright, desktop 1280 + mobile 390) : 32 px sous
la barre sur /p/:…, 74 px jusqu'à la 1re carte sur /t/:…, build ng OK,
test:search vert. Instance locale 4200 rebuildée.
2026-10-01 20:46:32 -04:00
bruno f6fdf8ee08 feat(filtres): barre [Filtres] [Tri] unique sur recherche et themes
CI / build-and-test (push) Successful in 14m54s
Nouveau composant partage app-filter-toolbar, utilise tel quel par la page
de recherche ET les deux pages de themes (meme visuel, seul le cablage
differe : URL pour la recherche, signaux locaux pour les themes).

- Boutons : [Filtres] (pastilles de type, ligne Sources, selects periode /
  duree / langue, filtres actifs) et [Tri] (Pertinence, Date de mise en
  ligne, Vues, Note (likes), Duree).
- Page de recherche : le bouton oeil « filtres rapides » et le panneau de
  selects disparaissent, tout est sous [Filtres] ; le tri a son bouton.
- Sources : boutons Toutes / Aucune. « Aucune » masque tout (la garde
  « derniere source masquable » est retiree) et un bandeau vide dit de
  reactiver au moins un fournisseur.
- /t/:theme : les 3 pastilles maison sont remplacees par la barre ; les
  sources masquent des blocs entiers ; filtres et tri appliques sur les
  blocs deja charges.
- /p/:provider/t/:theme : les 5 selects et les pastilles sont remplaces par
  la barre ; signaux maison unifies sur le modele SearchFilters (defaut
  relevance) ; les onglets Twitch lisent/ecrivent filters().type.
- Tri etendu : FilterSort + rating + duration. Les providers ne connaissent
  que relevance/date/views (serveur inchange, repli relevance) : Note et
  Duree sont reordonnes cote client via sortItems(), partage avec les pages
  themes.
- Communs : sortItems / itemViews / itemRating / matchesLanguage /
  formatCount extraits de filters.ts (3 copies d'heuristiques supprimees) ;
  cles i18n fr+en (filters.sort.rating, filters.sort.duration,
  filters.sortHint, filters.localHint, « Date de mise en ligne »).

Tests : test:search, test:filters, test:suggest, test:kind, test:section
verts + test:api 41/41, build ng OK.
2026-10-01 16:26:29 -04:00
bruno 735255b7d8 feat(library): liste « À regarder plus tard » (watch later)
CI / build-and-test (push) Successful in 14m55s
Nouvelle liste dans la section Vous de la barre latérale, avec un bouton
horloge a cote du coeur sur toutes les cartes (accueil, themes, provider-
themes, recherche).

- db : les 4 fonctions de listes (like/unlike/isLiked/list) prennent un
  paramètre `tag` — la table `tags` sert déjà de mot-clés génériques, donc
  zéro migration.
- api : les 4 routes likes sont générées par registerVideoTagRoutes(prefix,
  tagName) ; /api/user/watch-later = même fabrique, même enrichissement
  yt-dlp, même middleware cookie-aware. Entrée OpenAPI ajoutée.
- front : LikesService prend un VideoTag, like-button un @Input() list
  (icône horloge, ambre quand enregistré, libellés a11y dédiés) — un seul
  composant pour les deux boutons, pas de copie.
- cartes de recherche : la grille partagée n'avait AUCUN bouton ; cœur +
  horloge ajoutés, masqués sur les lives et cartes chaîne (coin bas-gauche
  déjà occupé par le compteur de spectateurs / pas des vidéos).
- route /library/watch-later = LikedComponent via data.list (titre, icône,
  placeholder et état vide pilotés par la route), clés nav.watchLater FR/EN.

Tests : npm run test:api 41/41 (nouveau test watch-later : ajout, statut,
indépendance vis-à-vis des likes, liste, retrait, 400, 401) + 21 autres
suites vertes. Build OK, instance locale 4200 rebuildée. Sondage Playwright
desktop+mobile : 12/14 (2 faux positifs du sondage : accordéon replié sur
une route hors /library, 3×401 console pendant la phase déconnectée).
2026-10-01 14:13:43 -04:00
bruno 88a2d21133 feat(shorts): vraie variété du flux a chaque chargement
CI / build-and-test (push) Successful in 15m2s
Trois causes du « toujours les mêmes vidéos » (peu importe le fournisseur) :

- watch-short : queryIndex repartait à 0 a chaque loadFeed() -> toujours la
  même requête (« shorts ») et donc les mêmes résultats du fournisseur.
  Départ désormais aléatoire dans SHORTS_QUERIES (la rotation par page reste).
- watch-short : dedup() ne triait rien -> le 1er lot arrivait toujours dans
  l'ordre du fournisseur. Le lot neuf est maintenant mélangé (Fisher-Yates),
  comme le chemin « Pour toi » le fait déjà. Un seul point de passage, donc
  catalogue, historique, repli trending et Twitch sont couverts.
- youtube-api : la rotation des top games Twitch démarrait à l'index 0 ->
  mêmes clips à chaque session. Départ aléatoire, rotation conservée.

Le cache serveur (5 min / 30 min YT) n'est pas en cause.

Vérif instance locale : YouTube 3/3 premières vidéos distinctes sur 3
rechargements, Twitch 2 distinctes sur 3. tests verts.
2026-10-01 12:58:47 -04:00
bruno f3fd57014a feat(twitch): son par défaut + pas d'avance auto sur les clips
CI / build-and-test (push) Canceled after 0s
- /watch : muted=true supprime — son actif dès le démarrage (le chargement
  de l'embed a lieu dans un geste, donc l'autoplay sonore est autorisé).
- /shorts : muted piloté par le bouton Son du menu (défaut : son ON) — ce
  bouton était jusqu'ici sans effet sur Twitch.
- /shorts : plus d'avance auto pour Twitch. L'embed clips est cross-origin et
  n'expose AUCUNE API d'état : impossible de savoir si la vidéo est en pause,
  donc on ne change jamais de vidéo tout seul (swipe / flèches / molette).
- Verifs instance locale : 4/4 (URLs + aucune avance après 75 s d'un clip de
  49 s) + 6/6 (nudge) + 8/8 (chargement au premier geste).
2026-10-01 12:31:21 -04:00
bruno 059481b9ea feat(twitch): l'embed ne se charge qu'apres un geste (autoplay)
CI / build-and-test (push) Canceled after 10m52s
- Methode « touch listener b4 loading the iframe » : l'URL de l'embed Twitch
  n'est produite qu'apres un premier geste — navigator.userActivation.
  hasBeenActive a l'arrivee (clic sur un lien = fenetre d'activation de 5 s),
  sinon pointerdown / keydown / wheel. Le premier chargement de l'iframe
  tombe donc dans un contexte d'activation utilisateur.
- Invite « Touchez pour charger la vidéo » sur /watch et /shorts pendant
  l'attente (sinon spinner infini sur /watch, cadre noir sur /shorts).
- /shorts : pas d'avance auto tant que l'embed n'est pas charge — le flux
  ne switchait plus d'une video a l'autre sans jamais jouer.
- Le nudge (recharge &r=1 au premier geste) est conserve pour les videos
  montees sans geste (avance auto, arrivee avec activation deja consommee).
- Verifs instance locale : 8/8 (arrivee a froid : invite + montage au geste)
  et 6/6 (arrivee avec activation : montage immediat + recharge unique).
2026-10-01 12:08:03 -04:00
bruno a588b444c2 feat(twitch): rechargement de l'embed au premier geste (autoplay)
CI / build-and-test (push) Canceled after 0s
- /watch + /shorts : le player Twitch refuse de demarrer sans geste
  utilisateur (anti-bot) et l'iframe etant cross-origin on ne peut pas
  cliquer dedans. On ecoute document:pointerdown (+ le keydown existant) et
  on ajoute &r=1 a l'URL de l'embed de la video courante : l'iframe se
  recharge dans le contexte d'activation utilisateur.
- Une seule fois par video (twitchNudgedFor) : pas de boucle de
  rechargement, les videos suivantes sont re-armees automatiquement.
- Verifie sur l'instance de test : 6/6 (URL initiale sans r=1, r=1 apres
  le geste, aucune recharge au 2e geste) sur /watch et /shorts.
2026-10-01 10:33:45 -04:00
bruno 4463023c84 fix(twitch): lecture auto des videos Twitch sur /watch
CI / build-and-test (push) Canceled after 0s
- /watch : les embeds Twitch etaient en autoplay=false depuis le 1er commit
  (clips clips.twitch.tv + channel + VOD player.twitch.tv) alors que tous les
  autres fournisseurs de la page demarrent automatiquement en muet
  (YouTube mute=1, Dailymotion mute=1, Odysee muted=1, PeerTube autoplay=1).
- Passe a autoplay=true&muted=true : le lecteur demarre tout seul, le son se
  reactive via les commandes du player (meme convention que /shorts).
- /shorts : inchange, l'embed demandait deja autoplay=true&muted=true.
2026-10-01 09:27:23 -04:00
bruno 82d0a8fb3b fix(menu-fab): navigation du menu correcte sur /shorts + FAB plus compact
CI / build-and-test (push) Canceled after 31s
- app-player-menu : stopPropagation wheel/touchstart/touchend sur le host —
  les handlers window:wheel et window:touch* de /shorts ne volaient plus les
  gestes sur le panneau (impossible de scroller jusqu'en bas du menu, tap =
  play/pause, swipe = changement de short)
- /shorts : ArrowUp/Down ignore quand le menu est ouvert
- /shorts : le FAB passe en haut a DROITE du cadre (la fleche retour mobile
  z-20 passait par-dessus le FAB piege dans la scene z-10)
- Look/taille : FAB 40->36px, panneau 288->240px, items compacts,
  max-height 70vh/480 -> 65vh/430 (plus de rognage par overflow:hidden),
  pastille en degrade + relief, panneau rounded-xl + highlight interne
2026-10-01 08:26:41 -04:00
bruno 7ceec59841 feat(menu-lecteur): refonte UI/UX professionnelle du menu FAB
CI / build-and-test (push) Successful in 14m57s
- FAB : pastille verre depoli 40px + icone sliders (standard options)
- Panneau : en-tete (titre + fermer), verre depoli, ombre profonde, coins arrondis
- Animations ouverture/fermeture (fondu + slide + scale), fermeture Echap + clic exterieur
- Items : hover/active/focus, icones teintees, sections + separateurs raffinés
- Playlist : modale rendue HORS du panneau (backdrop-filter piegeait position:fixed)
2026-10-01 07:20:26 -04:00
bruno 01e0671de9 feat(lecteur): menu FAB partage /watch et /shorts
CI / build-and-test (push) Successful in 14m48s
- PlayerMenuComponent : FAB circulaire translucide (haut-gauche) + panneau
  deroulant, commun aux deux pages (position, style, open/close, clic exterieur)
- /shorts : consolidation de tous les boutons (fournisseurs, langue, actions)
  dans le menu FAB ; suppression du rail a droite et de la rangee du haut
- /watch : actions (cinema, qualite, j'aime, playlist, transcription,
  telechargement, partage, ouvrir) deplacees dans le menu FAB
- AddToPlaylistComponent : variante menuItem pour s'integrer au menu
- styles .pm-item/.pm-sep/.pm-section-label dans index.css
2026-09-30 22:50:46 -04:00
bruno 2bcc7321aa fix(dailymotion): Player ID obligatoire dans l'URL d'embed
CI / build-and-test (push) Successful in 14m58s
Dailymotion exige désormais un Player ID (fév. 2026) : player.html?video=...
sans Player ID répond HTTP 403 'Forbidden'. L'URL correcte est
geo.dailymotion.com/player/{PLAYER_ID}.html?video=...

- helper dailymotionPlayerId()/dailymotionEmbedBase() (config window.DAILYMOTION_PLAYER_ID, repli demo x8lr5)
- /watch et /shorts utilisent le nouvel endpoint avec Player ID
- config.local.example.js documente la config
2026-09-30 21:48:06 -04:00
bruno c391be80e5 fix(shorts): aligner referrerpolicy de l'iframe sur /watch (origin)
CI / build-and-test (push) Successful in 14m54s
L'iframe Shorts utilisait referrerpolicy=strict-origin-when-cross-origin
alors que /watch utilise 'origin'. C'est la seule difference restante entre
les deux pages pour Dailymotion (URL identique, allow_embed=true) et elle
fait repondre 'Forbidden' au player. On aligne sur 'origin'.

Revert du referrerPolicy helmet (no-op pour les iframes qui ont leur propre
attribut, risque de side-effects inutile sur les CDN de vignettes).
2026-09-30 21:22:33 -04:00
bruno 6b20b1ce79 fix(shorts): anti-Forbidden Dailymotion + selecteur icon-only
CI / build-and-test (push) Successful in 14m55s
- DM: embed mute par defaut (comme /watch) + son sur action explicite
- helmet: Referrer-Policy strict-origin-when-cross-origin (Dailymotion exige un Referer)
- searchVideosPage DM: champs allow_embed/width/height/language/owner.id
- iframes shorts/watch: allow fullscreen (conforme embed officiel DM)
- selecteur shorts icon-only (pastilles + globe cyclique)
2026-09-30 19:55:23 -04:00
bruno 49859f4e81 feat(shorts): pastilles a logos + filtre de langues preferees
CI / build-and-test (push) Successful in 14m49s
Boutons fournisseurs avec disque aux couleurs de la marque + glyphe SVG (play YT, bulle Twitch, initiale sinon) ; icone etincelles pour Pour toi. Selecteur de langues Profil/FR/EN/FR+EN/Toutes (persiste newtube.shorts.langFilter, invite compris) : chaque flux passe par effectiveShortsLangs (resolveShortsLangs pur + spec). Docs 9.9.
2026-09-30 12:57:31 -04:00
bruno 3e1980283a fix(shorts): hover-chrome, swipe mobile garanti, avance auto fiabilisee
CI / build-and-test (push) Successful in 15m2s
Chrome auto-hide au hover desktop (2,5 s). Capteur tactile mobile : swipe garanti (l'iframe mangeait les touchers), tap = play/pause YT ou 6 s d'acces direct au lecteur sinon. Fleche retour accueil mobile, cadre quasi plein ecran. Avance auto : handshake YT listening (les evenements player n'arrivaient jamais), onStateChange canonique, retry borne anti-prefetch. Docs 9.9.
2026-09-30 12:21:15 -04:00
bruno 8f100d2b78 fix(shorts): Odysee sans tap-to-unmute + avance auto tous providers
CI / build-and-test (push) Successful in 14m48s
Odysee demarre muet (pas de rectangle tap-to-unmute) et ne passe au son que sur clic explicite du bouton Son (le geste autorise l'autoplay sonore). Avance auto : playerState YT (fin -> suivant, pause -> minuteur desarme, reprise -> rearme) + minuteur generique sur duree connue ou estimee (clip Twitch 60 s, sinon plafond Short 90 s). Docs 9.9.
2026-09-30 11:36:45 -04:00
bruno 8e5f1d6c65 fix(shorts): son par defaut, avance auto, anti-Forbidden DM, autoplay Twitch/Rumble
CI / build-and-test (push) Successful in 14m56s
Son active par defaut (YT/OD/DM/PT avec son ; clips Twitch en muet force, sans API de volume). Avance auto en fin de lecture (infoDelivery YT + minuteur duree generique garde par index/videoId, ignore si onglet cache). Dailymotion : allow_embed demande cote serveur, mappe en embeddable, propage en raw.allowEmbed pour le garde isPlayable (fini les Forbidden). Rumble autoplay=1/2 selon muet + nudge playVideo YT.
2026-09-30 10:40:03 -04:00
bruno 8e087daf93 feat(shorts): page /shorts alimentee par le catalogue + refonte pro/mobile
CI / build-and-test (push) Successful in 14m45s
SearchService.fetchCatalogShorts (type shorts via /api/search : cache, provenance, FF) + pont pur shorts-catalog.ts (mapping, meta, fraicheur, spec 27 assertions). WatchShortComponent : catalogue d'abord (feed, pagination, Pour toi), repli legacy, Twitch clips inchanges, avatars paresseux via channelMeta. UI facon YouTube : plein ecran 100dvh, fond d'ambiance, rail overlay 48px, meta/fraicheur/pastille provider, safe-area.
2026-09-30 09:16:56 -04:00
bruno 665a0f0ebd feat(providers): phases 7.3/7.4/7.6/8.1 — provenance, health, NDJSON, contrat unique
CI / build-and-test (push) Successful in 14m43s
7.3: capturedAt/source au registre + 6 adaptateurs + module provenance.ts + ?debug=1 (search-transport.mjs). 7.4: ProviderHealthService + badge source degradee. 7.6: squelettes par provider + snapshots progressifs + transport NDJSON /api/search. 8.1: ProviderAdapter unifie (search enveloppe + channelContent/channelMeta/capabilities) via getProviderAdapter + test de contrat offline.
2026-09-30 07:57:11 -04:00
bruno 37681c4f53 fix(shorts): orientation autoritaire, langues YouTube et autoplay fiable
CI / build-and-test (push) Successful in 14m18s
- format: des que largeur/hauteur sont connues, l'orientation fait autorite (horizontale/carree => jamais short, meme <75 s). Repli duree reserve aux providers muets
- dimensions mappees: Dailymotion (width,height), PeerTube (aspectRatio), Odysee (value.video.width/height) => validation verticale reelle
- YouTube: langue via snippet.defaultAudioLanguage (filtre FR/EN)
- autoplay: demarrage muet, suppression du deblocage automatique au scroll/selection (reload d'embed hors geste => ecran noir); son via le bouton dedie
- PeerTube embed: muted=1 pour l'autoplay
- server/search-filters.mjs: miroir de la regle d'orientation + tests mis a jour
2026-09-29 14:51:56 -04:00
bruno d4895bae92 fix(shorts): autoplay muet, langues FR/EN, proxy prod et Dailymotion embeds
CI / build-and-test (push) Successful in 14m9s
- autoplay: les Shorts demarrent muets (comme Watch) pour garantir l'autoplay des iframes; le son se debloque a la premiere interaction (unlockSound) et reste controlable
- proxy prod: ajoute les alias /proxy/twitch-api, /proxy/twitch-auth, /proxy/dm, /proxy/odysee (evite le fallback SPA -> parse JSON 'status 200 ok:false'); forwardJson accepte les deux prefixes. Corrige aussi la validation des clips Twitch morts
- Dailymotion: allow_embed demande dans les flux shorts/trending -> les videos non-embeddables ('Forbidden') sont exclues
- langues: mapping language (Dailymotion/PeerTube/Odysee) + shortsLangOk applique a tous les fournisseurs; en+fr toujours acceptes
- PeerTube shorts: durationMin/Max cote serveur (plus de videos longues au hasard)
- Twitch: validation helix/clips de nouveau fonctionnelle + autoplay muet
2026-09-29 14:22:45 -04:00
bruno 983c6cb5ab refactor(video): uniformiser classification et routage par section
CI / build-and-test (push) Successful in 14m9s
Source unique de classification (video-kind.ts) + politique de section centralisee (section-policy.ts). Les composants ne portent plus de seuils ni de regles de type maison.

- section-policy.ts: sectionAccepts / routeForVideo / shortsFeedAccepts /
  isPlayable + classifyOnce (enrichissement _kind a l'ingestion)
- watch-short: separe classification (shortsFeedAccepts) et jouabilite
  (isPlayable: allowEmbed DM, live/channel Twitch, Rumble 404); supprime le
  seuil Rumble 90 s et l'import YOUTUBE_SHORT_MAX_SECONDS du composant
- home: filtre sectionAccepts(...,'home') (plus de shorts dans le trending),
  routeForVideo, pagination bouclee (min 20, max 5 iterations)
- themes: _kind pre-calcule une fois, sectionAccepts, capacites live lues
  depuis le registre providers (fin de LIVE_PROVIDERS codes en dur)
- video-kind: helpers isNativeVerticalProvider / hasKnownDuration
- routes: alias /shorts/:id et /live/:id pour routeForVideo
- tests: section-policy.spec.ts + video-kind.spec.ts etendus; npm run test:section
2026-09-29 13:34:58 -04:00
bruno f443a470f9 feat: détection short par orientation vidéo
CI / build-and-test (push) Successful in 14m6s
Règle partagée front/back : flag natif, verticale stricte (ratio ≤ 0.8, ≤ 90 s), verticale sans durée jamais short, repli durée (70 YT / 75 autres). Dimensions alimentées par Dailymotion/Odysee/PeerTube, flag maison Rumble supprimé, condition innertube morte simplifiée. Shorts : langues utilisateur (UI + transcripts) via relevanceLanguage/languageOneOf/Twitch language, son activé par défaut.
2026-09-29 12:17:10 -04:00