Commit Graph
100 Commits
Author SHA1 Message Date
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
bruno 3f4cd56f14 fix(shorts): pré-validation (clips morts, Rumble 404), autoplay Rumble, onError auto-skip, zone sous-vidéo retirée
CI / build-and-test (push) Successful in 14m16s
2026-09-29 11:23:33 -04:00
bruno f76a3795c2 fix(twitch,rumble,shorts): replis serveur, embed Rumble autoritaire, refonte UX Shorts
CI / build-and-test (push) Successful in 14m15s
2026-09-29 10:39:03 -04:00
bruno 3fe05b3c30 fix(twitch): token partagé + timeouts + fan-out réduit sur Home/Explore
CI / build-and-test (push) Successful in 14m10s
2026-09-29 09:56:15 -04:00
bruno 5994841bda fix(shorts): embeds DM/PT/Rumble/Twitch + flux Pour toi multi-fournisseurs
CI / build-and-test (push) Successful in 14m16s
2026-09-29 09:38:13 -04:00
bruno 34f432a7c1 fix(shorts): embeds par fournisseur + flux infini (153/Forbidden/not-found) 2026-09-29 09:04:09 -04:00
bruno 937436658a fix(search): le repli des rails ne masque plus la grille + logos de marque
CI / build-and-test (push) Successful in 14m9s
Bug : le @if (hasQuery()) englobant toute la section basse avait ete passe en
@if (hasQuery() && railsOpen()), donc replier le rail masquait aussi la
grille, le loader et le sentinel de scroll infini. Seul le panneau
' types + sources ' est desormais imbrique dans @if (railsOpen()).

Logos : les formes geometriques approximees sont remplacees par les six
marques officielles (traces et couleurs Simple Icons, CC0-1.0), stockees en
local pour eviter toute dependance CDN a l'execution. Dailymotion garde le
bleu de la marque (#0066DC) plutot que le #0A0A0A de Simple Icons, invisible
sur fond sombre.

L'etat actif/masque n'est plus porte par la couleur de l'icone (fixe par
marque) mais par le bouton : fond plein + logo couleur, ou fond transparent +
logo grayscale 40%.
2026-09-28 22:44:51 -04:00
bruno dccc1c8eb1 feat(search): repli des rails filtres + boutons sources en icones
CI / build-and-test (push) Successful in 14m10s
- nouveau bouton oeil a droite de 'Filtres' pour replier le bloc
  types + sources (railsOpen, persiste en localStorage) ; pastille d'accent
  quand un filtre de type est actif alors que le rail est replie
- nouveau composant app-provider-icon : marques des 6 sources en trait,
  currentColor pour que l'icone herite de l'etat actif/masque du bouton
- pastilles sources en icone seule (h-8 w-8), libelle et compte passes par
  aria-label / title ; plus de point de couleur ni de nom visible
- suppression de PROVIDER_DOTS et providerDotStyle
2026-09-28 22:28:08 -04:00
bruno 7c3471596e fix(search): en-tete de resultats thémable et ergonomique
CI / build-and-test (push) Successful in 14m9s
Les pastilles fournisseurs portaient des couleurs de marque en dur ([ngStyle])
qui ignoraient le systeme de themes : rendu illisible en thème clair. Tout
l'en-tete rebati sur les classes que le CSS remappe (bg/text/border-slate-*).

- sources : point de couleur plein (active) / creux (masquee) a la place des
  fonds de marque, nom complet + compte compact tabular-nums
- actions (Tout afficher / Toutes les sources) en style contour, plus de
  bordure pointillee qui ressemblait a un placeholder
- pastilles de type : bg-slate-200 pour l'etat actif, border-transparent
  pour eviter le saut de 2px au changement d'etat
- rails regroupes dans un panneau + filet, fade de droite (scroll-rail-fade)
- selects : appearance-none + chevron SVG, libelles visibles remplaces par
  sr-only (5 lignes de hauteur en moins)
