veza/apps/web/src/features/tracks/services/trackService.ts

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,
);
}
}