feat: recherche YouTube globale, avec défilement infini #11

Merged
kyuno merged 1 commit from feature/youtube-search into feature/playlist-support 2026-09-19 15:49:45 +02:00
Owner

Résumé

Empilée sur #10 (toujours ouverte) — à merger après elle.

Nouveau formulaire « Recherche YouTube » (mots-clés + « Limite » optionnelle), branché sur la zone de résultats existante : miniatures, durée, bouton « Ajouter » par ligne. Pas de « Tout ajouter » pour la recherche, comme demandé.

  • Avec une limite : les N résultats arrivent d'un coup, sans défilement infini.
  • Sans limite : défilement infini. La liste se prolonge quand on atteint sa fin, jusqu'à ce que YouTube n'ait plus rien à donner (~590 résultats), avec « Chargement… », une erreur avec « Réessayer » (une page en échec ne fait pas perdre les précédentes), puis « Fin des résultats ».

Pourquoi une « session » côté serveur (mesuré avec le vrai yt-dlp)

La version naïve — relancer ytsearchN: pour chaque page et trancher — n'est pas viable :

Constat Mesure
L'ordre de YouTube n'est pas déterministe, même pour deux appels identiques 5 résultats sur 20 à la même position ; 69 % de recouvrement sur 100
Conséquence d'une pagination sans état 15 à 30 % de doublons dès la 3e-4e page, et des résultats jamais montrés
Profondeur YouTube s'arrête vers ~590 résultats, même en demandant 1000
Doublons internes ~7 % (dédoublonnés à l'ingestion)
-j sans --lazy-playlist rien n'est imprimé avant la fin du parcours (~23 s pour 1000) ; avec, 20 résultats en ~2,3 s

Donc : un seul yt-dlp par recherche (--flat-playlist --lazy-playlist -j), dont les pages sont servies depuis un tampon → ordre cohérent, aucun doublon, chaque résultat récupéré une fois.

Le parcours n'avance qu'à la demande : quand le tampon a 20 résultats d'avance, la lecture de stdout est suspendue (nouvelle option onSpawn de runYtDlp). Le tube se remplit et yt-dlp bloque sur son écriture (mesuré : plus aucune ligne pendant 10 s de pause). On n'aspire donc pas ~590 résultats — ni ~30 requêtes vers YouTube — quand l'utilisateur ne défile pas.

API

  • POST /api/search { query, max?, limit? } → 201 { id, videos, nextOffset }
  • GET /api/search/:id?offset=&limit= → 200 { videos, nextOffset }
  • DELETE /api/search/:id → 204

nextOffset: null = fin. 400 requête invalide ou hors bornes, 404 recherche inconnue/expirée (« relancez-la »), 502 yt-dlp échoue avant le moindre résultat.

Cycle de vie des sessions

Chaque recherche ouverte garde un processus yt-dlp (suspendu) : 4 simultanées max (la moins récemment utilisée est fermée), expiration à 10 min d'inactivité / 30 min de vie, fermeture dès que la dernière page est servie, sur DELETE (liste remplacée côté client), au pagehide (fermeture/rechargement de l'onglet), et à l'arrêt du serveur. La requête est validée (1–200 caractères, sans caractère de contrôle) et transmise en un seul argument ytsearchN:<requête> : jamais interprétable comme une option, jamais un moyen de faire récupérer une autre URL.

À savoir

  • Place verticale : ce troisième formulaire laissait à la liste ~104 px (une seule ligne) à 1366×650. J'ai ajouté un filet : une liste non vide garde min-height: 13rem, et la colonne du formulaire défile au lieu de rogner (la page, elle, ne défile toujours jamais). Mesuré : 1920×950 → liste de 406 px, colonne fixe ; 1366×650 → 208 px, colonne défilante ; 800×450 → tout reste atteignable. Si trois entrées empilées te paraissent trop, des onglets (URL / recherche / chaîne) libéreraient ~130 px — dis-le-moi.
  • Sous Windows, tuer le parent d'un exécutable PyInstaller n'arrête pas son enfant : après une annulation il finit son parcours (~30 s) puis s'arrête seul. Transitoire et propre au poste de dev ; sous Linux (prod) le lanceur transmet SIGTERM à l'enfant, comme pour l'annulation de téléchargements existante.
  • Les résultats de recherche n'affichent pas le nom de la chaîne (utile en recherche globale quand plusieurs vidéos portent le même titre) : facile à ajouter si tu le veux.