- bannieres notice/error refaites en neutres themables (illisibles en clair)
- anneau focus-visible en var(--accent) sur tous les controles
- suppression de getProviderColors et formatViews (morts)
2026-09-28 21:51:44 -04:00
bruno d400c26249 feat(search): en-tete de resultats compact, fournisseurs en filtres
CI / build-and-test (push) Successful in 14m20s
- rail 1 : pills de type resserrees (h-6/text-xs), role=group coherent
- rail 2 : les pastilles fournisseur deviennent des boutons d'affichage
  (couleur de marque = visible, gris + opacite = masque), avec le compte de
  resultats par source, aria-pressed, hover/focus
- filtre d'affichage local (aucun refetch, aucun squelette) persiste en
  localStorage ; impossible de masquer la derniere source visible
- panneau de filtres repliable par defaut, selects h-8 inline labels sr-only,
  badge du nombre de filtres actifs sur le bouton
- nouveau computed scopedVideos : compteurs de types et tri inter-provider
  alignes sur les sources visibles
- suppression d'activeProvidersInfo devenu inutilise
2026-09-28 21:28:01 -04:00
bruno 1814334958 fix(search): loading par frame sur les cartes, plus de cadres vides
CI / build-and-test (push) Successful in 14m8s
- video-card: etat de chargement de la miniature (shimmer + spinner),
  fallback 'Apertu indisponible' avec retry, referrerpolicy no-referrer
- grid: eager sur les premieres cartes, bandeau de rafraichissement quand
  la grille est deja peuplee
- search: garde MIN_QUERY_LENGTH=2 (le loading partait pour une requete que
  request$ filtrait, grille figee sur des skeletons) + filet de securite 20s
- skeleton: ligne chaine pour coller a la hauteur reelle de la carte
2026-09-28 19:35:48 -04:00
bruno f1f673040e feat(search): panneau de filtres unifié, clavier/focus du typeahead
CI / build-and-test (push) Successful in 14m10s
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
bruno 9fb0ce7549 fix(mobile): barre de thèmes masquée, modale @ hors écran, pastille d'avatar
CI / build-and-test (push) Successful in 14m14s
Cinq correctifs signalés sur le mode mobile et le mode desktop.

1. La modale « @ » était confinée dans le header (desktop + mobile)
   <header> portait `backdrop-blur-sm`, et `backdrop-filter` fait de l'élément
   le containing block de ses descendants en position:fixed. Les overlays du
   provider-picker (et du menu Ctrl+K) se résolvaient donc contre le header :
   mesuré 1440x64 au lieu de 1440x900, dialogue réduit à 32px de haut pour 466px
   de contenu, haut de la fenêtre inatteignable.
   Le flou passe sur une couche enfant (`-z-10`) qui n'est pas un ancêtre des
   overlays : `fixed` redevient relatif au viewport.
   Desktop : dialogue 512x468 centré. Mobile : 358x686, entièrement visible.

2. La barre de thèmes disparaissait sous le header (desktop + mobile)
   `--header-offset` vaut 56/64px, mais le header mesure 57/65px (bordure) et
   118px quand la feuille de recherche mobile est ouverte. La barre était donc
   déjà glissée dessous, et totalement invisible dans ce dernier cas
   (hauteur visible mesurée : 0px).
   HeaderComponent publie maintenant sa hauteur réelle dans `--header-measured`
   via ResizeObserver ; `--header-offset` l'utilise quand elle existe et garde
   la valeur statique en repli. Barre visible dans tous les cas, y compris
   feuille de recherche ouverte.

