feat: add personal playlists and shareable track links
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
3a097c15c8
commit
fa79394869
46 files changed
+2307
-292
No files matched your search
@@ -75,6 +75,7 @@ export function fakeApi(initial: FakeUser[]) {
|
||||
let token = 0;
|
||||
const calls: { method: string; path: string; body: unknown }[] = [];
|
||||
const reactions = new Map<string, 'like' | 'dislike'>();
|
||||
const playlists = new Map<string, components['schemas']['Playlist']>();
|
||||
|
||||
const me = (u: FakeUser): Me => ({
|
||||
id: u.id,
|
||||
@@ -127,6 +128,23 @@ export function fakeApi(initial: FakeUser[]) {
|
||||
|
||||
if (!user || !req.headers.get('Authorization')) return problem(401, 'auth.unauthorized');
|
||||
|
||||
if (path === '/api/v1/me/playlists' && req.method === 'GET') return json({ items: [...playlists.values()] });
|
||||
const playlistPath = /^\/api\/v1\/me\/playlists\/([^/]+)$/.exec(path);
|
||||
if (path === '/api/v1/me/playlists' && req.method === 'POST' || playlistPath) {
|
||||
const id = playlistPath?.[1] ?? `00000000-0000-4000-9000-${String(++seq).padStart(12, '0')}`;
|
||||
const old = playlists.get(id);
|
||||
if (playlistPath && !old) return problem(404, 'playlist.not_found');
|
||||
if (req.method === 'GET') return json(old);
|
||||
if (req.method === 'DELETE') { playlists.delete(id); return new Response(null, { status: 204 }); }
|
||||
if (old && body?.revision !== old.revision) return problem(409, 'playlist.conflict');
|
||||
const tracks = (body?.track_ids as string[]).map(id => albumTracks.find(t => t.id === id)!);
|
||||
const item = { id, name: body?.name as string, description: body?.description as string, tracks,
|
||||
track_count: tracks.length, duration_ms: tracks.reduce((sum, t) => sum + t.duration_ms, 0),
|
||||
revision: (old?.revision ?? 0) + 1, created_at: old?.created_at ?? new Date().toISOString(), updated_at: new Date().toISOString() };
|
||||
playlists.set(id, item);
|
||||
return json(item, old ? 200 : 201);
|
||||
}
|
||||
|
||||
switch (route) {
|
||||
case 'POST /api/v1/auth/logout':
|
||||
case 'POST /api/v1/auth/logout-all':
|
||||
@@ -193,11 +211,14 @@ export function fakeApi(initial: FakeUser[]) {
|
||||
// «rbyj» — «кино» в английской раскладке: сервер находит и сообщает исправление.
|
||||
const q = url.searchParams.get('q') ?? '';
|
||||
const hit = q === 'кино' || q === 'rbyj';
|
||||
const tracks = path === '/api/v1/search' && hit
|
||||
? albumTracks.map((t) => ({ ...t, reaction: reactions.get(t.id) ?? null }))
|
||||
: [];
|
||||
return json({
|
||||
artists: hit ? [kino] : [],
|
||||
albums: hit ? [albumSummary] : [],
|
||||
tracks: [],
|
||||
totals: { artists: hit ? 1 : 0, albums: hit ? 1 : 0, tracks: 0 },
|
||||
tracks,
|
||||
totals: { artists: hit ? 1 : 0, albums: hit ? 1 : 0, tracks: tracks.length },
|
||||
corrected_query: q === 'rbyj' ? 'кино' : null,
|
||||
next_cursor: null,
|
||||
});
|
||||
@@ -260,6 +281,11 @@ export function fakeApi(initial: FakeUser[]) {
|
||||
return json(admin(target));
|
||||
}
|
||||
const reactionPath = /^\/api\/v1\/me\/reactions\/tracks\/([^/]+)$/.exec(path);
|
||||
const trackPath = /^\/api\/v1\/tracks\/([^/]+)$/.exec(path);
|
||||
if (trackPath && req.method === 'GET') {
|
||||
const track = albumTracks.find(t => t.id === trackPath[1]);
|
||||
return track ? json({ ...track, reaction: reactions.get(track.id) ?? null }) : problem(404, 'common.not_found');
|
||||
}
|
||||
if (reactionPath) {
|
||||
if (req.method === 'PUT') reactions.set(reactionPath[1]!, body?.reaction as 'like' | 'dislike');
|
||||
else reactions.delete(reactionPath[1]!);
|
||||
@@ -268,5 +294,5 @@ export function fakeApi(initial: FakeUser[]) {
|
||||
return problem(404, 'common.not_found');
|
||||
};
|
||||
|
||||
return { fetch: fetchImpl as typeof fetch, calls, users, signOut: () => (current = null) };
|
||||
return { fetch: fetchImpl as typeof fetch, calls, users, playlists, signOut: () => (current = null) };
|
||||
}
|
||||
Reference in new issue
Block a user