- 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>
138 lines
4.4 KiB
TypeScript
138 lines
4.4 KiB
TypeScript
/** État d'avancement d'un élément de la file d'attente (miroir du DTO serveur). */
|
|
export type QueueItemStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
|
|
/** Mode de téléchargement d'un élément (miroir du DTO serveur). */
|
|
export type QueueItemMode =
|
|
| { kind: 'video'; options: { subtitles: boolean; embedThumbnail: boolean } }
|
|
| { kind: 'audio'; format: 'mp3' | 'm4a' | 'opus' }
|
|
| { kind: 'thumbnail' };
|
|
|
|
/** Élément de la file d'attente, tel que renvoyé par le serveur. */
|
|
export interface QueueItemDto {
|
|
id: string;
|
|
url: string;
|
|
title: string | null;
|
|
mode: QueueItemMode;
|
|
status: QueueItemStatus;
|
|
progress: number;
|
|
error: string | null;
|
|
fileName: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
/** Entrée acceptée par {@link enqueueItem} pour choisir le mode et ses options. */
|
|
export type EnqueueMode =
|
|
| { mode: 'video'; subtitles: boolean; embedThumbnail: boolean }
|
|
| { mode: 'audio'; audioFormat: 'mp3' | 'm4a' | 'opus' }
|
|
| { mode: 'thumbnail' };
|
|
|
|
/**
|
|
* Extrait le message d'erreur d'une réponse HTTP en échec, avec un message
|
|
* de repli si le corps n'est pas un JSON exploitable.
|
|
*
|
|
* @param response - Réponse HTTP dont le statut n'est pas OK.
|
|
* @returns Le message d'erreur à afficher.
|
|
*/
|
|
async function readErrorMessage(response: Response): Promise<string> {
|
|
try {
|
|
const body = (await response.json()) as { error?: string };
|
|
return body.error ?? `Échec de la requête (${response.status}).`;
|
|
} catch {
|
|
return `Échec de la requête (${response.status}).`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ajoute un élément à la file d'attente via `POST /api/queue`.
|
|
*
|
|
* @param url - URL de la vidéo YouTube à télécharger.
|
|
* @param mode - Mode de téléchargement et ses options.
|
|
* @param title - Titre connu de la vidéo, s'il y en a un (affichage uniquement).
|
|
* @returns L'élément créé.
|
|
* @throws {Error} Si la requête échoue ou si le serveur refuse l'ajout.
|
|
*/
|
|
export async function enqueueItem(
|
|
url: string,
|
|
mode: EnqueueMode,
|
|
title: string | null,
|
|
): Promise<QueueItemDto> {
|
|
let response: Response;
|
|
try {
|
|
response = await fetch('/api/queue', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ url, title: title ?? undefined, ...mode }),
|
|
});
|
|
} catch (error) {
|
|
throw new Error('Impossible de contacter le serveur.', { cause: error });
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(await readErrorMessage(response));
|
|
}
|
|
|
|
try {
|
|
const body = (await response.json()) as { item: QueueItemDto };
|
|
return body.item;
|
|
} catch (error) {
|
|
throw new Error('Réponse du serveur invalide.', { cause: error });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Liste tous les éléments de la file d'attente via `GET /api/queue`.
|
|
*
|
|
* @returns Les éléments de la file, dans leur ordre d'ajout.
|
|
* @throws {Error} Si la requête échoue.
|
|
*/
|
|
export async function listQueueItems(): Promise<QueueItemDto[]> {
|
|
let response: Response;
|
|
try {
|
|
response = await fetch('/api/queue');
|
|
} catch (error) {
|
|
throw new Error('Impossible de contacter le serveur.', { cause: error });
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(await readErrorMessage(response));
|
|
}
|
|
|
|
try {
|
|
const body = (await response.json()) as { items: QueueItemDto[] };
|
|
return body.items;
|
|
} catch (error) {
|
|
throw new Error('Réponse du serveur invalide.', { cause: error });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Supprime un élément de la file d'attente via `DELETE /api/queue/:id`,
|
|
* quel que soit son état (l'annule d'abord s'il était en attente/en cours).
|
|
*
|
|
* @param id - Identifiant de l'élément à supprimer.
|
|
* @throws {Error} Si la requête échoue ou si l'élément est introuvable.
|
|
*/
|
|
export async function removeQueueItem(id: string): Promise<void> {
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(`/api/queue/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
} catch (error) {
|
|
throw new Error('Impossible de contacter le serveur.', { cause: error });
|
|
}
|
|
|
|
if (!response.ok && response.status !== 204) {
|
|
throw new Error(await readErrorMessage(response));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Construit l'URL de téléchargement du fichier produit par un élément
|
|
* terminé (`GET /api/queue/:id/file`) — destinée à un lien `<a>`, pas à un
|
|
* appel `fetch` : c'est le navigateur qui doit gérer le téléchargement.
|
|
*
|
|
* @param id - Identifiant de l'élément terminé.
|
|
* @returns L'URL à utiliser comme `href`.
|
|
*/
|
|
export function queueItemFileUrl(id: string): string {
|
|
return `/api/queue/${encodeURIComponent(id)}/file`;
|
|
}
|