3. Barre de thèmes : icônes seules sur mobile
   Les libellés sont masqués sous md (`aria-label` conservé pour
   l'accessibilité). 7 pastilles visibles au lieu de 3, sans scroll.

4. Avatar en pastille
   `.avatar-circle` est renforcé : `rounded-full` dans le markup plus
   `border-radius: 9999px !important`, `aspect-ratio: 1/1`, min-width/min-height
   et `line-height: 1`, pour qu'aucun style de thème ne puisse le dégrader en
   rectangle.

Vérifié : audit navigateur sur 17 routes x 320/390/768/1440 px — aucun
débordement horizontal, barre de thèmes visible partout ; tests du dépôt verts ;
build production OK.
2026-09-28 16:25:55 -04:00
bruno fa775497ee fix(deploy): 404 de rétention sur les images OCI index (buildx)
CI / build-and-test (push) Successful in 14m0s
Le push réussissait puis le script sortait en 22 :

    curl: (22) The requested URL returned error: 404
    Write-Error: ... code d'erreur: 22

Deux causes cumulées, visibles seulement une fois MAX_VERSIONS dépassé.

1. Accept trop restrictif
   `docker build` via buildx produit un OCI image index
   (application/vnd.oci.image.index.v1+json), pas un manifest v2 plat.
   delete_by_tag demandait le digest avec
   `Accept: application/vnd.docker.distribution.manifest.v2+json` : le registre
   répond 404 — et non 406 — quand l'Accept ne couvre pas le type stocké.
   Le HEAD échouait donc sur TOUS les tags, y compris celui venait d'être
   poussé. On liste maintenant les quatre media types possibles.

2. La rétention pouvait tuer un déploiement réussi
   Sous `set -euo pipefail`, le curl de delete_by_tag n'était protégé par
   aucun `|| true` : son code de sortie 22 se propageait et tuait le script,
   transformant un push réussi en échec. Le ménage des anciennes versions est
   du best-effort et ne doit jamais faire échouer le push.

Symptôme d'origine : la rétention ne s'est jamais déclenchée tant que le
nombre de tags semver est resté <= MAX_VERSIONS (5). Au 7e tag, elle a
commencé à tourner et a fait échouer le script.

Vérifié : deploy-img.ps1 publie 1.0.20, `latest` aligné sur le digest de
l'image locale (sha256:25555d9ee4bf…), rétention purgée sans erreur.
2026-09-28 14:47:40 -04:00
bruno 66f1da1d72 feat(mobile): refonte responsive complète (header, drawer, dvh, cibles tactiles)
Le header était en grid-cols-[auto_1fr_auto] avec une barre de recherche
non-wrappable (7 chips + input + 2 boutons, aucun min-w-0) : ~1000px de
largeur intrinsèque dans un viewport de 375px. Toutes les routes
débordaient horizontalement et les boutons Connexion/Inscription partaient
hors écran.

Socle
- --header-h / --header-offset / --app-vh centralisent la métrique du
  header : supprime les top-16 codés en dur (channel-tabs était en top-0 et
  glissait sous le header au scroll).
- Safe-area iOS, viewport-fit=cover, color-scheme/theme-color.
- dvh partout (100vh > hauteur visible sur iOS), overflow-x: clip au lieu de
  hidden (hidden cassait silencieusement tous les sticky).
- Cible tactile 44px sur pointeur grossier, skip link, focus-ring clavier,
  scroll-rail pour les tab strips.

Header
- Barre mobile (burger, logo, recherche, menu ⋮) + barre desktop inchangée.
- Le menu ⋮ expose ce qui disparaissait : bande des thèmes, préférences,
  historique, favoris, téléchargements, thème, connexion/inscription.
- Recherche en feuille sous le header avec focus automatique.
- Échap / clic extérieur / navigation ferment les panneaux transitoires.

Drawer
- Partait à inset-y-0 : les 56 premiers px du menu étaient derrière le
  header. Descend sous le header, bouton fermer intégré, verrouille le
  scroll, se ferme à Échap.

Barre de recherche
- Chips masquées sous lg, remplacées par un bouton résumé qui ouvre le
  sélecteur de fournisseurs. Suggestions en dvh, menu Ctrl+K plein écran
  sur mobile.

Pages
- Shorts : cadre 9:16 contraint par dvh ET vw (il débordait en paysage),
  Préc./Suiv. qui étaient hors du conteneur height-capped.
- Watch : barre d'actions qui wrappe, mode cinéma en opacity-0 group-hover
  donc invisible au doigt, thumbnails fluides, playsinline sur les iframes.
- Historique, thèmes, chaîne, playlists, préférences, sessions : rails
  swipables, min-w-0, truncate, en-têtes qui wrappent.

Vérifié : audit navigateur (CDP) sur 17 routes x 7 largeurs (320→1440),
zéro débordement horizontal ; 19 tests d'interaction mobile ; suites de
tests du dépôt vertes (api 40/40, transcript 27/27, search, suggest,
playlists, preferences, subscriptions, highlight, telemetry).
2026-09-28 14:38:31 -04:00
bruno 69c722fdf9 Document env vars and remove theme collapse button
CI / build-and-test (push) Successful in 14m7s
- Expand .env.example with full runtime, cache, Docker, and AI keys
- Add deployment secrets to .gitignore (docker-compose/.env,
  docker/.registry.env)
- Remove VIMEO_ACCESS_TOKEN from compose files; add OAuth redirect URIs
- Load registry credentials from .registry.env in deploy-img.sh
- Fail fast if protected registry lacks auth during pre-checks
- Pass DOCKER_USERNAME/PASSWORD explicitly to WSL in deploy-img.ps1
- Remove inline collapse buttons from themes-nav.component.html
2026-09-28 08:34:07 -04:00
bruno 23d5e58a8e feat(history): surlignage des mots-cles recherches (mode highlight)
CI / build-and-test (push) Successful in 14m19s
Pipe histHighlight + utilitaire pur (HTML source echappe, matchs
insensibles a la casse, multi-mots, chevauchements fusionnes) :
titres/queries/apercus des 3 onglets + lignes des transcripts
deplies, avec badge de comptage d occurrences. test:highlight.
2026-09-27 22:03:49 -04:00
bruno 71278eb0b1 fix(history): recherche et filtres fonctionnels sur les 3 onglets
CI / build-and-test (push) Successful in 14m0s
- distinctUntilChanged() sur Subject<void> avalait toutes les frappes
  apres la premiere : barre de recherche morte sur les 3 onglets.
- Filtre provider recherches : ids courts stockes (yt, dm) vs filtre
  long (youtube) -> zero resultat. Match court+long cote serveur.
- LIKE echappes (%, _, \\) sur q des 3 onglets.
- Watch/transcripts : provider normalise (court+long), lang
  insensible a la casse avec prefixe (fr -> fr, fr-ca).
- Ecriture watch canonique (nom long). Test test:history.
2026-09-27 21:34:40 -04:00
bruno a57bc0f7ae fix(transcript): tlang en dernier recours + garde anti-blob
CI / build-and-test (push) Successful in 14m5s
Le fallback &tlang= peut renvoyer la piste source en un seul bloc
de plusieurs Ko (throttle YouTube) : il passait devant les pistes
directes et son blob anglais etait mis en cache comme 'fr'.
tlang desormais etape 3 (apres directes signees), isBlobTranscript()
refuse les cues uniques > 2000 chars (fetch, yt-dlp et post-dedupe).
Tests 27/27.
2026-09-27 20:56:08 -04:00
bruno a79831f406 fix(transcript): pipeline URLs signees yt-dlp + fetch durci anti-429
CI / build-and-test (push) Successful in 14m1s
Les URLs timedtext InnerTube repondent 200-vide (throttle YouTube) :
sonde rapide max 2 sans retry, puis dump yt-dlp -> URLs signees en
direct (max 4), puis --write-sub. 200-vide traite comme 429
(retryAfterSec), fmt= plus jamais duplique (signature), cookies
YT_COOKIES_FILE reutilises par le fetch direct. Tests 26/26.
2026-09-27 20:47:28 -04:00
bruno 148a00e60f feat(couverture): 100pc API/MCP/tests/docs - volet 2
CI / build-and-test (push) Successful in 14m6s
- tests: server/tests/api_coverage2.test.mjs (23 cas offline-safe) :
  oauth (status/url/connections/preview), channels (validations),
  subscriptions batch + groupes CRUD, transcripts history, likes/history
  DELETE, playlists public/view/reorder, telemetry summary, downloads
  401/403/400, ai 503, twitch-token, config.js, rumble 400
- mcp v1.1.0 : 4 outils authentifies (list_playlists/likes/
  subscriptions/watch_history, NEWTUBE_TOKEN) + tests stub
- openapi.json v1.1 : 15 chemins documentes (sante, recherche,
  details, trending, transcript, auth, prefs, likes, subs, downloads,
  oauth, playlists) ; guide maj (11 outils)
2026-09-27 18:51:50 -04:00
bruno 1812b86dd4 feat(mcp): serveur MCP stdio 7 outils + guide API/MCP + couverture tests
CI / build-and-test (push) Successful in 14m3s
- mcp/server.mjs : search, suggest, details, trending, transcript,
  transcript_text, health_check (client fin vers API REST)
- docs/API_MCP_GUIDE.md : reference complete REST + MCP + transcript
- tests : server/tests/api_coverage.test.mjs (17 cas HTTP),
  mcp/tools.test.mjs (7 outils sur stub), scripts test:mcp/test:api
- fix(db): DELETE playlist 500 FK -> metrique avant delete + cleanup
  enfants + recordPlaylistMetric tolerant (observabilite)
2026-09-27 18:39:26 -04:00
bruno d0d1c07535 fix(shorts): flux dedie avec filtre duree serveur + loop + replis 2026-09-27 16:53:01 -04:00
bruno ebfb39fec2 feat(groups): anti-bruit prénoms, 4 thèmes, persistance analyse, fix filtre provider 2026-09-27 14:34:47 -04:00
bruno a2b4139887 feat(groups): ratissage élargi (3 lettres, sous-chaînes, singletons, plafond, copie inclassables) 2026-09-27 14:05:08 -04:00
bruno 63a2bddf47 fix(groups): assignation en lot 1 requête + trackBy (plus de wipe/429) 2026-09-27 13:52:20 -04:00
bruno 9b620aaab6 feat(groups): sujets émergents + analyse approfondie des chaînes 2026-09-27 13:30:11 -04:00
bruno dde0747ba3 feat(groups): 216 icones N&B + fermeture auto menu + suggestions auto 2026-09-27 13:03:58 -04:00
bruno 856b38bbea feat(groups): panneau groupe pro + 175 icones avec recherche 2026-09-27 12:33:56 -04:00
bruno 201fe979b8 feat(history): miniatures des vidéos importées (i.ytimg déterministe) 2026-09-27 12:16:09 -04:00
bruno 6c218e8773 feat(sync): refonte import Takeout complet (playlists, musique, recherches, identité) 2026-09-27 11:10:05 -04:00
bruno 03ad46e91b feat(sync): import Takeout HTML/CSV (historique, WL, abos) sans API 2026-09-27 09:31:45 -04:00
bruno 1ae70b5c41 chore(docker): exclut aussi les zips Takeout 2026-09-27 09:13:34 -04:00
bruno a3d29fe3cd debug(sync): diag inclut la chaîne mine=true (id+titre) pour comparaison 2026-09-27 09:01:06 -04:00
bruno fdb5a48023 debug(sync): endpoint diag multi-endpoints (Data API + InnerTube) + bouton UI 2026-09-27 08:26:10 -04:00
bruno 720367fe13 debug(sync): expose le corps d'erreur YouTube (reason) + log yt-channels 2026-09-27 08:14:26 -04:00
bruno f038f19b71 feat(sync): sélecteur de chaîne YouTube (multi-chaînes/marque) via PageId 2026-09-27 08:05:01 -04:00
bruno d3836989ea debug(sync): logs serveur yt-history/WL (counts + detail erreur) 2026-09-27 07:49:30 -04:00
bruno 8edf9de0c6 fix(sync): historique/WL via youtubei.js authentifié + erreurs détaillées 2026-09-27 07:36:16 -04:00
bruno 186294f518 fix(sync): historique via youtubei.js authentifié (contexte officiel) + WL alias WL 2026-09-26 23:45:29 -04:00
bruno 464d6f1b80 fix(sync): 403 insufficientPermissions -> scope youtube complet + invite reconnect 2026-09-26 23:26:04 -04:00
bruno 41875dc50d fix(sync): 403 InnerTube = version client inventée; aligne WEB 2.20260623.01.00 + expose ytReason 2026-09-26 23:01:10 -04:00
bruno 5a19f8f11f fix(sync): 403 InnerTube + WL manquant via fallbacks
- InnerTube 403: la YOUTUBE_API_KEY restreinte par referrer etait
  rejetee sans Referer envoye; ajout headers navigateur + repli cle
  publique du client web (probe live: 401 propre = transport OK)
- Watch Later: repli lecture via InnerTube (browseId WL, sans ID) quand
  l'API Data ne renvoie pas d'ID; repli push via editPlaylist; note
  'via' affichee dans l'UI; debug cles relatedPlaylists conserve
2026-09-26 22:50:53 -04:00
bruno 050cea3a92 fix(sync): WL diagnostic, import-all paginé + totaux, sync auto historique InnerTube
- Watch Later: youtube_no_channel / watchlater_not_found avec hint
  actionnable + log cles relatedPlaylists (sans PII)
- Preview Google: totaux compte (subscriptionTotal/likeTotal), UI
  'X / N, l'import prend tout'; import-all paginé (subs<=1000,
  likes<=500) au lieu de 50/25
