diff --git a/package-lock.json b/package-lock.json index 6effc0847..f682d14b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "@vitejs/plugin-react": "^2.2.0", "ag-grid-community": "^28.2.1", "ag-grid-react": "^28.2.1", - "axios": "^0.27.2", "electron-localshortcut": "^3.2.1", "electron-store": "^8.1.0", "electron-updater": "5.3.0", @@ -3731,7 +3730,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "node_modules/at-least-node": { "version": "1.0.0", @@ -3824,15 +3824,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -4868,6 +4859,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5355,6 +5347,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -8031,25 +8024,6 @@ "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -8063,6 +8037,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -11097,6 +11072,7 @@ "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -11105,6 +11081,7 @@ "version": "2.1.34", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "dev": true, "dependencies": { "mime-db": "1.51.0" }, @@ -20133,7 +20110,8 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "at-least-node": { "version": "1.0.0", @@ -20196,15 +20174,6 @@ "integrity": "sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA==", "dev": true }, - "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -20985,6 +20954,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -21340,7 +21310,8 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true }, "detect-newline": { "version": "3.1.0", @@ -23256,11 +23227,6 @@ "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, - "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" - }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -23271,6 +23237,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -25555,12 +25522,14 @@ "mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true }, "mime-types": { "version": "2.1.34", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "dev": true, "requires": { "mime-db": "1.51.0" } diff --git a/package.json b/package.json index ce203b759..adba48c3f 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,6 @@ "@vitejs/plugin-react": "^2.2.0", "ag-grid-community": "^28.2.1", "ag-grid-react": "^28.2.1", - "axios": "^0.27.2", "electron-localshortcut": "^3.2.1", "electron-store": "^8.1.0", "electron-updater": "5.3.0", diff --git a/packages/renderer/src/api/controller.ts b/packages/renderer/src/api/controller.ts index 285ffffb9..e367e9259 100644 --- a/packages/renderer/src/api/controller.ts +++ b/packages/renderer/src/api/controller.ts @@ -1,63 +1,173 @@ -import { useAuthStore } from '../store/auth.store'; -import { navidromeApi } from './navidrome.api'; +import { useAuthStore } from '/@/store'; +import { navidromeApi } from '/@/api/navidrome.api'; +import { toast } from '/@/components'; import type { - AlbumDetailQuery, - AlbumDetailResponse, - AlbumListParams, - AlbumListResponse, -} from './types'; + AlbumDetailArgs, + RawAlbumDetailResponse, + RawAlbumListResponse, + AlbumListArgs, + SongListArgs, + RawSongListResponse, + SongDetailArgs, + RawSongDetailResponse, + AlbumArtistDetailArgs, + RawAlbumArtistDetailResponse, + AlbumArtistListArgs, + RawAlbumArtistListResponse, + RatingArgs, + RawRatingResponse, + FavoriteArgs, + RawFavoriteResponse, + GenreListArgs, + RawGenreListResponse, + CreatePlaylistArgs, + RawCreatePlaylistResponse, + DeletePlaylistArgs, + RawDeletePlaylistResponse, + PlaylistDetailArgs, + RawPlaylistDetailResponse, + PlaylistListArgs, + RawPlaylistListResponse, +} from '/@/api/types'; +import { subsonicApi } from '/@/api/subsonic.api'; -export const getServerType = () => { - const server = useAuthStore.getState().currentServer; +export type ControllerEndpoint = Partial<{ + clearPlaylist: () => void; + createFavorite: (args: FavoriteArgs) => Promise; + createPlaylist: (args: CreatePlaylistArgs) => Promise; + deleteFavorite: (args: FavoriteArgs) => Promise; + deletePlaylist: (args: DeletePlaylistArgs) => Promise; + getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise; + getAlbumArtistList: (args: AlbumArtistListArgs) => Promise; + getAlbumDetail: (args: AlbumDetailArgs) => Promise; + getAlbumList: (args: AlbumListArgs) => Promise; + getArtistDetail: () => void; + getArtistList: () => void; + getFavoritesList: () => void; + getFolderItemList: () => void; + getFolderList: () => void; + getFolderSongs: () => void; + getGenreList: (args: GenreListArgs) => Promise; + getMusicFolderList: () => void; + getPlaylistDetail: (args: PlaylistDetailArgs) => Promise; + getPlaylistList: (args: PlaylistListArgs) => Promise; + getSongDetail: (args: SongDetailArgs) => Promise; + getSongList: (args: SongListArgs) => Promise; + updatePlaylist: () => void; + updateRating: (args: RatingArgs) => Promise; +}>; - if (!server) { - return null; - } - - return server.type; +type ApiController = { + jellyfin: ControllerEndpoint; + navidrome: ControllerEndpoint; + subsonic: ControllerEndpoint; }; -const getAlbumDetail = async ( - query: AlbumDetailQuery, - signal?: AbortSignal, -): Promise => { - const serverType = getServerType(); - if (!serverType) return null; - - const functions = { - jellyfin: null, - navidrome: navidromeApi.getAlbumDetail, - subsonic: null, - }; - - if (functions[serverType] === null) { - return null; - } - - return functions[serverType]?.(query, signal); +const endpoints: ApiController = { + jellyfin: { + clearPlaylist: undefined, + createFavorite: undefined, + createPlaylist: undefined, + deleteFavorite: undefined, + deletePlaylist: undefined, + getAlbumArtistDetail: undefined, + getAlbumArtistList: undefined, + getAlbumDetail: undefined, + getAlbumList: undefined, + getArtistDetail: undefined, + getArtistList: undefined, + getFavoritesList: undefined, + getFolderItemList: undefined, + getFolderList: undefined, + getFolderSongs: undefined, + getGenreList: undefined, + getMusicFolderList: undefined, + getPlaylistDetail: undefined, + getPlaylistList: undefined, + getSongList: undefined, + updatePlaylist: undefined, + updateRating: undefined, + }, + navidrome: { + clearPlaylist: undefined, + createFavorite: subsonicApi.createFavorite, + createPlaylist: navidromeApi.createPlaylist, + deleteFavorite: subsonicApi.deleteFavorite, + deletePlaylist: navidromeApi.deletePlaylist, + getAlbumArtistDetail: navidromeApi.getAlbumArtistDetail, + getAlbumArtistList: navidromeApi.getAlbumArtistList, + getAlbumDetail: navidromeApi.getAlbumDetail, + getAlbumList: navidromeApi.getAlbumList, + getArtistDetail: undefined, + getArtistList: undefined, + getFavoritesList: undefined, + getFolderItemList: undefined, + getFolderList: undefined, + getFolderSongs: undefined, + getGenreList: navidromeApi.getGenreList, + getMusicFolderList: undefined, + getPlaylistDetail: navidromeApi.getPlaylistDetail, + getPlaylistList: navidromeApi.getPlaylistList, + getSongList: navidromeApi.getSongList, + updatePlaylist: undefined, + updateRating: subsonicApi.updateRating, + }, + subsonic: { + clearPlaylist: undefined, + createFavorite: subsonicApi.createFavorite, + createPlaylist: undefined, + deleteFavorite: subsonicApi.deleteFavorite, + deletePlaylist: undefined, + getAlbumArtistDetail: subsonicApi.getAlbumArtistDetail, + getAlbumArtistList: subsonicApi.getAlbumArtistList, + getAlbumDetail: subsonicApi.getAlbumDetail, + getAlbumList: subsonicApi.getAlbumList, + getArtistDetail: undefined, + getArtistList: undefined, + getFavoritesList: undefined, + getFolderItemList: undefined, + getFolderList: undefined, + getFolderSongs: undefined, + getGenreList: undefined, + getMusicFolderList: undefined, + getPlaylistDetail: undefined, + getPlaylistList: undefined, + getSongList: undefined, + updatePlaylist: undefined, + updateRating: undefined, + }, }; -const getAlbumList = async ( - params: AlbumListParams, - signal?: AbortSignal, -): Promise => { - const serverType = getServerType(); - if (!serverType) return null; +const apiController = (endpoint: keyof ControllerEndpoint) => { + const serverType = useAuthStore.getState().currentServer?.type; - const functions = { - jellyfin: null, - navidrome: navidromeApi.getAlbumList, - subsonic: null, - }; - - if (functions[serverType] === null) { - return null; + if (!serverType) { + toast.error({ message: 'No server selected', title: 'Unable to route request' }); + return () => undefined; } - return functions[serverType]?.(params, signal); + const controllerFn = endpoints[serverType][endpoint]; + + if (typeof controllerFn !== 'function') { + toast.error({ + message: `Endpoint ${endpoint} is not implemented for ${serverType}`, + title: 'Unable to route request', + }); + return () => undefined; + } + + return endpoints[serverType][endpoint]; }; -export const apiController = { +const getAlbumList = async (args: AlbumListArgs) => { + return (apiController('getAlbumList') as ControllerEndpoint['getAlbumList'])?.(args); +}; + +const getAlbumDetail = async (args: AlbumDetailArgs) => { + return (apiController('getAlbumDetail') as ControllerEndpoint['getAlbumDetail'])?.(args); +}; + +export const controller = { getAlbumDetail, getAlbumList, }; diff --git a/packages/renderer/src/api/jellyfin.types.ts b/packages/renderer/src/api/jellyfin.types.ts new file mode 100644 index 000000000..5af58a9c8 --- /dev/null +++ b/packages/renderer/src/api/jellyfin.types.ts @@ -0,0 +1,433 @@ +export type JFBaseResponse = { + StartIndex: number; + TotalRecordCount: number; +}; + +export interface JFMusicFoldersResponse extends JFBaseResponse { + Items: JFMusicFolder[]; +} + +export interface JFGenreResponse extends JFBaseResponse { + Items: JFGenre[]; +} + +export interface JFAlbumArtistsResponse extends JFBaseResponse { + Items: JFAlbumArtist[]; +} + +export interface JFArtistsResponse extends JFBaseResponse { + Items: JFAlbumArtist[]; +} + +export interface JFAlbumsResponse extends JFBaseResponse { + Items: JFAlbum[]; +} + +export interface JFSongsResponse extends JFBaseResponse { + Items: JFSong[]; +} + +export type JFRequestParams = { + albumArtistIds?: string; + artistIds?: string; + enableImageTypes?: string; + enableTotalRecordCount?: boolean; + enableUserData?: boolean; + excludeItemTypes?: string; + fields?: string; + imageTypeLimit?: number; + includeItemTypes?: string; + isFavorite?: boolean; + limit?: number; + parentId?: string; + recursive?: boolean; + searchTerm?: string; + sortBy?: string; + sortOrder?: 'Ascending' | 'Descending'; + startIndex?: number; + userId?: string; +}; + +export type JFMusicFolder = { + BackdropImageTags: string[]; + ChannelId: null; + CollectionType: string; + Id: string; + ImageBlurHashes: ImageBlurHashes; + ImageTags: ImageTags; + IsFolder: boolean; + LocationType: string; + Name: string; + ServerId: string; + Type: string; + UserData: UserData; +}; + +export type JFGenre = { + BackdropImageTags: any[]; + ChannelId: null; + Id: string; + ImageBlurHashes: any; + ImageTags: ImageTags; + LocationType: string; + Name: string; + ServerId: string; + Type: string; +}; + +export type JFAlbumArtist = { + BackdropImageTags: string[]; + ChannelId: null; + DateCreated: string; + ExternalUrls: ExternalURL[]; + GenreItems: GenreItem[]; + Genres: string[]; + Id: string; + ImageBlurHashes: any; + ImageTags: ImageTags; + LocationType: string; + Name: string; + Overview?: string; + RunTimeTicks: number; + ServerId: string; + Type: string; +}; + +export type JFArtist = { + BackdropImageTags: string[]; + ChannelId: null; + DateCreated: string; + ExternalUrls: ExternalURL[]; + GenreItems: GenreItem[]; + Genres: string[]; + Id: string; + ImageBlurHashes: any; + ImageTags: string[]; + LocationType: string; + Name: string; + Overview?: string; + RunTimeTicks: number; + ServerId: string; + Type: string; +}; + +export type JFAlbum = { + AlbumArtist: string; + AlbumArtists: JFGenericItem[]; + ArtistItems: JFGenericItem[]; + Artists: string[]; + ChannelId: null; + DateCreated: string; + ExternalUrls: ExternalURL[]; + GenreItems: JFGenericItem[]; + Genres: string[]; + Id: string; + ImageBlurHashes: ImageBlurHashes; + ImageTags: ImageTags; + IsFolder: boolean; + LocationType: string; + Name: string; + ParentLogoImageTag: string; + ParentLogoItemId: string; + PremiereDate?: string; + ProductionYear: number; + RunTimeTicks: number; + ServerId: string; + Type: string; +} & { + songs?: JFSong[]; +}; + +export type JFSong = { + Album: string; + AlbumArtist: string; + AlbumArtists: JFGenericItem[]; + AlbumId: string; + AlbumPrimaryImageTag: string; + ArtistItems: JFGenericItem[]; + Artists: string[]; + BackdropImageTags: string[]; + ChannelId: null; + DateCreated: string; + ExternalUrls: ExternalURL[]; + GenreItems: JFGenericItem[]; + Genres: string[]; + Id: string; + ImageBlurHashes: ImageBlurHashes; + ImageTags: ImageTags; + IndexNumber: number; + IsFolder: boolean; + LocationType: string; + MediaSources: MediaSources[]; + MediaType: string; + Name: string; + ParentIndexNumber: number; + PremiereDate?: string; + ProductionYear: number; + RunTimeTicks: number; + ServerId: string; + SortName: string; + Type: string; +}; + +type ImageBlurHashes = { + Backdrop?: any; + Logo?: any; + Primary?: any; +}; + +type ImageTags = { + Logo?: string; + Primary?: string; +}; + +type UserData = { + IsFavorite: boolean; + Key: string; + PlayCount: number; + PlaybackPositionTicks: number; + Played: boolean; +}; + +type ExternalURL = { + Name: string; + Url: string; +}; + +type GenreItem = { + Id: string; + Name: string; +}; + +export type JFGenericItem = { + Id: string; + Name: string; +}; + +type MediaSources = { + Bitrate: number; + Container: string; + DefaultAudioStreamIndex: number; + ETag: string; + Formats: any[]; + GenPtsInput: boolean; + Id: string; + IgnoreDts: boolean; + IgnoreIndex: boolean; + IsInfiniteStream: boolean; + IsRemote: boolean; + MediaAttachments: any[]; + MediaStreams: MediaStream[]; + Name: string; + Path: string; + Protocol: string; + ReadAtNativeFramerate: boolean; + RequiredHttpHeaders: any; + RequiresClosing: boolean; + RequiresLooping: boolean; + RequiresOpening: boolean; + RunTimeTicks: number; + Size: number; + SupportsDirectPlay: boolean; + SupportsDirectStream: boolean; + SupportsProbing: boolean; + SupportsTranscoding: boolean; + Type: string; +}; + +type MediaStream = { + AspectRatio?: string; + BitDepth?: number; + BitRate?: number; + ChannelLayout?: string; + Channels?: number; + Codec: string; + CodecTimeBase: string; + ColorSpace?: string; + Comment?: string; + DisplayTitle?: string; + Height?: number; + Index: number; + IsDefault: boolean; + IsExternal: boolean; + IsForced: boolean; + IsInterlaced: boolean; + IsTextSubtitleStream: boolean; + Level: number; + PixelFormat?: string; + Profile?: string; + RealFrameRate?: number; + RefFrames?: number; + SampleRate?: number; + SupportsExternalStream: boolean; + TimeBase: string; + Type: string; + Width?: number; +}; + +export enum JFExternalType { + MUSICBRAINZ = 'MusicBrainz', + THEAUDIODB = 'TheAudioDb', +} + +export enum JFImageType { + LOGO = 'Logo', + PRIMARY = 'Primary', +} + +export enum JFItemType { + AUDIO = 'Audio', + MUSICALBUM = 'MusicAlbum', +} + +export enum JFCollectionType { + MUSIC = 'music', + PLAYLISTS = 'playlists', +} + +export interface JFAuthenticate { + AccessToken: string; + ServerId: string; + SessionInfo: SessionInfo; + User: User; +} + +type SessionInfo = { + AdditionalUsers: any[]; + ApplicationVersion: string; + Capabilities: Capabilities; + Client: string; + DeviceId: string; + DeviceName: string; + HasCustomDeviceName: boolean; + Id: string; + IsActive: boolean; + LastActivityDate: string; + LastPlaybackCheckIn: string; + NowPlayingQueue: any[]; + NowPlayingQueueFullItems: any[]; + PlayState: PlayState; + PlayableMediaTypes: any[]; + RemoteEndPoint: string; + ServerId: string; + SupportedCommands: any[]; + SupportsMediaControl: boolean; + SupportsRemoteControl: boolean; + UserId: string; + UserName: string; +}; + +type Capabilities = { + PlayableMediaTypes: any[]; + SupportedCommands: any[]; + SupportsContentUploading: boolean; + SupportsMediaControl: boolean; + SupportsPersistentIdentifier: boolean; + SupportsSync: boolean; +}; + +type PlayState = { + CanSeek: boolean; + IsMuted: boolean; + IsPaused: boolean; + RepeatMode: string; +}; + +type User = { + Configuration: Configuration; + EnableAutoLogin: boolean; + HasConfiguredEasyPassword: boolean; + HasConfiguredPassword: boolean; + HasPassword: boolean; + Id: string; + LastActivityDate: string; + LastLoginDate: string; + Name: string; + Policy: Policy; + ServerId: string; +}; + +type Configuration = { + DisplayCollectionsView: boolean; + DisplayMissingEpisodes: boolean; + EnableLocalPassword: boolean; + EnableNextEpisodeAutoPlay: boolean; + GroupedFolders: any[]; + HidePlayedInLatest: boolean; + LatestItemsExcludes: any[]; + MyMediaExcludes: any[]; + OrderedViews: any[]; + PlayDefaultAudioTrack: boolean; + RememberAudioSelections: boolean; + RememberSubtitleSelections: boolean; + SubtitleLanguagePreference: string; + SubtitleMode: string; +}; + +type Policy = { + AccessSchedules: any[]; + AuthenticationProviderId: string; + BlockUnratedItems: any[]; + BlockedChannels: any[]; + BlockedMediaFolders: any[]; + BlockedTags: any[]; + EnableAllChannels: boolean; + EnableAllDevices: boolean; + EnableAllFolders: boolean; + EnableAudioPlaybackTranscoding: boolean; + EnableContentDeletion: boolean; + EnableContentDeletionFromFolders: any[]; + EnableContentDownloading: boolean; + EnableLiveTvAccess: boolean; + EnableLiveTvManagement: boolean; + EnableMediaConversion: boolean; + EnableMediaPlayback: boolean; + EnablePlaybackRemuxing: boolean; + EnablePublicSharing: boolean; + EnableRemoteAccess: boolean; + EnableRemoteControlOfOtherUsers: boolean; + EnableSharedDeviceControl: boolean; + EnableSyncTranscoding: boolean; + EnableUserPreferenceAccess: boolean; + EnableVideoPlaybackTranscoding: boolean; + EnabledChannels: any[]; + EnabledDevices: any[]; + EnabledFolders: any[]; + ForceRemoteSourceTranscoding: boolean; + InvalidLoginAttemptCount: number; + IsAdministrator: boolean; + IsDisabled: boolean; + IsHidden: boolean; + LoginAttemptsBeforeLockout: number; + MaxActiveSessions: number; + PasswordResetProviderId: string; + RemoteClientBitrateLimit: number; + SyncPlayAccess: string; +}; + +export enum JFSortOrder { + ASC = 'Ascending', + DESC = 'Descending', +} + +export enum JFAlbumListSort { + ALBUM_ARTIST = 'AlbumArtist,SortName', + CRITIC_RATING = 'CriticRating,SortName', + NAME = 'SortName', + RANDOM = 'Random,SortName', + RATING = 'CommunityRating,SortName', + RECENTLY_ADDED = 'DateCreated,SortName', + RELEASE_DATE = 'ProductionYear,PremiereDate,SortName', +} + +export type JFAlbumListParams = { + enableImageTypes: JFImageType[]; + imageTypeLimit: number; + includeItemTypes: 'MusicAlbum'; + limit?: number; + parentId: string; + recursive: boolean; + sortBy: JFAlbumListSort; + sortOrder: JFSortOrder; + startIndex: number; +}; diff --git a/packages/renderer/src/api/navidrome.api.ts b/packages/renderer/src/api/navidrome.api.ts index db4b2fbf5..630b3a8af 100644 --- a/packages/renderer/src/api/navidrome.api.ts +++ b/packages/renderer/src/api/navidrome.api.ts @@ -1,111 +1,253 @@ -import ky from 'ky'; import { nanoid } from 'nanoid/non-secure'; -import type { ServerListItem } from '../store'; -import { useAuthStore } from '../store'; -import { ServerType } from '../types'; +import ky from 'ky'; import type { - NDAlbumListResponse, NDGenreListResponse, - NDAlbumListParams, - NDGenreListParams, - NDSongListParams, NDArtistListResponse, - NDAuthenticate, - NDAlbum, - NDAlbumListSort, - NDAlbumDetailResponse, - NDSong, + NDAlbumDetail, NDSongListResponse, -} from './navidrome.types'; -import { NDSortOrder } from './navidrome.types'; + NDAlbumListParams, + NDAlbumList, + NDSongDetailResponse, + NDAlbum, + NDSong, + NDAuthenticationResponse, + NDAlbumDetailResponse, + NDSongList, + NDSongDetail, + NDGenreList, + NDAlbumArtistListParams, + NDAlbumArtistListSort, + NDAlbumArtistDetail, + NDAlbumListResponse, + NDAlbumArtistDetailResponse, + NDAlbumArtistList, + NDSongListParams, + NDSongListSort, + NDCreatePlaylist, + NDCreatePlaylistParams, + NDCreatePlaylistResponse, + NDDeletePlaylist, + NDDeletePlaylistResponse, + NDPlaylistListParams, + NDPlaylistDetail, + NDPlaylistList, + NDPlaylistListSort, + NDPlaylistListResponse, + NDPlaylistDetailResponse, +} from '/@/api/navidrome.types'; +import { NDAlbumListSort } from '/@/api/navidrome.types'; +import { NDSortOrder } from '/@/api/navidrome.types'; import type { Album, - AlbumDetailQuery, - AlbumDetailResponse, - AlbumListParams, - AlbumListResponse, Song, -} from './types'; -import { SortOrder } from './types'; + AuthenticationResponse, + AlbumDetailArgs, + GenreListArgs, + AlbumListArgs, + AlbumArtistListArgs, + AlbumArtistDetailArgs, + SongListArgs, + SongDetailArgs, + CreatePlaylistArgs, + DeletePlaylistArgs, + PlaylistListArgs, + PlaylistDetailArgs, +} from '/@/api/types'; +import { SortOrder } from '/@/api/types'; +import { toast } from '/@/components'; +import type { ServerListItem } from '/@/store'; +import { useAuthStore } from '/@/store'; +import { ServerType } from '/@/types'; const api = ky.create({ hooks: { afterResponse: [ - (request, _options, response) => { - // const serverId = request.headers.get('--local-id'); + async (_request, _options, response) => { + const serverId = useAuthStore.getState().currentServer?.id; + + if (serverId) { + useAuthStore.getState().actions.updateServer(serverId, { + ndCredential: response.headers.get('x-nd-authorization') as string, + }); + } + return response; }, ], - beforeRequest: [ - (request, options) => { - const { headers } = options; + beforeError: [ + (error) => { + if (error.response && error.response.status === 401) { + toast.error({ + message: 'Your session has expired.', + }); - console.log('headers', headers); - const { currentServer } = useAuthStore.getState(); - const { ndCredential } = currentServer || {}; + const serverId = useAuthStore.getState().currentServer?.id; - if (ndCredential) { - request.headers.set('x-nd-authorization', `Bearer ${ndCredential}`); - request.headers.set('--local-id', currentServer?.id || ''); + if (serverId) { + useAuthStore.getState().actions.setCurrentServer(null); + useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined }); + } } + + return error; }, ], }, }); -const authenticate = async (options: { password: string; url: string; username: string }) => { - const { password, url, username } = options; +// api.interceptors.request.use( +// (config) => { +// const server = useAuthStore.getState().currentServer; + +// config.baseURL = server?.url; +// config.headers = { +// 'Content-Type': 'application/json', +// 'x-nd-authorization': `Bearer ${server?.ndCredential}`, +// }; + +// return config; +// }, +// (err) => { +// return Promise.reject(err); +// }, +// ); + +// api.interceptors.response.use( +// (res) => { +// const serverId = useAuthStore.getState().currentServer?.id; + +// if (serverId) { +// useAuthStore.getState().actions.updateServer(serverId, { +// ndCredential: res.headers['x-nd-authorization'] as string, +// }); +// } + +// return res; +// }, +// async (err) => { +// if (!err.response) { +// return Promise.reject(err); +// } + +// if (err.response && err.response.status === 401) { +// toast.error({ +// message: 'Your session has expired.', +// }); + +// const serverId = useAuthStore.getState().currentServer?.id; + +// if (serverId) { +// useAuthStore.getState().actions.setCurrentServer(null); +// useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined }); +// } +// } +// return Promise.reject(err); +// }, +// ); + +const authenticate = async ( + url: string, + body: { password: string; username: string }, +): Promise => { const cleanServerUrl = url.replace(/\/$/, ''); const data = await ky .post(`${cleanServerUrl}/auth/login`, { json: { - password, - username, + password: body.password, + username: body.username, }, }) - .json(); + .json(); return { - credential: `u=${options.username}&s=${data.subsonicSalt}&t=${data.subsonicToken}`, + credential: `u=${body.username}&s=${data.subsonicSalt}&t=${data.subsonicToken}`, ndCredential: data.token, + userId: data.id, username: data.username, }; }; -const getGenreList = async (params?: NDGenreListParams) => { +const getGenreList = async (args: GenreListArgs): Promise => { + const { server, signal } = args; + const data = await api .get('api/genre', { - prefixUrl: useAuthStore.getState().currentServer?.url, - searchParams: params, + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + prefixUrl: server?.url, + signal, }) .json(); return data; }; -const getArtistList = async (params?: NDGenreListParams) => { +const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs): Promise => { + const { query, server, signal } = args; + + const data = await api + .get(`api/artist/${query.id}`, { + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + prefixUrl: server?.url, + signal, + }) + .json(); + + const albumsData = await api + .get('api/album', { + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + prefixUrl: server?.url, + searchParams: { + _end: 0, + _order: NDSortOrder.ASC, + _sort: NDAlbumListSort.YEAR, + _start: 0, + artist_id: query.id, + } as NDAlbumListParams, + signal, + }) + .json(); + + return { ...data, albums: albumsData }; +}; + +const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise => { + const { query, server, signal } = args; + + const searchParams: NDAlbumArtistListParams = { + _end: query.startIndex + (query.limit || 0), + _order: query.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC, + _sort: query.sortBy as NDAlbumArtistListSort, + _start: query.startIndex, + ...query.ndParams, + }; + const data = await api .get('api/artist', { - prefixUrl: useAuthStore.getState().currentServer?.url, - searchParams: params, + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + searchParams, + signal, }) .json(); return data; }; -const getAlbumDetail = async (query: AlbumDetailQuery, signal?: AbortSignal) => { - const albumDetail = await api +const getAlbumDetail = async (args: AlbumDetailArgs): Promise => { + const { query, server, signal } = args; + + const data = await api .get(`api/album/${query.id}`, { - prefixUrl: useAuthStore.getState().currentServer?.url, + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + prefixUrl: server?.url, signal, }) .json(); - const albumSongs = await api - .get('api/song/', { - prefixUrl: useAuthStore.getState().currentServer?.url, + const songsData = await api + .get('api/song', { + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + prefixUrl: server?.url, searchParams: { _end: 0, _order: NDSortOrder.ASC, @@ -113,57 +255,153 @@ const getAlbumDetail = async (query: AlbumDetailQuery, signal?: AbortSignal) => _start: 0, album_id: query.id, }, + signal, }) .json(); - return { ...albumDetail, songs: albumSongs } as AlbumDetailResponse; + return { ...data, songs: songsData }; }; -const getAlbumList = async (params: AlbumListParams, signal?: AbortSignal) => { +const getAlbumList = async (args: AlbumListArgs): Promise => { + const { query, server, signal } = args; + const searchParams: NDAlbumListParams = { - _end: params._skip + (params._take || 0), - _order: params.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC, - _sort: params.sortBy as NDAlbumListSort, - _start: params._skip, - ...params.nd, + _end: query.startIndex + (query.limit || 0), + _order: query.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC, + _sort: query.sortBy as NDAlbumListSort, + _start: query.startIndex, + ...query.ndParams, }; const res = await api.get('api/album', { - prefixUrl: useAuthStore.getState().currentServer?.url, + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + prefixUrl: server?.url, searchParams, signal, }); - const itemCount = res.headers.get('X-Total-Count'); const data = await res.json(); + const itemCount = res.headers.get('x-total-count'); return { items: data, - pagination: { - startIndex: params?._skip || 0, - totalEntries: Number(itemCount), - }, - } as AlbumListResponse; + startIndex: query?.startIndex || 0, + totalRecordCount: Number(itemCount), + }; }; -const getSongList = async (params?: NDSongListParams) => { +const getSongList = async (args: SongListArgs): Promise => { + const { query, server, signal } = args; + + const searchParams: NDSongListParams = { + _end: query.startIndex + (query.limit || 0), + _order: query.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC, + _sort: query.sortBy as NDSongListSort, + _start: query.startIndex, + ...query.ndParams, + }; + + const res = await api.get('api/song', { + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + prefixUrl: server?.url, + searchParams, + signal, + }); + + const data = await res.json(); + const itemCount = res.headers.get('x-total-count'); + + return { + items: data, + startIndex: query?.startIndex || 0, + totalRecordCount: Number(itemCount), + }; +}; + +const getSongDetail = async (args: SongDetailArgs): Promise => { + const { query, server, signal } = args; + const data = await api - .get('api/song', { - prefixUrl: useAuthStore.getState().currentServer?.url, - searchParams: params, + .get(`api/song/${query.id}`, { + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + signal, }) - .json(); + .json(); return data; }; -export const navidromeApi = { - authenticate, - getAlbumDetail, - getAlbumList, - getArtistList, - getGenreList, - getSongList, +const createPlaylist = async (args: CreatePlaylistArgs): Promise => { + const { query, server, signal } = args; + + const json: NDCreatePlaylistParams = { + comment: query.comment, + name: query.name, + public: query.public || false, + }; + + const data = await api + .post('api/playlist', { + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + json, + signal, + }) + .json(); + + return data; +}; + +const deletePlaylist = async (args: DeletePlaylistArgs): Promise => { + const { query, server, signal } = args; + + const data = await api + .delete(`api/playlist/${query.id}`, { + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + signal, + }) + .json(); + + return data; +}; + +const getPlaylistList = async (args: PlaylistListArgs): Promise => { + const { query, server, signal } = args; + + const searchParams: NDPlaylistListParams = { + _end: query.startIndex + (query.limit || 0), + _order: query.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC, + _sort: query.sortBy as NDPlaylistListSort, + _start: query.startIndex, + }; + + const res = await api.get('api/song', { + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + prefixUrl: server?.url, + searchParams, + signal, + }); + + const data = await res.json(); + const itemCount = res.headers.get('x-total-count'); + + return { + items: data, + startIndex: query?.startIndex || 0, + totalRecordCount: Number(itemCount), + }; +}; + +const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise => { + const { query, server, signal } = args; + + const data = await api + .get(`api/song/${query.id}`, { + headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, + signal, + }) + .json(); + + return data; }; const getCoverArtUrl = (args: { @@ -264,6 +502,21 @@ const normalizeSong = ( } as Song; }; +export const navidromeApi = { + authenticate, + createPlaylist, + deletePlaylist, + getAlbumArtistDetail, + getAlbumArtistList, + getAlbumDetail, + getAlbumList, + getGenreList, + getPlaylistDetail, + getPlaylistList, + getSongDetail, + getSongList, +}; + export const ndNormalize = { album: normalizeAlbum, song: normalizeSong, diff --git a/packages/renderer/src/api/navidrome.types.ts b/packages/renderer/src/api/navidrome.types.ts index 81bf0835d..07f923479 100644 --- a/packages/renderer/src/api/navidrome.types.ts +++ b/packages/renderer/src/api/navidrome.types.ts @@ -92,7 +92,7 @@ export type NDSong = { year: number; }; -export type NDArtist = { +export type NDAlbumArtist = { albumCount: number; biography: string; externalInfoUpdatedAt: string; @@ -115,15 +115,43 @@ export type NDArtist = { starredAt: string; }; +export type NDAuthenticationResponse = NDAuthenticate; + +export type NDAlbumArtistList = NDAlbumArtist[]; + +export type NDAlbumArtistDetail = NDAlbumArtist & { albums: NDAlbumListResponse }; + +export type NDAlbumArtistDetailResponse = NDAlbumArtist; + +export type NDGenreList = NDGenre[]; + export type NDGenreListResponse = NDGenre[]; export type NDAlbumDetailResponse = NDAlbum; +export type NDAlbumDetail = NDAlbum & { songs: NDSongListResponse }; + export type NDAlbumListResponse = NDAlbum[]; +export type NDAlbumList = { + items: NDAlbum[]; + startIndex: number; + totalRecordCount: number; +}; + +export type NDSongDetail = NDSong; + +export type NDSongDetailResponse = NDSong; + export type NDSongListResponse = NDSong[]; -export type NDArtistListResponse = NDArtist[]; +export type NDSongList = { + items: NDSong[]; + startIndex: number; + totalRecordCount: number; +}; + +export type NDArtistListResponse = NDAlbumArtist[]; export type NDPagination = { _end?: number; @@ -177,8 +205,107 @@ export type NDAlbumListParams = { } & NDPagination & NDOrder; +export enum NDSongListSort { + ALBUM = 'album', + ALBUM_ARTIST = 'albumArtist', + ARTIST = 'artist', + BPM = 'bpm', + CHANNELS = 'channels', + COMMENT = 'comment', + DURATION = 'duration', + GENRE = 'genre', + NAME = 'name', + PLAY_COUNT = 'playCount', + PLAY_DATE = 'playDate', + RATING = 'rating', + TRACK = 'track', + YEAR = 'year', +} + export type NDSongListParams = { + _sort?: NDSongListSort; genre_id?: string; starred?: boolean; } & NDPagination & NDOrder; + +export enum NDAlbumArtistListSort { + ALBUM_COUNT = 'albumCount', + NAME = 'name', + PLAY_COUNT = 'playCount', + RATING = 'rating', + SONG_COUNT = 'songCount', + STARRED = 'starred ASC, starredAt ASC', +} + +export type NDAlbumArtistListParams = { + _sort?: NDAlbumArtistListSort; + genre_id?: string; + starred?: boolean; +} & NDPagination & + NDOrder; + +export type NDCreatePlaylistParams = { + comment?: string; + name: string; + public: boolean; +}; + +export type NDCreatePlaylistResponse = { + id: string; +}; + +export type NDCreatePlaylist = NDCreatePlaylistResponse; + +export type NDDeletePlaylistParams = { + id: string; +}; + +export type NDDeletePlaylistResponse = null; + +export type NDDeletePlaylist = NDDeletePlaylistResponse; + +export type NDPlaylist = { + comment: string; + createdAt: string; + duration: number; + evaluatedAt: string; + id: string; + name: string; + ownerId: string; + ownerName: string; + path: string; + public: boolean; + rules: null; + size: number; + songCount: number; + sync: boolean; + updatedAt: string; +}; + +export type NDPlaylistDetail = NDPlaylist; + +export type NDPlaylistDetailResponse = NDPlaylist; + +export type NDPlaylistList = { + items: NDPlaylist[]; + startIndex: number; + totalRecordCount: number; +}; + +export type NDPlaylistListResponse = NDPlaylist[]; + +export enum NDPlaylistListSort { + DURATION = 'duration', + NAME = 'name', + OWNER = 'owner', + PUBLIC = 'public', + SONG_COUNT = 'songCount', + UPDATED_AT = 'updatedAt', +} + +export type NDPlaylistListParams = { + _sort?: NDPlaylistListSort; + owner_id?: string; +} & NDPagination & + NDOrder; diff --git a/packages/renderer/src/api/query-keys.ts b/packages/renderer/src/api/query-keys.ts index 22d996861..5fd62156a 100644 --- a/packages/renderer/src/api/query-keys.ts +++ b/packages/renderer/src/api/query-keys.ts @@ -1,10 +1,10 @@ -import type { AlbumListParams } from './types'; +import type { AlbumListQuery } from './types'; import type { AlbumDetailQuery } from './types'; export const queryKeys = { albums: { detail: (serverId: string, query: AlbumDetailQuery) => ['albums', serverId, query] as const, - list: (serverId: string, params: AlbumListParams) => + list: (serverId: string, params: AlbumListQuery) => [serverId, 'albums', 'list', serverId, params] as const, root: ['albums'], }, diff --git a/packages/renderer/src/api/subsonic.api.ts b/packages/renderer/src/api/subsonic.api.ts index d859d81a7..26cd3da6b 100644 --- a/packages/renderer/src/api/subsonic.api.ts +++ b/packages/renderer/src/api/subsonic.api.ts @@ -1,17 +1,40 @@ -import axios from 'axios'; +import ky from 'ky'; import md5 from 'md5'; -import { randomString } from '../utils/random-string'; +import { randomString } from '/@/utils'; import type { - SSAlbumListEntry, SSAlbumListResponse, - SSAlbumResponse, - SSAlbumsParams, + SSAlbumDetailResponse, SSArtistIndex, - SSArtistInfoResponse, - SSArtistsResponse, - SSGenresResponse, - SSMusicFoldersResponse, -} from './subsonic.types'; + SSAlbumArtistList, + SSAlbumArtistListResponse, + SSGenreListResponse, + SSMusicFolderList, + SSMusicFolderListResponse, + SSGenreList, + SSAlbumDetail, + SSAlbumList, + SSAlbumArtistDetail, + SSAlbumArtistDetailResponse, + SSFavoriteParams, + SSFavorite, + SSFavoriteResponse, + SSRatingParams, + SSRatingResponse, + SSAlbumArtistDetailParams, + SSAlbumArtistListParams, +} from '/@/api/subsonic.types'; +import type { + AlbumArtistDetailArgs, + AlbumArtistListArgs, + AlbumDetailArgs, + AlbumListArgs, + AuthenticationResponse, + FavoriteArgs, + GenreListArgs, + RatingArgs, +} from '/@/api/types'; +import { useAuthStore } from '/@/store'; +import { toast } from '/@/components'; const getCoverArtUrl = (args: { baseUrl: string; @@ -35,136 +58,235 @@ const getCoverArtUrl = (args: { ); }; -const api = axios.create({ - validateStatus: (status) => status >= 200, +const api = ky.create({ + hooks: { + afterResponse: [ + async (_request, _options, response) => { + const data = await response.json(); + if (data['subsonic-response'].status !== 'ok') { + toast.warn({ message: 'Issue from Subsonic API' }); + } + + return new Response(JSON.stringify(data['subsonic-response']), { status: 200 }); + }, + ], + beforeRequest: [ + (request) => { + const server = useAuthStore.getState().currentServer; + + const searchParams = new URLSearchParams(); + + if (server) { + const authParams = server.credential.split(/&?\w=/gm); + + searchParams.set('u', server.username); + searchParams.set('v', '1.13.0'); + searchParams.set('c', 'Feishin'); + searchParams.set('f', 'json'); + + if (authParams?.length === 4) { + searchParams.set('s', authParams[2]); + searchParams.set('t', authParams[3]); + } else if (authParams?.length === 3) { + searchParams.set('p', authParams[2]); + } + } + + return ky(request, { searchParams }); + }, + ], + }, }); -api.interceptors.response.use( - (res: any) => { - res.data = res.data['subsonic-response']; - return res; +const authenticate = async ( + url: string, + body: { + legacy?: boolean; + password: string; + username: string; }, - (err: any) => { - return Promise.reject(err); - }, -); +): Promise => { + let credential; + const cleanServerUrl = url.replace(/\/$/, ''); -const authenticate = async (options: { - legacy?: boolean; - password: string; - url: string; - username: string; -}) => { - let token; - - const cleanServerUrl = options.url.replace(/\/$/, ''); - - if (options.legacy) { - token = `u=${options.username}&p=${options.password}`; + if (body.legacy) { + credential = `u=${body.username}&p=${body.password}`; } else { const salt = randomString(12); - const hash = md5(options.password + salt); - token = `u=${options.username}&s=${salt}&t=${hash}`; + const hash = md5(body.password + salt); + credential = `u=${body.username}&s=${salt}&t=${hash}`; } - const { data } = await api.get( - `${cleanServerUrl}/rest/ping.view?v=1.13.0&c=Feishin&f=json&${token}`, - ); + await ky.get(`${cleanServerUrl}/rest/ping.view?v=1.13.0&c=Feishin&f=json&${credential}`); - return { token, ...data }; + return { + credential, + userId: null, + username: body.username, + }; }; -const getMusicFolders = async (server: Partial) => { - const { data } = await api.get( - `${server.url}/rest/getMusicFolders.view?v=1.13.0&c=Feishin&f=json&${server.token}`, - ); +const getMusicFolderList = async ( + server: any, + signal?: AbortSignal, +): Promise => { + const data = await api + .get('rest/getMusicFolders.view', { + prefixUrl: server.url, + signal, + }) + .json(); return data.musicFolders.musicFolder; }; -const getArtists = async (server: Server, musicFolderId: string) => { - const { data } = await api.get( - `${server.url}/rest/getArtists.view?v=1.13.0&c=Feishin&f=json&${server.token}`, - { params: { musicFolderId } }, - ); +export const getAlbumArtistDetail = async ( + args: AlbumArtistDetailArgs, +): Promise => { + const { signal, query } = args; + + const searchParams: SSAlbumArtistDetailParams = { + id: query.id, + }; + + const data = await api + .get('/getArtist.view', { + searchParams, + signal, + }) + .json(); + + return data.artist; +}; + +const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise => { + const { signal, query } = args; + + const searchParams: SSAlbumArtistListParams = { + musicFolderId: query.musicFolderId, + }; + + const data = await api + .get('/rest/getArtists.view', { + searchParams, + signal, + }) + .json(); const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist); return artists; }; -const getGenres = async (server: Server) => { - const { data: genres } = await api.get( - `${server.url}/rest/getGenres.view?v=1.13.0&c=Feishin&f=json&${server.token}`, - ); +const getGenreList = async (args: GenreListArgs): Promise => { + const { signal } = args; - return genres; -}; - -const getAlbum = async (server: Server, id: string) => { - const { data: album } = await api.get( - `${server.url}/rest/getAlbum.view?v=1.13.0&c=Feishin&f=json&${server.token}`, - { params: { id } }, - ); - - return album; -}; - -const getAlbums = async (server: Server, params: SSAlbumsParams, recursiveData: any[] = []) => { - const albums: any = api - .get( - `${server.url}/rest/getAlbumList2.view?v=1.13.0&c=Feishin&f=json&${server.token}`, - { params }, - ) - .then((res) => { - if (!res.data.albumList2?.album || res.data.albumList2?.album?.length === 0) { - // Flatten and return once there are no more albums left - return recursiveData.flatMap((album) => album); - } - - // On every iteration, push the existing combined album array and increase the offset - recursiveData.push(res.data.albumList2.album); - return getAlbums( - server, - { - musicFolderId: params.musicFolderId, - offset: (params.offset || 0) + (params.size || 0), - size: params.size, - type: 'newest', - }, - - recursiveData, - ); + const data = await api + .get('/rest/getGenres.view', { + signal, }) - .catch((err) => console.log(err)); + .json(); - return albums as SSAlbumListEntry[]; + return data.genres.genre; }; -const getArtistInfo = async (server: Server, id: string) => { - const { data: artistInfo } = await api.get( - `${server.url}/rest/getArtistInfo2.view?v=1.13.0&c=Feishin&f=json&${server.token}`, - { params: { id } }, - ); +const getAlbumDetail = async (args: AlbumDetailArgs): Promise => { + const { query, signal } = args; + + const data = await api + .get('/rest/getAlbum.view', { + searchParams: { id: query.id }, + signal, + }) + .json(); + + return data.album; +}; + +const getAlbumList = async (args: AlbumListArgs): Promise => { + const { query, signal } = args; + + const normalizedParams = {}; + const data = await api + .get('/rest/getAlbumList2.view', { + searchParams: normalizedParams, + signal, + }) + .json(); return { - ...artistInfo, - artistInfo2: { - ...artistInfo.artistInfo2, - biography: artistInfo.artistInfo2.biography - .replaceAll(//gm, '') - .replace('Biography not available', ''), - }, + items: data.albumList2.album, + startIndex: query.startIndex, + totalRecordCount: null, }; }; +const createFavorite = async (args: FavoriteArgs): Promise => { + const { query, signal } = args; + + const searchParams: SSFavoriteParams = { + albumId: query.type === 'album' ? query.id : undefined, + artistId: query.type === 'albumArtist' ? query.id : undefined, + id: query.type === 'song' ? query.id : undefined, + }; + + const data = await api + .get('/rest/star.view', { + searchParams, + signal, + }) + .json(); + + return data; +}; + +const deleteFavorite = async (args: FavoriteArgs) => { + const { query, signal } = args; + + const searchParams: SSFavoriteParams = { + albumId: query.type === 'album' ? query.id : undefined, + artistId: query.type === 'albumArtist' ? query.id : undefined, + id: query.type === 'song' ? query.id : undefined, + }; + + const data = await api + .get('/rest/unstar.view', { + searchParams, + signal, + }) + .json(); + + return data; +}; + +const updateRating = async (args: RatingArgs) => { + const { query, signal } = args; + + const searchParams: SSRatingParams = { + id: query.id, + rating: query.rating, + }; + + const data = await api + .get('/rest/setRating.view', { + searchParams, + signal, + }) + .json(); + + return data; +}; + export const subsonicApi = { authenticate, - getAlbum, - getAlbums, - getArtistInfo, - getArtists, + createFavorite, + deleteFavorite, + getAlbumArtistDetail, + getAlbumArtistList, + getAlbumDetail, + getAlbumList, getCoverArtUrl, - getGenres, - getMusicFolders, + getGenreList, + getMusicFolderList, + updateRating, }; diff --git a/packages/renderer/src/api/subsonic.types.ts b/packages/renderer/src/api/subsonic.types.ts index c87f1f18a..d3b9dc716 100644 --- a/packages/renderer/src/api/subsonic.types.ts +++ b/packages/renderer/src/api/subsonic.types.ts @@ -1,78 +1,104 @@ -export interface SSBaseResponse { +export type SSBaseResponse = { serverVersion?: 'string'; status: 'string'; type?: 'string'; version: 'string'; -} +}; -export interface SSMusicFoldersResponse extends SSBaseResponse { +export type SSMusicFolderList = SSMusicFolder[]; + +export type SSMusicFolderListResponse = { musicFolders: { musicFolder: SSMusicFolder[]; }; -} +}; -export interface SSGenresResponse extends SSBaseResponse { +export type SSGenreList = SSGenre[]; + +export type SSGenreListResponse = { genres: { genre: SSGenre[]; }; -} +}; -export interface SSArtistsResponse extends SSBaseResponse { +export type SSAlbumArtistDetailParams = { + id: string; +}; + +export type SSAlbumArtistDetail = SSAlbumArtistListEntry & { album: SSAlbumListEntry[] }; + +export type SSAlbumArtistDetailResponse = { + artist: SSAlbumArtistListEntry & { + album: SSAlbumListEntry[]; + }; +}; + +export type SSAlbumArtistList = SSAlbumArtistListEntry[]; + +export type SSAlbumArtistListResponse = { artists: { ignoredArticles: string; index: SSArtistIndex[]; lastModified: number; }; -} +}; -export interface SSAlbumListResponse extends SSBaseResponse { +export type SSAlbumList = { + items: SSAlbumListEntry[]; + startIndex: number; + totalRecordCount: number | null; +}; + +export type SSAlbumListResponse = { albumList2: { album: SSAlbumListEntry[]; }; -} +}; -export interface SSAlbumResponse extends SSBaseResponse { +export type SSAlbumDetail = SSAlbum; + +export type SSAlbumDetailResponse = { album: SSAlbum; -} +}; -export interface SSArtistInfoResponse extends SSBaseResponse { +export type SSArtistInfoResponse = { artistInfo2: SSArtistInfo; -} +}; -export interface SSArtistInfo { +export type SSArtistInfo = { biography: string; largeImageUrl?: string; lastFmUrl?: string; mediumImageUrl?: string; musicBrainzId?: string; smallImageUrl?: string; -} +}; -export interface SSMusicFolder { +export type SSMusicFolder = { id: number; name: string; -} +}; -export interface SSGenre { +export type SSGenre = { albumCount?: number; songCount?: number; value: string; -} +}; -export interface SSArtistIndex { - artist: SSArtistListEntry[]; +export type SSArtistIndex = { + artist: SSAlbumArtistListEntry[]; name: string; -} +}; -export interface SSArtistListEntry { +export type SSAlbumArtistListEntry = { albumCount: string; artistImageUrl?: string; coverArt?: string; id: string; name: string; -} +}; -export interface SSAlbumListEntry { +export type SSAlbumListEntry = { album: string; artist: string; artistId: string; @@ -90,13 +116,13 @@ export interface SSAlbumListEntry { title: string; userRating?: number; year: number; -} +}; -export interface SSAlbum extends SSAlbumListEntry { +export type SSAlbum = { song: SSSong[]; -} +} & SSAlbumListEntry; -export interface SSSong { +export type SSSong = { album: string; albumId: string; artist: string; @@ -122,9 +148,9 @@ export interface SSSong { type: string; userRating?: number; year: number; -} +}; -export interface SSAlbumsParams { +export type SSAlbumListParams = { fromYear?: number; genre?: string; musicFolderId?: string; @@ -132,8 +158,27 @@ export interface SSAlbumsParams { size?: number; toYear?: number; type: string; -} +}; -export interface SSArtistsParams { - musicFolderId?: number; -} +export type SSAlbumArtistListParams = { + musicFolderId?: string; +}; + +export type SSFavoriteParams = { + albumId?: string; + artistId?: string; + id?: string; +}; + +export type SSFavorite = null; + +export type SSFavoriteResponse = null; + +export type SSRatingParams = { + id: string; + rating: number; +}; + +export type SSRating = null; + +export type SSRatingResponse = null; diff --git a/packages/renderer/src/api/types.ts b/packages/renderer/src/api/types.ts index 4fdd46868..5997d1a7a 100644 --- a/packages/renderer/src/api/types.ts +++ b/packages/renderer/src/api/types.ts @@ -1,7 +1,30 @@ -import type { ServerListItem } from '../store'; -import type { ServerType } from '../types'; -import type { JFAlbum, JFAlbumListSort, JFSortOrder } from './jellyfin.types'; -import type { NDAlbum, NDAlbumListSort, NDOrder } from './navidrome.types'; +import type { ServerListItem } from '/@/store'; +import type { ServerType } from '/@//types'; +import type { JFAlbumListSort, JFSortOrder } from '/@/api/jellyfin.types'; +import type { + NDAlbumArtistDetail, + NDAlbumArtistList, + NDAlbumArtistListSort, + NDAlbumDetail, + NDAlbumList, + NDAlbumListSort, + NDCreatePlaylist, + NDDeletePlaylist, + NDGenreList, + NDOrder, + NDPlaylistDetail, + NDPlaylistList, + NDPlaylistListSort, + NDSongDetail, + NDSongList, + NDSongListSort, +} from '/@/api/navidrome.types'; +import type { + SSAlbumArtistDetail, + SSAlbumArtistList, + SSAlbumDetail, + SSAlbumList, +} from '/@/api/subsonic.types'; export enum SortOrder { ASC = 'ASC', @@ -27,33 +50,15 @@ export enum ImageType { SCREENSHOT = 'SCREENSHOT', } -export enum TaskType { - FULL_SCAN = 'FULL_SCAN', - LASTFM = 'LASTFM', - MUSICBRAINZ = 'MUSICBRAINZ', - QUICK_SCAN = 'QUICK_SCAN', - REFRESH = 'REFRESH', - SPOTIFY = 'SPOTIFY', -} - export type EndpointDetails = { server: ServerListItem; }; -// export interface BaseResponse { -// error?: string | any; -// items: T; -// response: 'Success' | 'Error'; -// statusCode: number; -// } - export interface BasePaginatedResponse { error?: string | any; items: T; - pagination?: { - startIndex: number; - totalEntries: number; - }; + startIndex: number; + totalRecordCount: number; } export type ApiError = { @@ -66,47 +71,14 @@ export type ApiError = { statusCode: number; }; -export type AuthResponse = { +export type AuthenticationResponse = { credential: string; ndCredential?: string; + userId: string | null; username: string; }; -// export type NullResponse = BaseResponse; - -export type PaginationParams = { - skip: number; - take: number; -}; - -export type RelatedServerFolder = { - enabled: boolean; - id: string; - lastScannedAt: string | null; - name: string; -}; - -export type ServerFolder = { - createdAt: string; - enabled: boolean; - id: string; - lastScannedAt: string | null; - name: string; - serverId: string; - updatedAt: string; -}; - export type Genre = { - albumArtistCount: number; - albumCount: number; - artistCount: number; - id: string; - name: string; - songCount: number; - totalCount: number; -}; - -export type RelatedGenre = { id: string; name: string; }; @@ -116,7 +88,7 @@ export type Album = { artists: RelatedArtist[]; backdropImageUrl: string | null; createdAt: string; - genres: RelatedGenre[]; + genres: Genre[]; id: string; imagePlaceholderUrl: string | null; imageUrl: string | null; @@ -147,7 +119,7 @@ export type Song = { createdAt: string; discNumber: number; duration: number; - genres: RelatedGenre[]; + genres: Genre[]; id: string; imageUrl: string; isFavorite: boolean; @@ -193,69 +165,207 @@ export type RelatedArtist = { name: string; }; -export type RelatedServer = { +export type MusicFolder = { id: string; name: string; - type: ServerType; - url: string; }; -export type RelatedUser = { - enabled: boolean; +export type Playlist = { + duration?: number; id: string; - isAdmin: boolean; + name: string; + public?: boolean; + size?: number; + songCount?: number; + userId: string; username: string; }; -export type Task = { - createdAt: string; - id: string; - isCompleted: boolean; - isError: boolean; - message: string; - server: RelatedServer | null; - type: TaskType; - updatedAt: string; - user: RelatedUser | null; -}; +export type GenresResponse = Genre[]; -export type AlbumListSort = NDAlbumListSort | JFAlbumListSort; +export type MusicFoldersResponse = MusicFolder[]; export type ListSortOrder = NDOrder | JFSortOrder; -export type AlbumListParams = { - _skip: number; - _take?: number; - musicFolderId: string | null; - nd?: { +type BaseEndpointArgs = { + server: ServerListItem | null; + signal?: AbortSignal; +}; + +// Genre List --------------------------------------------------------------------------- +export type RawGenreListResponse = NDGenreList | undefined; + +export type GenreListResponse = BasePaginatedResponse | null | undefined; + +export type GenreListArgs = { query: GenreListQuery } & BaseEndpointArgs; + +export type GenreListQuery = null; + +// Album List --------------------------------------------------------------------------- +export type RawAlbumListResponse = NDAlbumList | SSAlbumList | undefined; + +export type AlbumListResponse = BasePaginatedResponse | null | undefined; + +export type AlbumListSort = NDAlbumListSort | JFAlbumListSort; + +export type AlbumListQuery = { + limit?: number; + musicFolderId?: string; + ndParams?: { + artist_id?: string; + compilation?: boolean; + genre_id?: string; + has_rating?: boolean; + name?: string; + starred?: boolean; + year?: number; + }; + sortBy: AlbumListSort; + sortOrder: SortOrder; + startIndex: number; +}; + +export type AlbumListArgs = { query: AlbumListQuery } & BaseEndpointArgs; + +// Album Detail ------------------------------------------------------------------------- +export type RawAlbumDetailResponse = NDAlbumDetail | SSAlbumDetail | undefined; + +export type AlbumDetailResponse = Album | null | undefined; + +export type AlbumDetailQuery = { id: string }; + +export type AlbumDetailArgs = { query: AlbumDetailQuery } & BaseEndpointArgs; + +// Song List ---------------------------------------------------------------------------- +export type RawSongListResponse = NDSongList | undefined; + +export type SongListResponse = BasePaginatedResponse; + +export type SongListSort = NDSongListSort; + +export type SongListQuery = { + limit?: number; + musicFolderId?: string; + ndParams?: { artist_id?: string; compilation?: boolean; genre_id?: string; has_rating?: boolean; starred?: boolean; + title?: string; year?: number; }; - sortBy: NDAlbumListSort | JFAlbumListSort; + sortBy: SongListSort; sortOrder: SortOrder; + startIndex: number; }; -export type AlbumListResponse = - | BasePaginatedResponse - | null - | undefined; +export type SongListArgs = { query: SongListQuery } & BaseEndpointArgs; -export type AlbumDetailQuery = { +// Song Detail ------------------------------------------------------------------------- +export type RawSongDetailResponse = NDSongDetail | undefined; + +export type SongDetailResponse = Song | null | undefined; + +export type SongDetailQuery = { id: string }; + +export type SongDetailArgs = { query: SongDetailQuery } & BaseEndpointArgs; + +// Album Artist List ------------------------------------------------------------------- +export type RawAlbumArtistListResponse = NDAlbumArtistList | SSAlbumArtistList | undefined; + +export type AlbumArtistListResponse = BasePaginatedResponse; + +export type AlbumArtistListSort = NDAlbumArtistListSort; + +export type AlbumArtistListQuery = { + limit?: number; + musicFolderId?: string; + ndParams?: { + genre_id?: string; + name?: string; + starred?: boolean; + }; + sortBy: AlbumArtistListSort; + sortOrder: SortOrder; + startIndex: number; +}; + +export type AlbumArtistListArgs = { query: AlbumArtistListQuery } & BaseEndpointArgs; + +// Album Artist Detail ----------------------------------------------------------------- +export type RawAlbumArtistDetailResponse = NDAlbumArtistDetail | SSAlbumArtistDetail | undefined; + +export type AlbumArtistDetailResponse = BasePaginatedResponse; + +export type AlbumArtistDetailQuery = { id: string }; + +export type AlbumArtistDetailArgs = { query: AlbumArtistDetailQuery } & BaseEndpointArgs; + +// Artist List ------------------------------------------------------------------------- + +// Artist Detail ----------------------------------------------------------------------- + +// Favorite ---------------------------------------------------------------------------- +export type RawFavoriteResponse = null | undefined; + +export type FavoriteResponse = null; + +export type FavoriteQuery = { id: string; type?: 'song' | 'album' | 'albumArtist' }; + +export type FavoriteArgs = { query: FavoriteQuery } & BaseEndpointArgs; + +// Rating ------------------------------------------------------------------------------- +export type RawRatingResponse = null | undefined; + +export type RatingResponse = null; + +export type RatingQuery = { id: string; rating: number }; + +export type RatingArgs = { query: RatingQuery } & BaseEndpointArgs; + +// Create Playlist ----------------------------------------------------------------------- +export type RawCreatePlaylistResponse = NDCreatePlaylist | undefined; + +export type CreatePlaylistResponse = null; + +export type CreatePlaylistQuery = { comment?: string; name: string; public?: boolean }; + +export type CreatePlaylistArgs = { query: CreatePlaylistQuery } & BaseEndpointArgs; + +// Delete Playlist ----------------------------------------------------------------------- +export type RawDeletePlaylistResponse = NDDeletePlaylist | undefined; + +export type DeletePlaylistResponse = null; + +export type DeletePlaylistQuery = { id: string }; + +export type DeletePlaylistArgs = { query: DeletePlaylistQuery } & BaseEndpointArgs; + +// Playlist List ------------------------------------------------------------------------- +export type RawPlaylistListResponse = NDPlaylistList | undefined; + +export type PlaylistListResponse = BasePaginatedResponse; + +export type PlaylistListSort = NDPlaylistListSort; + +export type PlaylistListQuery = { + limit?: number; + musicFolderId?: string; + sortBy: PlaylistListSort; + sortOrder: SortOrder; + startIndex: number; +}; + +export type PlaylistListArgs = { query: PlaylistListQuery } & BaseEndpointArgs; + +// Playlist Detail ----------------------------------------------------------------------- +export type RawPlaylistDetailResponse = NDPlaylistDetail | undefined; + +export type PlaylistDetailResponse = BasePaginatedResponse; + +export type PlaylistDetailQuery = { id: string; }; -export type AlbumDetailResponse = Album | NDAlbum | JFAlbum | null | undefined; - -export type Count = { - artists?: number; - externals?: number; - favorites?: number; - genres?: number; - images?: number; - ratings?: number; - songs?: number; -}; +export type PlaylistDetailArgs = { query: PlaylistDetailQuery } & BaseEndpointArgs; diff --git a/packages/renderer/src/features/albums/queries/album-detail-query.ts b/packages/renderer/src/features/albums/queries/album-detail-query.ts index f5f487c3c..a989b0cb9 100644 --- a/packages/renderer/src/features/albums/queries/album-detail-query.ts +++ b/packages/renderer/src/features/albums/queries/album-detail-query.ts @@ -2,14 +2,14 @@ import { useQuery } from '@tanstack/react-query'; import { queryKeys } from '/@/api/query-keys'; import type { QueryOptions } from '/@/lib/react-query'; import { useCurrentServer } from '../../../store/auth.store'; -import { apiController } from '/@/api/controller'; import type { AlbumDetailQuery } from '/@/api/types'; +import { controller } from '/@/api/controller'; export const useAlbumDetail = (query: AlbumDetailQuery, options: QueryOptions) => { const server = useCurrentServer(); return useQuery({ - queryFn: ({ signal }) => apiController.getAlbumDetail(query, signal), + queryFn: ({ signal }) => controller.getAlbumDetail({ query, server, signal }), queryKey: queryKeys.albums.detail(server?.id || '', query), ...options, }); diff --git a/packages/renderer/src/features/albums/queries/album-list-query.ts b/packages/renderer/src/features/albums/queries/album-list-query.ts index 6e081c8ee..9cf5d00cb 100644 --- a/packages/renderer/src/features/albums/queries/album-list-query.ts +++ b/packages/renderer/src/features/albums/queries/album-list-query.ts @@ -1,23 +1,22 @@ import { useCallback } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { apiController } from '/@/api/controller'; +import { controller } from '/@/api/controller'; import { queryKeys } from '/@/api/query-keys'; -import type { AlbumListParams, AlbumListResponse } from '/@/api/types'; +import type { AlbumListQuery, RawAlbumListResponse } from '/@/api/types'; import type { QueryOptions } from '/@/lib/react-query'; -import { useCurrentServer, useCurrentServerId } from '/@/store'; +import { useCurrentServer } from '/@/store'; import { ndNormalize } from '/@/api/navidrome.api'; import type { NDAlbum } from '/@/api/navidrome.types'; -export const useAlbumList = (params: AlbumListParams, options?: QueryOptions) => { - const serverId = useCurrentServerId(); +export const useAlbumList = (query: AlbumListQuery, options?: QueryOptions) => { const server = useCurrentServer(); return useQuery({ - enabled: !!serverId, - queryFn: ({ signal }) => apiController.getAlbumList(params, signal), - queryKey: queryKeys.albums.list(serverId, params), + enabled: !!server?.id, + queryFn: ({ signal }) => controller.getAlbumList({ query, server, signal }), + queryKey: queryKeys.albums.list(server?.id || '', query), select: useCallback( - (data: AlbumListResponse) => { + (data: RawAlbumListResponse | undefined) => { let albums; switch (server?.type) { case 'jellyfin': @@ -30,8 +29,9 @@ export const useAlbumList = (params: AlbumListParams, options?: QueryOptions) => } return { - ...data, items: albums, + startIndex: data?.startIndex, + totalRecordCount: data?.totalRecordCount, }; }, [server], diff --git a/packages/renderer/src/features/albums/routes/album-list-route.tsx b/packages/renderer/src/features/albums/routes/album-list-route.tsx index 6aa2a137e..0899b4cac 100644 --- a/packages/renderer/src/features/albums/routes/album-list-route.tsx +++ b/packages/renderer/src/features/albums/routes/album-list-route.tsx @@ -29,7 +29,7 @@ import { useAlbumList } from '../queries/album-list-query'; import { JFAlbumListSort } from '/@/api/jellyfin.types'; import type { NDAlbum } from '/@/api/navidrome.types'; import { NDAlbumListSort } from '/@/api/navidrome.types'; -import { apiController } from '/@/api/controller'; +import { controller } from '/@/api/controller'; import { ndNormalize } from '/@/api/navidrome.api'; const FILTERS = { @@ -70,28 +70,30 @@ export const AlbumListRoute = () => { const filters = page.list.filter; const albumListQuery = useAlbumList({ - _skip: 0, - _take: 1, - musicFolderId: null, + limit: 1, sortBy: filters.sortBy, sortOrder: filters.sortOrder, + startIndex: 0, }); const fetch = useCallback( async ({ skip, take }: { skip: number; take: number }) => { const queryKey = queryKeys.albums.list(server?.id || '', { - _skip: skip, - _take: take, + limit: take, + startIndex: skip, ...filters, }); - const albums = await queryClient.fetchQuery(queryKey, async () => - apiController.getAlbumList({ - _skip: skip, - _take: take, - musicFolderId: null, - sortBy: filters.sortBy, - sortOrder: filters.sortOrder, + const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) => + controller.getAlbumList({ + query: { + limit: take, + sortBy: filters.sortBy, + sortOrder: filters.sortOrder, + startIndex: skip, + }, + server, + signal, }), ); @@ -110,10 +112,8 @@ export const AlbumListRoute = () => { return { items, - pagination: { - startIndex: skip, - totalEntries: albums?.pagination?.totalEntries || 0, - }, + startIndex: skip, + totalRecordCount: albums?.totalRecordCount || 0, } as AlbumListResponse; }, [filters, queryClient, server], @@ -445,7 +445,7 @@ export const AlbumListRoute = () => { fetchFn={fetch} height={height} initialScrollOffset={page.list?.gridScrollOffset || 0} - itemCount={albumListQuery?.data?.pagination?.totalEntries || 0} + itemCount={albumListQuery?.data?.totalRecordCount || 0} itemGap={20} itemSize={150 + page.list?.size} itemType={LibraryItem.ALBUM} diff --git a/packages/renderer/src/store/app.store.ts b/packages/renderer/src/store/app.store.ts index 829a9d527..4af711687 100644 --- a/packages/renderer/src/store/app.store.ts +++ b/packages/renderer/src/store/app.store.ts @@ -22,7 +22,7 @@ type LibraryPageProps = { }; type ListFilter = { - musicFolderId: string | null; + musicFolderId?: string; sortBy: AlbumListSort; sortOrder: SortOrder; }; @@ -89,7 +89,7 @@ export const useAppStore = create()( ...state.albums.list, filter: { ...state.albums.list.filter, - musicFolderId: null, + musicFolderId: undefined, }, gridScrollOffset: 0, listScrollOffset: 0, @@ -118,7 +118,7 @@ export const useAppStore = create()( }, display: CardDisplayType.CARD, filter: { - musicFolderId: null, + musicFolderId: undefined, sortBy: JFAlbumListSort.NAME, sortOrder: SortOrder.DESC, },