Skip to content

Commit

Permalink
Playlist Entries: Proper Paginated route
Browse files Browse the repository at this point in the history
  • Loading branch information
Arthi-chaud committed Jun 8, 2024
1 parent 81efbc9 commit 83275db
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 125 deletions.
36 changes: 28 additions & 8 deletions front/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,8 @@ import { InfiniteQuery, Query } from "./use-query";
import * as yup from "yup";
import { RequireExactlyOne } from "type-fest";
import Playlist, {
PlaylistInclude,
PlaylistEntryWithRelations,
PlaylistSortingKeys,
PlaylistWithRelations,
} from "../models/playlist";
import { isSSR } from "../utils/is-ssr";
import { ActiveTask, Task } from "../models/task";
Expand Down Expand Up @@ -261,21 +260,42 @@ export default class API {
* Fetch one playlist
* @returns An query for a playlist
*/
static getPlaylist<I extends PlaylistInclude | never = never>(
static getPlaylist(playlistSlugOrId: string | number): Query<Playlist> {
return {
key: ["playlist", playlistSlugOrId],
exec: () =>
API.fetch({
route: `/playlists/${playlistSlugOrId}`,
parameters: {},
validator: Playlist,
}),
};
}

/**
* Fetch all entries in a playlist
* @returns An InfiniteQuery of Songs
*/
static getPlaylistEntires<I extends SongInclude | never = never>(
playlistSlugOrId: string | number,
include?: I[],
): Query<PlaylistWithRelations<I>> {
): InfiniteQuery<PlaylistEntryWithRelations<I>> {
return {
key: [
"playlist",
playlistSlugOrId,
"entries",
...API.formatIncludeKeys(include),
],
exec: () =>
exec: (pagination) =>
API.fetch({
route: `/playlists/${playlistSlugOrId}`,
parameters: { include },
validator: PlaylistWithRelations(include ?? []),
route: `/playlists/${playlistSlugOrId}/entries`,
errorMessage: "Songs could not be loaded",
parameters: { pagination: pagination, include },
otherParameters: {},
validator: PaginatedResponse(
PlaylistEntryWithRelations(include ?? []),
),
}),
};
}
Expand Down
31 changes: 11 additions & 20 deletions front/src/models/playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import Illustration from "./illustration";
import Resource from "./resource";
import * as yup from "yup";
import Song from "./song";
import Song, { SongInclude, SongWithRelations } from "./song";

const PlaylistEntry = Song.concat(
yup.object({
Expand All @@ -32,6 +32,15 @@ const PlaylistEntry = Song.concat(

export type PlaylistEntry = yup.InferType<typeof PlaylistEntry>;

const PlaylistEntryWithRelations = <
Selection extends SongInclude | never = never,
>(
relation: Selection[],
) => PlaylistEntry.concat(SongWithRelations(relation));

type PlaylistEntryWithRelations<Selection extends SongInclude | never = never> =
yup.InferType<ReturnType<typeof PlaylistEntryWithRelations<Selection>>>;

const Playlist = Resource.concat(Illustration).concat(
yup.object({
/**
Expand All @@ -52,24 +61,6 @@ const Playlist = Resource.concat(Illustration).concat(

type Playlist = yup.InferType<typeof Playlist>;

export type PlaylistInclude = "entries";

const PlaylistWithRelations = <
Selection extends PlaylistInclude | never = never,
>(
relation: Selection[],
) =>
Playlist.concat(
yup
.object({
entries: yup.array(PlaylistEntry.required()).required(),
})
.pick(relation),
);

type PlaylistWithRelations<Selection extends PlaylistInclude | never = never> =
yup.InferType<ReturnType<typeof PlaylistWithRelations<Selection>>>;

export default Playlist;

export const PlaylistSortingKeys = [
Expand All @@ -78,4 +69,4 @@ export const PlaylistSortingKeys = [
"creationDate",
] as const;

export { PlaylistWithRelations };
export { PlaylistEntryWithRelations };
86 changes: 38 additions & 48 deletions front/src/pages/playlists/[slugOrId]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ import {
import PlaylistContextualMenu from "../../../components/contextual-menu/playlist-contextual-menu";
import Illustration from "../../../components/illustration";
import { Box, Button, Divider, Grid, IconButton, Stack } from "@mui/material";
import Artist from "../../../models/artist";
import { TrackWithRelations } from "../../../models/track";
import { SongWithRelations } from "../../../models/song";
import {
ContextualMenuIcon,
Expand All @@ -57,26 +55,35 @@ import { useTranslation } from "react-i18next";
import { generateArray } from "../../../utils/gen-list";
import { usePlayerContext } from "../../../contexts/player";
import { NextPageContext } from "next";
import Release from "../../../models/release";

const playlistQuery = (idOrSlug: number | string) =>
API.getPlaylist(idOrSlug, ["entries"]);
const masterTrackQuery = (songId: number | string) =>
API.getMasterTrack(songId, ["release"]);
const playlistQuery = (idOrSlug: number | string) => API.getPlaylist(idOrSlug);
const playlistEntriesQuery = (idOrSlug: number | string) => {
const query = API.getPlaylistEntires(idOrSlug, [
"artist",
"featuring",
"master",
]);
return {
key: query.key,
exec: () => query.exec({ pageSize: 10000 }).then(({ items }) => items),
};
};

const prepareSSR = async (
context: NextPageContext,
queryClient: QueryClient,
) => {
const playlistIdentifier = getSlugOrId(context.query);
const playlist = await queryClient.fetchQuery(
prepareMeeloQuery(() => playlistQuery(playlistIdentifier)),
const entries = await queryClient.fetchQuery(
prepareMeeloQuery(() => playlistEntriesQuery(playlistIdentifier)),
);

return {
additionalProps: { playlistIdentifier },
queries: [
...playlist.entries.map((entry) => masterTrackQuery(entry.id)),
...playlist.entries.map((entry) => API.getArtist(entry.artistId)),
playlistQuery(playlistIdentifier),
...entries.map((entry) => API.getRelease(entry.master.releaseId)),
],
};
};
Expand Down Expand Up @@ -204,64 +211,47 @@ const PlaylistPage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({
() => router.push("/playlists"),
);
const playlist = useQuery(playlistQuery, playlistIdentifier);
const artistsQueries = useQueries(
...(playlist.data?.entries.map(
({
artistId,
}): Parameters<
typeof useQuery<Artist, Parameters<typeof API.getArtist>>
> => [API.getArtist, artistId],
) ?? []),
);
const masterTracksQueries = useQueries(
...(playlist.data?.entries.map(
const entriesQuery = useQuery(playlistEntriesQuery, playlistIdentifier);
const masterTracksReleaseQueries = useQueries(
...(entriesQuery.data?.map(
({
id,
master,
}): Parameters<
typeof useQuery<
TrackWithRelations<"release">,
Parameters<typeof masterTrackQuery>
>
> => [masterTrackQuery, id],
typeof useQuery<Release, Parameters<typeof API.getRelease>>
> => [API.getRelease, master.releaseId],
) ?? []),
);
const reorderMutation = useMutation((reorderedEntries: number[]) => {
return API.reorderPlaylist(playlistIdentifier, reorderedEntries)
.then(() => {
toast.success(t("playlistReorderSuccess"));
return playlist.refetch();
return entriesQuery.refetch();
})
.catch(() => toast.error(t("playlistReorderFail")));
});

const entries = useMemo(() => {
const artists = artistsQueries.map((query) => query.data);
const masterTracks = masterTracksQueries.map((query) => query.data);
const resolvedTracks = masterTracks.filter(
(data) => data !== undefined,
);
const resolvedArtists = artists.filter((data) => data !== undefined);
const releases = masterTracksReleaseQueries.map((query) => query.data);
const resolvedReleases = releases.filter((data) => data !== undefined);

if (
resolvedTracks.length !== masterTracks.length ||
resolvedArtists.length !== artists.length
) {
if (resolvedReleases.length !== entriesQuery.data?.length) {
return undefined;
}

return playlist.data?.entries.map((entry) => ({
return entriesQuery.data.map((entry) => ({
...entry,
track: masterTracks.find((master) => master!.songId == entry.id)!,
artist: artists.find((artist) => artist!.id == entry.artistId)!,
release: releases.find(
(release) => release!.id == entry.master.releaseId,
)!,
}));
}, [artistsQueries, masterTracksQueries, playlist.data]);
}, [entriesQuery.data, masterTracksReleaseQueries]);
const playPlaylist = (fromIndex: number) =>
entries &&
playTracks({
tracks: entries.map((entry) => ({
track: entry.track,
track: entry.master,
artist: entry.artist,
release: entry.track.release,
release: entry.release,
})),
cursor: fromIndex,
});
Expand All @@ -270,9 +260,9 @@ const PlaylistPage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({
playTracks({
tracks: shuffle(
entries.map((entry) => ({
track: entry.track,
track: entry.master,
artist: entry.artist,
release: entry.track.release,
release: entry.release,
})),
),
cursor: 0,
Expand Down Expand Up @@ -313,7 +303,7 @@ const PlaylistPage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({
"shuffle",
() => <ShuffleIcon />,
"outlined",
() => shufflePlaylist,
() => shufflePlaylist(),
],
] as const
).map(([label, Icon, variant, callback], index) => (
Expand All @@ -339,7 +329,7 @@ const PlaylistPage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({
/>
) : (
<Stack spacing={1}>
{(entries ?? generateArray(6)).map((entry, index) => (
{(entries ?? generateArray(2)).map((entry, index) => (
<PlaylistEntryItem
key={index}
entry={entry}
Expand Down
24 changes: 24 additions & 0 deletions server/src/playlist/models/playlist-entry.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Meelo is a music server and application to enjoy your personal music files anywhere, anytime you want.
* Copyright (C) 2023
*
* Meelo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Meelo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { SongWithRelations } from "src/prisma/models";

export type PlaylistEntryModel = SongWithRelations & {
entryId: number;
index: number;
};
8 changes: 0 additions & 8 deletions server/src/playlist/models/playlist.query-parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { Playlist } from "src/prisma/models";
import Slug from "src/slug/slug";
import SongQueryParameters from "src/song/models/song.query-params";
import type { RequireAtLeastOne, RequireExactlyOne } from "type-fest";
import type { RelationInclude as BaseRelationInclude } from "src/relation-include/models/relation-include";
import { ModelSortingParameter } from "src/sort/models/sorting-parameter";
import AlbumQueryParameters from "src/album/models/album.query-parameters";

Expand Down Expand Up @@ -65,13 +64,6 @@ namespace PlaylistQueryParameters {
*/
export type DeleteInput = WhereInput;

/**
* Defines what relations to include in query
*/
export const AvailableIncludes = ["entries"] as const;
export const AvailableAtomicIncludes = AvailableIncludes;
export type RelationInclude = BaseRelationInclude<typeof AvailableIncludes>;

/**
* Defines how to sort fetched entries
*/
Expand Down
Loading

0 comments on commit 83275db

Please sign in to comment.