Commit Graph
102 Commits
Author SHA1 Message Date
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