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", "@vitejs/plugin-react": "^2.2.0",
"ag-grid-community": "^28.2.1", "ag-grid-community": "^28.2.1",
"ag-grid-react": "^28.2.1", "ag-grid-react": "^28.2.1",
"axios": "^0.27.2",
"electron-localshortcut": "^3.2.1", "electron-localshortcut": "^3.2.1",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"electron-updater": "5.3.0", "electron-updater": "5.3.0",
@@ -3731,7 +3730,8 @@
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
}, },
"node_modules/at-least-node": { "node_modules/at-least-node": {
"version": "1.0.0", "version": "1.0.0",
@@ -3824,15 +3824,6 @@
"node": ">=4" "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": { "node_modules/axobject-query": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -4868,6 +4859,7 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
}, },
@@ -5355,6 +5347,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true,
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
} }
@@ -8031,25 +8024,6 @@
"integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==",
"dev": true "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": { "node_modules/for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -8063,6 +8037,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
@@ -11097,6 +11072,7 @@
"version": "1.51.0", "version": "1.51.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", "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,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@@ -11105,6 +11081,7 @@
"version": "2.1.34", "version": "2.1.34",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
"dev": true,
"dependencies": { "dependencies": {
"mime-db": "1.51.0" "mime-db": "1.51.0"
}, },
@@ -20133,7 +20110,8 @@
"asynckit": { "asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
}, },
"at-least-node": { "at-least-node": {
"version": "1.0.0", "version": "1.0.0",
@@ -20196,15 +20174,6 @@
"integrity": "sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA==", "integrity": "sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA==",
"dev": true "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": { "axobject-query": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -20985,6 +20954,7 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": { "requires": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
} }
@@ -21340,7 +21310,8 @@
"delayed-stream": { "delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "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": { "detect-newline": {
"version": "3.1.0", "version": "3.1.0",
@@ -23256,11 +23227,6 @@
"integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==",
"dev": true "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": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -23271,6 +23237,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"requires": { "requires": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
@@ -25555,12 +25522,14 @@
"mime-db": { "mime-db": {
"version": "1.51.0", "version": "1.51.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", "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": { "mime-types": {
"version": "2.1.34", "version": "2.1.34",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
"integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
"dev": true,
"requires": { "requires": {
"mime-db": "1.51.0" "mime-db": "1.51.0"
} }
-1
View File
@@ -92,7 +92,6 @@
"@vitejs/plugin-react": "^2.2.0", "@vitejs/plugin-react": "^2.2.0",
"ag-grid-community": "^28.2.1", "ag-grid-community": "^28.2.1",
"ag-grid-react": "^28.2.1", "ag-grid-react": "^28.2.1",
"axios": "^0.27.2",
"electron-localshortcut": "^3.2.1", "electron-localshortcut": "^3.2.1",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",
"electron-updater": "5.3.0", "electron-updater": "5.3.0",
+158 -48
View File
@@ -1,63 +1,173 @@
import { useAuthStore } from '../store/auth.store'; import { useAuthStore } from '/@/store';
import { navidromeApi } from './navidrome.api'; import { navidromeApi } from '/@/api/navidrome.api';
import { toast } from '/@/components';
import type { import type {
AlbumDetailQuery, AlbumDetailArgs,
AlbumDetailResponse, RawAlbumDetailResponse,
AlbumListParams, RawAlbumListResponse,
AlbumListResponse, AlbumListArgs,
} from './types'; 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 = () => { export type ControllerEndpoint = Partial<{
const server = useAuthStore.getState().currentServer; 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) { type ApiController = {
return null; jellyfin: ControllerEndpoint;
} navidrome: ControllerEndpoint;
subsonic: ControllerEndpoint;
return server.type;
}; };
const getAlbumDetail = async ( const endpoints: ApiController = {
query: AlbumDetailQuery, jellyfin: {
signal?: AbortSignal, clearPlaylist: undefined,
): Promise<AlbumDetailResponse> => { createFavorite: undefined,
const serverType = getServerType(); createPlaylist: undefined,
if (!serverType) return null; deleteFavorite: undefined,
deletePlaylist: undefined,
const functions = { getAlbumArtistDetail: undefined,
jellyfin: null, getAlbumArtistList: undefined,
navidrome: navidromeApi.getAlbumDetail, getAlbumDetail: undefined,
subsonic: null, getAlbumList: undefined,
}; getArtistDetail: undefined,
getArtistList: undefined,
if (functions[serverType] === null) { getFavoritesList: undefined,
return null; getFolderItemList: undefined,
} getFolderList: undefined,
getFolderSongs: undefined,
return functions[serverType]?.(query, signal); 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 ( const apiController = (endpoint: keyof ControllerEndpoint) => {
params: AlbumListParams, const serverType = useAuthStore.getState().currentServer?.type;
signal?: AbortSignal,
): Promise<AlbumListResponse> => {
const serverType = getServerType();
if (!serverType) return null;
const functions = { if (!serverType) {
jellyfin: null, toast.error({ message: 'No server selected', title: 'Unable to route request' });
navidrome: navidromeApi.getAlbumList, return () => undefined;
subsonic: null,
};
if (functions[serverType] === null) {
return null;
} }
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, getAlbumDetail,
getAlbumList, 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 { nanoid } from 'nanoid/non-secure';
import type { ServerListItem } from '../store'; import ky from 'ky';
import { useAuthStore } from '../store';
import { ServerType } from '../types';
import type { import type {
NDAlbumListResponse,
NDGenreListResponse, NDGenreListResponse,
NDAlbumListParams,
NDGenreListParams,
NDSongListParams,
NDArtistListResponse, NDArtistListResponse,
NDAuthenticate, NDAlbumDetail,
NDAlbum,
NDAlbumListSort,
NDAlbumDetailResponse,
NDSong,
NDSongListResponse, NDSongListResponse,
} from './navidrome.types'; NDAlbumListParams,
import { NDSortOrder } from './navidrome.types'; 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 { import type {
Album, Album,
AlbumDetailQuery,
AlbumDetailResponse,
AlbumListParams,
AlbumListResponse,
Song, Song,
} from './types'; AuthenticationResponse,
import { SortOrder } from './types'; 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({ const api = ky.create({
hooks: { hooks: {
afterResponse: [ afterResponse: [
(request, _options, response) => { async (_request, _options, response) => {
// const serverId = request.headers.get('--local-id'); const serverId = useAuthStore.getState().currentServer?.id;
if (serverId) {
useAuthStore.getState().actions.updateServer(serverId, {
ndCredential: response.headers.get('x-nd-authorization') as string,
});
}
return response; return response;
}, },
], ],
beforeRequest: [ beforeError: [
(request, options) => { (error) => {
const { headers } = options; if (error.response && error.response.status === 401) {
toast.error({
message: 'Your session has expired.',
});
console.log('headers', headers); const serverId = useAuthStore.getState().currentServer?.id;
const { currentServer } = useAuthStore.getState();
const { ndCredential } = currentServer || {};
if (ndCredential) { if (serverId) {
request.headers.set('x-nd-authorization', `Bearer ${ndCredential}`); useAuthStore.getState().actions.setCurrentServer(null);
request.headers.set('--local-id', currentServer?.id || ''); useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
}
} }
return error;
}, },
], ],
}, },
}); });
const authenticate = async (options: { password: string; url: string; username: string }) => { // api.interceptors.request.use(
const { password, url, username } = options; // (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 cleanServerUrl = url.replace(/\/$/, '');
const data = await ky const data = await ky
.post(`${cleanServerUrl}/auth/login`, { .post(`${cleanServerUrl}/auth/login`, {
json: { json: {
password, password: body.password,
username, username: body.username,
}, },
}) })
.json<NDAuthenticate>(); .json<NDAuthenticationResponse>();
return { return {
credential: `u=${options.username}&s=${data.subsonicSalt}&t=${data.subsonicToken}`, credential: `u=${body.username}&s=${data.subsonicSalt}&t=${data.subsonicToken}`,
ndCredential: data.token, ndCredential: data.token,
userId: data.id,
username: data.username, username: data.username,
}; };
}; };
const getGenreList = async (params?: NDGenreListParams) => { const getGenreList = async (args: GenreListArgs): Promise<NDGenreList> => {
const { server, signal } = args;
const data = await api const data = await api
.get('api/genre', { .get('api/genre', {
prefixUrl: useAuthStore.getState().currentServer?.url, headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
searchParams: params, prefixUrl: server?.url,
signal,
}) })
.json<NDGenreListResponse>(); .json<NDGenreListResponse>();
return data; 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 const data = await api
.get('api/artist', { .get('api/artist', {
prefixUrl: useAuthStore.getState().currentServer?.url, headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
searchParams: params, searchParams,
signal,
}) })
.json<NDArtistListResponse>(); .json<NDArtistListResponse>();
return data; return data;
}; };
const getAlbumDetail = async (query: AlbumDetailQuery, signal?: AbortSignal) => { const getAlbumDetail = async (args: AlbumDetailArgs): Promise<NDAlbumDetail> => {
const albumDetail = await api const { query, server, signal } = args;
const data = await api
.get(`api/album/${query.id}`, { .get(`api/album/${query.id}`, {
prefixUrl: useAuthStore.getState().currentServer?.url, headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
prefixUrl: server?.url,
signal, signal,
}) })
.json<NDAlbumDetailResponse>(); .json<NDAlbumDetailResponse>();
const albumSongs = await api const songsData = await api
.get('api/song/', { .get('api/song', {
prefixUrl: useAuthStore.getState().currentServer?.url, headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
prefixUrl: server?.url,
searchParams: { searchParams: {
_end: 0, _end: 0,
_order: NDSortOrder.ASC, _order: NDSortOrder.ASC,
@@ -113,57 +255,153 @@ const getAlbumDetail = async (query: AlbumDetailQuery, signal?: AbortSignal) =>
_start: 0, _start: 0,
album_id: query.id, album_id: query.id,
}, },
signal,
}) })
.json<NDSongListResponse>(); .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 = { const searchParams: NDAlbumListParams = {
_end: params._skip + (params._take || 0), _end: query.startIndex + (query.limit || 0),
_order: params.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC, _order: query.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC,
_sort: params.sortBy as NDAlbumListSort, _sort: query.sortBy as NDAlbumListSort,
_start: params._skip, _start: query.startIndex,
...params.nd, ...query.ndParams,
}; };
const res = await api.get('api/album', { const res = await api.get('api/album', {
prefixUrl: useAuthStore.getState().currentServer?.url, headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
prefixUrl: server?.url,
searchParams, searchParams,
signal, signal,
}); });
const itemCount = res.headers.get('X-Total-Count');
const data = await res.json<NDAlbumListResponse>(); const data = await res.json<NDAlbumListResponse>();
const itemCount = res.headers.get('x-total-count');
return { return {
items: data, items: data,
pagination: { startIndex: query?.startIndex || 0,
startIndex: params?._skip || 0, totalRecordCount: Number(itemCount),
totalEntries: Number(itemCount), };
},
} as AlbumListResponse;
}; };
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 const data = await api
.get('api/song', { .get(`api/song/${query.id}`, {
prefixUrl: useAuthStore.getState().currentServer?.url, headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
searchParams: params, signal,
}) })
.json<NDSongListResponse>(); .json<NDSongDetailResponse>();
return data; return data;
}; };
export const navidromeApi = { const createPlaylist = async (args: CreatePlaylistArgs): Promise<NDCreatePlaylist> => {
authenticate, const { query, server, signal } = args;
getAlbumDetail,
getAlbumList, const json: NDCreatePlaylistParams = {
getArtistList, comment: query.comment,
getGenreList, name: query.name,
getSongList, 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: { const getCoverArtUrl = (args: {
@@ -264,6 +502,21 @@ const normalizeSong = (
} as Song; } as Song;
}; };
export const navidromeApi = {
authenticate,
createPlaylist,
deletePlaylist,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumDetail,
getAlbumList,
getGenreList,
getPlaylistDetail,
getPlaylistList,
getSongDetail,
getSongList,
};
export const ndNormalize = { export const ndNormalize = {
album: normalizeAlbum, album: normalizeAlbum,
song: normalizeSong, song: normalizeSong,
+129 -2
View File
@@ -92,7 +92,7 @@ export type NDSong = {
year: number; year: number;
}; };
export type NDArtist = { export type NDAlbumArtist = {
albumCount: number; albumCount: number;
biography: string; biography: string;
externalInfoUpdatedAt: string; externalInfoUpdatedAt: string;
@@ -115,15 +115,43 @@ export type NDArtist = {
starredAt: string; 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 NDGenreListResponse = NDGenre[];
export type NDAlbumDetailResponse = NDAlbum; export type NDAlbumDetailResponse = NDAlbum;
export type NDAlbumDetail = NDAlbum & { songs: NDSongListResponse };
export type NDAlbumListResponse = NDAlbum[]; 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 NDSongListResponse = NDSong[];
export type NDArtistListResponse = NDArtist[]; export type NDSongList = {
items: NDSong[];
startIndex: number;
totalRecordCount: number;
};
export type NDArtistListResponse = NDAlbumArtist[];
export type NDPagination = { export type NDPagination = {
_end?: number; _end?: number;
@@ -177,8 +205,107 @@ export type NDAlbumListParams = {
} & NDPagination & } & NDPagination &
NDOrder; 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 = { export type NDSongListParams = {
_sort?: NDSongListSort;
genre_id?: string; genre_id?: string;
starred?: boolean; starred?: boolean;
} & NDPagination & } & NDPagination &
NDOrder; 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'; import type { AlbumDetailQuery } from './types';
export const queryKeys = { export const queryKeys = {
albums: { albums: {
detail: (serverId: string, query: AlbumDetailQuery) => ['albums', serverId, query] as const, 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, [serverId, 'albums', 'list', serverId, params] as const,
root: ['albums'], root: ['albums'],
}, },
+230 -108
View File
@@ -1,17 +1,40 @@
import axios from 'axios'; import ky from 'ky';
import md5 from 'md5'; import md5 from 'md5';
import { randomString } from '../utils/random-string'; import { randomString } from '/@/utils';
import type { import type {
SSAlbumListEntry,
SSAlbumListResponse, SSAlbumListResponse,
SSAlbumResponse, SSAlbumDetailResponse,
SSAlbumsParams,
SSArtistIndex, SSArtistIndex,
SSArtistInfoResponse, SSAlbumArtistList,
SSArtistsResponse, SSAlbumArtistListResponse,
SSGenresResponse, SSGenreListResponse,
SSMusicFoldersResponse, SSMusicFolderList,
} from './subsonic.types'; 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: { const getCoverArtUrl = (args: {
baseUrl: string; baseUrl: string;
@@ -35,136 +58,235 @@ const getCoverArtUrl = (args: {
); );
}; };
const api = axios.create({ const api = ky.create({
validateStatus: (status) => status >= 200, 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( const authenticate = async (
(res: any) => { url: string,
res.data = res.data['subsonic-response']; body: {
return res; legacy?: boolean;
password: string;
username: string;
}, },
(err: any) => { ): Promise<AuthenticationResponse> => {
return Promise.reject(err); let credential;
}, const cleanServerUrl = url.replace(/\/$/, '');
);
const authenticate = async (options: { if (body.legacy) {
legacy?: boolean; credential = `u=${body.username}&p=${body.password}`;
password: string;
url: string;
username: string;
}) => {
let token;
const cleanServerUrl = options.url.replace(/\/$/, '');
if (options.legacy) {
token = `u=${options.username}&p=${options.password}`;
} else { } else {
const salt = randomString(12); const salt = randomString(12);
const hash = md5(options.password + salt); const hash = md5(body.password + salt);
token = `u=${options.username}&s=${salt}&t=${hash}`; credential = `u=${body.username}&s=${salt}&t=${hash}`;
} }
const { data } = await api.get( await ky.get(`${cleanServerUrl}/rest/ping.view?v=1.13.0&c=Feishin&f=json&${credential}`);
`${cleanServerUrl}/rest/ping.view?v=1.13.0&c=Feishin&f=json&${token}`,
);
return { token, ...data }; return {
credential,
userId: null,
username: body.username,
};
}; };
const getMusicFolders = async (server: Partial<Server>) => { const getMusicFolderList = async (
const { data } = await api.get<SSMusicFoldersResponse>( server: any,
`${server.url}/rest/getMusicFolders.view?v=1.13.0&c=Feishin&f=json&${server.token}`, signal?: AbortSignal,
); ): Promise<SSMusicFolderList> => {
const data = await api
.get('rest/getMusicFolders.view', {
prefixUrl: server.url,
signal,
})
.json<SSMusicFolderListResponse>();
return data.musicFolders.musicFolder; return data.musicFolders.musicFolder;
}; };
const getArtists = async (server: Server, musicFolderId: string) => { export const getAlbumArtistDetail = async (
const { data } = await api.get<SSArtistsResponse>( args: AlbumArtistDetailArgs,
`${server.url}/rest/getArtists.view?v=1.13.0&c=Feishin&f=json&${server.token}`, ): Promise<SSAlbumArtistDetail> => {
{ params: { musicFolderId } }, 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); const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist);
return artists; return artists;
}; };
const getGenres = async (server: Server) => { const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
const { data: genres } = await api.get<SSGenresResponse>( const { signal } = args;
`${server.url}/rest/getGenres.view?v=1.13.0&c=Feishin&f=json&${server.token}`,
);
return genres; const data = await api
}; .get('/rest/getGenres.view', {
signal,
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,
);
}) })
.catch((err) => console.log(err)); .json<SSGenreListResponse>();
return albums as SSAlbumListEntry[]; return data.genres.genre;
}; };
const getArtistInfo = async (server: Server, id: string) => { const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> => {
const { data: artistInfo } = await api.get<SSArtistInfoResponse>( const { query, signal } = args;
`${server.url}/rest/getArtistInfo2.view?v=1.13.0&c=Feishin&f=json&${server.token}`,
{ params: { id } }, 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 { return {
...artistInfo, items: data.albumList2.album,
artistInfo2: { startIndex: query.startIndex,
...artistInfo.artistInfo2, totalRecordCount: null,
biography: artistInfo.artistInfo2.biography
.replaceAll(/<a target.*<\/a>/gm, '')
.replace('Biography not available', ''),
},
}; };
}; };
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 = { export const subsonicApi = {
authenticate, authenticate,
getAlbum, createFavorite,
getAlbums, deleteFavorite,
getArtistInfo, getAlbumArtistDetail,
getArtists, getAlbumArtistList,
getAlbumDetail,
getAlbumList,
getCoverArtUrl, getCoverArtUrl,
getGenres, getGenreList,
getMusicFolders, getMusicFolderList,
updateRating,
}; };
+81 -36
View File
@@ -1,78 +1,104 @@
export interface SSBaseResponse { export type SSBaseResponse = {
serverVersion?: 'string'; serverVersion?: 'string';
status: 'string'; status: 'string';
type?: 'string'; type?: 'string';
version: 'string'; version: 'string';
} };
export interface SSMusicFoldersResponse extends SSBaseResponse { export type SSMusicFolderList = SSMusicFolder[];
export type SSMusicFolderListResponse = {
musicFolders: { musicFolders: {
musicFolder: SSMusicFolder[]; musicFolder: SSMusicFolder[];
}; };
} };
export interface SSGenresResponse extends SSBaseResponse { export type SSGenreList = SSGenre[];
export type SSGenreListResponse = {
genres: { genres: {
genre: SSGenre[]; 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: { artists: {
ignoredArticles: string; ignoredArticles: string;
index: SSArtistIndex[]; index: SSArtistIndex[];
lastModified: number; lastModified: number;
}; };
} };
export interface SSAlbumListResponse extends SSBaseResponse { export type SSAlbumList = {
items: SSAlbumListEntry[];
startIndex: number;
totalRecordCount: number | null;
};
export type SSAlbumListResponse = {
albumList2: { albumList2: {
album: SSAlbumListEntry[]; album: SSAlbumListEntry[];
}; };
} };
export interface SSAlbumResponse extends SSBaseResponse { export type SSAlbumDetail = SSAlbum;
export type SSAlbumDetailResponse = {
album: SSAlbum; album: SSAlbum;
} };
export interface SSArtistInfoResponse extends SSBaseResponse { export type SSArtistInfoResponse = {
artistInfo2: SSArtistInfo; artistInfo2: SSArtistInfo;
} };
export interface SSArtistInfo { export type SSArtistInfo = {
biography: string; biography: string;
largeImageUrl?: string; largeImageUrl?: string;
lastFmUrl?: string; lastFmUrl?: string;
mediumImageUrl?: string; mediumImageUrl?: string;
musicBrainzId?: string; musicBrainzId?: string;
smallImageUrl?: string; smallImageUrl?: string;
} };
export interface SSMusicFolder { export type SSMusicFolder = {
id: number; id: number;
name: string; name: string;
} };
export interface SSGenre { export type SSGenre = {
albumCount?: number; albumCount?: number;
songCount?: number; songCount?: number;
value: string; value: string;
} };
export interface SSArtistIndex { export type SSArtistIndex = {
artist: SSArtistListEntry[]; artist: SSAlbumArtistListEntry[];
name: string; name: string;
} };
export interface SSArtistListEntry { export type SSAlbumArtistListEntry = {
albumCount: string; albumCount: string;
artistImageUrl?: string; artistImageUrl?: string;
coverArt?: string; coverArt?: string;
id: string; id: string;
name: string; name: string;
} };
export interface SSAlbumListEntry { export type SSAlbumListEntry = {
album: string; album: string;
artist: string; artist: string;
artistId: string; artistId: string;
@@ -90,13 +116,13 @@ export interface SSAlbumListEntry {
title: string; title: string;
userRating?: number; userRating?: number;
year: number; year: number;
} };
export interface SSAlbum extends SSAlbumListEntry { export type SSAlbum = {
song: SSSong[]; song: SSSong[];
} } & SSAlbumListEntry;
export interface SSSong { export type SSSong = {
album: string; album: string;
albumId: string; albumId: string;
artist: string; artist: string;
@@ -122,9 +148,9 @@ export interface SSSong {
type: string; type: string;
userRating?: number; userRating?: number;
year: number; year: number;
} };
export interface SSAlbumsParams { export type SSAlbumListParams = {
fromYear?: number; fromYear?: number;
genre?: string; genre?: string;
musicFolderId?: string; musicFolderId?: string;
@@ -132,8 +158,27 @@ export interface SSAlbumsParams {
size?: number; size?: number;
toYear?: number; toYear?: number;
type: string; type: string;
} };
export interface SSArtistsParams { export type SSAlbumArtistListParams = {
musicFolderId?: number; 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 { ServerListItem } from '/@/store';
import type { ServerType } from '../types'; import type { ServerType } from '/@//types';
import type { JFAlbum, JFAlbumListSort, JFSortOrder } from './jellyfin.types'; import type { JFAlbumListSort, JFSortOrder } from '/@/api/jellyfin.types';
import type { NDAlbum, NDAlbumListSort, NDOrder } from './navidrome.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 { export enum SortOrder {
ASC = 'ASC', ASC = 'ASC',
@@ -27,33 +50,15 @@ export enum ImageType {
SCREENSHOT = 'SCREENSHOT', SCREENSHOT = 'SCREENSHOT',
} }
export enum TaskType {
FULL_SCAN = 'FULL_SCAN',
LASTFM = 'LASTFM',
MUSICBRAINZ = 'MUSICBRAINZ',
QUICK_SCAN = 'QUICK_SCAN',
REFRESH = 'REFRESH',
SPOTIFY = 'SPOTIFY',
}
export type EndpointDetails = { export type EndpointDetails = {
server: ServerListItem; server: ServerListItem;
}; };
// export interface BaseResponse<T> {
// error?: string | any;
// items: T;
// response: 'Success' | 'Error';
// statusCode: number;
// }
export interface BasePaginatedResponse<T> { export interface BasePaginatedResponse<T> {
error?: string | any; error?: string | any;
items: T; items: T;
pagination?: { startIndex: number;
startIndex: number; totalRecordCount: number;
totalEntries: number;
};
} }
export type ApiError = { export type ApiError = {
@@ -66,47 +71,14 @@ export type ApiError = {
statusCode: number; statusCode: number;
}; };
export type AuthResponse = { export type AuthenticationResponse = {
credential: string; credential: string;
ndCredential?: string; ndCredential?: string;
userId: string | null;
username: string; 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 = { export type Genre = {
albumArtistCount: number;
albumCount: number;
artistCount: number;
id: string;
name: string;
songCount: number;
totalCount: number;
};
export type RelatedGenre = {
id: string; id: string;
name: string; name: string;
}; };
@@ -116,7 +88,7 @@ export type Album = {
artists: RelatedArtist[]; artists: RelatedArtist[];
backdropImageUrl: string | null; backdropImageUrl: string | null;
createdAt: string; createdAt: string;
genres: RelatedGenre[]; genres: Genre[];
id: string; id: string;
imagePlaceholderUrl: string | null; imagePlaceholderUrl: string | null;
imageUrl: string | null; imageUrl: string | null;
@@ -147,7 +119,7 @@ export type Song = {
createdAt: string; createdAt: string;
discNumber: number; discNumber: number;
duration: number; duration: number;
genres: RelatedGenre[]; genres: Genre[];
id: string; id: string;
imageUrl: string; imageUrl: string;
isFavorite: boolean; isFavorite: boolean;
@@ -193,69 +165,207 @@ export type RelatedArtist = {
name: string; name: string;
}; };
export type RelatedServer = { export type MusicFolder = {
id: string; id: string;
name: string; name: string;
type: ServerType;
url: string;
}; };
export type RelatedUser = { export type Playlist = {
enabled: boolean; duration?: number;
id: string; id: string;
isAdmin: boolean; name: string;
public?: boolean;
size?: number;
songCount?: number;
userId: string;
username: string; username: string;
}; };
export type Task = { export type GenresResponse = Genre[];
createdAt: string;
id: string;
isCompleted: boolean;
isError: boolean;
message: string;
server: RelatedServer | null;
type: TaskType;
updatedAt: string;
user: RelatedUser | null;
};
export type AlbumListSort = NDAlbumListSort | JFAlbumListSort; export type MusicFoldersResponse = MusicFolder[];
export type ListSortOrder = NDOrder | JFSortOrder; export type ListSortOrder = NDOrder | JFSortOrder;
export type AlbumListParams = { type BaseEndpointArgs = {
_skip: number; server: ServerListItem | null;
_take?: number; signal?: AbortSignal;
musicFolderId: string | null; };
nd?: {
// 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; artist_id?: string;
compilation?: boolean; compilation?: boolean;
genre_id?: string; genre_id?: string;
has_rating?: boolean; has_rating?: boolean;
starred?: boolean; starred?: boolean;
title?: string;
year?: number; year?: number;
}; };
sortBy: NDAlbumListSort | JFAlbumListSort; sortBy: SongListSort;
sortOrder: SortOrder; sortOrder: SortOrder;
startIndex: number;
}; };
export type AlbumListResponse = export type SongListArgs = { query: SongListQuery } & BaseEndpointArgs;
| BasePaginatedResponse<Album[] | NDAlbum[] | JFAlbum[]>
| null
| undefined;
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; id: string;
}; };
export type AlbumDetailResponse = Album | NDAlbum | JFAlbum | null | undefined; export type PlaylistDetailArgs = { query: PlaylistDetailQuery } & BaseEndpointArgs;
export type Count = {
artists?: number;
externals?: number;
favorites?: number;
genres?: number;
images?: number;
ratings?: number;
songs?: number;
};
@@ -2,14 +2,14 @@ import { useQuery } from '@tanstack/react-query';
import { queryKeys } from '/@/api/query-keys'; import { queryKeys } from '/@/api/query-keys';
import type { QueryOptions } from '/@/lib/react-query'; import type { QueryOptions } from '/@/lib/react-query';
import { useCurrentServer } from '../../../store/auth.store'; import { useCurrentServer } from '../../../store/auth.store';
import { apiController } from '/@/api/controller';
import type { AlbumDetailQuery } from '/@/api/types'; import type { AlbumDetailQuery } from '/@/api/types';
import { controller } from '/@/api/controller';
export const useAlbumDetail = (query: AlbumDetailQuery, options: QueryOptions) => { export const useAlbumDetail = (query: AlbumDetailQuery, options: QueryOptions) => {
const server = useCurrentServer(); const server = useCurrentServer();
return useQuery({ return useQuery({
queryFn: ({ signal }) => apiController.getAlbumDetail(query, signal), queryFn: ({ signal }) => controller.getAlbumDetail({ query, server, signal }),
queryKey: queryKeys.albums.detail(server?.id || '', query), queryKey: queryKeys.albums.detail(server?.id || '', query),
...options, ...options,
}); });
@@ -1,23 +1,22 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { apiController } from '/@/api/controller'; import { controller } from '/@/api/controller';
import { queryKeys } from '/@/api/query-keys'; 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 type { QueryOptions } from '/@/lib/react-query';
import { useCurrentServer, useCurrentServerId } from '/@/store'; import { useCurrentServer } from '/@/store';
import { ndNormalize } from '/@/api/navidrome.api'; import { ndNormalize } from '/@/api/navidrome.api';
import type { NDAlbum } from '/@/api/navidrome.types'; import type { NDAlbum } from '/@/api/navidrome.types';
export const useAlbumList = (params: AlbumListParams, options?: QueryOptions) => { export const useAlbumList = (query: AlbumListQuery, options?: QueryOptions) => {
const serverId = useCurrentServerId();
const server = useCurrentServer(); const server = useCurrentServer();
return useQuery({ return useQuery({
enabled: !!serverId, enabled: !!server?.id,
queryFn: ({ signal }) => apiController.getAlbumList(params, signal), queryFn: ({ signal }) => controller.getAlbumList({ query, server, signal }),
queryKey: queryKeys.albums.list(serverId, params), queryKey: queryKeys.albums.list(server?.id || '', query),
select: useCallback( select: useCallback(
(data: AlbumListResponse) => { (data: RawAlbumListResponse | undefined) => {
let albums; let albums;
switch (server?.type) { switch (server?.type) {
case 'jellyfin': case 'jellyfin':
@@ -30,8 +29,9 @@ export const useAlbumList = (params: AlbumListParams, options?: QueryOptions) =>
} }
return { return {
...data,
items: albums, items: albums,
startIndex: data?.startIndex,
totalRecordCount: data?.totalRecordCount,
}; };
}, },
[server], [server],
@@ -29,7 +29,7 @@ import { useAlbumList } from '../queries/album-list-query';
import { JFAlbumListSort } from '/@/api/jellyfin.types'; import { JFAlbumListSort } from '/@/api/jellyfin.types';
import type { NDAlbum } from '/@/api/navidrome.types'; import type { NDAlbum } from '/@/api/navidrome.types';
import { NDAlbumListSort } 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'; import { ndNormalize } from '/@/api/navidrome.api';
const FILTERS = { const FILTERS = {
@@ -70,28 +70,30 @@ export const AlbumListRoute = () => {
const filters = page.list.filter; const filters = page.list.filter;
const albumListQuery = useAlbumList({ const albumListQuery = useAlbumList({
_skip: 0, limit: 1,
_take: 1,
musicFolderId: null,
sortBy: filters.sortBy, sortBy: filters.sortBy,
sortOrder: filters.sortOrder, sortOrder: filters.sortOrder,
startIndex: 0,
}); });
const fetch = useCallback( const fetch = useCallback(
async ({ skip, take }: { skip: number; take: number }) => { async ({ skip, take }: { skip: number; take: number }) => {
const queryKey = queryKeys.albums.list(server?.id || '', { const queryKey = queryKeys.albums.list(server?.id || '', {
_skip: skip, limit: take,
_take: take, startIndex: skip,
...filters, ...filters,
}); });
const albums = await queryClient.fetchQuery(queryKey, async () => const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
apiController.getAlbumList({ controller.getAlbumList({
_skip: skip, query: {
_take: take, limit: take,
musicFolderId: null, sortBy: filters.sortBy,
sortBy: filters.sortBy, sortOrder: filters.sortOrder,
sortOrder: filters.sortOrder, startIndex: skip,
},
server,
signal,
}), }),
); );
@@ -110,10 +112,8 @@ export const AlbumListRoute = () => {
return { return {
items, items,
pagination: { startIndex: skip,
startIndex: skip, totalRecordCount: albums?.totalRecordCount || 0,
totalEntries: albums?.pagination?.totalEntries || 0,
},
} as AlbumListResponse; } as AlbumListResponse;
}, },
[filters, queryClient, server], [filters, queryClient, server],
@@ -445,7 +445,7 @@ export const AlbumListRoute = () => {
fetchFn={fetch} fetchFn={fetch}
height={height} height={height}
initialScrollOffset={page.list?.gridScrollOffset || 0} initialScrollOffset={page.list?.gridScrollOffset || 0}
itemCount={albumListQuery?.data?.pagination?.totalEntries || 0} itemCount={albumListQuery?.data?.totalRecordCount || 0}
itemGap={20} itemGap={20}
itemSize={150 + page.list?.size} itemSize={150 + page.list?.size}
itemType={LibraryItem.ALBUM} itemType={LibraryItem.ALBUM}
+3 -3
View File
@@ -22,7 +22,7 @@ type LibraryPageProps = {
}; };
type ListFilter = { type ListFilter = {
musicFolderId: string | null; musicFolderId?: string;
sortBy: AlbumListSort; sortBy: AlbumListSort;
sortOrder: SortOrder; sortOrder: SortOrder;
}; };
@@ -89,7 +89,7 @@ export const useAppStore = create<AppSlice>()(
...state.albums.list, ...state.albums.list,
filter: { filter: {
...state.albums.list.filter, ...state.albums.list.filter,
musicFolderId: null, musicFolderId: undefined,
}, },
gridScrollOffset: 0, gridScrollOffset: 0,
listScrollOffset: 0, listScrollOffset: 0,
@@ -118,7 +118,7 @@ export const useAppStore = create<AppSlice>()(
}, },
display: CardDisplayType.CARD, display: CardDisplayType.CARD,
filter: { filter: {
musicFolderId: null, musicFolderId: undefined,
sortBy: JFAlbumListSort.NAME, sortBy: JFAlbumListSort.NAME,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
}, },