diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index b735d5272..c8b2bfdbf 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -546,6 +546,18 @@ export const controller: GeneralController = { server.type, )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, + getPlaylistSongIds(args) { + const server = getServerById(args.apiClientProps.serverId); + + if (!server) { + throw new Error(`${i18n.t('error.apiRouteError')}: getPlaylistSongIds`); + } + + return apiController( + 'getPlaylistSongIds', + server.type, + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); + }, getPlaylistSongList(args) { const server = getServerById(args.apiClientProps.serverId); diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 89e96f8d0..7df7d48e0 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -197,8 +197,8 @@ const JF_FIELDS = { 'SortName', 'ProviderIds', ], - ALBUM_DETAIL: ['Genres', 'DateCreated', 'ChildCount', 'People', 'Tags', 'ProviderIds'], - ALBUM_LIST: ['People', 'Tags', 'Studios', 'SortName', 'ProviderIds', 'ChildCount'], + ALBUM_DETAIL: ['Genres', 'DateCreated', 'ChildCount', 'Tags', 'ProviderIds'], + ALBUM_LIST: ['Tags', 'Studios', 'SortName', 'ProviderIds', 'ChildCount'], FOLDER: ['Genres', 'DateCreated', 'MediaSources', 'ParentId'], GENRE: ['ItemCounts'], PLAYLIST_DETAIL: [ @@ -210,16 +210,7 @@ const JF_FIELDS = { 'SortName', ], PLAYLIST_LIST: ['ChildCount', 'Genres', 'DateCreated', 'ParentId', 'Overview'], - SONG: [ - 'Genres', - 'DateCreated', - 'MediaSources', - 'ParentId', - 'People', - 'Tags', - 'SortName', - 'ProviderIds', - ], + SONG: ['Genres', 'DateCreated', 'MediaSources', 'ParentId', 'Tags', 'SortName', 'ProviderIds'], } as const; export const JellyfinController: InternalControllerEndpoint = { @@ -1056,6 +1047,35 @@ export const JellyfinController: InternalControllerEndpoint = { apiClientProps, query: { ...query, limit: 1, startIndex: 0 }, }).then((result) => result!.totalRecordCount!), + getPlaylistSongIds: async (args) => { + const { apiClientProps, query } = args; + + if (!apiClientProps.server?.userId) { + throw new Error('No userId found'); + } + + const res = await jfApiClient(apiClientProps).getPlaylistSongList({ + params: { + id: query.id, + }, + query: { + // XXX: No fields are required for only IDs, which saves processing time between + // the Jellyfin server query, network (MBs vs KBs), and in-app parsing. + IncludeItemTypes: 'Audio', + UserId: apiClientProps.server?.userId, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get playlist song list IDs'); + } + + return { + items: res.body.Items.map((item) => item.Id), + startIndex: 0, + totalRecordCount: res.body.TotalRecordCount, + }; + }, getPlaylistSongList: async (args) => { const { apiClientProps, query } = args; @@ -1068,7 +1088,7 @@ export const JellyfinController: InternalControllerEndpoint = { id: query.id, }, query: { - Fields: JF_FIELDS.SONG, + Fields: JF_FIELDS.PLAYLIST_DETAIL, IncludeItemTypes: 'Audio', UserId: apiClientProps.server?.userId, }, diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index fcecea158..d1080d0a8 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -663,6 +663,11 @@ export const NavidromeController: InternalControllerEndpoint = { apiClientProps, query: { ...query, limit: 1, startIndex: 0 }, }).then((result) => result!.totalRecordCount!), + getPlaylistSongIds: async (args) => + NavidromeController.getPlaylistSongList(args).then((result) => ({ + ...result, + items: result.items.map((song) => song.id), + })), getPlaylistSongList: async (args: PlaylistSongListArgs): Promise => { const { apiClientProps, query } = args; diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index 84f0f2121..94ac40302 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -338,6 +338,9 @@ export const queryKeys: Record< return [serverId, 'playlists', 'songList'] as const; }, + songListIds: (serverId: string, id: string) => { + return [serverId, 'playlists', 'songListIds', id] as const; + }, }, radio: { list: (serverId: string) => [serverId, 'radio', 'list'] as const, diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 946612c03..0af0f095e 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -1229,6 +1229,11 @@ export const SubsonicController: InternalControllerEndpoint = { return results.length; }, + getPlaylistSongIds: async (args) => + SubsonicController.getPlaylistSongList(args).then((result) => ({ + ...result, + items: result.items.map((song) => song.id), + })), getPlaylistSongList: async ({ apiClientProps, query }) => { const res = await ssApiClient(apiClientProps).getPlaylist({ query: { diff --git a/src/renderer/features/context-menu/actions/add-to-playlist-action.tsx b/src/renderer/features/context-menu/actions/add-to-playlist-action.tsx index 5926bedcc..2134f476c 100644 --- a/src/renderer/features/context-menu/actions/add-to-playlist-action.tsx +++ b/src/renderer/features/context-menu/actions/add-to-playlist-action.tsx @@ -211,11 +211,11 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp let songsToAdd: string[] = allSongIds; if (skipDuplicates) { - const queryKey = queryKeys.playlists.songList(serverId, playlistId); + const queryKey = queryKeys.playlists.songListIds(serverId, playlistId); const playlistSongsRes = await queryClient.fetchQuery({ queryFn: ({ signal }) => { - return api.controller.getPlaylistSongList({ + return api.controller.getPlaylistSongIds({ apiClientProps: { serverId, signal, @@ -228,7 +228,7 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp queryKey, }); - const playlistSongIds = playlistSongsRes?.items?.map((song) => song.id); + const playlistSongIds = playlistSongsRes?.items; const uniqueSongIds: string[] = []; for (const songId of allSongIds) { diff --git a/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx b/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx index c7bfe41e8..5d8260091 100644 --- a/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx +++ b/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx @@ -231,11 +231,11 @@ export const AddToPlaylistContextModal = ({ const uniqueSongIds: string[] = []; if (values.skipDuplicates) { - const queryKey = queryKeys.playlists.songList(serverId, playlistId); + const queryKey = queryKeys.playlists.songListIds(serverId, playlistId); const playlistSongsRes = await queryClient.fetchQuery({ queryFn: ({ signal }) => { - return api.controller.getPlaylistSongList({ + return api.controller.getPlaylistSongIds({ apiClientProps: { serverId, signal, @@ -248,7 +248,7 @@ export const AddToPlaylistContextModal = ({ queryKey, }); - const playlistSongIds = playlistSongsRes?.items?.map((song) => song.id); + const playlistSongIds = playlistSongsRes?.items; for (const songId of allSongIds) { if (!playlistSongIds?.includes(songId)) { diff --git a/src/shared/api/jellyfin/jellyfin-normalize.ts b/src/shared/api/jellyfin/jellyfin-normalize.ts index bfa827869..b3658b1a3 100644 --- a/src/shared/api/jellyfin/jellyfin-normalize.ts +++ b/src/shared/api/jellyfin/jellyfin-normalize.ts @@ -10,7 +10,6 @@ import { LibraryItem, MusicFolder, Playlist, - RelatedArtist, Song, } from '/@/shared/types/domain-types'; import { ServerListItem, ServerType } from '/@/shared/types/types'; @@ -19,42 +18,6 @@ const TICKS_PER_MS = 10000; type AlbumOrSong = z.infer | z.infer; -const KEYS_TO_OMIT = new Set(['AlbumArtist', 'Artist']); - -const getPeople = (item: AlbumOrSong): null | Record => { - if (item.People) { - const participants: Record = {}; - - for (const person of item.People) { - const key = person.Type || ''; - if (KEYS_TO_OMIT.has(key)) { - continue; - } - - const item: RelatedArtist = { - // for other roles, we just want to display this and not filter. - // filtering (and links) would require a separate field, PersonIds - id: '', - imageId: null, - imageUrl: null, - name: person.Name, - userFavorite: false, - userRating: null, - }; - - if (key in participants) { - participants[key].push(item); - } else { - participants[key] = [item]; - } - } - - return participants; - } - - return null; -}; - const getTags = (item: AlbumOrSong): null | Record => { if (item.Tags) { const tags: Record = {}; @@ -106,39 +69,6 @@ const getPlaylistImageId = (item: z.infer): nu return null; }; -const getArtists = ( - item: z.infer, - participants?: null | Record, -): RelatedArtist[] => { - if (!item?.ArtistItems?.length && !item.AlbumArtists && !participants) { - return []; - } - - const result: RelatedArtist[] = []; - - (item?.ArtistItems?.length ? item.ArtistItems : item.AlbumArtists)?.forEach((entry) => { - result.push({ - id: entry.Id, - imageId: null, - imageUrl: null, - name: entry.Name, - userFavorite: false, - userRating: null, - }); - }); - - if (participants?.['Remixer']) { - const existingIds = new Set(result.map((artist) => artist.id)); - for (const participant of participants['Remixer']) { - if (!existingIds.has(participant.id)) { - result.push(participant); - } - } - } - - return result; -}; - const jellyfinPremiereFields = (item: { PremiereDate?: string; ProductionYear?: number; @@ -189,10 +119,6 @@ const normalizeSong = ( console.warn('Jellyfin song retrieved with no media sources', item); } - const participants = getPeople(item); - - const artists = getArtists(item, participants); - const { releaseDate, releaseYear } = jellyfinPremiereFields(item); return { @@ -211,7 +137,16 @@ const normalizeSong = ( })), albumId: item.AlbumId || `dummy/${item.Id}`, artistName: item?.ArtistItems?.map((entry) => entry.Name).join(', ') || '', - artists, + artists: (item?.ArtistItems?.length ? item.ArtistItems : item.AlbumArtists)?.map( + (entry) => ({ + id: entry.Id, + imageId: null, + imageUrl: null, + name: entry.Name, + userFavorite: false, + userRating: null, + }), + ), bitDepth, bitRate, bpm: null, @@ -253,7 +188,7 @@ const normalizeSong = ( mbzRecordingId: null, mbzTrackId: item.ProviderIds?.MusicBrainzTrack || null, name: item.Name, - participants, + participants: null, path: path || '', peak: null, playCount: (item.UserData && item.UserData.PlayCount) || 0, @@ -328,7 +263,7 @@ const normalizeAlbum = ( name: item.Name, originalDate: releaseDate, originalYear, - participants: getPeople(item), + participants: null, playCount: item.UserData?.PlayCount || 0, recordLabels: item.Studios?.map((entry) => entry.Name) || [], releaseDate, diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 686b67949..34ed0f7b5 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -636,6 +636,8 @@ export type AlbumInfo = { notes: null | string; }; +export type SongIdListResponse = BasePaginatedResponse; + export type SongListArgs = BaseEndpointArgs & { query: SongListQuery }; export type SongListCountArgs = BaseEndpointArgs & { query: ListCountQuery }; @@ -1522,6 +1524,7 @@ export type ControllerEndpoint = { getPlaylistDetail: (args: PlaylistDetailArgs) => Promise; getPlaylistList: (args: PlaylistListArgs) => Promise; getPlaylistListCount: (args: PlaylistListCountArgs) => Promise; + getPlaylistSongIds: (args: PlaylistSongListArgs) => Promise; getPlaylistSongList: (args: PlaylistSongListArgs) => Promise; getPlayQueue: (args: GetQueueArgs) => Promise; getRandomSongList: (args: RandomSongListArgs) => Promise; @@ -1676,6 +1679,9 @@ export type InternalControllerEndpoint = { args: ReplaceApiClientProps, ) => Promise; getPlaylistListCount: (args: ReplaceApiClientProps) => Promise; + getPlaylistSongIds: ( + args: ReplaceApiClientProps, + ) => Promise; getPlaylistSongList: ( args: ReplaceApiClientProps, ) => Promise;