refactor(web): migrate playlists.ts to api.* wrapper (#375)

Drop 9 hand-rolled apiFetch calls with redundant Content-Type
headers + manual JSON.stringify. The api.* wrapper in client.ts
already handles both. Identical wire shape; existing tests cover
all endpoints.

Also makes api.del generic so DELETE endpoints with typed returns
(playlist track removal) can use the wrapper instead of raw
apiFetch. Default `T = null` preserves existing callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 15:30:34 -04:00
parent 714039bae5
commit e8eff1baa1
2 changed files with 21 additions and 39 deletions
+2 -2
View File
@@ -58,6 +58,6 @@ export const api = {
apiFetch(path, { method: 'PUT', body: JSON.stringify(body) }) as Promise<T>, apiFetch(path, { method: 'PUT', body: JSON.stringify(body) }) as Promise<T>,
patch: <T>(path: string, body: unknown): Promise<T> => patch: <T>(path: string, body: unknown): Promise<T> =>
apiFetch(path, { method: 'PATCH', body: JSON.stringify(body) }) as Promise<T>, apiFetch(path, { method: 'PATCH', body: JSON.stringify(body) }) as Promise<T>,
del: (path: string): Promise<null> => del: <T = null>(path: string): Promise<T> =>
apiFetch(path, { method: 'DELETE' }) as Promise<null> apiFetch(path, { method: 'DELETE' }) as Promise<T>
}; };
+19 -37
View File
@@ -1,5 +1,5 @@
import { createQuery } from '@tanstack/svelte-query'; import { createQuery } from '@tanstack/svelte-query';
import { apiFetch } from './client'; import { api } from './client';
import { qk } from './queries'; import { qk } from './queries';
import type { Playlist, PlaylistDetail } from './types'; import type { Playlist, PlaylistDetail } from './types';
@@ -12,13 +12,11 @@ export type ListPlaylistsResponse = {
export async function listPlaylists(kind?: PlaylistKind): Promise<ListPlaylistsResponse> { export async function listPlaylists(kind?: PlaylistKind): Promise<ListPlaylistsResponse> {
const k = kind ?? 'user'; const k = kind ?? 'user';
return (await apiFetch(`/api/playlists?kind=${k}`, { method: 'GET' })) as ListPlaylistsResponse; return api.get<ListPlaylistsResponse>(`/api/playlists?kind=${k}`);
} }
export async function getPlaylist(id: string): Promise<PlaylistDetail> { export async function getPlaylist(id: string): Promise<PlaylistDetail> {
return (await apiFetch(`/api/playlists/${encodeURIComponent(id)}`, { return api.get<PlaylistDetail>(`/api/playlists/${encodeURIComponent(id)}`);
method: 'GET'
})) as PlaylistDetail;
} }
export type CreatePlaylistInput = { export type CreatePlaylistInput = {
@@ -28,11 +26,7 @@ export type CreatePlaylistInput = {
}; };
export async function createPlaylist(input: CreatePlaylistInput): Promise<Playlist> { export async function createPlaylist(input: CreatePlaylistInput): Promise<Playlist> {
return (await apiFetch('/api/playlists', { return api.post<Playlist>('/api/playlists', input);
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input)
})) as Playlist;
} }
export type UpdatePlaylistInput = { export type UpdatePlaylistInput = {
@@ -42,38 +36,31 @@ export type UpdatePlaylistInput = {
}; };
export async function updatePlaylist(id: string, input: UpdatePlaylistInput): Promise<Playlist> { export async function updatePlaylist(id: string, input: UpdatePlaylistInput): Promise<Playlist> {
return (await apiFetch(`/api/playlists/${encodeURIComponent(id)}`, { return api.patch<Playlist>(`/api/playlists/${encodeURIComponent(id)}`, input);
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input)
})) as Playlist;
} }
export async function deletePlaylist(id: string): Promise<void> { export async function deletePlaylist(id: string): Promise<void> {
await apiFetch(`/api/playlists/${encodeURIComponent(id)}`, { method: 'DELETE' }); await api.del(`/api/playlists/${encodeURIComponent(id)}`);
} }
export async function appendTracks(playlistID: string, trackIDs: string[]): Promise<PlaylistDetail> { export async function appendTracks(playlistID: string, trackIDs: string[]): Promise<PlaylistDetail> {
return (await apiFetch(`/api/playlists/${encodeURIComponent(playlistID)}/tracks`, { return api.post<PlaylistDetail>(
method: 'POST', `/api/playlists/${encodeURIComponent(playlistID)}/tracks`,
headers: { 'Content-Type': 'application/json' }, { track_ids: trackIDs }
body: JSON.stringify({ track_ids: trackIDs }) );
})) as PlaylistDetail;
} }
export async function removePlaylistTrack(playlistID: string, position: number): Promise<PlaylistDetail> { export async function removePlaylistTrack(playlistID: string, position: number): Promise<PlaylistDetail> {
return (await apiFetch( return api.del<PlaylistDetail>(
`/api/playlists/${encodeURIComponent(playlistID)}/tracks/${position}`, `/api/playlists/${encodeURIComponent(playlistID)}/tracks/${position}`
{ method: 'DELETE' } );
)) as PlaylistDetail;
} }
export async function reorderPlaylist(playlistID: string, orderedPositions: number[]): Promise<PlaylistDetail> { export async function reorderPlaylist(playlistID: string, orderedPositions: number[]): Promise<PlaylistDetail> {
return (await apiFetch(`/api/playlists/${encodeURIComponent(playlistID)}/tracks`, { return api.put<PlaylistDetail>(
method: 'PUT', `/api/playlists/${encodeURIComponent(playlistID)}/tracks`,
headers: { 'Content-Type': 'application/json' }, { ordered_positions: orderedPositions }
body: JSON.stringify({ ordered_positions: orderedPositions }) );
})) as PlaylistDetail;
} }
export function createPlaylistsQuery(kind?: PlaylistKind) { export function createPlaylistsQuery(kind?: PlaylistKind) {
@@ -103,10 +90,7 @@ export type RefreshDiscoverResponse = {
// returns the resulting Discover playlist's id + track count. // returns the resulting Discover playlist's id + track count.
// Used by the detail-page Refresh button and the home tile kebab. // Used by the detail-page Refresh button and the home tile kebab.
export async function refreshDiscover(): Promise<RefreshDiscoverResponse> { export async function refreshDiscover(): Promise<RefreshDiscoverResponse> {
return (await apiFetch('/api/playlists/system/discover/refresh', { return api.post<RefreshDiscoverResponse>('/api/playlists/system/discover/refresh', {});
method: 'POST',
body: JSON.stringify({})
})) as RefreshDiscoverResponse;
} }
// For-You refresh ------------------------------------------------------------ // For-You refresh ------------------------------------------------------------
@@ -125,7 +109,5 @@ export type RefreshForYouResponse = {
// triggers refresh, the frontend then calls getPlaylist(new_id) to // triggers refresh, the frontend then calls getPlaylist(new_id) to
// get full TrackRef snapshots and enqueues for playback. // get full TrackRef snapshots and enqueues for playback.
export async function refreshForYou(): Promise<RefreshForYouResponse> { export async function refreshForYou(): Promise<RefreshForYouResponse> {
return (await apiFetch('/api/playlists/system/for-you/refresh', { return api.post<RefreshForYouResponse>('/api/playlists/system/for-you/refresh', {});
method: 'POST'
})) as RefreshForYouResponse;
} }