Separate responsibilities: start-listening command, API-side JSON converters, bulk track endpoints, mix kinds
This commit is contained in:
Vendored
+131
-80
@@ -142,6 +142,23 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/albums/{id}/tracks": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Add Your Uploaded Tracks To An Album, Skipping The Ones It Already Has. */
|
||||
post: operations["AddAlbumTracks"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/albums/{albumId}/tracks/{trackId}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -151,8 +168,7 @@ export interface paths {
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Add One Of Your Uploaded Tracks To An Album. */
|
||||
post: operations["AddTrackToAlbum"];
|
||||
post?: never;
|
||||
/** Remove A Track From An Album. */
|
||||
delete: operations["RemoveTrackFromAlbum"];
|
||||
options?: never;
|
||||
@@ -323,8 +339,7 @@ export interface paths {
|
||||
/** Get Paginated Tracks Of A PlayList. */
|
||||
get: operations["GetPlayListTracks"];
|
||||
put?: never;
|
||||
/** Add One Of Your Tracks To A PlayList. */
|
||||
post: operations["AddPlayListTrack"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
@@ -400,6 +415,23 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/playlists/{id}/tracks": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Add Tracks To A PlayList, Skipping The Ones It Already Has. */
|
||||
post: operations["AddPlayListTracks"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/playlists/{playlistId}/tracks/{trackId}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -729,9 +761,8 @@ export interface paths {
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
AddPlayListTrackRequest: {
|
||||
/** Format: uuid */
|
||||
trackId: string;
|
||||
AddTracksRequest: {
|
||||
trackIds: string[];
|
||||
};
|
||||
AlbumApplicationResponse: {
|
||||
/** Format: uuid */
|
||||
@@ -810,8 +841,7 @@ export interface components {
|
||||
MixApplicationResponse: {
|
||||
/** Format: uuid */
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: null | string;
|
||||
kind: components["schemas"]["MixKind"];
|
||||
/** Format: int32 */
|
||||
itemCount: number | string;
|
||||
items: components["schemas"]["MixItemApplicationResponse"][];
|
||||
@@ -826,6 +856,8 @@ export interface components {
|
||||
/** Format: int32 */
|
||||
listensCount: number | string;
|
||||
};
|
||||
/** @enum {unknown} */
|
||||
MixKind: "Discovery" | "Daily";
|
||||
PaginatedResponseOfAlbumApplicationResponse: {
|
||||
items: components["schemas"]["AlbumApplicationResponse"][];
|
||||
/** Format: int32 */
|
||||
@@ -1157,7 +1189,7 @@ export interface operations {
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
userId: number | string;
|
||||
userId: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -1415,17 +1447,20 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
AddTrackToAlbum: {
|
||||
AddAlbumTracks: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
albumId: string;
|
||||
trackId: string;
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["AddTracksRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description No Content */
|
||||
204: {
|
||||
@@ -1434,6 +1469,15 @@ export interface operations {
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["HttpValidationProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
@@ -1735,7 +1779,7 @@ export interface operations {
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
userId: number | string;
|
||||
userId: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -1784,60 +1828,6 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
AddPlayListTrack: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
playlistId: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["AddPlayListTrackRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Created */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["TrackApplicationResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Not Found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Conflict */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
GetPlayListCover: {
|
||||
parameters: {
|
||||
query?: {
|
||||
@@ -2058,6 +2048,67 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
AddPlayListTracks: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["AddTracksRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description No Content */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["HttpValidationProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Not Found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Conflict */
|
||||
409: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
RemoveTrackFromPlayList: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -2329,7 +2380,7 @@ export interface operations {
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
userId: number | string;
|
||||
userId: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -2477,7 +2528,7 @@ export interface operations {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | string;
|
||||
id: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -2506,7 +2557,7 @@ export interface operations {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | string;
|
||||
id: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -2535,7 +2586,7 @@ export interface operations {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | string;
|
||||
id: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -2571,7 +2622,7 @@ export interface operations {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | string;
|
||||
id: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -2600,7 +2651,7 @@ export interface operations {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | string;
|
||||
id: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -2632,7 +2683,7 @@ export interface operations {
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | string;
|
||||
id: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -2671,7 +2722,7 @@ export interface operations {
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | string;
|
||||
id: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -2700,7 +2751,7 @@ export interface operations {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | string;
|
||||
id: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -2729,7 +2780,7 @@ export interface operations {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | string;
|
||||
id: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -2772,7 +2823,7 @@ export interface operations {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | string;
|
||||
id: unknown;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,21 @@ import type { ApiTrackLike } from '$lib/player/player.svelte';
|
||||
export type Mix = components['schemas']['MixApplicationResponse'];
|
||||
export type MixItem = components['schemas']['MixItemApplicationResponse'];
|
||||
|
||||
const MIX_TEXTS: Record<Mix['kind'], { title: string; subtitle: string }> = {
|
||||
Discovery: {
|
||||
title: 'Descubrimiento',
|
||||
subtitle: 'Canciones de tu biblioteca que todavía no has escuchado.'
|
||||
},
|
||||
Daily: {
|
||||
title: 'Tu mezcla diaria',
|
||||
subtitle: 'Lo que más escuchas, con alguna sorpresa de tu biblioteca.'
|
||||
}
|
||||
};
|
||||
|
||||
export function mixText(mix: Pick<Mix, 'kind'>): { title: string; subtitle: string } {
|
||||
return MIX_TEXTS[mix.kind];
|
||||
}
|
||||
|
||||
export function mixItemKey(item: MixItem): string {
|
||||
return `mf:${item.trackId}`;
|
||||
}
|
||||
|
||||
@@ -15,15 +15,12 @@ export async function addTrackAction({ request, params, locals, fetch }: Request
|
||||
}
|
||||
|
||||
const api = createApiClient({ fetch, accessToken });
|
||||
const { error: err, response } = await api.POST('/playlists/{playlistId}/tracks', {
|
||||
params: { path: { playlistId } },
|
||||
body: { trackId }
|
||||
const { error: err } = await api.POST('/playlists/{id}/tracks', {
|
||||
params: { path: { id: playlistId } },
|
||||
body: { trackIds: [trackId] }
|
||||
});
|
||||
|
||||
if (err) {
|
||||
if (response?.status === 409) {
|
||||
return fail(409, { message: 'La canción ya está en esa playlist.', trackId });
|
||||
}
|
||||
return fail(502, { message: 'No se pudo añadir la canción.', trackId });
|
||||
}
|
||||
|
||||
@@ -51,14 +48,18 @@ export async function addAlbumToPlaylistAction({ request, params, locals, fetch
|
||||
return fail(502, { message: 'No se pudo cargar el álbum.', albumId });
|
||||
}
|
||||
|
||||
let added = 0;
|
||||
for (const track of tracks.items) {
|
||||
const { error: err } = await api.POST('/playlists/{playlistId}/tracks', {
|
||||
params: { path: { playlistId } },
|
||||
body: { trackId: String(track.id) }
|
||||
});
|
||||
if (!err) added++;
|
||||
const trackIds = tracks.items.map((track) => String(track.id));
|
||||
if (trackIds.length === 0) {
|
||||
return { addedAlbum: true, albumId, playlistId, added: 0 };
|
||||
}
|
||||
|
||||
return { addedAlbum: true, albumId, playlistId, added };
|
||||
const { error: addErr } = await api.POST('/playlists/{id}/tracks', {
|
||||
params: { path: { id: playlistId } },
|
||||
body: { trackIds }
|
||||
});
|
||||
if (addErr) {
|
||||
return fail(502, { message: 'No se pudo añadir el álbum.', albumId });
|
||||
}
|
||||
|
||||
return { addedAlbum: true, albumId, playlistId, added: trackIds.length };
|
||||
}
|
||||
|
||||
@@ -54,8 +54,9 @@ export const actions: Actions = {
|
||||
if (!trackId) return fail(400, { message: 'Falta la canción.' });
|
||||
|
||||
const api = createApiClient({ fetch, accessToken });
|
||||
const result = await api.POST('/albums/{albumId}/tracks/{trackId}', {
|
||||
params: { path: { albumId: params.id, trackId } }
|
||||
const result = await api.POST('/albums/{id}/tracks', {
|
||||
params: { path: { id: params.id } },
|
||||
body: { trackIds: [trackId] }
|
||||
});
|
||||
const failure = unwrapOrFail(result, 'No se pudo añadir la canción.');
|
||||
if (failure) return failure;
|
||||
|
||||
@@ -18,11 +18,12 @@
|
||||
import ContextMenu from '$lib/components/ui/overlay/ContextMenu.svelte';
|
||||
import ListPlus from '@lucide/svelte/icons/list-plus';
|
||||
import { createTrackMenu } from '$lib/state/menus.svelte';
|
||||
import { mixItemTrack } from '$lib/data/mixes';
|
||||
import { mixItemTrack, mixText } from '$lib/data/mixes';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
const mix = $derived(data.mix);
|
||||
const text = $derived(mixText(mix));
|
||||
const items = $derived(mix.items);
|
||||
const tracks = $derived(items.map(mixItemTrack));
|
||||
const queue = $derived(tracks.map(toQueueItem));
|
||||
@@ -49,8 +50,8 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{mix.title}</title>
|
||||
<meta name="description" content="Mezcla {mix.title}, hecha para ti." />
|
||||
<title>{text.title}</title>
|
||||
<meta name="description" content="Mezcla {text.title}, hecha para ti." />
|
||||
</svelte:head>
|
||||
|
||||
<Page>
|
||||
@@ -58,15 +59,15 @@
|
||||
|
||||
<PageHeader
|
||||
eyebrow="Mezcla"
|
||||
title={mix.title}
|
||||
description={mix.subtitle ?? undefined}
|
||||
title={text.title}
|
||||
description={text.subtitle}
|
||||
meta="{plural(items.length, 'canción', 'canciones')} · {fmtTime(totalSeconds)}"
|
||||
>
|
||||
{#snippet cover()}
|
||||
<Artwork
|
||||
trackIds={items.map((item) => item.trackId)}
|
||||
size="hero"
|
||||
alt={mix.title}
|
||||
alt={text.title}
|
||||
class="shrink-0"
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
} from '$lib/player/player.svelte';
|
||||
import { pressable } from '$lib/actions/pressable';
|
||||
import { mergeRecentlyPlayed } from '$lib/data/recentlyPlayed';
|
||||
import { mixItemTrack } from '$lib/data/mixes';
|
||||
import { mixItemTrack, mixText } from '$lib/data/mixes';
|
||||
import { fmtTime, fmtDurationLong, fmtPlays, plural } from '$lib/utils/format';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
import Artwork from '$lib/components/ui/media/Artwork.svelte';
|
||||
@@ -279,8 +279,8 @@
|
||||
{#each mixes as mix, i (mix.id)}
|
||||
<MediaCard
|
||||
href="/mixes/{mix.id}"
|
||||
title={mix.title}
|
||||
subtitle={mix.subtitle}
|
||||
title={mixText(mix).title}
|
||||
subtitle={mixText(mix).subtitle}
|
||||
trackIds={mix.items.map((item) => item.trackId)}
|
||||
onPlay={(event) => playMix(mix, event)}
|
||||
index={i}
|
||||
|
||||
Reference in New Issue
Block a user