Vérifications

  • npm run lint (Biome) + typecheck + build serveur/client : clean ; tests : 189 serveur (dont 62 nouveaux : session, service, route, validation, onSpawn) + 35 client.
  • Contre le vrai yt-dlp : page déjà tamponnée en 4 ms, page 200 demandée après 12 s d'inactivité en ~4 s (preuve que la suspension tient) ; parcours complet en 29 pages → 576 vidéos, 576 uniques ; plus aucun processus à la fin, sur DELETE, ni au plafond (max) ; requêtes invalides → 400/404.
  • Dans le navigateur : 580 résultats sans doublon jusqu'à « Fin des résultats » (observateur déconnecté, session fermée par le serveur) ; deux signaux « fin de liste visible » simultanés → une seule requête de page ; échec de page + « Réessayer » ; session expirée (message du serveur affiché, résultats conservés) ; nouvelle recherche → l'ancienne session est fermée côté serveur (journal : « fermée par le client ») ; mode limite (12 résultats, pas de sentinelle) ; ajout d'un résultat à la file (titre + miniature) ; pagehide ; playlist et chaîne en non-régression.
  • ⚠️ Limite de la vérification : le volet de navigation n'affichait plus de frames pendant les tests (requestAnimationFrame et même un IntersectionObserver trivial ne se déclenchaient plus), j'ai donc piloté le défilement avec un observateur factice (mêmes appels observe/unobserve/disconnect, déclenchement à la demande). Le IntersectionObserver natif n'a pas été exercé en conditions réelles — usage standard (racine = élément défilant, marge, réarmement par unobserve/observe), mais à confirmer d'un coup d'œil dans ton navigateur : lancer une recherche sans limite et faire défiler la liste.

🤖 Generated with Claude Code

