diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index c8ac48646..471a60074 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -1,62 +1,64 @@ +import isElectron from 'is-electron'; +import { z } from 'zod'; +import packageJson from '../../../../package.json'; +import { jfNormalize } from './jellyfin-normalize'; +import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types'; +import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api'; +import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types'; import { - AuthenticationResponse, - MusicFolderListArgs, - MusicFolderListResponse, - GenreListArgs, - AlbumArtistDetailArgs, - AlbumArtistListArgs, - albumArtistListSortMap, - sortOrderMap, - ArtistListArgs, - artistListSortMap, - AlbumDetailArgs, - AlbumListArgs, - albumListSortMap, - TopSongListArgs, - SongListArgs, - songListSortMap, AddToPlaylistArgs, - RemoveFromPlaylistArgs, - PlaylistDetailArgs, - PlaylistSongListArgs, - PlaylistListArgs, - playlistListSortMap, + AddToPlaylistResponse, + AlbumArtistDetailArgs, + AlbumArtistDetailResponse, + AlbumArtistListArgs, + AlbumArtistListResponse, + AlbumDetailArgs, + AlbumDetailResponse, + AlbumListArgs, + AlbumListResponse, + AuthenticationResponse, + ControllerEndpoint, CreatePlaylistArgs, CreatePlaylistResponse, - UpdatePlaylistArgs, - UpdatePlaylistResponse, DeletePlaylistArgs, FavoriteArgs, FavoriteResponse, - ScrobbleArgs, - ScrobbleResponse, + GenreListArgs, GenreListResponse, - AlbumArtistDetailResponse, - AlbumArtistListResponse, - AlbumDetailResponse, - AlbumListResponse, - SongListResponse, - AddToPlaylistResponse, - RemoveFromPlaylistResponse, - PlaylistDetailResponse, - PlaylistListResponse, - SearchArgs, - SearchResponse, - RandomSongListResponse, - RandomSongListArgs, LyricsArgs, LyricsResponse, - genreListSortMap, + MusicFolderListArgs, + MusicFolderListResponse, + PlaylistDetailArgs, + PlaylistDetailResponse, + PlaylistListArgs, + PlaylistListResponse, + PlaylistSongListArgs, + RandomSongListArgs, + RandomSongListResponse, + RemoveFromPlaylistArgs, + RemoveFromPlaylistResponse, + ScrobbleArgs, + ScrobbleResponse, + SearchArgs, + SearchResponse, SongDetailArgs, SongDetailResponse, + SongListArgs, + SongListResponse, + SongListSort, + SortOrder, + TopSongListArgs, + UpdatePlaylistArgs, + UpdatePlaylistResponse, + albumArtistListSortMap, + albumListSortMap, + genreListSortMap, + playlistListSortMap, + songListSortMap, + sortOrderMap, } 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'; -import isElectron from 'is-electron'; +import { sortSongList } from '/@/renderer/api/utils'; const formatCommaDelimitedString = (value: string[]) => { return value.join(','); @@ -244,31 +246,56 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise => { +const getAlbumArtistListCount = async (args: AlbumArtistListArgs): Promise => { const { query, apiClientProps } = args; const res = await jfApiClient(apiClientProps).getAlbumArtistList({ query: { - Limit: query.limit, + Fields: 'Genres, DateCreated, ExternalUrls, Overview', + ImageTypeLimit: 1, + Limit: 1, ParentId: query.musicFolderId, Recursive: true, - SortBy: artistListSortMap.jellyfin[query.sortBy] || 'Name,SortName', + SearchTerm: query.searchTerm, + SortBy: albumArtistListSortMap.jellyfin[query.sortBy] || 'Name,SortName', SortOrder: sortOrderMap.jellyfin[query.sortOrder], - StartIndex: query.startIndex, + StartIndex: 0, + UserId: apiClientProps.server?.userId || undefined, }, }); if (res.status !== 200) { - throw new Error('Failed to get artist list'); + throw new Error('Failed to get album artist list count'); } - return { - items: res.body.Items.map((item) => jfNormalize.albumArtist(item, apiClientProps.server)), - startIndex: query.startIndex, - totalRecordCount: res.body.TotalRecordCount, - }; + return res.body.TotalRecordCount; }; +// const getArtistList = async (args: ArtistListArgs): Promise => { +// const { query, apiClientProps } = args; + +// const res = await jfApiClient(apiClientProps).getAlbumArtistList({ +// query: { +// Limit: query.limit, +// ParentId: query.musicFolderId, +// Recursive: true, +// SortBy: artistListSortMap.jellyfin[query.sortBy] || 'Name,SortName', +// SortOrder: sortOrderMap.jellyfin[query.sortOrder], +// StartIndex: query.startIndex, +// }, +// }); + +// if (res.status !== 200) { +// throw new Error('Failed to get artist list'); +// } + +// return { +// items: res.body.Items.map((item) => jfNormalize.albumArtist(item, apiClientProps.server)), +// startIndex: query.startIndex, +// totalRecordCount: res.body.TotalRecordCount, +// }; +// }; + const getAlbumDetail = async (args: AlbumDetailArgs): Promise => { const { query, apiClientProps } = args; @@ -358,6 +385,55 @@ const getAlbumList = async (args: AlbumListArgs): Promise => }; }; +const getAlbumListCount = async (args: AlbumListArgs): Promise => { + const { query, apiClientProps } = args; + + if (!apiClientProps.server?.userId) { + throw new Error('No userId found'); + } + + const yearsGroup = []; + if (query._custom?.jellyfin?.minYear && query._custom?.jellyfin?.maxYear) { + for ( + let i = Number(query._custom?.jellyfin?.minYear); + i <= Number(query._custom?.jellyfin?.maxYear); + i += 1 + ) { + yearsGroup.push(String(i)); + } + } + + const yearsFilter = yearsGroup.length ? yearsGroup.join(',') : undefined; + + const res = await jfApiClient(apiClientProps).getAlbumList({ + params: { + userId: apiClientProps.server?.userId, + }, + query: { + AlbumArtistIds: query.artistIds + ? formatCommaDelimitedString(query.artistIds) + : undefined, + ContributingArtistIds: query.isCompilation ? query.artistIds?.[0] : undefined, + IncludeItemTypes: 'MusicAlbum', + Limit: 1, + ParentId: query.musicFolderId, + Recursive: true, + SearchTerm: query.searchTerm, + SortBy: albumListSortMap.jellyfin[query.sortBy] || 'SortName', + SortOrder: sortOrderMap.jellyfin[query.sortOrder], + StartIndex: 0, + ...query._custom?.jellyfin, + Years: yearsFilter, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get album list count'); + } + + return res.body.TotalRecordCount; +}; + const getTopSongList = async (args: TopSongListArgs): Promise => { const { apiClientProps, query } = args; @@ -385,8 +461,11 @@ const getTopSongList = async (args: TopSongListArgs): Promise throw new Error('Failed to get top song list'); } + const songs = res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')); + const songsByPlayCount = sortSongList(songs, SongListSort.PLAY_COUNT, SortOrder.DESC); + return { - items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')), + items: songsByPlayCount, startIndex: 0, totalRecordCount: res.body.TotalRecordCount, }; @@ -450,6 +529,58 @@ const getSongList = async (args: SongListArgs): Promise => { }; }; +const getSongListCount = async (args: SongListArgs): Promise => { + const { query, apiClientProps } = args; + + if (!apiClientProps.server?.userId) { + throw new Error('No userId found'); + } + + const yearsGroup = []; + if (query._custom?.jellyfin?.minYear && query._custom?.jellyfin?.maxYear) { + for ( + let i = Number(query._custom?.jellyfin?.minYear); + i <= Number(query._custom?.jellyfin?.maxYear); + i += 1 + ) { + yearsGroup.push(String(i)); + } + } + + const yearsFilter = yearsGroup.length ? formatCommaDelimitedString(yearsGroup) : undefined; + const albumIdsFilter = query.albumIds ? formatCommaDelimitedString(query.albumIds) : undefined; + const artistIdsFilter = query.artistIds + ? formatCommaDelimitedString(query.artistIds) + : undefined; + + const res = await jfApiClient(apiClientProps).getSongList({ + params: { + userId: apiClientProps.server?.userId, + }, + query: { + AlbumIds: albumIdsFilter, + ArtistIds: artistIdsFilter, + Fields: 'Genres, DateCreated, MediaSources, ParentId', + IncludeItemTypes: 'Audio', + Limit: 1, + ParentId: query.musicFolderId, + Recursive: true, + SearchTerm: query.searchTerm, + SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName', + SortOrder: sortOrderMap.jellyfin[query.sortOrder], + StartIndex: 0, + ...query._custom?.jellyfin, + Years: yearsFilter, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get song list count'); + } + + return res.body.TotalRecordCount; +}; + const addToPlaylist = async (args: AddToPlaylistArgs): Promise => { const { query, body, apiClientProps } = args; @@ -481,6 +612,7 @@ const removeFromPlaylist = async ( const { query, apiClientProps } = args; const res = await jfApiClient(apiClientProps).removeFromPlaylist({ + body: null, params: { id: query.id, }, @@ -588,6 +720,37 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise => { + const { query, apiClientProps } = args; + + if (!apiClientProps.server?.userId) { + throw new Error('No userId found'); + } + + const res = await jfApiClient(apiClientProps).getPlaylistList({ + params: { + userId: apiClientProps.server?.userId, + }, + query: { + Fields: 'ChildCount, Genres, DateCreated, ParentId, Overview', + IncludeItemTypes: 'Playlist', + Limit: 1, + MediaTypes: 'Audio', + Recursive: true, + SearchTerm: query.searchTerm, + SortBy: playlistListSortMap.jellyfin[query.sortBy], + SortOrder: sortOrderMap.jellyfin[query.sortOrder], + StartIndex: 0, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get playlist list count'); + } + + return res.body.TotalRecordCount; +}; + const createPlaylist = async (args: CreatePlaylistArgs): Promise => { const { body, apiClientProps } = args; @@ -647,6 +810,7 @@ const deletePlaylist = async (args: DeletePlaylistArgs): Promise => { const { query, apiClientProps } = args; const res = await jfApiClient(apiClientProps).deletePlaylist({ + body: null, params: { id: query.id, }, @@ -944,7 +1108,7 @@ const getSongDetail = async (args: SongDetailArgs): Promise return jfNormalize.song(res.body, apiClientProps.server, ''); }; -export const jfController = { +export const JellyfinController: ControllerEndpoint = { addToPlaylist, authenticate, createFavorite, @@ -953,19 +1117,22 @@ export const jfController = { deletePlaylist, getAlbumArtistDetail, getAlbumArtistList, + getAlbumArtistListCount, getAlbumDetail, getAlbumList, - getArtistList, + getAlbumListCount, getGenreList, getLyrics, getMusicFolderList, getPlaylistDetail, getPlaylistList, + getPlaylistListCount, getPlaylistSongList, getRandomSongList, getSongDetail, getSongList, - getTopSongList, + getSongListCount, + getTopSongs: getTopSongList, removeFromPlaylist, scrobble, search, diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 166b23ddc..76b7de40b 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -194,6 +194,27 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise => { + const { query, apiClientProps } = args; + + const res = await ndApiClient(apiClientProps).getAlbumArtistList({ + query: { + _end: 1, + _order: sortOrderMap.navidrome[query.sortOrder], + _sort: albumArtistListSortMap.navidrome[query.sortBy], + _start: 0, + name: query.searchTerm, + ...query._custom?.navidrome, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get album artist list count'); + } + + return Number(res.body.headers.get('x-total-count') || 0); +}; + const getAlbumDetail = async (args: AlbumDetailArgs): Promise => { const { query, apiClientProps } = args; @@ -251,6 +272,30 @@ const getAlbumList = async (args: AlbumListArgs): Promise => }; }; +const getAlbumListCount = async (args: AlbumListArgs): Promise => { + const { query, apiClientProps } = args; + + const res = await ndApiClient(apiClientProps).getAlbumList({ + query: { + _end: 1, + _order: sortOrderMap.navidrome[query.sortOrder], + _sort: albumListSortMap.navidrome[query.sortBy], + _start: 0, + artist_id: query.artistIds?.[0], + compilation: query.isCompilation, + genre_id: query.genre, + name: query.searchTerm, + ...query._custom?.navidrome, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get album list'); + } + + return Number(res.body.headers.get('x-total-count') || 0); +}; + const getSongList = async (args: SongListArgs): Promise => { const { query, apiClientProps } = args; @@ -280,6 +325,29 @@ const getSongList = async (args: SongListArgs): Promise => { }; }; +const getSongListCount = async (args: SongListArgs): Promise => { + const { query, apiClientProps } = args; + + const res = await ndApiClient(apiClientProps).getSongList({ + query: { + _end: 1, + _order: sortOrderMap.navidrome[query.sortOrder], + _sort: songListSortMap.navidrome[query.sortBy], + _start: 0, + album_artist_id: query.artistIds, + album_id: query.albumIds, + title: query.searchTerm, + ...query._custom?.navidrome, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get song list count'); + } + + return Number(res.body.headers.get('x-total-count') || 0); +}; + const getSongDetail = async (args: SongDetailArgs): Promise => { const { query, apiClientProps } = args; @@ -345,6 +413,7 @@ const deletePlaylist = async (args: DeletePlaylistArgs): Promise => { + const { query, apiClientProps } = args; + + const res = await ndApiClient(apiClientProps).getPlaylistList({ + query: { + _end: 1, + _order: sortOrderMap.navidrome[query.sortOrder], + _sort: query.sortBy + ? playlistListSortMap.navidrome[query.sortBy] + : playlistListSortMap.navidrome.name, + _start: 0, + q: query.searchTerm, + ...query._custom?.navidrome, + }, + }); + + if (res.status !== 200) { + throw new Error('Failed to get playlist list count'); + } + + return Number(res.body.headers.get('x-total-count') || 0); +}; + const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise => { const { query, apiClientProps } = args; @@ -454,6 +546,7 @@ const removeFromPlaylist = async ( const { query, apiClientProps } = args; const res = await ndApiClient(apiClientProps).removeFromPlaylist({ + body: null, params: { id: query.id, }, @@ -479,8 +572,10 @@ export const NavidromeController: ControllerEndpoint = { deletePlaylist, getAlbumArtistDetail, getAlbumArtistList, + getAlbumArtistListCount, getAlbumDetail, getAlbumList, + getAlbumListCount, getArtistDetail: undefined, getArtistInfo: undefined, getFavoritesList: undefined, @@ -491,10 +586,12 @@ export const NavidromeController: ControllerEndpoint = { getMusicFolderList: SubsonicController.getMusicFolderList, getPlaylistDetail, getPlaylistList, + getPlaylistListCount, getPlaylistSongList, getRandomSongList: SubsonicController.getRandomSongList, getSongDetail, getSongList, + getSongListCount, getTopSongs: SubsonicController.getTopSongs, getUserList, removeFromPlaylist, @@ -503,23 +600,3 @@ export const NavidromeController: ControllerEndpoint = { setRating: SubsonicController.setRating, updatePlaylist, }; - -export const ndController = { - addToPlaylist, - authenticate, - createPlaylist, - deletePlaylist, - getAlbumArtistDetail, - getAlbumArtistList, - getAlbumDetail, - getAlbumList, - getGenreList, - getPlaylistDetail, - getPlaylistList, - getPlaylistSongList, - getSongDetail, - getSongList, - getUserList, - removeFromPlaylist, - updatePlaylist, -};