Add initial API controller

- Migrate from axios to ky
This commit is contained in:
jeffvli
2022-12-09 02:46:59 -08:00
parent 4d64a96f75
commit 516b895efe
14 changed files with 1622 additions and 454 deletions
+16 -47
View File
@@ -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"
}
-1
View File
@@ -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",
+158 -48
View File
@@ -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<RawFavoriteResponse>;
createPlaylist: (args: CreatePlaylistArgs) => Promise<RawCreatePlaylistResponse>;
deleteFavorite: (args: FavoriteArgs) => Promise<RawFavoriteResponse>;
deletePlaylist: (args: DeletePlaylistArgs) => Promise<RawDeletePlaylistResponse>;
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<RawAlbumArtistDetailResponse>;
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<RawAlbumArtistListResponse>;
getAlbumDetail: (args: AlbumDetailArgs) => Promise<RawAlbumDetailResponse>;
getAlbumList: (args: AlbumListArgs) => Promise<RawAlbumListResponse>;
getArtistDetail: () => void;
getArtistList: () => void;
getFavoritesList: () => void;
getFolderItemList: () => void;
getFolderList: () => void;
getFolderSongs: () => void;
getGenreList: (args: GenreListArgs) => Promise<RawGenreListResponse>;
getMusicFolderList: () => void;
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<RawPlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<RawPlaylistListResponse>;
getSongDetail: (args: SongDetailArgs) => Promise<RawSongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<RawSongListResponse>;
updatePlaylist: () => void;
updateRating: (args: RatingArgs) => Promise<RawRatingResponse>;
}>;
if (!server) {
return null;
}
return server.type;
type ApiController = {
jellyfin: ControllerEndpoint;
navidrome: ControllerEndpoint;
subsonic: ControllerEndpoint;
};
const getAlbumDetail = async (
query: AlbumDetailQuery,
signal?: AbortSignal,
): Promise<AlbumDetailResponse> => {
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<AlbumListResponse> => {
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,
};
+433
View File
@@ -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;
};
+329 -76
View File
@@ -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<AuthenticationResponse> => {
const cleanServerUrl = url.replace(/\/$/, '');
const data = await ky
.post(`${cleanServerUrl}/auth/login`, {
json: {
password,
username,
password: body.password,
username: body.username,
},
})
.json<NDAuthenticate>();
.json<NDAuthenticationResponse>();
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<NDGenreList> => {
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<NDGenreListResponse>();
return data;
};
const getArtistList = async (params?: NDGenreListParams) => {
const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs): Promise<NDAlbumArtistDetail> => {
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<NDAlbumArtistDetailResponse>();
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<NDAlbumListResponse>();
return { ...data, albums: albumsData };
};
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<NDAlbumArtistList> => {
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<NDArtistListResponse>();
return data;
};
const getAlbumDetail = async (query: AlbumDetailQuery, signal?: AbortSignal) => {
const albumDetail = await api
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<NDAlbumDetail> => {
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<NDAlbumDetailResponse>();
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<NDSongListResponse>();
return { ...albumDetail, songs: albumSongs } as AlbumDetailResponse;
return { ...data, songs: songsData };
};
const getAlbumList = async (params: AlbumListParams, signal?: AbortSignal) => {
const getAlbumList = async (args: AlbumListArgs): Promise<NDAlbumList> => {
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<NDAlbumListResponse>();
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<NDSongList> => {
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<NDSongListResponse>();
const itemCount = res.headers.get('x-total-count');
return {
items: data,
startIndex: query?.startIndex || 0,
totalRecordCount: Number(itemCount),
};
};
const getSongDetail = async (args: SongDetailArgs): Promise<NDSongDetail> => {
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<NDSongListResponse>();
.json<NDSongDetailResponse>();
return data;
};
export const navidromeApi = {
authenticate,
getAlbumDetail,
getAlbumList,
getArtistList,
getGenreList,
getSongList,
const createPlaylist = async (args: CreatePlaylistArgs): Promise<NDCreatePlaylist> => {
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<NDCreatePlaylistResponse>();
return data;
};
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<NDDeletePlaylist> => {
const { query, server, signal } = args;
const data = await api
.delete(`api/playlist/${query.id}`, {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
signal,
})
.json<NDDeletePlaylistResponse>();
return data;
};
const getPlaylistList = async (args: PlaylistListArgs): Promise<NDPlaylistList> => {
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<NDPlaylistListResponse>();
const itemCount = res.headers.get('x-total-count');
return {
items: data,
startIndex: query?.startIndex || 0,
totalRecordCount: Number(itemCount),
};
};
const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise<NDPlaylistDetail> => {
const { query, server, signal } = args;
const data = await api
.get(`api/song/${query.id}`, {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
signal,
})
.json<NDPlaylistDetailResponse>();
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,
+129 -2
View File
@@ -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;
+2 -2
View File
@@ -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'],
},
+230 -108
View File
@@ -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<AuthenticationResponse> => {
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<Server>) => {
const { data } = await api.get<SSMusicFoldersResponse>(
`${server.url}/rest/getMusicFolders.view?v=1.13.0&c=Feishin&f=json&${server.token}`,
);
const getMusicFolderList = async (
server: any,
signal?: AbortSignal,
): Promise<SSMusicFolderList> => {
const data = await api
.get('rest/getMusicFolders.view', {
prefixUrl: server.url,
signal,
})
.json<SSMusicFolderListResponse>();
return data.musicFolders.musicFolder;
};
const getArtists = async (server: Server, musicFolderId: string) => {
const { data } = await api.get<SSArtistsResponse>(
`${server.url}/rest/getArtists.view?v=1.13.0&c=Feishin&f=json&${server.token}`,
{ params: { musicFolderId } },
);
export const getAlbumArtistDetail = async (
args: AlbumArtistDetailArgs,
): Promise<SSAlbumArtistDetail> => {
const { signal, query } = args;
const searchParams: SSAlbumArtistDetailParams = {
id: query.id,
};
const data = await api
.get('/getArtist.view', {
searchParams,
signal,
})
.json<SSAlbumArtistDetailResponse>();
return data.artist;
};
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArtistList> => {
const { signal, query } = args;
const searchParams: SSAlbumArtistListParams = {
musicFolderId: query.musicFolderId,
};
const data = await api
.get('/rest/getArtists.view', {
searchParams,
signal,
})
.json<SSAlbumArtistListResponse>();
const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist);
return artists;
};
const getGenres = async (server: Server) => {
const { data: genres } = await api.get<SSGenresResponse>(
`${server.url}/rest/getGenres.view?v=1.13.0&c=Feishin&f=json&${server.token}`,
);
const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
const { signal } = args;
return genres;
};
const getAlbum = async (server: Server, id: string) => {
const { data: album } = await api.get<SSAlbumResponse>(
`${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<SSAlbumListResponse>(
`${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<SSGenreListResponse>();
return albums as SSAlbumListEntry[];
return data.genres.genre;
};
const getArtistInfo = async (server: Server, id: string) => {
const { data: artistInfo } = await api.get<SSArtistInfoResponse>(
`${server.url}/rest/getArtistInfo2.view?v=1.13.0&c=Feishin&f=json&${server.token}`,
{ params: { id } },
);
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> => {
const { query, signal } = args;
const data = await api
.get('/rest/getAlbum.view', {
searchParams: { id: query.id },
signal,
})
.json<SSAlbumDetailResponse>();
return data.album;
};
const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => {
const { query, signal } = args;
const normalizedParams = {};
const data = await api
.get('/rest/getAlbumList2.view', {
searchParams: normalizedParams,
signal,
})
.json<SSAlbumListResponse>();
return {
...artistInfo,
artistInfo2: {
...artistInfo.artistInfo2,
biography: artistInfo.artistInfo2.biography
.replaceAll(/<a target.*<\/a>/gm, '')
.replace('Biography not available', ''),
},
items: data.albumList2.album,
startIndex: query.startIndex,
totalRecordCount: null,
};
};
const createFavorite = async (args: FavoriteArgs): Promise<SSFavorite> => {
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<SSFavoriteResponse>();
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<SSFavoriteResponse>();
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<SSRatingResponse>();
return data;
};
export const subsonicApi = {
authenticate,
getAlbum,
getAlbums,
getArtistInfo,
getArtists,
createFavorite,
deleteFavorite,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumDetail,
getAlbumList,
getCoverArtUrl,
getGenres,
getMusicFolders,
getGenreList,
getMusicFolderList,
updateRating,
};
+81 -36
View File
@@ -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;
+211 -101
View File
@@ -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<T> {
// error?: string | any;
// items: T;
// response: 'Success' | 'Error';
// statusCode: number;
// }
export interface BasePaginatedResponse<T> {
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<null>;
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<Genre[]> | null | undefined;
export type GenreListArgs = { query: GenreListQuery } & BaseEndpointArgs;
export type GenreListQuery = null;
// Album List ---------------------------------------------------------------------------
export type RawAlbumListResponse = NDAlbumList | SSAlbumList | undefined;
export type AlbumListResponse = BasePaginatedResponse<Album[]> | 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<Song[]>;
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<Album[] | NDAlbum[] | JFAlbum[]>
| 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<AlbumArtist[]>;
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<AlbumArtist[]>;
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<Playlist[]>;
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<Playlist[]>;
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;
@@ -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,
});
@@ -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],
@@ -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}
+3 -3
View File
@@ -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<AppSlice>()(
...state.albums.list,
filter: {
...state.albums.list.filter,
musicFolderId: null,
musicFolderId: undefined,
},
gridScrollOffset: 0,
listScrollOffset: 0,
@@ -118,7 +118,7 @@ export const useAppStore = create<AppSlice>()(
},
display: CardDisplayType.CARD,
filter: {
musicFolderId: null,
musicFolderId: undefined,
sortBy: JFAlbumListSort.NAME,
sortOrder: SortOrder.DESC,
},