- 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
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
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%.
- 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
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)
- 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
- 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
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.
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.
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.
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).
- 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
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.
- 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)
- ecoute des events du player via postMessage (listening ->
onReady/infoDelivery/onStateChange), extrapolation du temps entre
events, fige a la pause; surlignage + autoscroll actifs aussi en embed
- seek: ancre l'estimation sur la destination (pas de saut visuel)
- toggle Suivi: saut immediat au passage en cours + intitulés
explicites; (load) iframe pour (re)abonnement; reset par video
- watch: panneau transcription deplace en tete de sidebar au-dessus de
A suivre (+ scroll auto a l'ouverture sur mobile)
- seek transcript: seekTo postMessage YouTube (sans rechargement),
time= Twitch VOD, t= Odysee, start= Rumble (start/dm/pt existants)
- watch: lecteur monte des les parametres de route (embed natif),
squelettes partiels pendant /details au lieu du plein ecran
- themes-nav: bouton replier/deplier persistant (localStorage),
barre fine avec theme actif quand repliee, cles i18n FR/EN
- 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