Fix query and API bugs: 403 for non-owners, page bounds, lifecycle filters, cover fallback, race-safe inserts, async album delete

This commit is contained in:
2026-09-24 05:14:05 +02:00
parent ab388e6ad6
commit 0e3576922f
90 changed files with 2083 additions and 322 deletions
+65 -2
View File
@@ -1396,6 +1396,13 @@ export interface operations {
};
content?: never;
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
@@ -1430,6 +1437,13 @@ export interface operations {
};
content?: never;
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
@@ -1465,6 +1479,13 @@ export interface operations {
};
content?: never;
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
@@ -1507,6 +1528,13 @@ export interface operations {
};
content?: never;
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
@@ -1825,6 +1853,13 @@ export interface operations {
};
content?: never;
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
@@ -1957,6 +1992,13 @@ export interface operations {
};
content?: never;
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
@@ -1991,6 +2033,13 @@ export interface operations {
};
content?: never;
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
@@ -2073,6 +2122,13 @@ export interface operations {
};
content?: never;
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
@@ -2353,6 +2409,13 @@ export interface operations {
};
content?: never;
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
@@ -2561,8 +2624,8 @@ export interface operations {
"application/json": components["schemas"]["TrackApplicationResponse"];
};
};
/** @description Not Found */
404: {
/** @description No Content */
204: {
headers: {
[name: string]: unknown;
};
+3 -2
View File
@@ -3,7 +3,7 @@ export const SEARCH_DEBOUNCE_MS = 450;
export const SEARCH_MIN_LENGTH = 2;
export const INFINITE_SCROLL_ROOT_MARGIN = '300px';
export const ALBUMS_PAGE_SIZE = 50;
export const ALBUM_TRACKS_PAGE_SIZE = 200;
export const ALBUM_TRACKS_PAGE_SIZE = 100;
export const ALBUM_EARLIEST_YEAR = 1877;
export const LIBRARY_PICKER_PAGE_SIZE = 50;
export const EXPLORE_ALBUMS_PAGE_SIZE = 10;
@@ -15,7 +15,8 @@ export const HOME_CONTINUE_LIMIT = 10;
export const HOME_SPOTLIGHT_TRACKS_LIMIT = 5;
export const HOME_POPULAR_MAX = 8;
export const PLAYLIST_PICKER_PAGE_SIZE = 50;
export const LIKED_TRACKS_PAGE_SIZE = 500;
export const LIKED_TRACKS_LIMIT = 500;
export const PLAYLIST_TRACKS_LIMIT = 500;
export const EXPLORE_USERS_LIMIT = 12;
export const FOLLOW_LIST_PAGE_SIZE = 30;
export const MENU_WIDTH = 240;
+22
View File
@@ -0,0 +1,22 @@
/** Largest page size the API accepts (`PageRequest.MaxPageSize` in the backend). */
export const API_MAX_PAGE_SIZE = 100;
type Page<T> = { items: T[]; hasNextPage: boolean };
/** Loads consecutive pages until the list ends or `maxItems` items were collected. */
export async function fetchAllPages<T>(
loadPage: (pageNumber: number, pageSize: number) => Promise<Page<T> | undefined>,
maxItems: number
): Promise<T[]> {
const items: T[] = [];
for (let pageNumber = 1; items.length < maxItems; pageNumber++) {
const page = await loadPage(pageNumber, API_MAX_PAGE_SIZE);
if (!page) break;
items.push(...page.items);
if (!page.hasNextPage) break;
}
return items.slice(0, maxItems);
}
@@ -7,6 +7,8 @@ import {
unwrapOrError
} from '$lib/server/api';
import { uploadPresignedImage } from '$lib/server/upload';
import { fetchAllPages } from '$lib/server/pagination';
import { PLAYLIST_TRACKS_LIMIT } from '$lib/config';
const MAX_NAME = 100;
@@ -21,11 +23,15 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
const tracksByPlaylist = await Promise.all(
data.items.map((playlist) =>
api
.GET('/playlists/{playlistId}/tracks', {
params: { path: { playlistId: playlist.id }, query: { pageNumber: 1, pageSize: 500 } }
})
.then((res) => res.data?.items ?? [])
fetchAllPages(
async (pageNumber, pageSize) =>
(
await api.GET('/playlists/{playlistId}/tracks', {
params: { path: { playlistId: playlist.id }, query: { pageNumber, pageSize } }
})
).data,
PLAYLIST_TRACKS_LIMIT
)
)
);
@@ -8,6 +8,8 @@ import {
unwrapOrFail
} from '$lib/server/api';
import { uploadPresignedImage } from '$lib/server/upload';
import { fetchAllPages } from '$lib/server/pagination';
import { PLAYLIST_TRACKS_LIMIT } from '$lib/config';
const MAX_NAME = 100;
@@ -16,15 +18,20 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
optionalUser(locals, url, allowAnonymousListening);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const [playlistRes, tracksRes] = await Promise.all([
const [playlistRes, tracks] = await Promise.all([
api.GET('/playlists/{id}', { params: { path: { id: params.id } } }),
api.GET('/playlists/{playlistId}/tracks', {
params: { path: { playlistId: params.id }, query: { pageNumber: 1, pageSize: 200 } }
})
fetchAllPages(
async (pageNumber, pageSize) =>
(
await api.GET('/playlists/{playlistId}/tracks', {
params: { path: { playlistId: params.id }, query: { pageNumber, pageSize } }
})
).data,
PLAYLIST_TRACKS_LIMIT
)
]);
const playlist = unwrapOrError(playlistRes, 'Playlist no encontrada.', 404);
const tracks = tracksRes.data?.items ?? [];
return { playlist, tracks };
};
+7 -5
View File
@@ -3,7 +3,8 @@ import type { LayoutServerLoad } from './$types';
import { getAllowAnonymousListening } from '$lib/server/playbackConfig';
import { createApiClient } from '$lib/server/api';
import { authConfig } from '$lib/server/config';
import { LIKED_TRACKS_PAGE_SIZE } from '$lib/config';
import { LIKED_TRACKS_LIMIT } from '$lib/config';
import { fetchAllPages } from '$lib/server/pagination';
const PUBLIC_PATHS = new Set(['/auth']);
const SIDEBAR_PLAYLISTS_LIMIT = 8;
@@ -30,10 +31,11 @@ async function fetchSidebarPlaylists(
async function fetchLikedTracks(fetchFn: typeof fetch, accessToken: string | null) {
try {
const api = createApiClient({ fetch: fetchFn, accessToken: accessToken ?? undefined });
const { data } = await api.GET('/likes', {
params: { query: { pageNumber: 1, pageSize: LIKED_TRACKS_PAGE_SIZE } }
});
return data?.items ?? [];
return await fetchAllPages(
async (pageNumber, pageSize) =>
(await api.GET('/likes', { params: { query: { pageNumber, pageSize } } })).data,
LIKED_TRACKS_LIMIT
);
} catch {
return [];
}