Files
minstrel/web/src/lib/api/playlists.ts
T
bvandeusen d67c0de596 refactor(playlists): #411 R2 — generic registry-driven system endpoints
Replaces the per-kind refresh/shuffle handlers with one generic
pair driven off the kind registry, in lockstep across both clients.

Server:
- systemPlaylistKind gains Singleton; RefreshableSystemKind(key)
  exported. for_you/discover singleton; songs_like_artist not.
- New generic POST /api/playlists/system/{kind}/refresh and
  GET /api/playlists/system/{kind}/shuffle ({kind} = raw
  system_variant). Non-singleton/unknown kind → 404. Deleted
  playlists_{foryou,discover}_refresh.go and the per-kind shuffle
  wrappers; serveSystemPlaylistShuffle core kept.
- playlistRowView.refreshable: server-derived flag so clients show
  the refresh affordance generically without hardcoding kinds.

Web (not drift-cached → uses the server flag):
- refreshSystem(variant) replaces refreshForYou/refreshDiscover;
  systemShuffle drops the for_you→for-you mapping (raw variant).
- PlaylistCard + detail page gate the kebab/Refresh button on
  playlist.refreshable; label is "Refresh {name}". Tests reworked;
  obsolete refresh-foryou/discover api tests deleted.

Flutter (list tiles are drift-cache-sourced → derive, no migration):
- Playlist.refreshable getter = isSystem && variant !=
  songs_like_artist (holds for all current + planned kinds).
- refreshSystem/systemShuffle use the raw variant; PlaylistCard +
  detail screen gate kebab/Regenerate/source-tagging on refreshable
  so songs_like_artist plays via get() (no by-kind endpoint).

Pure-plumbing refactor; CI verifies parity. Next (R3): the five
discovery mixes — each a candidate query + one registry entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:21:09 -04:00

108 lines
3.4 KiB
TypeScript

import { createQuery } from '@tanstack/svelte-query';
import { api } from './client';
import { qk } from './queries';
import type { Playlist, PlaylistDetail } from './types';
export type PlaylistKind = 'user' | 'system' | 'all';
export type ListPlaylistsResponse = {
owned: Playlist[];
public: Playlist[];
};
export async function listPlaylists(kind?: PlaylistKind): Promise<ListPlaylistsResponse> {
const k = kind ?? 'user';
return api.get<ListPlaylistsResponse>(`/api/playlists?kind=${k}`);
}
export async function getPlaylist(id: string): Promise<PlaylistDetail> {
return api.get<PlaylistDetail>(`/api/playlists/${encodeURIComponent(id)}`);
}
// #415 stage 2/3: rotation-aware play order for a system playlist.
// Same shape as getPlaylist but tracks are server-ordered (unplayed
// this rotation first, then heard; resets when exhausted).
// Intentionally uncached — it varies per play, unlike getPlaylist.
// {variant} is the raw system_variant (#411 R2: generic by-kind).
export async function systemShuffle(variant: string): Promise<PlaylistDetail> {
return api.get<PlaylistDetail>(
`/api/playlists/system/${encodeURIComponent(variant)}/shuffle`,
);
}
// #411 R2: generic by-kind refresh. Rebuilds the caller's system
// playlists and returns the named kind's fresh row. Replaces the
// former per-kind refreshForYou/refreshDiscover.
export type RefreshSystemResponse = {
playlist_id: string | null;
track_count: number;
track_ids: string[];
};
export async function refreshSystem(variant: string): Promise<RefreshSystemResponse> {
return api.post<RefreshSystemResponse>(
`/api/playlists/system/${encodeURIComponent(variant)}/refresh`,
{},
);
}
export type CreatePlaylistInput = {
name: string;
description?: string;
is_public?: boolean;
};
export async function createPlaylist(input: CreatePlaylistInput): Promise<Playlist> {
return api.post<Playlist>('/api/playlists', input);
}
export type UpdatePlaylistInput = {
name?: string;
description?: string;
is_public?: boolean;
};
export async function updatePlaylist(id: string, input: UpdatePlaylistInput): Promise<Playlist> {
return api.patch<Playlist>(`/api/playlists/${encodeURIComponent(id)}`, input);
}
export async function deletePlaylist(id: string): Promise<void> {
await api.del(`/api/playlists/${encodeURIComponent(id)}`);
}
export async function appendTracks(playlistID: string, trackIDs: string[]): Promise<PlaylistDetail> {
return api.post<PlaylistDetail>(
`/api/playlists/${encodeURIComponent(playlistID)}/tracks`,
{ track_ids: trackIDs }
);
}
export async function removePlaylistTrack(playlistID: string, position: number): Promise<PlaylistDetail> {
return api.del<PlaylistDetail>(
`/api/playlists/${encodeURIComponent(playlistID)}/tracks/${position}`
);
}
export async function reorderPlaylist(playlistID: string, orderedPositions: number[]): Promise<PlaylistDetail> {
return api.put<PlaylistDetail>(
`/api/playlists/${encodeURIComponent(playlistID)}/tracks`,
{ ordered_positions: orderedPositions }
);
}
export function createPlaylistsQuery(kind?: PlaylistKind) {
return createQuery({
queryKey: qk.playlists(kind),
queryFn: () => listPlaylists(kind),
staleTime: 30_000
});
}
export function createPlaylistQuery(id: string) {
return createQuery({
queryKey: qk.playlist(id),
queryFn: () => getPlaylist(id),
staleTime: 30_000
});
}