mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Add initial API controller
- Migrate from axios to ky
This commit is contained in:
Generated
+16
-47
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user