## Résumé Empilée sur #10 (toujours ouverte) — à merger après elle. Nouveau formulaire **« Recherche YouTube »** (mots-clés + « Limite » optionnelle), branché sur la zone de résultats existante : miniatures, durée, bouton « Ajouter » par ligne. **Pas de « Tout ajouter »** pour la recherche, comme demandé. - **Avec une limite** : les N résultats arrivent d'un coup, sans défilement infini. - **Sans limite** : défilement infini. La liste se prolonge quand on atteint sa fin, jusqu'à ce que YouTube n'ait plus rien à donner (~590 résultats), avec « Chargement… », une erreur avec « Réessayer » (une page en échec ne fait pas perdre les précédentes), puis « Fin des résultats ». ## Pourquoi une « session » côté serveur (mesuré avec le vrai yt-dlp) La version naïve — relancer `ytsearchN:` pour chaque page et trancher — n'est pas viable : | Constat | Mesure | |---|---| | L'ordre de YouTube n'est **pas déterministe, même pour deux appels identiques** | 5 résultats sur 20 à la même position ; 69 % de recouvrement sur 100 | | Conséquence d'une pagination sans état | 15 à 30 % de doublons dès la 3e-4e page, et des résultats jamais montrés | | Profondeur | YouTube s'arrête vers **~590 résultats**, même en demandant 1000 | | Doublons internes | ~7 % (dédoublonnés à l'ingestion) | | `-j` sans `--lazy-playlist` | rien n'est imprimé avant la fin du parcours (~23 s pour 1000) ; avec, 20 résultats en ~2,3 s | Donc : **un seul yt-dlp par recherche** (`--flat-playlist --lazy-playlist -j`), dont les pages sont servies depuis un tampon → ordre cohérent, aucun doublon, chaque résultat récupéré une fois. **Le parcours n'avance qu'à la demande** : quand le tampon a 20 résultats d'avance, la lecture de stdout est suspendue (nouvelle option `onSpawn` de `runYtDlp`). Le tube se remplit et yt-dlp bloque sur son écriture (mesuré : plus aucune ligne pendant 10 s de pause). On n'aspire donc pas ~590 résultats — ni ~30 requêtes vers YouTube — quand l'utilisateur ne défile pas. ## API - `POST /api/search` `{ query, max?, limit? }` → `201 { id, videos, nextOffset }` - `GET /api/search/:id?offset=&limit=` → `200 { videos, nextOffset }` - `DELETE /api/search/:id` → `204` `nextOffset: null` = fin. `400` requête invalide ou hors bornes, `404` recherche inconnue/expirée (« relancez-la »), `502` yt-dlp échoue avant le moindre résultat. ## Cycle de vie des sessions Chaque recherche ouverte garde un processus yt-dlp (suspendu) : 4 simultanées max (la moins récemment utilisée est fermée), expiration à 10 min d'inactivité / 30 min de vie, fermeture dès que la dernière page est servie, sur `DELETE` (liste remplacée côté client), au `pagehide` (fermeture/rechargement de l'onglet), et à l'arrêt du serveur. La requête est validée (1–200 caractères, sans caractère de contrôle) et transmise en **un seul argument** `ytsearchN:<requête>` : jamais interprétable comme une option, jamais un moyen de faire récupérer une autre URL. ## À savoir - **Place verticale** : ce troisième formulaire laissait à la liste ~104 px (une seule ligne) à 1366×650. J'ai ajouté un filet : une liste non vide garde `min-height: 13rem`, et la colonne du formulaire défile au lieu de rogner (la page, elle, ne défile toujours jamais). Mesuré : 1920×950 → liste de 406 px, colonne fixe ; 1366×650 → 208 px, colonne défilante ; 800×450 → tout reste atteignable. **Si trois entrées empilées te paraissent trop, des onglets (URL / recherche / chaîne) libéreraient ~130 px** — dis-le-moi. - Sous **Windows**, tuer le parent d'un exécutable PyInstaller n'arrête pas son enfant : après une annulation il finit son parcours (~30 s) puis s'arrête seul. Transitoire et propre au poste de dev ; sous Linux (prod) le lanceur transmet SIGTERM à l'enfant, comme pour l'annulation de téléchargements existante. - Les résultats de recherche n'affichent pas le nom de la chaîne (utile en recherche globale quand plusieurs vidéos portent le même titre) : facile à ajouter si tu le veux. ## Vérifications - `npm run lint` (Biome) + typecheck + build serveur/client : clean ; tests : **189 serveur** (dont 62 nouveaux : session, service, route, validation, `onSpawn`) + **35 client**. - **Contre le vrai yt-dlp** : page déjà tamponnée en **4 ms**, page 200 demandée après 12 s d'inactivité en **~4 s** (preuve que la suspension tient) ; parcours complet en 29 pages → **576 vidéos, 576 uniques** ; plus aucun processus à la fin, sur `DELETE`, ni au plafond (`max`) ; requêtes invalides → 400/404. - **Dans le navigateur** : 580 résultats **sans doublon** jusqu'à « Fin des résultats » (observateur déconnecté, session fermée par le serveur) ; deux signaux « fin de liste visible » simultanés → **une seule** requête de page ; échec de page + « Réessayer » ; session expirée (message du serveur affiché, résultats conservés) ; nouvelle recherche → l'ancienne session est fermée côté serveur (journal : « fermée par le client ») ; mode limite (12 résultats, pas de sentinelle) ; ajout d'un résultat à la file (titre + miniature) ; `pagehide` ; playlist et chaîne en non-régression. - ⚠️ Limite de la vérification : le volet de navigation n'affichait plus de frames pendant les tests (`requestAnimationFrame` et même un `IntersectionObserver` trivial ne se déclenchaient plus), j'ai donc piloté le défilement avec un **observateur factice** (mêmes appels `observe/unobserve/disconnect`, déclenchement à la demande). Le `IntersectionObserver` natif n'a pas été exercé en conditions réelles — usage standard (racine = élément défilant, marge, réarmement par `unobserve`/`observe`), mais à confirmer d'un coup d'œil dans ton navigateur : lancer une recherche sans limite et faire défiler la liste. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
kyuno added 1 commit 2026-09-19 15:44:20 +02:00
Nouveau formulaire "Recherche YouTube" (mots-cles + limite optionnelle),
branche sur la zone de resultats existante (miniatures, bouton "Ajouter"
par ligne ; pas de "Tout ajouter", comme demande).

