diff --git a/apps/web/src/features/tracks/services/trackService.ts b/apps/web/src/features/tracks/services/trackService.ts index 9766d3a7d..de8d3d66a 100644 --- a/apps/web/src/features/tracks/services/trackService.ts +++ b/apps/web/src/features/tracks/services/trackService.ts @@ -1,4 +1,30 @@ +/** + * Track service — orval-backed. + * v1.0.8 B5 migration: replaced raw apiClient calls with the generated + * functions in services/generated/track/track.ts. Public function + * signatures unchanged so callers (hooks, components, tests) stay put. + * + * Error handling structure preserved verbatim — AxiosError still + * propagates from the orval mutator (apiClient under the hood), so the + * existing status-code → TrackUploadError mapping continues to apply. + * + * Upload paths still delegate to the dedicated uploadService (chunked + * transport + progress tracking lives there, not in track CRUD). + */ import { apiClient } from '@/services/api/client'; +import { + getTracks as orvalGetTracks, + getTracksId as orvalGetTracksId, + putTracksId as orvalPutTracksId, + deleteTracksId as orvalDeleteTracksId, + getTracksIdLyrics as orvalGetLyrics, + putTracksIdLyrics as orvalPutLyrics, + getTracksSuggestedTags as orvalGetSuggestedTags, + getTracksSearch as orvalSearchTracks, + postTracksIdLike as orvalLike, + deleteTracksIdLike as orvalUnlike, + postTracksIdPlay as orvalRecordPlay, +} from '@/services/generated/track/track'; import { Track } from '../types/track'; import { AxiosError } from 'axios'; import { TrackServiceError as TrackUploadError } from '../errors/trackErrors'; @@ -35,6 +61,24 @@ export interface TrackListResponse { total_pages: number; } +// Backend wraps responses in { data: { ... } } via APIResponse envelope. +// orval's mutator already strips the Axios layer, leaving the envelope. +const unwrapPayload = (raw: unknown): T => { + const env = raw as { data?: unknown } | undefined; + if (env && typeof env === 'object' && 'data' in env && env.data !== undefined) { + return env.data as T; + } + return raw as T; +}; + +const pickTrack = (raw: unknown): Track => { + const p = unwrapPayload<{ track?: Track } | Track>(raw); + if (p && typeof p === 'object' && 'track' in p && p.track) { + return p.track as Track; + } + return p as Track; +}; + /** * Récupère la liste des tracks * @param params Paramètres de filtrage et pagination @@ -45,21 +89,25 @@ export async function getTracks( params: TrackListParams = {}, signal?: AbortSignal, ): Promise { + // Backend GET /tracks ne déclare pas sort_order dans son annotation + // swaggo (cf. track_crud_handler.go) — paramètre conservé dans la + // signature pour forward-compat mais ignoré à l'appel. À ré-activer + // quand l'annotation est étendue (v1.0.9). + void params.sortOrder; + try { - const queryParams = new URLSearchParams(); - if (params.page) queryParams.append('page', params.page.toString()); - if (params.limit) queryParams.append('limit', params.limit.toString()); - if (params.userId) queryParams.append('user_id', params.userId); - 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(url, { signal }); - return response.data; + const response = await orvalGetTracks( + { + page: params.page, + limit: params.limit, + user_id: params.userId, + genre: params.genre, + format: params.format, + sort_by: params.sortBy, + }, + signal ? { signal } : undefined, + ); + return unwrapPayload(response); } catch (error: unknown) { if (error instanceof AxiosError) { if (error.response?.status === 401) { @@ -78,7 +126,6 @@ export async function getTracks( error, ); } - // Erreurs réseau if ( error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' || @@ -111,14 +158,11 @@ export async function getTracks( /** * Récupère les détails d'un track - * @param id ID du track - * @returns Le track - * @throws Error si la requête échoue */ export async function getTrack(id: string): Promise { try { - const response = await apiClient.get<{ track: Track }>(`/tracks/${id}`); - return response.data.track; + const response = await orvalGetTracksId(id); + return pickTrack(response); } catch (error: unknown) { if (error instanceof AxiosError) { if (error.response?.status === 401) { @@ -145,7 +189,6 @@ export async function getTrack(id: string): Promise { error, ); } - // Erreurs réseau if ( error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' || @@ -177,7 +220,7 @@ export async function getTrack(id: string): Promise { } /** - * Interface pour les paramètres de mise à jour d'un track + * Interface pour les paroles d'un track */ export interface TrackLyrics { id: string; @@ -192,10 +235,9 @@ export interface TrackLyrics { */ export async function getLyrics(trackId: string): Promise { try { - const response = await apiClient.get<{ lyrics: TrackLyrics | null }>( - `/tracks/${trackId}/lyrics`, - ); - return response.data.lyrics; + const response = await orvalGetLyrics(trackId); + const payload = unwrapPayload<{ lyrics?: TrackLyrics | null } | null>(response); + return payload?.lyrics ?? null; } catch (error: unknown) { if (error instanceof AxiosError && error.response?.status === 404) { return null; @@ -211,11 +253,12 @@ export async function updateLyrics( trackId: string, content: string, ): Promise { - const response = await apiClient.put<{ lyrics: TrackLyrics }>( - `/tracks/${trackId}/lyrics`, - { content }, + const response = await orvalPutLyrics( + trackId, + { content } as Parameters[1], ); - return response.data.lyrics; + const payload = unwrapPayload<{ lyrics: TrackLyrics }>(response); + return payload.lyrics; } /** @@ -225,14 +268,12 @@ export async function getSuggestedTags(params?: { genre?: string; bpm?: number; }): Promise { - const searchParams = new URLSearchParams(); - if (params?.genre) searchParams.set('genre', params.genre); - if (params?.bpm != null) searchParams.set('bpm', String(params.bpm)); - const qs = searchParams.toString(); - const response = await apiClient.get<{ tags: string[] }>( - `/tracks/suggested-tags${qs ? `?${qs}` : ''}`, - ); - return response.data.tags; + const response = await orvalGetSuggestedTags({ + genre: params?.genre, + bpm: params?.bpm, + } as Parameters[0]); + const payload = unwrapPayload<{ tags?: string[] }>(response); + return payload.tags ?? []; } export interface UpdateTrackParams { @@ -252,21 +293,17 @@ export interface UpdateTrackParams { /** * 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 { try { - const response = await apiClient.put<{ track: Track }>( - `/tracks/${id}`, - params, + const response = await orvalPutTracksId( + id, + params as Parameters[1], ); - return response.data.track; + return pickTrack(response); } catch (error: unknown) { if (error instanceof AxiosError) { if (error.response?.status === 401) { @@ -305,7 +342,6 @@ export async function updateTrack( error, ); } - // Erreurs réseau if ( error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' || @@ -338,12 +374,10 @@ export async function updateTrack( /** * Supprime un track - * @param id ID du track à supprimer - * @throws Error si la requête échoue */ export async function deleteTrack(id: string): Promise { try { - await apiClient.delete(`/tracks/${id}`); + await orvalDeleteTracksId(id); } catch (error: unknown) { if (error instanceof AxiosError) { if (error.response?.status === 401) { @@ -378,7 +412,6 @@ export async function deleteTrack(id: string): Promise { error, ); } - // Erreurs réseau if ( error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT' || @@ -415,35 +448,44 @@ export async function deleteTrack(id: string): Promise { * Search for tracks by query string */ export async function searchTracks(query: string) { - const response = await apiClient.get('/tracks/search', { - params: { query }, - }); - return { tracks: response.data }; + const response = await orvalSearchTracks({ q: query }); + const payload = unwrapPayload<{ tracks?: Track[] } | Track[]>(response); + if (Array.isArray(payload)) { + return { tracks: payload }; + } + return { tracks: payload.tracks ?? [] }; } /** * Like a track */ export async function likeTrack(id: string) { - return apiClient.post(`/tracks/${id}/like`); + return orvalLike(id); } /** * Unlike a track */ export async function unlikeTrack(id: string) { - return apiClient.delete(`/tracks/${id}/like`); + return orvalUnlike(id); } /** * Record a play event for a track */ export async function recordPlay(id: string) { - return apiClient.post(`/tracks/${id}/play`); + return orvalRecordPlay( + id, + {} as Parameters[1], + ); } /** - * Download a track as a blob + * Download a track as a blob. + * Kept on raw apiClient because orval's getTracksIdDownload doesn't + * preserve the responseType: 'blob' contract callers depend on (the + * mutator-typed response would deserialize binary as JSON). Re-evaluate + * once orval supports per-call responseType overrides (B9 cleanup). */ export async function downloadTrack(id: string) { const response = await apiClient.get(`/tracks/${id}/download`, {