From de50002ea7f152335804661ac3b83e0602d83b62 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 21 May 2023 07:30:28 -0700 Subject: [PATCH] Add random song list query --- src/renderer/api/controller.ts | 15 +++++- .../api/jellyfin/jellyfin-controller.ts | 48 +++++++++++++++++++ src/renderer/api/query-keys.ts | 9 +++- src/renderer/api/subsonic/subsonic-api.ts | 8 ++++ .../api/subsonic/subsonic-controller.ts | 31 ++++++++++++ src/renderer/api/subsonic/subsonic-types.ts | 16 +++++++ src/renderer/api/types.ts | 14 ++++++ 7 files changed, 138 insertions(+), 3 deletions(-) diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index 770e56fa..ffea7de0 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -48,7 +48,7 @@ import type { SearchResponse, } from '/@/renderer/api/types'; import { ServerType } from '/@/renderer/types'; -import { DeletePlaylistResponse } from './types'; +import { DeletePlaylistResponse, RandomSongListArgs } from './types'; import { ndController } from '/@/renderer/api/navidrome/navidrome-controller'; import { ssController } from '/@/renderer/api/subsonic/subsonic-controller'; import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller'; @@ -80,6 +80,7 @@ export type ControllerEndpoint = Partial<{ getPlaylistDetail: (args: PlaylistDetailArgs) => Promise; getPlaylistList: (args: PlaylistListArgs) => Promise; getPlaylistSongList: (args: PlaylistSongListArgs) => Promise; + getRandomSongList: (args: RandomSongListArgs) => Promise; getSongDetail: (args: SongDetailArgs) => Promise; getSongList: (args: SongListArgs) => Promise; getTopSongs: (args: TopSongListArgs) => Promise; @@ -122,6 +123,7 @@ const endpoints: ApiController = { getPlaylistDetail: jfController.getPlaylistDetail, getPlaylistList: jfController.getPlaylistList, getPlaylistSongList: jfController.getPlaylistSongList, + getRandomSongList: jfController.getRandomSongList, getSongDetail: undefined, getSongList: jfController.getSongList, getTopSongs: jfController.getTopSongList, @@ -156,6 +158,7 @@ const endpoints: ApiController = { getPlaylistDetail: ndController.getPlaylistDetail, getPlaylistList: ndController.getPlaylistList, getPlaylistSongList: ndController.getPlaylistSongList, + getRandomSongList: ssController.getRandomSongList, getSongDetail: ndController.getSongDetail, getSongList: ndController.getSongList, getTopSongs: ssController.getTopSongList, @@ -436,6 +439,15 @@ const search = async (args: SearchArgs) => { )?.(args); }; +const getRandomSongList = async (args: RandomSongListArgs) => { + return ( + apiController( + 'getRandomSongList', + args.apiClientProps.server?.type, + ) as ControllerEndpoint['getRandomSongList'] + )?.(args); +}; + export const controller = { addToPlaylist, authenticate, @@ -453,6 +465,7 @@ export const controller = { getPlaylistDetail, getPlaylistList, getPlaylistSongList, + getRandomSongList, getSongDetail, getSongList, getTopSongList, diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 6dcbe46e..7492e0ef 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -42,12 +42,15 @@ import { PlaylistListResponse, SearchArgs, SearchResponse, + RandomSongListResponse, + RandomSongListArgs, } from '/@/renderer/api/types'; import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api'; import { jfNormalize } from './jellyfin-normalize'; import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types'; import packageJson from '../../../../package.json'; import { z } from 'zod'; +import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types'; const formatCommaDelimitedString = (value: string[]) => { return value.join(','); @@ -798,6 +801,50 @@ const search = async (args: SearchArgs): Promise => { }; }; +const getRandomSongList = async (args: RandomSongListArgs): Promise => { + const { query, apiClientProps } = args; + + if (!apiClientProps.server?.userId) { + throw new Error('No userId found'); + } + + const yearsGroup = []; + if (query.minYear && query.maxYear) { + for (let i = Number(query.minYear); i <= Number(query.maxYear); i += 1) { + yearsGroup.push(String(i)); + } + } + + const yearsFilter = yearsGroup.length ? formatCommaDelimitedString(yearsGroup) : undefined; + + const res = await jfApiClient(apiClientProps).getSongList({ + params: { + userId: apiClientProps.server?.userId, + }, + query: { + Fields: 'Genres, DateCreated, MediaSources, ParentId', + GenreIds: query.genre ? query.genre : undefined, + IncludeItemTypes: 'Audio', + Limit: query.limit, + ParentId: query.musicFolderId, + Recursive: true, + SortBy: JFSongListSort.RANDOM, + SortOrder: JFSortOrder.ASC, + StartIndex: 0, + Years: yearsFilter, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get random songs'); + } + + return { + items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')), + startIndex: 0, + totalRecordCount: res.body.Items.length || 0, + }; +}; export const jfController = { addToPlaylist, authenticate, @@ -815,6 +862,7 @@ export const jfController = { getPlaylistDetail, getPlaylistList, getPlaylistSongList, + getRandomSongList, getSongList, getTopSongList, removeFromPlaylist, diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index ffec8089..26f56039 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -13,6 +13,7 @@ import type { TopSongListQuery, SearchQuery, SongDetailQuery, + RandomSongListQuery, } from './types'; export const queryKeys: Record< @@ -76,8 +77,8 @@ export const queryKeys: Record< return [serverId, 'playlists', 'list'] as const; }, root: (serverId: string) => [serverId, 'playlists'] as const, - songList: (serverId: string, id: string, query?: PlaylistSongListQuery) => { - if (query) return [serverId, 'playlists', id, 'songList', query] as const; + songList: (serverId: string, id?: string, query?: PlaylistSongListQuery) => { + if (query && id) return [serverId, 'playlists', id, 'songList', query] as const; if (id) return [serverId, 'playlists', id, 'songList'] as const; return [serverId, 'playlists', 'songList'] as const; }, @@ -101,6 +102,10 @@ export const queryKeys: Record< if (query) return [serverId, 'songs', 'list', query] as const; return [serverId, 'songs', 'list'] as const; }, + randomSongList: (serverId: string, query?: RandomSongListQuery) => { + if (query) return [serverId, 'songs', 'randomSongList', query] as const; + return [serverId, 'songs', 'randomSongList'] as const; + }, root: (serverId: string) => [serverId, 'songs'] as const, }, users: { diff --git a/src/renderer/api/subsonic/subsonic-api.ts b/src/renderer/api/subsonic/subsonic-api.ts index fb908f47..29cf4156 100644 --- a/src/renderer/api/subsonic/subsonic-api.ts +++ b/src/renderer/api/subsonic/subsonic-api.ts @@ -41,6 +41,14 @@ export const contract = c.router({ 200: ssType._response.musicFolderList, }, }, + getRandomSongList: { + method: 'GET', + path: 'getRandomSongs.view', + query: ssType._parameters.randomSongList, + responses: { + 200: ssType._response.randomSongList, + }, + }, getTopSongsList: { method: 'GET', path: 'getTopSongs.view', diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 76c1d9b7..959afe14 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -19,6 +19,8 @@ import { TopSongListArgs, SearchArgs, SearchResponse, + RandomSongListResponse, + RandomSongListArgs, } from '/@/renderer/api/types'; import { randomString } from '/@/renderer/utils'; @@ -339,11 +341,40 @@ const search3 = async (args: SearchArgs): Promise => { }; }; +const getRandomSongList = async (args: RandomSongListArgs): Promise => { + const { query, apiClientProps } = args; + + const res = await ssApiClient(apiClientProps).getRandomSongList({ + query: { + fromYear: query.minYear, + genre: query.genre, + musicFolderId: query.musicFolderId, + size: query.limit, + toYear: query.maxYear, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get random songs'); + } + + console.log('res', res); + + return { + items: res.body.randomSongs?.song?.map((song) => + ssNormalize.song(song, apiClientProps.server, ''), + ), + startIndex: 0, + totalRecordCount: res.body.randomSongs?.song?.length || 0, + }; +}; + export const ssController = { authenticate, createFavorite, getArtistInfo, getMusicFolderList, + getRandomSongList, getTopSongList, removeFavorite, scrobble, diff --git a/src/renderer/api/subsonic/subsonic-types.ts b/src/renderer/api/subsonic/subsonic-types.ts index f70248b2..1c8dd7f7 100644 --- a/src/renderer/api/subsonic/subsonic-types.ts +++ b/src/renderer/api/subsonic/subsonic-types.ts @@ -192,12 +192,27 @@ const search3Parameters = z.object({ songOffset: z.number().optional(), }); +const randomSongListParameters = z.object({ + fromYear: z.number().optional(), + genre: z.string().optional(), + musicFolderId: z.string().optional(), + size: z.number().optional(), + toYear: z.number().optional(), +}); + +const randomSongList = z.object({ + randomSongs: z.object({ + song: z.array(song), + }), +}); + export const ssType = { _parameters: { albumList: albumListParameters, artistInfo: artistInfoParameters, authenticate: authenticateParameters, createFavorite: createFavoriteParameters, + randomSongList: randomSongListParameters, removeFavorite: removeFavoriteParameters, scrobble: scrobbleParameters, search3: search3Parameters, @@ -214,6 +229,7 @@ export const ssType = { baseResponse, createFavorite, musicFolderList, + randomSongList, removeFavorite, scrobble, search3, diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index e592c288..4141d319 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -1002,6 +1002,20 @@ export type SearchResponse = { songs: Song[]; }; +export type RandomSongListQuery = { + genre?: string; + limit?: number; + maxYear?: number; + minYear?: number; + musicFolderId?: string; +}; + +export type RandomSongListArgs = { + query: RandomSongListQuery; +} & BaseEndpointArgs; + +export type RandomSongListResponse = SongListResponse; + export const instanceOfCancellationError = (error: any) => { return 'revert' in error; };