- Avec une limite : tous les resultats d'un coup (une seule page).
- Sans limite : defilement infini. Un element sentinelle en fin de liste,
  observe par un IntersectionObserver (racine = la liste, marge 300 px),
  charge la page suivante ; il est rearme apres chaque page, affiche
  chargement / erreur avec "Reessayer" / "Fin des resultats", et une page
  en echec ne fait pas perdre les precedentes.

Serveur (GET/POST/DELETE /api/search) : une "session" = un seul yt-dlp
(--flat-playlist --lazy-playlist -j -- ytsearch1000:<requete>) servi page
par page depuis un tampon. Pourquoi pas un appel yt-dlp par page - mesure
avec le vrai yt-dlp : l'ordre des resultats de YouTube n'est pas
deterministe meme pour deux appels identiques (5/20 aux memes positions,
69 % de recouvrement sur 100), donc paginer en relancant la recherche
donnait 15 a 30 % de doublons des la 3e-4e page et des trous ; YouTube
s'arrete vers ~590 resultats ; ~7 % de doublons internes (dedoublonnes a
l'ingestion) ; sans --lazy-playlist yt-dlp n'imprime rien avant la fin
(~23 s), avec il emet au fil des pages (20 en ~2,3 s).

Le parcours n'avance qu'a la demande : quand le tampon a 20 resultats
d'avance, la lecture de stdout est suspendue (nouvelle option onSpawn de
runYtDlp) ; le tube se remplit et yt-dlp bloque (mesure : plus aucune
ligne pendant 10 s de pause). On n'aspire donc pas ~590 resultats, et ~30
requetes vers YouTube, quand l'utilisateur ne defile pas.

Cycle de vie : 4 sessions simultanees max (la moins recemment utilisee est
fermee), expiration a 10 min d'inactivite / 30 min de vie, fermeture des
que la derniere page est servie, sur DELETE (liste remplacee cote client),
au pagehide du client, et a l'arret du serveur. Requete validee (1 a 200
caracteres, sans caractere de controle) et transmise en un seul argument
ytsearchN:<requete>, jamais interpretable comme une option.

Mise en page : ce troisieme formulaire laissait a la liste ~104 px (une
ligne) a 1366x650. Une liste non vide garde min-height 13rem et la colonne
du formulaire defile au lieu de rogner (la page ne defile toujours pas).

Verifie : contre le vrai yt-dlp (page tamponnee en 4 ms, page 200 apres
12 s d'inactivite en ~4 s : la suspension tient ; 29 pages -> 576 videos,
576 uniques ; processus fermes a la fin / sur DELETE / au plafond) et dans
le navigateur (580 resultats sans doublon jusqu'a "Fin des resultats",
requetes de page uniques malgre deux signaux simultanes, echec + reprise,
session expiree, remplacement de la recherche, mode limite, ajout d'un
resultat a la file, pagehide, playlist et chaine en non-regression). Le
volet IntersectionObserver a ete pilote par un observateur factice : le
panneau de navigation n'affichait plus de frames (rAF/IO natifs muets).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kyuno merged commit da0fff12f1 into feature/playlist-support 2026-09-19 15:49:45 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: kyuno/youtubeDownloader#11
No description provided.