- Added filtering by genre and format with dropdown selects - Added sorting by date, title, and popularity with order toggle - Added bulk operations: select multiple tracks, bulk delete, bulk update - Added bulk mode toggle with selection checkboxes - Added batch delete and batch update API functions - Added pagination controls - Improved UI with filter bar and sort dropdown - Added toast notifications for operations - Added select all/deselect all functionality
842 lines
22 KiB
TypeScript
842 lines
22 KiB
TypeScript
import { apiClient } from '@/services/api/client';
|
|
import { Track, TrackStatus } from '../types/track';
|
|
import { AxiosError, AxiosProgressEvent } from 'axios';
|
|
|
|
/**
|
|
* Track API
|
|
* API layer pour l'upload et la récupération de tracks
|
|
* Utilise FormData pour l'upload et gère la pagination
|
|
*/
|
|
|
|
export interface TrackMetadata {
|
|
title?: string;
|
|
artist?: string;
|
|
album?: string;
|
|
genre?: string;
|
|
year?: number;
|
|
is_public?: boolean;
|
|
}
|
|
|
|
export interface UpdateTrackRequest {
|
|
title?: string;
|
|
artist?: string;
|
|
album?: string;
|
|
genre?: string;
|
|
year?: number;
|
|
is_public?: boolean;
|
|
}
|
|
|
|
export interface TrackStats {
|
|
// Structure à définir selon l'implémentation backend
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface TrackHistory {
|
|
// Structure à définir selon l'implémentation backend
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface CreateShareRequest {
|
|
permissions: 'read' | 'write' | 'admin';
|
|
expires_at?: string;
|
|
}
|
|
|
|
export interface Share {
|
|
id: string;
|
|
track_id: string;
|
|
token: string;
|
|
permissions: string;
|
|
expires_at?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface TrackLikesResponse {
|
|
count: number;
|
|
is_liked: boolean;
|
|
}
|
|
|
|
export interface GetTracksParams {
|
|
page?: number;
|
|
limit?: number;
|
|
userId?: string;
|
|
genre?: string;
|
|
format?: string;
|
|
sortBy?: 'created_at' | 'title' | 'popularity';
|
|
sortOrder?: 'asc' | 'desc';
|
|
}
|
|
|
|
export interface GetTracksResponse {
|
|
tracks: Track[];
|
|
pagination: {
|
|
page: number;
|
|
limit: number;
|
|
total: number;
|
|
total_pages: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Interface pour la réponse 202 Accepted lors d'un upload async
|
|
*/
|
|
interface AsyncUploadResponse {
|
|
track_id: string;
|
|
status: TrackStatus;
|
|
status_url: string;
|
|
message: string;
|
|
}
|
|
|
|
/**
|
|
* Poll le statut d'un track jusqu'à ce qu'il soit completed ou failed
|
|
* @param trackId ID du track à poller
|
|
* @param onProgress Callback optionnel pour suivre la progression
|
|
* @returns Le track complet une fois terminé
|
|
* @throws Error si le track échoue ou si le polling timeout
|
|
*/
|
|
async function pollTrackStatus(
|
|
trackId: string,
|
|
onProgress?: (progress: number) => void,
|
|
): Promise<Track> {
|
|
const maxAttempts = 120; // 2 minutes max (1 seconde par tentative)
|
|
const pollInterval = 1000; // 1 seconde entre chaque tentative
|
|
let attempts = 0;
|
|
|
|
while (attempts < maxAttempts) {
|
|
try {
|
|
// Récupérer le track pour vérifier son status
|
|
const response = await apiClient.get<Track>(`/tracks/${trackId}`);
|
|
const track = response.data;
|
|
|
|
// Mettre à jour la progression si callback fourni
|
|
if (onProgress) {
|
|
// Estimer la progression basée sur le status
|
|
let progress = 0;
|
|
if (track.status === 'uploading') {
|
|
progress = 30; // Upload en cours
|
|
} else if (track.status === 'processing') {
|
|
progress = 70; // Traitement en cours
|
|
} else if (track.status === 'completed') {
|
|
progress = 100; // Terminé
|
|
}
|
|
onProgress(progress);
|
|
}
|
|
|
|
// Si le track est terminé (completed ou failed), le retourner
|
|
if (track.status === 'completed') {
|
|
return track;
|
|
}
|
|
if (track.status === 'failed') {
|
|
throw new Error(
|
|
track.status_message ||
|
|
'Le traitement du fichier a échoué',
|
|
);
|
|
}
|
|
|
|
// Attendre avant le prochain poll
|
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
attempts++;
|
|
} catch (error) {
|
|
// Si c'est une erreur 404, le track n'existe pas encore, continuer à poller
|
|
if (error instanceof AxiosError && error.response?.status === 404) {
|
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
attempts++;
|
|
continue;
|
|
}
|
|
// Sinon, propager l'erreur
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Timeout après maxAttempts
|
|
throw new Error(
|
|
'Le traitement du fichier prend plus de temps que prévu. Veuillez réessayer plus tard.',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Upload un fichier audio avec métadonnées
|
|
* @param file Fichier audio à uploader
|
|
* @param metadata Métadonnées optionnelles du track
|
|
* @param onProgress Callback optionnel pour suivre la progression
|
|
* @returns Le track créé
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function uploadTrack(
|
|
file: File,
|
|
metadata: TrackMetadata = {},
|
|
onProgress?: (progress: number) => void,
|
|
): Promise<Track> {
|
|
try {
|
|
const formData = new FormData();
|
|
// Le backend attend un champ nommé 'file'
|
|
formData.append('file', file);
|
|
|
|
// Ajouter les métadonnées au FormData
|
|
if (metadata.title) {
|
|
formData.append('title', metadata.title);
|
|
}
|
|
if (metadata.artist) {
|
|
formData.append('artist', metadata.artist);
|
|
}
|
|
if (metadata.album) {
|
|
formData.append('album', metadata.album);
|
|
}
|
|
if (metadata.genre) {
|
|
formData.append('genre', metadata.genre);
|
|
}
|
|
if (metadata.year) {
|
|
formData.append('year', metadata.year.toString());
|
|
}
|
|
if (metadata.is_public !== undefined) {
|
|
formData.append('is_public', metadata.is_public.toString());
|
|
}
|
|
|
|
// Construction de la config pour l'upload
|
|
// NOTE: Le token est automatiquement ajouté par l'interceptor de apiClient
|
|
// (voir apps/web/src/services/api/client.ts)
|
|
// CRITIQUE: Ne pas définir Content-Type - Axios le calcule automatiquement
|
|
// avec le boundary correct pour multipart/form-data
|
|
const config = {
|
|
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
|
|
if (progressEvent.total && onProgress) {
|
|
// Pour l'upload initial, on limite à 20% (le reste sera géré par le polling)
|
|
const uploadProgress = Math.round(
|
|
(progressEvent.loaded * 100) / progressEvent.total,
|
|
);
|
|
onProgress(Math.min(uploadProgress * 0.2, 20));
|
|
}
|
|
},
|
|
timeout: 300000, // 5 minutes timeout for large files
|
|
};
|
|
|
|
const response = await apiClient.post<Track | AsyncUploadResponse>(
|
|
'/tracks',
|
|
formData,
|
|
config,
|
|
);
|
|
|
|
// Vérifier si c'est une réponse 202 Accepted (upload async)
|
|
if (response.status === 202) {
|
|
// Extraire le track_id de la réponse
|
|
const asyncResponse = response.data as AsyncUploadResponse;
|
|
const trackId = asyncResponse.track_id;
|
|
|
|
// Mettre à jour la progression (upload terminé, traitement en cours)
|
|
if (onProgress) {
|
|
onProgress(25);
|
|
}
|
|
|
|
// Poller le statut jusqu'à completion
|
|
return await pollTrackStatus(trackId, onProgress);
|
|
}
|
|
|
|
// Sinon, c'est une réponse 201 Created avec le Track complet
|
|
return response.data as Track;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
// Gérer les erreurs spécifiques selon le code de statut HTTP
|
|
if (error.response?.status === 400) {
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
'Format ou taille invalide';
|
|
throw new Error(errorMessage);
|
|
}
|
|
if (error.response?.status === 413) {
|
|
throw new Error('Fichier trop volumineux');
|
|
}
|
|
if (error.response?.status === 415) {
|
|
throw new Error('Format de fichier non supporté');
|
|
}
|
|
|
|
if (error.response?.status === 500) {
|
|
throw new Error(
|
|
'Erreur serveur: Impossible de traiter le fichier. Veuillez réessayer plus tard.',
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new Error(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
"Erreur lors de l'upload";
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère la liste paginée des tracks
|
|
* @param page Numéro de page (défaut: 1)
|
|
* @param limit Nombre d'éléments par page (défaut: 20)
|
|
* @param params Paramètres optionnels supplémentaires
|
|
* @returns La liste des tracks avec les métadonnées de pagination
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getTracks(
|
|
page: number = 1,
|
|
limit: number = 20,
|
|
params?: Omit<GetTracksParams, 'page' | 'limit'>,
|
|
): Promise<GetTracksResponse> {
|
|
try {
|
|
const queryParams = new URLSearchParams();
|
|
queryParams.append('page', page.toString());
|
|
queryParams.append('limit', limit.toString());
|
|
|
|
if (params?.userId !== undefined) {
|
|
queryParams.append('user_id', params.userId.toString());
|
|
}
|
|
if (params?.genre) {
|
|
queryParams.append('genre', params.genre);
|
|
}
|
|
if (params?.format) {
|
|
queryParams.append('format', params.format);
|
|
}
|
|
if (params?.sortBy) {
|
|
queryParams.append('sort_by', params.sortBy);
|
|
}
|
|
if (params?.sortOrder) {
|
|
queryParams.append('sort_order', params.sortOrder);
|
|
}
|
|
|
|
const queryString = queryParams.toString();
|
|
const url = `/tracks${queryString ? `?${queryString}` : ''}`;
|
|
|
|
const response = await apiClient.get<GetTracksResponse>(url);
|
|
return response.data;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
|
|
if (error.response?.status === 500) {
|
|
throw new Error(
|
|
'Erreur serveur: Impossible de récupérer les tracks. Veuillez réessayer plus tard.',
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new Error(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec de la récupération des tracks';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Met à jour les métadonnées d'un track
|
|
* @param trackId ID du track à mettre à jour
|
|
* @param updates Métadonnées à mettre à jour
|
|
* @returns Le track mis à jour
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function updateTrack(
|
|
trackId: string,
|
|
updates: UpdateTrackRequest,
|
|
): Promise<Track> {
|
|
try {
|
|
const { data } = await apiClient.put<{ track: Track }>(
|
|
`/tracks/${trackId}`,
|
|
updates,
|
|
);
|
|
return data.track;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
|
|
if (error.response?.status === 404) {
|
|
throw new Error('Track introuvable');
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec de la mise à jour du track';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère les statistiques d'un track
|
|
* @param trackId ID du track
|
|
* @returns Les statistiques du track
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getTrackStats(
|
|
trackId: string,
|
|
): Promise<TrackStats> {
|
|
try {
|
|
const { data } = await apiClient.get<{ stats: TrackStats }>(
|
|
`/tracks/${trackId}/stats`,
|
|
);
|
|
return data.stats;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 404) {
|
|
throw new Error('Track introuvable');
|
|
}
|
|
if (error.response?.status === 501) {
|
|
throw new Error('Statistiques non disponibles pour le moment');
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec de la récupération des statistiques';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère l'historique d'un track
|
|
* @param trackId ID du track
|
|
* @returns L'historique du track
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getTrackHistory(
|
|
trackId: string,
|
|
): Promise<TrackHistory> {
|
|
try {
|
|
const { data } = await apiClient.get<{ history: TrackHistory }>(
|
|
`/tracks/${trackId}/history`,
|
|
);
|
|
return data.history;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 404) {
|
|
throw new Error('Track introuvable');
|
|
}
|
|
if (error.response?.status === 501) {
|
|
throw new Error('Historique non disponible pour le moment');
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec de la récupération de l\'historique';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Télécharge un track
|
|
* @param trackId ID du track à télécharger
|
|
* @param shareToken Token de partage optionnel
|
|
* @returns Le blob du fichier audio
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function downloadTrack(
|
|
trackId: string,
|
|
shareToken?: string,
|
|
): Promise<Blob> {
|
|
try {
|
|
const config = shareToken
|
|
? { params: { share_token: shareToken }, responseType: 'blob' as const }
|
|
: { responseType: 'blob' as const };
|
|
const response = await apiClient.get(`/tracks/${trackId}/download`, config);
|
|
return response.data;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
|
|
if (error.response?.status === 404) {
|
|
throw new Error('Track introuvable');
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec du téléchargement';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Like un track
|
|
* @param trackId ID du track à liker
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function likeTrack(trackId: string): Promise<void> {
|
|
try {
|
|
await apiClient.post(`/tracks/${trackId}/like`);
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
|
|
if (error.response?.status === 404) {
|
|
throw new Error('Track introuvable');
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec du like';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unlike un track
|
|
* @param trackId ID du track à unliker
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function unlikeTrack(trackId: string): Promise<void> {
|
|
try {
|
|
await apiClient.delete(`/tracks/${trackId}/like`);
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
|
|
if (error.response?.status === 404) {
|
|
throw new Error('Track introuvable');
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec du unlike';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Supprime un track
|
|
* @param trackId ID du track à supprimer
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function deleteTrack(trackId: string): Promise<void> {
|
|
try {
|
|
await apiClient.delete(`/tracks/${trackId}`);
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 404) {
|
|
throw new Error('Track introuvable');
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec de la suppression du track';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère les likes d'un track
|
|
* @param trackId ID du track
|
|
* @returns Le nombre de likes et si l'utilisateur actuel a liké
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getTrackLikes(
|
|
trackId: string,
|
|
): Promise<TrackLikesResponse> {
|
|
try {
|
|
const { data } = await apiClient.get<TrackLikesResponse>(
|
|
`/tracks/${trackId}/likes`,
|
|
);
|
|
return data;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
|
|
if (error.response?.status === 404) {
|
|
throw new Error('Track introuvable');
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec de la récupération des likes';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Crée un lien de partage pour un track
|
|
* @param trackId ID du track à partager
|
|
* @param request Paramètres de partage (permissions, expires_at)
|
|
* @returns Le lien de partage créé
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function createTrackShare(
|
|
trackId: string,
|
|
request: CreateShareRequest,
|
|
): Promise<Share> {
|
|
try {
|
|
const { data } = await apiClient.post<{ share: Share }>(
|
|
`/tracks/${trackId}/share`,
|
|
request,
|
|
);
|
|
return data.share;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
|
|
if (error.response?.status === 404) {
|
|
throw new Error('Track introuvable');
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec de la création du lien de partage';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Interfaces pour le chunked upload
|
|
interface UploadChunkResponse {
|
|
message: string;
|
|
upload_id: string;
|
|
received_chunks: number;
|
|
total_chunks: number;
|
|
progress: number;
|
|
}
|
|
|
|
interface InitiateUploadResponse {
|
|
upload_id: string;
|
|
message: string;
|
|
}
|
|
|
|
interface CompleteUploadResponse {
|
|
message: string;
|
|
track: Track;
|
|
md5: string;
|
|
}
|
|
|
|
/**
|
|
* Initialise un upload par chunks
|
|
* @param totalChunks Nombre total de chunks
|
|
* @param totalSize Taille totale du fichier
|
|
* @param filename Nom du fichier
|
|
* @returns ID de l'upload
|
|
*/
|
|
export async function initiateChunkedUpload(
|
|
totalChunks: number,
|
|
totalSize: number,
|
|
filename: string,
|
|
): Promise<string> {
|
|
try {
|
|
const { data } = await apiClient.post<InitiateUploadResponse>(
|
|
'/tracks/initiate',
|
|
{
|
|
total_chunks: totalChunks,
|
|
total_size: totalSize,
|
|
filename,
|
|
},
|
|
);
|
|
return data.upload_id;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
"Échec de l'initialisation de l'upload";
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upload un chunk
|
|
* @param uploadId ID de l'upload
|
|
* @param chunkNumber Numéro du chunk (1-based)
|
|
* @param totalChunks Nombre total de chunks
|
|
* @param totalSize Taille totale du fichier
|
|
* @param filename Nom du fichier
|
|
* @param chunk Données du chunk (Blob)
|
|
* @param onProgress Callback de progression
|
|
*/
|
|
export async function uploadChunk(
|
|
uploadId: string,
|
|
chunkNumber: number,
|
|
totalChunks: number,
|
|
totalSize: number,
|
|
filename: string,
|
|
chunk: Blob,
|
|
onProgress?: (progress: number) => void,
|
|
): Promise<UploadChunkResponse> {
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('upload_id', uploadId);
|
|
formData.append('chunk_number', chunkNumber.toString());
|
|
formData.append('total_chunks', totalChunks.toString());
|
|
formData.append('total_size', totalSize.toString());
|
|
formData.append('filename', filename);
|
|
formData.append('chunk', chunk);
|
|
|
|
// CRITIQUE: Ne pas définir Content-Type pour multipart/form-data avec Axios
|
|
const config = onProgress
|
|
? {
|
|
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
|
|
if (progressEvent.total) {
|
|
const progress = Math.round(
|
|
(progressEvent.loaded * 100) / progressEvent.total,
|
|
);
|
|
onProgress(progress);
|
|
}
|
|
},
|
|
}
|
|
: {};
|
|
|
|
const { data } = await apiClient.post<UploadChunkResponse>(
|
|
'/tracks/chunk',
|
|
formData,
|
|
config,
|
|
);
|
|
return data;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
`Échec de l'upload du chunk ${chunkNumber}`;
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Termine un upload par chunks
|
|
* @param uploadId ID de l'upload
|
|
* @returns Le track créé
|
|
*/
|
|
export async function completeChunkedUpload(
|
|
uploadId: string,
|
|
): Promise<Track> {
|
|
try {
|
|
const { data } = await apiClient.post<CompleteUploadResponse>(
|
|
'/tracks/complete',
|
|
{ upload_id: uploadId },
|
|
);
|
|
return data.track;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
"Échec de la finalisation de l'upload";
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* FE-PAGE-002: Batch operations interfaces
|
|
*/
|
|
export interface BatchDeleteRequest {
|
|
track_ids: string[];
|
|
}
|
|
|
|
export interface BatchDeleteResponse {
|
|
deleted_count: number;
|
|
message: string;
|
|
}
|
|
|
|
export interface BatchUpdateRequest {
|
|
track_ids: string[];
|
|
updates: UpdateTrackRequest;
|
|
}
|
|
|
|
export interface BatchUpdateResponse {
|
|
updated_count: number;
|
|
message: string;
|
|
}
|
|
|
|
/**
|
|
* Supprime plusieurs tracks en une seule requête
|
|
* @param trackIds Liste des IDs de tracks à supprimer
|
|
* @returns Nombre de tracks supprimés
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function batchDeleteTracks(
|
|
trackIds: string[],
|
|
): Promise<BatchDeleteResponse> {
|
|
try {
|
|
const { data } = await apiClient.post<BatchDeleteResponse>(
|
|
'/tracks/batch/delete',
|
|
{ track_ids: trackIds },
|
|
);
|
|
return data;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec de la suppression en masse';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Met à jour plusieurs tracks en une seule requête
|
|
* @param trackIds Liste des IDs de tracks à mettre à jour
|
|
* @param updates Métadonnées à appliquer à tous les tracks
|
|
* @returns Nombre de tracks mis à jour
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function batchUpdateTracks(
|
|
trackIds: string[],
|
|
updates: UpdateTrackRequest,
|
|
): Promise<BatchUpdateResponse> {
|
|
try {
|
|
const { data } = await apiClient.post<BatchUpdateResponse>(
|
|
'/tracks/batch/update',
|
|
{
|
|
track_ids: trackIds,
|
|
updates,
|
|
},
|
|
);
|
|
return data;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
const errorMessage =
|
|
error.response?.data?.error?.message ||
|
|
error.response?.data?.message ||
|
|
error.message ||
|
|
'Échec de la mise à jour en masse';
|
|
throw new Error(errorMessage);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|