Commit graph

20 commits

Author SHA1 Message Date
383e6767a2 feat: theme sombre/clair selon la preference navigateur
All checks were successful
Publier l'image Docker / test (push) Successful in 23s
Publier l'image Docker / publish (push) Successful in 42s
Un seul bloc @media (prefers-color-scheme: dark) dans base.scss
redefinit les custom properties CSS existantes (--color-*) - tout le
reste du CSS de l'app y puise deja, rien d'autre a toucher. Pas de
bascule manuelle demandee.

Verifie en navigateur reel dans les deux modes (emulation
prefers-color-scheme), y compris les etats de couleur de la file
d'attente (echec/termine/en cours) qui restent lisibles en sombre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:46:25 +02:00
a8cd7ae907 fix: miniatures manquantes (resultats de recherche + file d'attente)
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
Deux causes distinctes au meme symptome (aucune ligne de liste n'affichait
de miniature) :

- ChannelDiscoveryService : en --flat-playlist, l'extracteur YouTube de
  yt-dlp ne renseigne quasiment jamais le champ singulier `thumbnail`,
  seulement `thumbnails` (tableau, trie par resolution croissante). Le
  parsing ne lisait que `thumbnail` -> thumbnailUrl toujours null. Passe
  inapercu en test car le mock ne reproduisait que la forme theorique,
  jamais la forme reelle observee en prod. extractThumbnailUrl() lit
  desormais aussi `thumbnails`, en repli.
- QueuePanel (file d'attente) : aucun champ thumbnailUrl n'existait sur
  QueueItem/QueueItemDto - fonctionnalite jamais implementee, pas de
  regression. Ajoutee de bout en bout : POST /api/queue accepte
  thumbnailUrl (purement informatif, jamais transmis a yt-dlp),
  QueueService la conserve et l'expose, DownloadForm la transmet depuis
  les resultats de recherche, QueuePanel l'affiche (miniature 96x54 a
  gauche de chaque ligne).

Verifie en navigateur reel (pas seulement les tests) : element ajoute via
POST direct avec thumbnailUrl, image reellement chargee (naturalWidth 480)
et positionnee dans le DOM rendu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 21:43:25 +02:00
25c6c7c9e4 Merge branch 'fix/layout-scroll-and-thumbnails' into main (rattrapage : contenait la PR #7 mergée par erreur dessus au lieu de main)
All checks were successful
Publier l'image Docker / test (push) Successful in 22s
Publier l'image Docker / publish (push) Successful in 48s
2026-09-17 21:33:44 +02:00
97e14d97e5 Merge pull request 'feat: téléchargement groupé en zip' (#7) from feat/queue-zip-download into fix/layout-scroll-and-thumbnails
Reviewed-on: #7
2026-09-16 23:41:32 +02:00
7fa28ea4f7 feat: téléchargement groupé en zip
Remplace le "tout télécharger" par téléchargements individuels décalés
(PR précédente) par une unique archive zip, streamée sans être bufferisée
en mémoire (paquet `archiver`).

- server/src/routes/queue.ts : GET /api/queue/download-all, construit le
  zip à la volée depuis les fichiers des éléments `completed`, noms
  dédupliqués en cas de titres identiques. Point d'attention : le flux
  est envoyé à la réponse AVANT archive.finalize(), pas l'inverse -
  finalize() n'aboutit qu'une fois tout le contenu écrit dans le flux, ce
  qui bloquerait indéfiniment sur une grosse archive si rien ne la lit
  encore.
- QueueService : buildDelivery() factorise la logique de livraison
  (chemin + nettoyage post-envoi) entre consumeCompletedFile (un fichier)
  et le nouveau consumeAllCompletedFiles (tous les fichiers prêts).
- QueuePanel : le bouton "Tout télécharger" déclenche maintenant un seul
  lien vers /api/queue/download-all. Tous les liens de téléchargement
  (individuel et groupé) portent target="_blank" : si jamais rien n'est
  disponible côté serveur au moment du clic, l'erreur s'ouvre dans un
  nouvel onglet plutôt que de remplacer l'application.
- test/helpers.ts : extractOutputDir et waitFor factorisés (dupliqués
  entre deux fichiers de test) ; waitFor accepte maintenant un prédicat
  async (la version précédente, sync uniquement, aurait toujours vu un
  prédicat async comme "vrai" immédiatement - une Promise est truthy).

4 nouveaux tests, dont un qui vérifie la signature zip réelle (PK\x03\x04)
sur une archive construite à partir de vrais fichiers temporaires (pas
juste des mocks) - 65 tests serveur au total. Vérifié aussi sur le
serveur réel en cours d'exécution (pas seulement les tests automatisés) :
GET /api/queue/download-all répond bien 404 sur une file vide, bouton
correctement cablé et masqué côté client.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 23:39:18 +02:00
f67a138cda Merge pull request 'fix(client): page sans overflow, listes scrollables, miniatures agrandies' (#6) from fix/layout-scroll-and-thumbnails into main
All checks were successful
Publier l'image Docker / test (push) Successful in 21s
Publier l'image Docker / publish (push) Successful in 26s
Reviewed-on: #6
2026-09-16 23:24:27 +02:00
d8dc263690 fix(client): page sans overflow, listes scrollables, miniatures agrandies
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
- Layout en hauteur pleine (html/body/.shell à 100%/100vh, overflow
  hidden sur body) : la page ne défile plus jamais. Seules
  download-form__results et queue-panel__list défilent en interne
  (flex:1 1 0 + min-height:0 + overflow-y:auto), propagé depuis .shell
  via min-height:0 à chaque niveau flex descendant.
- .shell__main : ratio de colonnes 3fr/2fr -> 2fr/1fr et max-width
  960px -> 1280px, pour une liste de vidéos plus large.
- Miniatures de résultats agrandies (96x54 -> 144x81, ratio 16:9) et
  titre sur 2 lignes (line-clamp) plutôt qu'une troncature sur une seule
  ligne, pour mieux différencier les vidéos au premier coup d'œil.

Vérifié en navigateur avec des données de test (16 éléments de file,
12 résultats factices injectés) : bodyScrollHeight reste égal à
windowInnerHeight (page sans overflow) alors que les deux listes ont un
scrollHeight très supérieur à leur clientHeight (défilement interne
réel, pas juste visuel).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 23:12:16 +02:00
915a2af0de Merge pull request 'feat: suppression d'éléments de la file, téléchargement groupé' (#5) from feat/queue-remove-and-bulk-download into main
All checks were successful
Publier l'image Docker / test (push) Successful in 23s
Publier l'image Docker / publish (push) Successful in 27s
2026-09-16 22:51:55 +02:00
a96322f64e feat: suppression d'éléments de la file, téléchargement groupé
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
- QueueService.remove() remplace cancel() : supprime l'élément de la
  liste quel que soit son état (annule d'abord s'il était en attente/en
  cours), au lieu de se limiter aux deux cas actifs et renvoyer false
  pour un élément déjà terminé.
- consumeCompletedFile() met fileName à null une fois le fichier livré
  (et son dossier nettoyé) : évite qu'un second appel tente de relire un
  fichier qui n'existe déjà plus.
- QueuePanel : un même bouton retire l'élément quel que soit son état -
  "Annuler" en attente/en cours, "Supprimer" une fois terminé. Bouton
  "Tout télécharger" dans l'en-tête, qui déclenche avec un léger décalage
  chaque téléchargement encore disponible (évite le blocage navigateur
  des téléchargements multiples simultanés).
- Liste de résultats de recherche de chaîne agrandie (320px -> 65vh).

11 nouveaux tests côté serveur (63 -> alignés). Vérifié en navigateur :
ajout, échec, suppression effective (l'élément disparaît de la liste).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 22:50:54 +02:00
36c1038561 Merge pull request 'fix: image Docker glibc + UI plus douce + placeholders génériques' (#4) from fix/docker-glibc-and-ui-polish into main
All checks were successful
Publier l'image Docker / test (push) Successful in 27s
Publier l'image Docker / publish (push) Successful in 3m2s
2026-09-16 22:26:47 +02:00
a73ead75a0 fix: image Docker glibc + UI plus douce + placeholders génériques
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
- server/Dockerfile : base node:22-bookworm-slim au lieu d'alpine. Le
  binaire autonome yt-dlp_linux est lié à la glibc ; sur musl (Alpine),
  spawn échouait en prod avec ENOENT bien que le fichier soit présent et
  exécutable (interpréteur ELF introuvable). Repéré via un rapport de bug
  en conditions réelles. Même choix que mediaDrop pour un souci similaire.
- client/src/styles/base.scss : palette partagée (custom properties CSS),
  fond/texte/bordures adoucis, boutons/inputs cohérents, variante
  "secondary" pour les actions non-principales (Annuler). Remplace le
  rendu par défaut trop contrasté (fond très sombre côté navigateur non
  compensé, rouge vif sur les erreurs).
- Placeholders des champs chaîne/mots-clés rendus génériques
  (@nomdelachaine, mot-clé1/mot-clé2) au lieu d'un exemple spécifique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 22:22:44 +02:00
96460965bd Merge des PR #1, #2, #3 (chaîne empilée) dans main
All checks were successful
Publier l'image Docker / test (push) Successful in 25s
Publier l'image Docker / publish (push) Successful in 42s
Les trois PR (découverte de chaîne, file d'attente/téléchargement,
frontend) avaient été mergées côté Forgejo dans le désordre par rapport
à la pile (traitement asynchrone côté serveur qui n'a pas respecté
l'ordre des appels), laissant main avec seulement le contenu de la
PR #1. Ce merge apporte le contenu complet de la pile (déjà validé :
lint/typecheck/tests/build clean, testé en navigateur réel), via
feature/frontend-ui qui contenait tout linéairement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 21:29:16 +02:00
cbfce996ed fix(server): rendre les chemins par défaut indépendants du cwd
CLIENT_DIST_PATH et TMP_DIR se résolvaient via process.cwd(), donc
uniquement correct si le process est lancé depuis server/. Repéré en
mettant en place la prévisualisation locale (.claude/launch.json, non
versionné) : npm --prefix server run dev depuis la racine du dépôt ne
change pas le cwd du script lancé, `npm --prefix server exec -- node -e
"console.log(process.cwd())"` le confirme (reste la racine du dépôt).

Résout désormais les deux chemins depuis l'emplacement du module
(import.meta.url) plutôt que process.cwd() - invariant quel que soit le
répertoire de lancement, y compris en Docker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 19:27:42 +02:00
1a92322896 Merge pull request 'feat(server): service de découverte de chaîne (GET /api/channel)' (#1) from feature/channel-discovery into main
All checks were successful
Publier l'image Docker / test (push) Successful in 23s
Publier l'image Docker / publish (push) Successful in 37s
Reviewed-on: #1
2026-09-16 09:29:31 +02:00
725c5a5a7f feat(client): frontend (formulaire d'ajout + file d'attente)
Interface web complète : formulaire d'ajout (URL directe ou recherche de
chaîne) et panneau de file d'attente avec sondage de la progression.

- views/downloadForm/ : sélecteur de mode (vidéo/audio/miniature) partagé,
  ajout direct par URL, recherche de chaîne (GET /api/channel) avec ajout
  par résultat. Construction DOM via createElement/textContent (jamais
  d'innerHTML avec du texte venant de YouTube), pas juste par prudence :
  ça évite aussi tout risque d'injection depuis un titre de vidéo.
- views/queuePanel/ : sonde GET /api/queue toutes les 1,5s, affiche statut/
  progression/erreur par élément, annulation et lien de téléchargement
  natif (<a download>, pas de fetch+blob).
- api/ : clients fetch typés (channelApi, queueApi), chaque await dans son
  propre try/catch avec message d'erreur adapté (réseau vs HTTP vs JSON).
- QueueItem gagne un champ title optionnel (purement informatif, jamais
  utilisé pour construire la commande yt-dlp) pour un affichage plus
  parlant qu'une URL brute.
- client/tsconfig.json + build.mjs : cible ES2022 (nécessaire pour
  Error.cause, jusque-là ES2020).
- Vitest ajouté côté client (fonctions pures de src/format.ts ; le câblage
  DOM est vérifié manuellement, pas de jsdom pour l'instant).

Deux bugs réels trouvés et corrigés en testant dans un vrai navigateur
(pas seulement en lisant le code) :
- CSS : un attribut `hidden` sur un conteneur `display: flex` se faisait
  écraser par cette règle (même spécificité, la feuille de l'auteur gagne)
  - les deux panneaux d'options restaient visibles en même temps.
- server/src/routes/channel.ts : ne mappait vers 502 que les YtDlpError
  (code de sortie non nul), pas les erreurs brutes de spawn (ex. binaire
  introuvable - ENOENT), qui remontaient donc en 500 générique. Corrigé +
  test de non-régression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 09:28:49 +02:00
63ef42cc20 feat(server): file d'attente + téléchargement (vidéo/audio/miniature)
Cœur du pipeline : ajoute une file d'attente exécutant les téléchargements
yt-dlp avec une concurrence bornée, et livre le fichier produit au
navigateur (pas de dépôt durable côté serveur).

- downloadPlan.ts : construction des arguments yt-dlp par mode (vidéo avec
  sous-titres/miniature en option, audio dans un format courant, miniature
  seule), toujours avec séparateur `--` avant l'URL. Parsing de la
  progression depuis stdout (`[download] NN.N%`).
- videoUrl.ts : valide qu'une URL cible bien youtube.com/youtu.be avant de
  la passer à yt-dlp - ferme le risque de relais SSRF via l'extracteur
  générique de yt-dlp (des centaines de sites supportés) si une URL
  arbitraire était acceptée.
- QueueService.ts : file en mémoire, N téléchargements simultanés
  (MAX_CONCURRENT_DOWNLOADS), annulation via AbortSignal, nettoyage du
  dossier de sortie après échec/annulation ou après livraison du fichier.
- ytDlpRunner.ts : étendu pour le suivi ligne par ligne de stdout
  (progression) et l'annulation (AbortSignal passé à child_process.spawn).
- routes/queue.ts : POST/GET /api/queue, DELETE /api/queue/:id,
  GET /api/queue/:id/file (streaming + nettoyage après envoi).

45 nouveaux tests (59 au total) : fonctions pures, services (vrai système
de fichiers temporaire, yt-dlp mocké), routes. Vérifié aussi en conditions
réelles (serveur démarré localement) : validation d'URL, ajout en file,
et passage correct à l'état "failed" + nettoyage quand le binaire yt-dlp
est absent (ENOENT).

Empilée sur feature/channel-discovery (réutilise ytDlpRunner.ts).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 09:02:37 +02:00
0e78b44a55 feat(server): service de découverte de chaîne (GET /api/channel)
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
Première brique du pipeline : lister les vidéos/streams d'une chaîne
YouTube (yt-dlp --flat-playlist), filtrables par mots-clés sur le titre.

- ytDlpRunner.ts : exécution de yt-dlp via child_process.spawn (jamais de
  shell, arguments en tableau - exclut l'injection de commande)
- ChannelDiscoveryService.ts : validation stricte du handle/URL de chaîne
  + séparateur `--` avant l'URL (défense en profondeur contre l'injection
  d'options yt-dlp), parsing tolérant des lignes JSON, filtrage mots-clés
- routes/channel.ts : GET /api/channel (validation de schéma Fastify,
  400 si entrée invalide, 502 si yt-dlp échoue)
- 14 tests (unitaires services + intégration route via app.inject) - un
  test de route a révélé une regex de validation trop permissive qu'un
  test de service avait manquée, corrigée avant ce commit

La file d'attente et le téléchargement suivront dans une PR séparée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:05:29 +02:00
29aadd28a4 feat: squelette serveur/client + Docker + CI de build/release
All checks were successful
Publier l'image Docker / test (push) Successful in 30s
Publier l'image Docker / publish (push) Successful in 1m10s
Pose l'infrastructure de base avant l'implémentation des fonctionnalités :
- server/ : Fastify + TypeScript (ESM), route /health, sert client/dist
- client/ : TypeScript vanilla + esbuild/sass, vue placeholder (shell)
- server/Dockerfile : image multi-étapes avec yt-dlp + ffmpeg embarqués
- .forgejo/workflows/publish-docker.yml : test puis publication sur le
  registre de conteneurs Forgejo (nécessite le secret REGISTRY_TOKEN)
- biome.json : lint du repo entier (downloader/ exclu)
- spec/architecture.md : documentation de ces choix

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 07:55:43 +02:00
097b367319 chore: init .gitignore 2026-09-16 07:33:38 +02:00
fa8bcfdca2 docs: init README 2026-09-16 07:33:32 +02:00