youtubeDownloader/server/test/playlistRoute.test.ts
Nicolas f0dec86b19
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
feat: playlists YouTube (parcourir, ajouter une par une ou d'un coup)
Coller un lien de playlist (youtube.com/playlist?list=...) dans le champ
URL affiche toutes ses videos dans la meme zone de resultats que la
recherche de chaine ; chaque ligne a son bouton "Ajouter", et un bouton
"Tout ajouter a la file" ajoute le reste d'un coup.

Serveur :
- GET /api/playlist (PlaylistDiscoveryService) : yt-dlp --flat-playlist
  -J. -J plutot que -j : le titre de la playlist n'existe qu'a la racine,
  et une playlist vide reste un resultat valide.
- playlistUrl.ts : n'accepte que les liens youtube.com/playlist?list=<id>
  et reconstruit l'URL canonique depuis le seul identifiant valide (rien
  d'autre de l'entree n'atteint yt-dlp). Un lien watch?v=...&list=...
  reste une video (--no-playlist etait deja passe au telechargement).
- videoSummary.ts : conversion d'une entree --flat-playlist en resume de
  video, extraite de ChannelDiscoveryService pour etre partagee (chaine
  et playlist parsent desormais identiquement). ChannelVideoSummary
  devient VideoSummary (et ChannelVideoDto -> VideoDto cote client).

Client :
- Le champ URL accepte video ou playlist (isPlaylistUrl aiguille) ; zone
  de resultats partagee (createResultsView).
- "Tout ajouter" : POST /api/queue sequentiels (ordre conserve, pas de
  doublon avec les lignes deja ajoutees). Un echec n'interrompt pas le
  lot : nombre d'echecs + premier message affiches, seules les lignes en
  echec redeviennent ajoutables. Le mode est lu une seule fois pour tout
  le lot.
- Miniatures de resultats en loading="lazy" (une playlist peut compter
  des centaines de videos).

Verifie contre le vrai yt-dlp (2026.07.04) : forme reelle de -J relevee
(fixtures de test calquees dessus), playlist de 1607 videos listee en
~11 s (delai fixe a 120 s). Teste dans le navigateur reel : playlist de
13 videos, ajout unitaire, "tout ajouter" (13 elements, aucun doublon),
echec partiel simule + reprise, playlist inexistante, lien video avec
list=, recherche de chaine (non-regression).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 14:55:38 +02:00

117 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Fastify from 'fastify';
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('../src/services/ytDlpRunner.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../src/services/ytDlpRunner.js')>();
return { ...actual, runYtDlp: vi.fn() };
});
import { playlistRoutes } from '../src/routes/playlist.js';
import { YtDlpError, runYtDlp } from '../src/services/ytDlpRunner.js';
const validUrl = encodeURIComponent('https://www.youtube.com/playlist?list=PLKDZ1ig0uz-U');
beforeEach(() => {
vi.mocked(runYtDlp).mockReset();
});
describe('GET /api/playlist', () => {
it('retourne le titre et les vidéos d’une playlist valide', async () => {
vi.mocked(runYtDlp).mockResolvedValue(
JSON.stringify({
_type: 'playlist',
title: 'Ma playlist',
entries: [
{
id: 'v1',
title: 'Vidéo 1',
url: 'https://www.youtube.com/watch?v=v1',
duration: 90,
thumbnails: [{ url: 'https://x/v1.jpg' }],
},
],
}),
);
const app = Fastify();
await app.register(playlistRoutes);
const response = await app.inject({ method: 'GET', url: `/api/playlist?url=${validUrl}` });
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({
title: 'Ma playlist',
videos: [
{
id: 'v1',
title: 'Vidéo 1',
url: 'https://www.youtube.com/watch?v=v1',
thumbnailUrl: 'https://x/v1.jpg',
durationSeconds: 90,
uploadDate: null,
},
],
});
});
it.each([
['un domaine étranger', 'https://evil.com/playlist?list=PLabc'],
['un lien de vidéo', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'],
['un lien de vidéo avec list', 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLabc'],
['du texte quelconque', 'pas une url'],
])('renvoie 400 pour %s, sans appeler yt-dlp', async (_label, url) => {
const app = Fastify();
await app.register(playlistRoutes);
const response = await app.inject({
method: 'GET',
url: `/api/playlist?url=${encodeURIComponent(url)}`,
});
expect(response.statusCode).toBe(400);
expect(runYtDlp).not.toHaveBeenCalled();
});
it('renvoie 400 si le paramètre url est manquant (validation de schéma)', async () => {
const app = Fastify();
await app.register(playlistRoutes);
const response = await app.inject({ method: 'GET', url: '/api/playlist' });
expect(response.statusCode).toBe(400);
});
it('renvoie 502 quand yt-dlp échoue (ex. playlist introuvable)', async () => {
vi.mocked(runYtDlp).mockRejectedValue(new YtDlpError(1, 'The playlist does not exist.'));
const app = Fastify();
await app.register(playlistRoutes);
const response = await app.inject({ method: 'GET', url: `/api/playlist?url=${validUrl}` });
expect(response.statusCode).toBe(502);
});
it('renvoie 502 (pas 500) quand le binaire yt-dlp est introuvable (ENOENT)', async () => {
const spawnError = Object.assign(new Error('spawn yt-dlp ENOENT'), { code: 'ENOENT' });
vi.mocked(runYtDlp).mockRejectedValue(spawnError);
const app = Fastify();
await app.register(playlistRoutes);
const response = await app.inject({ method: 'GET', url: `/api/playlist?url=${validUrl}` });
expect(response.statusCode).toBe(502);
});
it('renvoie 502 (pas 500) quand la sortie de yt-dlp est illisible', async () => {
vi.mocked(runYtDlp).mockResolvedValue('pas du JSON');
const app = Fastify();
await app.register(playlistRoutes);
const response = await app.inject({ method: 'GET', url: `/api/playlist?url=${validUrl}` });
expect(response.statusCode).toBe(502);
});
});