- Sync auto historique: GET /oauth/google/yt-history (browse FEhistory
  authentifie, continuations, <=500), bouton Sync auto dans carte
  Takeout, import via lots takeout existants
2026-09-26 22:25:50 -04:00
bruno 1c013452fa feat(sync): phase 1 - import Takeout + Watch Later YouTube bidirectionnel
- Takeout: POST /user/history/takeout (lots <=1000, validation videoId,
  upsert sans doublons), parsing local watch-history.json cote client
  (vie privee, dedup derniere date), progression par lots de 500
- Watch Later: GET/POST /oauth/google/watchlater (lecture readonly,
  push force-ssl), import vers playlist NewTube 'Watch Later (YouTube)',
  export playlist NewTube -> WL YouTube, writeGranted + invite reconnect
- Scopes Google elargis (youtube.force-ssl), aide /library/import a jour
  (limite historique API documentee)
2026-09-26 21:58:28 -04:00
bruno bb203f405c fix(auth): login Google retombait sur le callback import (userId null) -> route par purpose vers completeGoogleLogin 2026-09-26 21:30:54 -04:00
bruno 2d56bd70e7 config(oauth): identifiants Google OAuth pour instance locale (login + import) 2026-09-26 21:18:00 -04:00
bruno 783003822d feat(auth): connexion avec un compte Google (find-or-create, import likes/abos inclus)
- Backend: GET /api/auth/google/{url,callback} (state login distinct du
  link import), echange code, profil userinfo, find-or-create (OAuth sub,
  puis email), session + cookies comme login interne, connexion stockee
  dans oauth_connections pour l'import direct
