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>
55 lines
2.8 KiB
TypeScript
55 lines
2.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { InvalidPlaylistUrlError, assertValidPlaylistUrl } from '../src/services/playlistUrl.js';
|
||
|
||
describe('assertValidPlaylistUrl', () => {
|
||
it.each([
|
||
['https://www.youtube.com/playlist?list=PLKDZ1ig0uz-U', 'PLKDZ1ig0uz-U'],
|
||
['https://youtube.com/playlist?list=PLKDZ1ig0uz-U', 'PLKDZ1ig0uz-U'],
|
||
['https://m.youtube.com/playlist?list=PLKDZ1ig0uz-U', 'PLKDZ1ig0uz-U'],
|
||
['https://music.youtube.com/playlist?list=OLAK5uy_abcDEF123', 'OLAK5uy_abcDEF123'],
|
||
[
|
||
' https://www.youtube.com/playlist?list=UUBR8-60-B28hp2BmDPdntcQ ',
|
||
'UUBR8-60-B28hp2BmDPdntcQ',
|
||
],
|
||
])('accepte %j et retourne l’URL canonique', (input, id) => {
|
||
expect(assertValidPlaylistUrl(input)).toBe(`https://www.youtube.com/playlist?list=${id}`);
|
||
});
|
||
|
||
it('ignore les paramètres annexes, quel que soit leur ordre', () => {
|
||
expect(
|
||
assertValidPlaylistUrl(
|
||
'https://youtube.com/playlist?si=abc123&list=PLxyz_-9&index=4&foo=bar',
|
||
),
|
||
).toBe('https://www.youtube.com/playlist?list=PLxyz_-9');
|
||
});
|
||
|
||
it.each([
|
||
['un schéma http', 'http://www.youtube.com/playlist?list=PLabc'],
|
||
['un autre domaine', 'https://evil.com/playlist?list=PLabc'],
|
||
['un sous-domaine trompeur', 'https://youtube.com.evil.com/playlist?list=PLabc'],
|
||
['un domaine préfixé', 'https://evilyoutube.com/playlist?list=PLabc'],
|
||
['un domaine avec point final', 'https://www.youtube.com./playlist?list=PLabc'],
|
||
['des identifiants dans l’URL', 'https://user:pass@www.youtube.com/playlist?list=PLabc'],
|
||
['un port explicite', 'https://www.youtube.com:8443/playlist?list=PLabc'],
|
||
[
|
||
'un lien de vidéo avec paramètre list',
|
||
'https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PLabc',
|
||
],
|
||
['un lien court avec paramètre list', 'https://youtu.be/dQw4w9WgXcQ?list=PLabc'],
|
||
['un chemin voisin', 'https://www.youtube.com/playlists?list=PLabc'],
|
||
['un chemin plus profond', 'https://www.youtube.com/playlist/extra?list=PLabc'],
|
||
['un paramètre list absent', 'https://www.youtube.com/playlist'],
|
||
['un paramètre list vide', 'https://www.youtube.com/playlist?list='],
|
||
['un identifiant trop court', 'https://www.youtube.com/playlist?list=P'],
|
||
['un identifiant trop long', `https://www.youtube.com/playlist?list=${'a'.repeat(65)}`],
|
||
['un identifiant avec un slash encodé', 'https://www.youtube.com/playlist?list=PL%2Fabc'],
|
||
['un identifiant avec une espace encodée', 'https://www.youtube.com/playlist?list=PL%20abc'],
|
||
['un identifiant ressemblant à une option', 'https://www.youtube.com/playlist?list=--exec=rm'],
|
||
['un schéma javascript', 'javascript:alert(1)'],
|
||
['un schéma file', 'file:///etc/passwd'],
|
||
['du texte quelconque', 'pas une url'],
|
||
['une chaîne vide', ''],
|
||
])('rejette %s', (_label, input) => {
|
||
expect(() => assertValidPlaylistUrl(input)).toThrow(InvalidPlaylistUrlError);
|
||
});
|
||
});
|