1474 lines
39 KiB
TypeScript
1474 lines
39 KiB
TypeScript
import { apiClient } from '@/services/api/client';
|
|
import { Track, UploadProgress } from '../types/track';
|
|
import { AxiosError, AxiosProgressEvent } from 'axios';
|
|
|
|
/**
|
|
* Track Service
|
|
* T0260: Service frontend pour gérer l'upload de tracks
|
|
* T0264: Amélioration gestion erreurs upload
|
|
* T0275: Service frontend pour lister et récupérer les tracks
|
|
* T0278: Service frontend pour mettre à jour les tracks
|
|
* T0279: Service frontend pour supprimer les tracks
|
|
* T0284: Service frontend pour gérer les likes de tracks
|
|
* T0294: Service frontend pour analytics de lecture de tracks
|
|
*/
|
|
|
|
/**
|
|
* Classe d'erreur personnalisée pour les uploads de tracks
|
|
*/
|
|
export class TrackUploadError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public code: 'VALIDATION' | 'QUOTA' | 'NETWORK' | 'SERVER' | 'UNKNOWN',
|
|
public retryable: boolean = false,
|
|
public originalError?: unknown,
|
|
) {
|
|
super(message);
|
|
this.name = 'TrackUploadError';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upload un fichier audio
|
|
* @param file Fichier audio à uploader
|
|
* @param onProgress Callback optionnel pour suivre la progression de l'upload
|
|
* @returns Le track créé
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function uploadTrack(
|
|
file: File,
|
|
onProgress?: (progress: number) => void,
|
|
): Promise<Track> {
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const response = await apiClient.post<{ track: Track }>(
|
|
'/tracks',
|
|
formData,
|
|
{
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
|
|
if (progressEvent.total && onProgress) {
|
|
const progress = Math.round(
|
|
(progressEvent.loaded * 100) / progressEvent.total,
|
|
);
|
|
onProgress(progress);
|
|
}
|
|
},
|
|
timeout: 300000, // 5 minutes timeout for large files
|
|
},
|
|
);
|
|
|
|
return response.data.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 || 'Format ou taille invalide';
|
|
throw new TrackUploadError(errorMessage, 'VALIDATION', false, error);
|
|
}
|
|
// ... (keep existing error handling)
|
|
// ...
|
|
}
|
|
// Erreur inconnue
|
|
throw new TrackUploadError(
|
|
"Erreur inconnue lors de l'upload",
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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> {
|
|
const response = await apiClient.post<{ upload_id: string }>(
|
|
'/tracks/initiate',
|
|
{
|
|
total_chunks: totalChunks,
|
|
total_size: totalSize,
|
|
filename,
|
|
},
|
|
);
|
|
return response.data.upload_id;
|
|
}
|
|
|
|
/**
|
|
* Interface pour la réponse d'upload d'un chunk
|
|
*/
|
|
export interface UploadChunkResponse {
|
|
message: string;
|
|
upload_id: string;
|
|
received_chunks: number;
|
|
total_chunks: number;
|
|
progress: number;
|
|
}
|
|
|
|
/**
|
|
* 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 Blob du chunk
|
|
* @param onProgress Callback de progression
|
|
* @returns Réponse avec la progression de l'upload
|
|
*/
|
|
export async function uploadChunk(
|
|
uploadId: string,
|
|
chunkNumber: number,
|
|
totalChunks: number,
|
|
totalSize: number,
|
|
filename: string,
|
|
chunk: Blob,
|
|
onProgress?: (progress: number) => void,
|
|
): Promise<UploadChunkResponse> {
|
|
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);
|
|
|
|
const response = await apiClient.post<UploadChunkResponse>(
|
|
'/tracks/chunk',
|
|
formData,
|
|
{
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
},
|
|
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
|
|
if (progressEvent.total && onProgress) {
|
|
const progress = Math.round(
|
|
(progressEvent.loaded * 100) / progressEvent.total,
|
|
);
|
|
onProgress(progress);
|
|
}
|
|
},
|
|
},
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Complète un upload par chunks
|
|
* @param uploadId ID de l'upload
|
|
* @returns Le track créé
|
|
*/
|
|
export async function completeChunkedUpload(uploadId: string): Promise<Track> {
|
|
const response = await apiClient.post<{ track: Track }>('/tracks/complete', {
|
|
upload_id: uploadId,
|
|
});
|
|
return response.data.track;
|
|
}
|
|
|
|
/**
|
|
* Récupère la progression d'un upload de track
|
|
* @param trackId ID du track
|
|
* @returns Les informations de progression
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getUploadProgress(
|
|
trackId: string,
|
|
): Promise<UploadProgress> {
|
|
try {
|
|
const response = await apiClient.get<{ progress: UploadProgress }>(
|
|
`/tracks/${trackId}/status`,
|
|
);
|
|
return response.data.progress;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 401) {
|
|
throw new TrackUploadError(
|
|
'Non autorisé: Veuillez vous connecter pour voir la progression',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Track introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de récupérer la progression. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la récupération de la progression';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la récupération de la progression',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface pour les informations de quota d'un utilisateur
|
|
*/
|
|
export interface UserQuota {
|
|
tracks_count: number;
|
|
tracks_limit: number;
|
|
storage_used: number; // bytes
|
|
storage_limit: number; // bytes
|
|
}
|
|
|
|
/**
|
|
* Récupère les informations de quota d'upload pour un utilisateur
|
|
* @param userId ID de l'utilisateur (ou "me" pour l'utilisateur authentifié)
|
|
* @returns Les informations de quota
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getUserQuota(
|
|
userId: string,
|
|
): Promise<UserQuota> {
|
|
try {
|
|
const userIdParam =
|
|
userId === 'me' || userId === 'current' ? 'me' : String(userId);
|
|
const response = await apiClient.get<{ quota: UserQuota }>(
|
|
`/users/${userIdParam}/upload-quota`,
|
|
);
|
|
return response.data.quota;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 401) {
|
|
throw new TrackUploadError(
|
|
'Non autorisé: Veuillez vous connecter pour voir votre quota',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 403) {
|
|
throw new TrackUploadError(
|
|
'Accès refusé: Vous ne pouvez voir que votre propre quota',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Utilisateur introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de récupérer le quota. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la récupération du quota';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la récupération du quota',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface pour l'état d'un upload résumable
|
|
*/
|
|
export interface UploadResumeState {
|
|
upload_id: string;
|
|
user_id: string;
|
|
total_chunks: number;
|
|
total_size: number;
|
|
filename: string;
|
|
chunks_received: number[];
|
|
received_count: number;
|
|
last_chunk: number;
|
|
progress: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
/**
|
|
* Récupère l'état d'un upload pour permettre la reprise
|
|
* @param uploadId ID de l'upload
|
|
* @returns L'état de l'upload
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function resumeUpload(
|
|
uploadId: string,
|
|
): Promise<UploadResumeState> {
|
|
try {
|
|
const response = await apiClient.get<UploadResumeState>(
|
|
`/tracks/upload/resume/${uploadId}`,
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 401) {
|
|
throw new TrackUploadError(
|
|
"Non autorisé: Veuillez vous connecter pour reprendre l'upload",
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 403) {
|
|
throw new TrackUploadError(
|
|
'Accès refusé: Vous ne pouvez reprendre que vos propres uploads',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Upload introuvable ou expiré',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
"Erreur serveur: Impossible de récupérer l'état de l'upload. Veuillez réessayer plus tard.",
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
"Échec de la récupération de l'état de l'upload";
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
"Erreur inconnue lors de la récupération de l'état de l'upload",
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface pour les paramètres de liste des tracks
|
|
*/
|
|
export interface TrackListParams {
|
|
page?: number;
|
|
limit?: number;
|
|
userId?: string;
|
|
genre?: string;
|
|
format?: string;
|
|
sortBy?: 'created_at' | 'title' | 'popularity';
|
|
sortOrder?: 'asc' | 'desc';
|
|
}
|
|
|
|
/**
|
|
* Interface pour la réponse de liste des tracks
|
|
*/
|
|
export interface TrackListResponse {
|
|
tracks: Track[];
|
|
pagination: {
|
|
page: number;
|
|
limit: number;
|
|
total: number;
|
|
total_pages: number;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Liste les tracks avec pagination, filtres et tri
|
|
* @param params Paramètres de filtrage et pagination
|
|
* @returns La liste des tracks avec les métadonnées de pagination
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function listTracks(
|
|
params: TrackListParams = {},
|
|
): Promise<TrackListResponse> {
|
|
try {
|
|
const queryParams = new URLSearchParams();
|
|
if (params.page !== undefined)
|
|
queryParams.append('page', params.page.toString());
|
|
if (params.limit !== undefined)
|
|
queryParams.append('limit', params.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<TrackListResponse>(url);
|
|
return response.data;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 401) {
|
|
throw new TrackUploadError(
|
|
'Non autorisé: Veuillez vous connecter pour voir les tracks',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de récupérer les tracks. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la récupération des tracks';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la récupération des tracks',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère les détails d'un track par son ID
|
|
* @param id ID du track
|
|
* @returns Le track
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getTrack(id: string): Promise<Track> {
|
|
try {
|
|
const response = await apiClient.get<{ track: Track }>(`/tracks/${id}`);
|
|
return response.data.track;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 401) {
|
|
throw new TrackUploadError(
|
|
'Non autorisé: Veuillez vous connecter pour voir ce track',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Track introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de récupérer le track. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la récupération du track';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la récupération du track',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface pour les paramètres de mise à jour d'un track
|
|
*/
|
|
export interface UpdateTrackParams {
|
|
title?: string;
|
|
artist?: string;
|
|
album?: string;
|
|
genre?: string;
|
|
year?: number;
|
|
is_public?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Met à jour les métadonnées d'un track
|
|
* @param id ID du track
|
|
* @param params Paramètres de mise à jour
|
|
* @returns Le track mis à jour
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function updateTrack(
|
|
id: string,
|
|
params: UpdateTrackParams,
|
|
): Promise<Track> {
|
|
try {
|
|
const response = await apiClient.put<{ track: Track }>(
|
|
`/tracks/${id}`,
|
|
params,
|
|
);
|
|
return response.data.track;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 401) {
|
|
throw new TrackUploadError(
|
|
'Non autorisé: Veuillez vous connecter pour modifier ce track',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 403) {
|
|
throw new TrackUploadError(
|
|
'Accès refusé: Vous ne pouvez modifier que vos propres tracks',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Track introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 400) {
|
|
const errorMessage = error.response?.data?.error || 'Données invalides';
|
|
throw new TrackUploadError(errorMessage, 'VALIDATION', false, error);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de mettre à jour le track. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la mise à jour du track';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la mise à jour du track',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Supprime un track
|
|
* @param id ID du track
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function deleteTrack(id: string): Promise<void> {
|
|
try {
|
|
await apiClient.delete(`/tracks/${id}`);
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 401) {
|
|
throw new TrackUploadError(
|
|
'Non autorisé: Veuillez vous connecter pour supprimer ce track',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 403) {
|
|
throw new TrackUploadError(
|
|
'Accès refusé: Vous ne pouvez supprimer que vos propres tracks',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Track introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de supprimer le track. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la suppression du track';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la suppression du track',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface pour la réponse de likes d'un track
|
|
*/
|
|
export interface TrackLikesResponse {
|
|
count: number;
|
|
is_liked: boolean;
|
|
}
|
|
|
|
/**
|
|
* Interface pour la réponse de tracks likés par un utilisateur
|
|
*/
|
|
export interface UserLikedTracksResponse {
|
|
tracks: Track[];
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
}
|
|
|
|
/**
|
|
* 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 === 401) {
|
|
throw new TrackUploadError(
|
|
'Non autorisé: Veuillez vous connecter pour liker ce track',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Track introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de liker le track. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec du like du track';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors du like du track',
|
|
'UNKNOWN',
|
|
false,
|
|
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 === 401) {
|
|
throw new TrackUploadError(
|
|
'Non autorisé: Veuillez vous connecter pour unliker ce track',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Track introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de unliker le track. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
"Échec de l'unlike du track";
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
"Erreur inconnue lors de l'unlike du track",
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère le nombre de likes et si l'utilisateur a liké un track
|
|
* @param trackId ID du track
|
|
* @returns Le nombre de likes et si l'utilisateur a liké
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getTrackLikes(
|
|
trackId: string,
|
|
): Promise<{ count: number; isLiked: boolean }> {
|
|
try {
|
|
const response = await apiClient.get<TrackLikesResponse>(
|
|
`/tracks/${trackId}/likes`,
|
|
);
|
|
return {
|
|
count: response.data.count,
|
|
isLiked: response.data.is_liked,
|
|
};
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Track introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de récupérer les likes. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la récupération des likes';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la récupération des likes',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère les tracks likés par un utilisateur
|
|
* @param userId ID de l'utilisateur
|
|
* @param limit Nombre de tracks à récupérer (défaut: 20)
|
|
* @param offset Offset pour la pagination (défaut: 0)
|
|
* @returns La liste des tracks likés avec pagination
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getUserLikedTracks(
|
|
userId: string,
|
|
limit: number = 20,
|
|
offset: number = 0,
|
|
): Promise<UserLikedTracksResponse> {
|
|
try {
|
|
const response = await apiClient.get<UserLikedTracksResponse>(
|
|
`/users/${userId}/liked-tracks?limit=${limit}&offset=${offset}`,
|
|
);
|
|
return response.data;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 401) {
|
|
throw new TrackUploadError(
|
|
'Non autorisé: Veuillez vous connecter pour voir les tracks likés',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Utilisateur introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de récupérer les tracks likés. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la récupération des tracks likés';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la récupération des tracks likés',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface pour les statistiques d'un track
|
|
*/
|
|
export interface TrackStats {
|
|
total_plays: number;
|
|
unique_listeners: number;
|
|
average_duration: number;
|
|
completion_rate: number;
|
|
}
|
|
|
|
/**
|
|
* Interface pour un point de données temporel
|
|
*/
|
|
export interface PlayTimePoint {
|
|
date: string;
|
|
count: number;
|
|
}
|
|
|
|
/**
|
|
* Interface pour un track dans le classement
|
|
*/
|
|
export interface TopTrack {
|
|
track_id: number;
|
|
title: string;
|
|
artist: string;
|
|
total_plays: number;
|
|
unique_listeners: number;
|
|
average_duration: number;
|
|
}
|
|
|
|
/**
|
|
* Interface pour la réponse des top tracks
|
|
*/
|
|
export interface TopTracksResponse {
|
|
tracks: TopTrack[];
|
|
}
|
|
|
|
/**
|
|
* Interface pour la réponse des statistiques utilisateur
|
|
*/
|
|
export interface UserStats {
|
|
total_plays: number;
|
|
unique_tracks: number;
|
|
total_duration: number;
|
|
average_duration: number;
|
|
}
|
|
|
|
/**
|
|
* Enregistre une lecture de track
|
|
* @param trackId ID du track
|
|
* @param duration Durée de lecture en secondes
|
|
* @param device Device optionnel (défaut: navigator.userAgent)
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function recordPlay(
|
|
trackId: number,
|
|
duration: number,
|
|
device?: string,
|
|
): Promise<void> {
|
|
try {
|
|
await apiClient.post(`/tracks/${trackId}/play`, {
|
|
duration,
|
|
device:
|
|
device || (typeof navigator !== 'undefined' ? navigator.userAgent : ''),
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Track introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
"Erreur serveur: Impossible d'enregistrer la lecture. Veuillez réessayer plus tard.",
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
"Échec de l'enregistrement de la lecture";
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
"Erreur inconnue lors de l'enregistrement de la lecture",
|
|
'UNKNOWN',
|
|
false,
|
|
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: number): Promise<TrackStats> {
|
|
try {
|
|
const response = await apiClient.get<{ stats: TrackStats }>(
|
|
`/tracks/${trackId}/stats`,
|
|
);
|
|
return response.data.stats;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Track introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de récupérer les statistiques. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la récupération des statistiques';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la récupération des statistiques',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère les tracks les plus écoutés
|
|
* @param limit Nombre de tracks à récupérer (défaut: 10, max: 100)
|
|
* @param startDate Date de début optionnelle (format ISO 8601)
|
|
* @param endDate Date de fin optionnelle (format ISO 8601)
|
|
* @returns La liste des tracks les plus écoutés
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getTopTracks(
|
|
limit: number = 10,
|
|
startDate?: string,
|
|
endDate?: string,
|
|
): Promise<TopTrack[]> {
|
|
try {
|
|
const queryParams = new URLSearchParams();
|
|
if (limit > 0 && limit <= 100) {
|
|
queryParams.append('limit', limit.toString());
|
|
}
|
|
if (startDate) {
|
|
queryParams.append('start_date', startDate);
|
|
}
|
|
if (endDate) {
|
|
queryParams.append('end_date', endDate);
|
|
}
|
|
|
|
const queryString = queryParams.toString();
|
|
const url = `/analytics/top-tracks${queryString ? `?${queryString}` : ''}`;
|
|
|
|
const response = await apiClient.get<TopTracksResponse>(url);
|
|
return response.data.tracks;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 400) {
|
|
const errorMessage =
|
|
error.response?.data?.error || 'Paramètres invalides';
|
|
throw new TrackUploadError(errorMessage, 'VALIDATION', false, error);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de récupérer les tracks populaires. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la récupération des tracks populaires';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la récupération des tracks populaires',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère les lectures sur une période pour un track
|
|
* @param trackId ID du track
|
|
* @param startDate Date de début (format ISO 8601, défaut: 30 jours avant)
|
|
* @param endDate Date de fin (format ISO 8601, défaut: maintenant)
|
|
* @param interval Intervalle de regroupement: 'hour', 'day', 'week', 'month' (défaut: 'day')
|
|
* @returns Les points de données temporels
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getPlaysOverTime(
|
|
trackId: number,
|
|
startDate?: string,
|
|
endDate?: string,
|
|
interval: 'hour' | 'day' | 'week' | 'month' = 'day',
|
|
): Promise<PlayTimePoint[]> {
|
|
try {
|
|
const queryParams = new URLSearchParams();
|
|
if (startDate) {
|
|
queryParams.append('start_date', startDate);
|
|
}
|
|
if (endDate) {
|
|
queryParams.append('end_date', endDate);
|
|
}
|
|
queryParams.append('interval', interval);
|
|
|
|
const queryString = queryParams.toString();
|
|
const url = `/tracks/${trackId}/plays-over-time${queryString ? `?${queryString}` : ''}`;
|
|
|
|
const response = await apiClient.get<{ points: PlayTimePoint[] }>(url);
|
|
return response.data.points;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Track introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 400) {
|
|
const errorMessage =
|
|
error.response?.data?.error || 'Paramètres invalides';
|
|
throw new TrackUploadError(errorMessage, 'VALIDATION', false, error);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de récupérer les données temporelles. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la récupération des données temporelles';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la récupération des données temporelles',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Récupère les statistiques d'un utilisateur
|
|
* @param userId ID de l'utilisateur
|
|
* @returns Les statistiques de l'utilisateur
|
|
* @throws Error si la requête échoue
|
|
*/
|
|
export async function getUserStats(userId: string): Promise<UserStats> {
|
|
try {
|
|
const response = await apiClient.get<{ stats: UserStats }>(
|
|
`/users/${userId}/stats`,
|
|
);
|
|
return response.data.stats;
|
|
} catch (error) {
|
|
if (error instanceof AxiosError) {
|
|
if (error.response?.status === 401) {
|
|
throw new TrackUploadError(
|
|
'Non autorisé: Veuillez vous connecter pour voir les statistiques',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 403) {
|
|
throw new TrackUploadError(
|
|
'Accès refusé: Vous ne pouvez voir que vos propres statistiques',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 404) {
|
|
throw new TrackUploadError(
|
|
'Utilisateur introuvable',
|
|
'VALIDATION',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
if (error.response?.status === 500) {
|
|
throw new TrackUploadError(
|
|
'Erreur serveur: Impossible de récupérer les statistiques. Veuillez réessayer plus tard.',
|
|
'SERVER',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
// Erreurs réseau
|
|
if (
|
|
error.code === 'ECONNABORTED' ||
|
|
error.code === 'ETIMEDOUT' ||
|
|
!error.response
|
|
) {
|
|
throw new TrackUploadError(
|
|
'Erreur réseau: Impossible de se connecter au serveur.',
|
|
'NETWORK',
|
|
true,
|
|
error,
|
|
);
|
|
}
|
|
const errorMessage =
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
'Échec de la récupération des statistiques';
|
|
throw new TrackUploadError(errorMessage, 'UNKNOWN', false, error);
|
|
}
|
|
if (error instanceof TrackUploadError) {
|
|
throw error;
|
|
}
|
|
throw new TrackUploadError(
|
|
'Erreur inconnue lors de la récupération des statistiques',
|
|
'UNKNOWN',
|
|
false,
|
|
error,
|
|
);
|
|
}
|
|
}
|