Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Shuffling Videos #777

Merged
merged 2 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions front/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,7 @@ export default class API {
artist?: Identifier;
album?: Identifier;
song?: Identifier;
random?: number;
},
sort?: SortingParameters<typeof SongSortingKeys>,
include?: I[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,90 @@ import InfiniteView from "../infinite-view";
import InfiniteResourceViewProps from "./infinite-resource-view-props";
import VideoTile from "../../tile/video-tile";
import { PaginationParameters } from "../../../models/pagination";
import { PlayIcon, ShuffleIcon } from "../../icons";
import { PlayerActions, usePlayerContext } from "../../../contexts/player";
import {
InfiniteQuery,
QueryClient,
prepareMeeloInfiniteQuery,
useQueryClient,
} from "../../../api/use-query";

const InfiniteVideoView = <T extends VideoWithRelations<"artist">>(
props: InfiniteResourceViewProps<T, typeof SongSortingKeys> &
const playVideosAction = (
emptyPlaylist: PlayerActions["emptyPlaylist"],
playTrack: PlayerActions["playTrack"],
playAfter: PlayerActions["playAfter"],
queryClient: QueryClient,
query: () => InfiniteQuery<VideoWithRelations<"artist" | "featuring">>,
) => {
emptyPlaylist();
queryClient.client
.fetchInfiniteQuery(prepareMeeloInfiniteQuery(query))
.then(async (res) => {
const videos = res.pages.flatMap(({ items }) => items);
let i = 0;
for (const video of videos) {
if (i == 0) {
playTrack(video);
} else {
playAfter(video);
}
i++;
}
});
};

const InfiniteVideoView = <
T extends VideoWithRelations<"artist" | "featuring">,
>(
props: InfiniteResourceViewProps<
T,
typeof SongSortingKeys,
{ random?: number }
> &
Omit<ComponentProps<typeof VideoTile>, "video">,
) => {
const router = useRouter();
const queryClient = useQueryClient();
const [options, setOptions] =
useState<OptionState<typeof SongSortingKeys>>();

const query = {
sortBy: options?.sortBy ?? props.initialSortingField ?? "name",
order: options?.order ?? props.initialSortingOrder ?? "asc",
view: "grid",
library: options?.library ?? null,
} as const;
const { emptyPlaylist, playAfter, playTrack } = usePlayerContext();
const playAction = {
label: "playAll",
icon: <PlayIcon />,
onClick: () => {
playVideosAction(
emptyPlaylist,
playTrack,
playAfter,
queryClient,
() => props.query(query),
);
},
} as const;
const shuffleAction = {
label: "shuffle",
icon: <ShuffleIcon />,
onClick: () => {
playVideosAction(
emptyPlaylist,
playTrack,
playAfter,
queryClient,
() =>
props.query({
...query,
random: Math.floor(Math.random() * 10000),
}),
);
},
} as const;
return (
<>
<Controls
Expand All @@ -44,6 +119,7 @@ const InfiniteVideoView = <T extends VideoWithRelations<"artist">>(
router={props.light == true ? undefined : router}
defaultLayout={"grid"}
disableLayoutToggle
actions={[playAction, shuffleAction]}
/>
<InfiniteView
view={options?.view ?? "grid"}
Expand Down
3 changes: 2 additions & 1 deletion front/src/pages/artists/[slugOrId]/videos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ const ArtistSongPage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({
<InfiniteVideoView
initialSortingField={props?.sortBy}
initialSortingOrder={props?.order}
query={({ sortBy, order, library }) =>
query={({ sortBy, order, library, random }) =>
API.getVideos(
{
artist: artistIdentifier,
random,
library: library ?? undefined,
},
{ sortBy, order },
Expand Down
4 changes: 2 additions & 2 deletions front/src/pages/videos/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ const LibraryVideosPage: Page<GetPropsTypesFrom<typeof prepareSSR>> = ({
<InfiniteVideoView
initialSortingField={props?.sortBy}
initialSortingOrder={props?.order}
query={({ library, sortBy, order }) =>
query={({ library, sortBy, order, random }) =>
API.getVideos(
{ library: library ?? undefined },
{ library: library ?? undefined, random },
{ sortBy, order },
["artist", "featuring"],
)
Expand Down
4 changes: 2 additions & 2 deletions server/src/video/video.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ export class VideoController {
@RelationIncludeQuery(SongQueryParameters.AvailableAtomicIncludes)
include: SongQueryParameters.RelationInclude,
) {
return this.videoService.getVideos(
return this.videoService.getMany(
selector,
paginationParameters,
include,
sort,
selector.random ?? sort,
);
}
}
6 changes: 3 additions & 3 deletions server/src/video/video.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe("Video Service", () => {

describe("Get Songs With Videos", () => {
it("should return the songs With video", async () => {
const videoSongs = await videoService.getVideos({});
const videoSongs = await videoService.getMany({});
expect(videoSongs.length).toBe(1);
expect(videoSongs[0]).toStrictEqual({
...dummyRepository.songA1,
Expand All @@ -70,11 +70,11 @@ describe("Video Service", () => {
});
});
it("should return an empty list (pagination)", async () => {
const videoSongs = await videoService.getVideos({}, { skip: 1 });
const videoSongs = await videoService.getMany({}, { skip: 1 });
expect(videoSongs.length).toBe(0);
});
it("should return songs with their artist", async () => {
const videoSongs = await videoService.getVideos(
const videoSongs = await videoService.getMany(
{},
{},
{ artist: true },
Expand Down
92 changes: 67 additions & 25 deletions server/src/video/video.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@
import { Injectable } from "@nestjs/common";
import { PaginationParameters } from "src/pagination/models/pagination-parameters";
import PrismaService from "src/prisma/prisma.service";
import { formatPaginationParameters } from "src/repository/repository.utils";
import {
formatPaginationParameters,
sortItemsUsingOrderedIdList,
} from "src/repository/repository.utils";
import SongQueryParameters from "src/song/models/song.query-params";
import SongService from "src/song/song.service";
import { Prisma, Song, Track } from "@prisma/client";
import { shuffle } from "src/utils/shuffle";

@Injectable()
export default class VideoService {
Expand All @@ -30,21 +35,78 @@ export default class VideoService {
private songService: SongService,
) {}

static formatManyWhereInput(
where: SongQueryParameters.ManyWhereInput,
): Prisma.SongWhereInput {
return {
OR: [
SongService.formatManyWhereInput(where),
{
group: {
versions: {
some: SongService.formatManyWhereInput(where),
},
},
},
],
AND: {
tracks: {
some: {
type: "Video",
},
},
},
};
}

private async getManyRandomIds(
where: SongQueryParameters.ManyWhereInput,
shuffleSeed: number,
pagination?: PaginationParameters,
) {
const ids = await this.prismaService.song
.findMany({
where: VideoService.formatManyWhereInput(where),
select: { id: true },
orderBy: { id: "asc" },
cursor: pagination?.afterId
? { id: pagination.afterId }
: undefined,
})
.then((items) => items.map(({ id }) => id));
return shuffle(shuffleSeed, ids).slice(
pagination?.skip ?? 0,
pagination?.take,
);
}

/**
* Get songs with at least one video track
* The songs are returned with its first video track
* @param where the query parameters to find the songs
* @param pagination the pagination parameters
* @param include the relations to include with the returned songs
*/
async getVideos<
async getMany<
I extends Omit<SongQueryParameters.RelationInclude, "tracks">,
>(
where: SongQueryParameters.ManyWhereInput,
pagination?: PaginationParameters,
include?: I,
sort?: SongQueryParameters.SortingParameter,
) {
sort?: SongQueryParameters.SortingParameter | number,
): Promise<(Song & { track: Track })[]> {
if (typeof sort == "number") {
const randomIds = await this.getManyRandomIds(
where,
sort,
pagination,
);
return this.getMany(
{ ...where, id: { in: randomIds } },
undefined,
include,
).then((items) => sortItemsUsingOrderedIdList(randomIds, items));
}
return this.prismaService.song
.findMany({
orderBy: sort?.sortBy
Expand Down Expand Up @@ -81,27 +143,7 @@ export default class VideoService {
},
},
...formatPaginationParameters(pagination),
where: {
OR: [
SongService.formatManyWhereInput(where),
{
group: {
versions: {
some: SongService.formatManyWhereInput(
where,
),
},
},
},
],
AND: {
tracks: {
some: {
type: "Video",
},
},
},
},
where: VideoService.formatManyWhereInput(where),
})
.then((songs) =>
songs.map(({ tracks, ...song }) => ({
Expand Down
Loading