- 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
- 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
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.
- 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.
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.
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.
- 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
- 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
- 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)
- likes + playlists: authMiddlewareCookieAware (cookies + Bearer) au lieu
du Bearer seul -> corrige 401 Unauthorized meme connecte (creation
playlist, ajout video, bouton J'aime)
- add-to-playlist/like-button/search-box: retire HttpClientModule local
qui court-circuitait les intercepteurs fonctionnels
- intercepteur: refresh+retry sur 401 meme sans token en memoire (F5)
- UserService: apiBase() au lieu de /proxy/api en dur (prod :4000)
- J'aime: envoie titre/miniature, erreur visible au lieu de bouton mort
- download: seaux separes (lectures 120/min, ecritures 30/min, formats
30/min) + reponses limiter en JSON rate_limited + Retry-After
- formats: cache memoire 10 min + ytdlpNetOpts + retry front avec backoff
- polling allege (downloads 5s, suivi job 4s, 429 transitoire non fatal)
- erreurs FR precises (http-error.util): 401/403/404/410/429/quota/500
avec compte a rebours + bouton Reessayer
- Supprime le hint debug 'touche s' (raccourci conserve, tooltip discret)
- server/transcript.mjs: dedupeTranscriptLines collapse les auto-captions
a fenetre glissante (12 lignes -> 3 sur l'exemple signale), applique dans
parseJson3/parseVtt/parseXmlCaptions + filet avant cache API
- watch: layout pro (grille 1fr/360px, carte chaine+actions, labels FR,
panneaus description/download, sidebar A suivre sticky, a11y/focus)
- transcript interactif: recherche, clic timestamp -> seek, suivi auto +
surlignage ligne active, copie, retry
- tests: 4 nouveaux cas dedupe (23/23 pass), build prod OK
- channel-registry DM: retire le champ invalide avatar_url (400 sur toute la requete -> titre null), URL officielle /user/<username>
- channel-content YT: ne plus envoyer pageToken=undefined (400 -> playlists vides); cache de pageTokens opaques pour la pagination videos/shorts/live/playlists
- channel-content.service: stoppe l'infinite scroll quand une page n'apporte rien de nouveau