- Front: bouton 'Se connecter avec Google' sur /auth/login (visible si
  configure), page /auth/google/callback (initSession + prefs + redirect
  /library/import), cles i18n nav.import deja en place
- Comptes Google-only: password_hash aleatoire (login mot de passe impossible)
2026-09-26 21:02:12 -04:00
bruno ecb4295a59 feat(oauth-ui): decouverte import Google/Twitch (sidebar, empty states, aide) 2026-09-26 20:21:27 -04:00
bruno df53445efe feat(oauth): import favoris/abos Google (YouTube) et Twitch vers abonnements/likes
- Backend: server/oauth.mjs (auth URL, echange code, refresh, fetch
  subscriptions/likes Google, follows Twitch), table oauth_connections
  + migration, routes /api/oauth/:provider/{url,callback,preview,import},
  /api/oauth/{status,connections}
- Front: /library/import (connect, preview, import), service
  oauth-import, lien depuis Abonnements
- Config: GOOGLE_CLIENT_ID/SECRET/REDIRECT_URI, TWITCH_REDIRECT_URI,
  OAUTH_APP_BASE_URL documentes (.env.example, docker-compose),
  passthrough docker-compose.local.yml, roadmap README cochee
2026-09-26 18:47:02 -04:00
bruno 21f2296c41 fix(themes): pages themes Twitch vides -> requetes Helix tokenisees + fallbacks game_id/top-streams 2026-09-26 18:46:55 -04:00
bruno db03b70854 feat(watch): bouton cinema visible uniquement au survol du lecteur 2026-09-26 14:27:29 -04:00
bruno 80afa9b0d0 fix(watch): bouton cinema decale sous les controles haut-droite du lecteur 2026-09-26 14:20:03 -04:00