/** É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 { 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 { 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 { 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 }); } } /** * Annule un élément de la file d'attente via `DELETE /api/queue/:id`. * * @param id - Identifiant de l'élément à annuler. * @throws {Error} Si la requête échoue ou si l'élément est introuvable. */ export async function cancelQueueItem(id: string): Promise { 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 ``, 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`; }