d67c0de596
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>
108 lines
3.4 KiB
TypeScript
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
|
|
});
|
|
}
|
|
|