Remove old API implementation
This commit is contained in:
parent
9b5bce34a0
commit
975c31635a
7 changed files with 0 additions and 2619 deletions
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -41,7 +41,6 @@
|
||||||
"i18next": "^21.6.16",
|
"i18next": "^21.6.16",
|
||||||
"immer": "^9.0.15",
|
"immer": "^9.0.15",
|
||||||
"is-electron": "^2.2.1",
|
"is-electron": "^2.2.1",
|
||||||
"ky": "^0.33.0",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
|
@ -13552,17 +13551,6 @@
|
||||||
"integrity": "sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==",
|
"integrity": "sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ky": {
|
|
||||||
"version": "0.33.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ky/-/ky-0.33.0.tgz",
|
|
||||||
"integrity": "sha512-peKzuOlN/q3Q3jOgi4t0cp6DOgif5rVnmiSIsjsmkiOcdnSjkrKSUqQmRWYCTqjUtR9b3xQQr8aj7KwSW1r49A==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.16"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sindresorhus/ky?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/language-subtag-registry": {
|
"node_modules/language-subtag-registry": {
|
||||||
"version": "0.3.21",
|
"version": "0.3.21",
|
||||||
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
|
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
|
||||||
|
@ -33640,11 +33628,6 @@
|
||||||
"integrity": "sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==",
|
"integrity": "sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ky": {
|
|
||||||
"version": "0.33.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ky/-/ky-0.33.0.tgz",
|
|
||||||
"integrity": "sha512-peKzuOlN/q3Q3jOgi4t0cp6DOgif5rVnmiSIsjsmkiOcdnSjkrKSUqQmRWYCTqjUtR9b3xQQr8aj7KwSW1r49A=="
|
|
||||||
},
|
|
||||||
"language-subtag-registry": {
|
"language-subtag-registry": {
|
||||||
"version": "0.3.21",
|
"version": "0.3.21",
|
||||||
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
|
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz",
|
||||||
|
|
|
@ -279,7 +279,6 @@
|
||||||
"i18next": "^21.6.16",
|
"i18next": "^21.6.16",
|
||||||
"immer": "^9.0.15",
|
"immer": "^9.0.15",
|
||||||
"is-electron": "^2.2.1",
|
"is-electron": "^2.2.1",
|
||||||
"ky": "^0.33.0",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { controller } from '/@/renderer/api/controller';
|
import { controller } from '/@/renderer/api/controller';
|
||||||
import { normalize } from '/@/renderer/api/normalize';
|
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
controller,
|
controller,
|
||||||
normalize,
|
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,756 +0,0 @@
|
||||||
import { nanoid } from 'nanoid/non-secure';
|
|
||||||
import ky from 'ky';
|
|
||||||
import type {
|
|
||||||
NDGenreListResponse,
|
|
||||||
NDArtistListResponse,
|
|
||||||
NDAlbumDetail,
|
|
||||||
NDAlbumListParams,
|
|
||||||
NDAlbumList,
|
|
||||||
NDSongDetailResponse,
|
|
||||||
NDAlbum,
|
|
||||||
NDSong,
|
|
||||||
NDAuthenticationResponse,
|
|
||||||
NDAlbumDetailResponse,
|
|
||||||
NDSongDetail,
|
|
||||||
NDGenreList,
|
|
||||||
NDAlbumArtistListParams,
|
|
||||||
NDAlbumArtistDetail,
|
|
||||||
NDAlbumListResponse,
|
|
||||||
NDAlbumArtistDetailResponse,
|
|
||||||
NDAlbumArtistList,
|
|
||||||
NDSongListParams,
|
|
||||||
NDCreatePlaylistParams,
|
|
||||||
NDCreatePlaylistResponse,
|
|
||||||
NDDeletePlaylist,
|
|
||||||
NDDeletePlaylistResponse,
|
|
||||||
NDPlaylistListParams,
|
|
||||||
NDPlaylistDetail,
|
|
||||||
NDPlaylistList,
|
|
||||||
NDPlaylistListResponse,
|
|
||||||
NDPlaylistDetailResponse,
|
|
||||||
NDSongList,
|
|
||||||
NDSongListResponse,
|
|
||||||
NDAlbumArtist,
|
|
||||||
NDPlaylist,
|
|
||||||
NDUpdatePlaylistParams,
|
|
||||||
NDUpdatePlaylistResponse,
|
|
||||||
NDPlaylistSongListResponse,
|
|
||||||
NDPlaylistSongList,
|
|
||||||
NDPlaylistSong,
|
|
||||||
NDUserList,
|
|
||||||
NDUserListResponse,
|
|
||||||
NDUserListParams,
|
|
||||||
NDUser,
|
|
||||||
NDAddToPlaylist,
|
|
||||||
NDAddToPlaylistBody,
|
|
||||||
NDAddToPlaylistResponse,
|
|
||||||
NDRemoveFromPlaylistParams,
|
|
||||||
NDRemoveFromPlaylistResponse,
|
|
||||||
NDRemoveFromPlaylist,
|
|
||||||
} from '/@/renderer/api/navidrome.types';
|
|
||||||
import { NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types';
|
|
||||||
import {
|
|
||||||
Album,
|
|
||||||
Song,
|
|
||||||
AuthenticationResponse,
|
|
||||||
AlbumDetailArgs,
|
|
||||||
GenreListArgs,
|
|
||||||
AlbumListArgs,
|
|
||||||
AlbumArtistListArgs,
|
|
||||||
AlbumArtistDetailArgs,
|
|
||||||
SongListArgs,
|
|
||||||
SongDetailArgs,
|
|
||||||
CreatePlaylistArgs,
|
|
||||||
DeletePlaylistArgs,
|
|
||||||
PlaylistListArgs,
|
|
||||||
PlaylistDetailArgs,
|
|
||||||
CreatePlaylistResponse,
|
|
||||||
PlaylistSongListArgs,
|
|
||||||
AlbumArtist,
|
|
||||||
Playlist,
|
|
||||||
UpdatePlaylistResponse,
|
|
||||||
UpdatePlaylistArgs,
|
|
||||||
UserListArgs,
|
|
||||||
userListSortMap,
|
|
||||||
playlistListSortMap,
|
|
||||||
albumArtistListSortMap,
|
|
||||||
songListSortMap,
|
|
||||||
albumListSortMap,
|
|
||||||
sortOrderMap,
|
|
||||||
User,
|
|
||||||
LibraryItem,
|
|
||||||
AddToPlaylistArgs,
|
|
||||||
RemoveFromPlaylistArgs,
|
|
||||||
} from '/@/renderer/api/types';
|
|
||||||
import { toast } from '/@/renderer/components/toast';
|
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
|
||||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
|
||||||
import { parseSearchParams } from '/@/renderer/utils';
|
|
||||||
import { subsonicApi } from '/@/renderer/api/subsonic.api';
|
|
||||||
|
|
||||||
const IGNORE_CORS = localStorage.getItem('IGNORE_CORS') === 'true';
|
|
||||||
|
|
||||||
const api = ky.create({
|
|
||||||
hooks: {
|
|
||||||
afterResponse: [
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
],
|
|
||||||
beforeError: [
|
|
||||||
(error) => {
|
|
||||||
if (error.response && error.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 error;
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
mode: IGNORE_CORS ? 'cors' : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
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: body.password,
|
|
||||||
username: body.username,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.json<NDAuthenticationResponse>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
credential: `u=${body.username}&s=${data.subsonicSalt}&t=${data.subsonicToken}`,
|
|
||||||
ndCredential: data.token,
|
|
||||||
userId: data.id,
|
|
||||||
username: data.username,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserList = async (args: UserListArgs): Promise<NDUserList> => {
|
|
||||||
const { query, server, signal } = args;
|
|
||||||
|
|
||||||
const searchParams: NDUserListParams = {
|
|
||||||
_end: query.startIndex + (query.limit || 0),
|
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
|
||||||
_sort: userListSortMap.navidrome[query.sortBy],
|
|
||||||
_start: query.startIndex,
|
|
||||||
...query.ndParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await api.get('api/user', {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json<NDUserListResponse>();
|
|
||||||
const itemCount = res.headers.get('x-total-count');
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: data,
|
|
||||||
startIndex: query?.startIndex || 0,
|
|
||||||
totalRecordCount: Number(itemCount),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGenreList = async (args: GenreListArgs): Promise<NDGenreList> => {
|
|
||||||
const { server, signal } = args;
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.get('api/genre', {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<NDGenreListResponse>();
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs): Promise<NDAlbumArtistDetail> => {
|
|
||||||
const { query, server, signal } = args;
|
|
||||||
|
|
||||||
const artistInfo = await subsonicApi.getArtistInfo({
|
|
||||||
query: {
|
|
||||||
artistId: query.id,
|
|
||||||
limit: 15,
|
|
||||||
},
|
|
||||||
server,
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.get(`api/artist/${query.id}`, {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<NDAlbumArtistDetailResponse>();
|
|
||||||
|
|
||||||
return { ...data, similarArtists: artistInfo.similarArtist };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<NDAlbumArtistList> => {
|
|
||||||
const { query, server, signal } = args;
|
|
||||||
|
|
||||||
const searchParams: NDAlbumArtistListParams = {
|
|
||||||
_end: query.startIndex + (query.limit || 0),
|
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
|
||||||
_sort: albumArtistListSortMap.navidrome[query.sortBy],
|
|
||||||
_start: query.startIndex,
|
|
||||||
name: query.searchTerm,
|
|
||||||
...query.ndParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await api.get('api/artist', {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json<NDArtistListResponse>();
|
|
||||||
const itemCount = res.headers.get('x-total-count');
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: data,
|
|
||||||
startIndex: query.startIndex,
|
|
||||||
totalRecordCount: Number(itemCount),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<NDAlbumDetail> => {
|
|
||||||
const { query, server, signal } = args;
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.get(`api/album/${query.id}`, {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<NDAlbumDetailResponse>();
|
|
||||||
|
|
||||||
const songsData = await api
|
|
||||||
.get('api/song', {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: {
|
|
||||||
_end: 0,
|
|
||||||
_order: NDSortOrder.ASC,
|
|
||||||
_sort: 'album',
|
|
||||||
_start: 0,
|
|
||||||
album_id: query.id,
|
|
||||||
},
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<NDSongListResponse>();
|
|
||||||
|
|
||||||
return { ...data, songs: songsData };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAlbumList = async (args: AlbumListArgs): Promise<NDAlbumList> => {
|
|
||||||
const { query, server, signal } = args;
|
|
||||||
|
|
||||||
const searchParams: NDAlbumListParams = {
|
|
||||||
_end: query.startIndex + (query.limit || 0),
|
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
|
||||||
_sort: albumListSortMap.navidrome[query.sortBy],
|
|
||||||
_start: query.startIndex,
|
|
||||||
artist_id: query.artistIds?.[0],
|
|
||||||
name: query.searchTerm,
|
|
||||||
...query.ndParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await api.get('api/album', {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json<NDAlbumListResponse>();
|
|
||||||
const itemCount = res.headers.get('x-total-count');
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: data,
|
|
||||||
startIndex: query?.startIndex || 0,
|
|
||||||
totalRecordCount: Number(itemCount),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSongList = async (args: SongListArgs): Promise<NDSongList> => {
|
|
||||||
const { query, server, signal } = args;
|
|
||||||
|
|
||||||
const searchParams: NDSongListParams = {
|
|
||||||
_end: query.startIndex + (query.limit || -1),
|
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
|
||||||
_sort: songListSortMap.navidrome[query.sortBy],
|
|
||||||
_start: query.startIndex,
|
|
||||||
album_id: query.albumIds,
|
|
||||||
artist_id: query.artistIds,
|
|
||||||
title: query.searchTerm,
|
|
||||||
...query.ndParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await api.get('api/song', {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(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/${query.id}`, {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<NDSongDetailResponse>();
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
|
||||||
const { body, server } = args;
|
|
||||||
|
|
||||||
const json: NDCreatePlaylistParams = {
|
|
||||||
comment: body.comment,
|
|
||||||
name: body.name,
|
|
||||||
...body.ndParams,
|
|
||||||
public: body.ndParams?.public || false,
|
|
||||||
rules: body.ndParams?.rules ? body.ndParams.rules : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.post('api/playlist', {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
json,
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
})
|
|
||||||
.json<NDCreatePlaylistResponse>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: data.id,
|
|
||||||
name: body.name,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistResponse> => {
|
|
||||||
const { query, body, server, signal } = args;
|
|
||||||
|
|
||||||
const json: NDUpdatePlaylistParams = {
|
|
||||||
comment: body.comment || '',
|
|
||||||
name: body.name,
|
|
||||||
ownerId: body.ndParams?.ownerId || undefined,
|
|
||||||
ownerName: body.ndParams?.owner || undefined,
|
|
||||||
public: body.ndParams?.public || false,
|
|
||||||
rules: body.ndParams?.rules ? body.ndParams?.rules : undefined,
|
|
||||||
sync: body.ndParams?.sync || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.put(`api/playlist/${query.id}`, {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
json,
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<NDUpdatePlaylistResponse>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: data.id,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
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 ? sortOrderMap.navidrome[query.sortOrder] : undefined,
|
|
||||||
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
|
|
||||||
_start: query.startIndex,
|
|
||||||
...query.ndParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await api.get('api/playlist', {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(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/playlist/${query.id}`, {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<NDPlaylistDetailResponse>();
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise<NDPlaylistSongList> => {
|
|
||||||
const { query, server, signal } = args;
|
|
||||||
|
|
||||||
const searchParams: NDSongListParams & { playlist_id: string } = {
|
|
||||||
_end: query.startIndex + (query.limit || 0),
|
|
||||||
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : NDSortOrder.ASC,
|
|
||||||
_sort: query.sortBy ? songListSortMap.navidrome[query.sortBy] : NDSongListSort.ID,
|
|
||||||
_start: query.startIndex,
|
|
||||||
playlist_id: query.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await api.get(`api/playlist/${query.id}/tracks`, {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json<NDPlaylistSongListResponse>();
|
|
||||||
const itemCount = res.headers.get('x-total-count');
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: data,
|
|
||||||
startIndex: query?.startIndex || 0,
|
|
||||||
totalRecordCount: Number(itemCount),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const addToPlaylist = async (args: AddToPlaylistArgs): Promise<NDAddToPlaylist> => {
|
|
||||||
const { query, body, server, signal } = args;
|
|
||||||
|
|
||||||
const json: NDAddToPlaylistBody = {
|
|
||||||
ids: body.songId,
|
|
||||||
};
|
|
||||||
|
|
||||||
await api
|
|
||||||
.post(`api/playlist/${query.id}/tracks`, {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
json,
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<NDAddToPlaylistResponse>();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFromPlaylist = async (args: RemoveFromPlaylistArgs): Promise<NDRemoveFromPlaylist> => {
|
|
||||||
const { query, server, signal } = args;
|
|
||||||
|
|
||||||
const searchParams: NDRemoveFromPlaylistParams = {
|
|
||||||
id: query.songId,
|
|
||||||
};
|
|
||||||
|
|
||||||
await api
|
|
||||||
.delete(`api/playlist/${query.id}/tracks`, {
|
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<NDRemoveFromPlaylistResponse>();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCoverArtUrl = (args: {
|
|
||||||
baseUrl: string;
|
|
||||||
coverArtId: string;
|
|
||||||
credential: string;
|
|
||||||
size: number;
|
|
||||||
}) => {
|
|
||||||
const size = args.size ? args.size : 250;
|
|
||||||
|
|
||||||
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
`${args.baseUrl}/rest/getCoverArt.view` +
|
|
||||||
`?id=${args.coverArtId}` +
|
|
||||||
`&${args.credential}` +
|
|
||||||
'&v=1.13.0' +
|
|
||||||
'&c=feishin' +
|
|
||||||
`&size=${size}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeSong = (
|
|
||||||
item: NDSong | NDPlaylistSong,
|
|
||||||
server: ServerListItem,
|
|
||||||
deviceId: string,
|
|
||||||
imageSize?: number,
|
|
||||||
): Song => {
|
|
||||||
let id;
|
|
||||||
let playlistItemId;
|
|
||||||
|
|
||||||
// Dynamically determine the id field based on whether or not the item is a playlist song
|
|
||||||
if ('mediaFileId' in item) {
|
|
||||||
id = item.mediaFileId;
|
|
||||||
playlistItemId = item.id;
|
|
||||||
} else {
|
|
||||||
id = item.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageUrl = getCoverArtUrl({
|
|
||||||
baseUrl: server.url,
|
|
||||||
coverArtId: id,
|
|
||||||
credential: server.credential,
|
|
||||||
size: imageSize || 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
const imagePlaceholderUrl = null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
album: item.album,
|
|
||||||
albumArtists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
|
||||||
albumId: item.albumId,
|
|
||||||
artistName: item.artist,
|
|
||||||
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
|
||||||
bitRate: item.bitRate,
|
|
||||||
bpm: item.bpm ? item.bpm : null,
|
|
||||||
channels: item.channels ? item.channels : null,
|
|
||||||
comment: item.comment ? item.comment : null,
|
|
||||||
compilation: item.compilation,
|
|
||||||
container: item.suffix,
|
|
||||||
createdAt: item.createdAt.split('T')[0],
|
|
||||||
discNumber: item.discNumber,
|
|
||||||
duration: item.duration,
|
|
||||||
genres: item.genres,
|
|
||||||
id,
|
|
||||||
imagePlaceholderUrl,
|
|
||||||
imageUrl,
|
|
||||||
itemType: LibraryItem.SONG,
|
|
||||||
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
|
||||||
name: item.title,
|
|
||||||
path: item.path,
|
|
||||||
playCount: item.playCount,
|
|
||||||
playlistItemId,
|
|
||||||
releaseDate: new Date(item.year, 0, 1).toISOString(),
|
|
||||||
releaseYear: String(item.year),
|
|
||||||
serverId: server.id,
|
|
||||||
serverType: ServerType.NAVIDROME,
|
|
||||||
size: item.size,
|
|
||||||
streamUrl: `${server.url}/rest/stream.view?id=${id}&v=1.13.0&c=feishin_${deviceId}&${server.credential}`,
|
|
||||||
trackNumber: item.trackNumber,
|
|
||||||
uniqueId: nanoid(),
|
|
||||||
updatedAt: item.updatedAt,
|
|
||||||
userFavorite: item.starred || false,
|
|
||||||
userRating: item.rating || null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeAlbum = (item: NDAlbum, server: ServerListItem, imageSize?: number): Album => {
|
|
||||||
const imageUrl = getCoverArtUrl({
|
|
||||||
baseUrl: server.url,
|
|
||||||
coverArtId: item.coverArtId || item.id,
|
|
||||||
credential: server.credential,
|
|
||||||
size: imageSize || 300,
|
|
||||||
});
|
|
||||||
|
|
||||||
const imagePlaceholderUrl = null;
|
|
||||||
|
|
||||||
const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
|
|
||||||
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
|
||||||
backdropImageUrl: imageBackdropUrl,
|
|
||||||
createdAt: item.createdAt.split('T')[0],
|
|
||||||
duration: item.duration * 1000 || null,
|
|
||||||
genres: item.genres,
|
|
||||||
id: item.id,
|
|
||||||
imagePlaceholderUrl,
|
|
||||||
imageUrl,
|
|
||||||
isCompilation: item.compilation,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
|
||||||
name: item.name,
|
|
||||||
playCount: item.playCount,
|
|
||||||
releaseDate: new Date(item.minYear, 0, 1).toISOString(),
|
|
||||||
releaseYear: item.minYear,
|
|
||||||
serverId: server.id,
|
|
||||||
serverType: ServerType.NAVIDROME,
|
|
||||||
size: item.size,
|
|
||||||
songCount: item.songCount,
|
|
||||||
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server, '')) : undefined,
|
|
||||||
uniqueId: nanoid(),
|
|
||||||
updatedAt: item.updatedAt,
|
|
||||||
userFavorite: item.starred,
|
|
||||||
userRating: item.rating,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeAlbumArtist = (item: NDAlbumArtist, server: ServerListItem): AlbumArtist => {
|
|
||||||
const imageUrl =
|
|
||||||
item.largeImageUrl === '/app/artist-placeholder.webp' ? null : item.largeImageUrl;
|
|
||||||
|
|
||||||
return {
|
|
||||||
albumCount: item.albumCount,
|
|
||||||
backgroundImageUrl: null,
|
|
||||||
biography: item.biography || null,
|
|
||||||
duration: null,
|
|
||||||
genres: item.genres,
|
|
||||||
id: item.id,
|
|
||||||
imageUrl: imageUrl || null,
|
|
||||||
itemType: LibraryItem.ALBUM_ARTIST,
|
|
||||||
lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate,
|
|
||||||
name: item.name,
|
|
||||||
playCount: item.playCount,
|
|
||||||
serverId: server.id,
|
|
||||||
serverType: ServerType.NAVIDROME,
|
|
||||||
similarArtists:
|
|
||||||
item.similarArtists?.map((artist) => ({
|
|
||||||
id: artist.id,
|
|
||||||
imageUrl: artist?.artistImageUrl || null,
|
|
||||||
name: artist.name,
|
|
||||||
})) || null,
|
|
||||||
songCount: item.songCount,
|
|
||||||
userFavorite: item.starred,
|
|
||||||
userRating: item.rating,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizePlaylist = (
|
|
||||||
item: NDPlaylist,
|
|
||||||
server: ServerListItem,
|
|
||||||
imageSize?: number,
|
|
||||||
): Playlist => {
|
|
||||||
const imageUrl = getCoverArtUrl({
|
|
||||||
baseUrl: server.url,
|
|
||||||
coverArtId: item.id,
|
|
||||||
credential: server.credential,
|
|
||||||
size: imageSize || 300,
|
|
||||||
});
|
|
||||||
|
|
||||||
const imagePlaceholderUrl = null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
description: item.comment,
|
|
||||||
duration: item.duration * 1000,
|
|
||||||
genres: [],
|
|
||||||
id: item.id,
|
|
||||||
imagePlaceholderUrl,
|
|
||||||
imageUrl,
|
|
||||||
itemType: LibraryItem.PLAYLIST,
|
|
||||||
name: item.name,
|
|
||||||
owner: item.ownerName,
|
|
||||||
ownerId: item.ownerId,
|
|
||||||
public: item.public,
|
|
||||||
rules: item?.rules || null,
|
|
||||||
serverId: server.id,
|
|
||||||
serverType: ServerType.NAVIDROME,
|
|
||||||
size: item.size,
|
|
||||||
songCount: item.songCount,
|
|
||||||
sync: item.sync,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeUser = (item: NDUser): User => {
|
|
||||||
return {
|
|
||||||
createdAt: item.createdAt,
|
|
||||||
email: item.email,
|
|
||||||
id: item.id,
|
|
||||||
isAdmin: item.isAdmin,
|
|
||||||
lastLoginAt: item.lastLoginAt,
|
|
||||||
name: item.userName,
|
|
||||||
updatedAt: item.updatedAt,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const navidromeApi = {
|
|
||||||
addToPlaylist,
|
|
||||||
authenticate,
|
|
||||||
createPlaylist,
|
|
||||||
deletePlaylist,
|
|
||||||
getAlbumArtistDetail,
|
|
||||||
getAlbumArtistList,
|
|
||||||
getAlbumDetail,
|
|
||||||
getAlbumList,
|
|
||||||
getGenreList,
|
|
||||||
getPlaylistDetail,
|
|
||||||
getPlaylistList,
|
|
||||||
getPlaylistSongList,
|
|
||||||
getSongDetail,
|
|
||||||
getSongList,
|
|
||||||
getUserList,
|
|
||||||
removeFromPlaylist,
|
|
||||||
updatePlaylist,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ndNormalize = {
|
|
||||||
album: normalizeAlbum,
|
|
||||||
albumArtist: normalizeAlbumArtist,
|
|
||||||
playlist: normalizePlaylist,
|
|
||||||
song: normalizeSong,
|
|
||||||
user: normalizeUser,
|
|
||||||
};
|
|
|
@ -1,292 +0,0 @@
|
||||||
import { jfNormalize } from '/@/renderer/api/jellyfin.api';
|
|
||||||
import type {
|
|
||||||
JFAlbum,
|
|
||||||
JFAlbumArtist,
|
|
||||||
JFGenreList,
|
|
||||||
JFMusicFolderList,
|
|
||||||
JFPlaylist,
|
|
||||||
JFSong,
|
|
||||||
} from '/@/renderer/api/jellyfin.types';
|
|
||||||
import { ndNormalize } from '/@/renderer/api/navidrome.api';
|
|
||||||
import type {
|
|
||||||
NDAlbum,
|
|
||||||
NDAlbumArtist,
|
|
||||||
NDGenreList,
|
|
||||||
NDPlaylist,
|
|
||||||
NDSong,
|
|
||||||
NDUser,
|
|
||||||
} from '/@/renderer/api/navidrome.types';
|
|
||||||
import { ssNormalize } from '/@/renderer/api/subsonic.api';
|
|
||||||
import { SSGenreList, SSMusicFolderList, SSSong } from '/@/renderer/api/subsonic.types';
|
|
||||||
import type {
|
|
||||||
Album,
|
|
||||||
AlbumArtist,
|
|
||||||
RawAlbumArtistDetailResponse,
|
|
||||||
RawAlbumArtistListResponse,
|
|
||||||
RawAlbumDetailResponse,
|
|
||||||
RawAlbumListResponse,
|
|
||||||
RawGenreListResponse,
|
|
||||||
RawMusicFolderListResponse,
|
|
||||||
RawPlaylistDetailResponse,
|
|
||||||
RawPlaylistListResponse,
|
|
||||||
RawSongListResponse,
|
|
||||||
RawTopSongListResponse,
|
|
||||||
RawUserListResponse,
|
|
||||||
} from '/@/renderer/api/types';
|
|
||||||
import { ServerListItem } from '/@/renderer/types';
|
|
||||||
|
|
||||||
const albumList = (data: RawAlbumListResponse | undefined, server: ServerListItem | null) => {
|
|
||||||
let albums;
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
albums = data?.items.map((item) => jfNormalize.album(item as JFAlbum, server));
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
albums = data?.items.map((item) => ndNormalize.album(item as NDAlbum, server));
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: albums,
|
|
||||||
startIndex: data?.startIndex,
|
|
||||||
totalRecordCount: data?.totalRecordCount,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const albumDetail = (
|
|
||||||
data: RawAlbumDetailResponse | undefined,
|
|
||||||
server: ServerListItem | null,
|
|
||||||
): Album | undefined => {
|
|
||||||
let album: Album | undefined;
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
album = jfNormalize.album(data as JFAlbum, server);
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
album = ndNormalize.album(data as NDAlbum, server);
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return album;
|
|
||||||
};
|
|
||||||
|
|
||||||
const songList = (data: RawSongListResponse | undefined, server: ServerListItem | null) => {
|
|
||||||
let songs;
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
songs = data?.items.map((item) => jfNormalize.song(item as JFSong, server, ''));
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
songs = data?.items.map((item) => ndNormalize.song(item as NDSong, server, ''));
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: songs,
|
|
||||||
startIndex: data?.startIndex,
|
|
||||||
totalRecordCount: data?.totalRecordCount,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const topSongList = (data: RawTopSongListResponse | undefined, server: ServerListItem | null) => {
|
|
||||||
let songs;
|
|
||||||
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
songs = data?.items.map((item) => jfNormalize.song(item as JFSong, server, ''));
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
songs = data?.items?.map((item) => ssNormalize.song(item as SSSong, server, ''));
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
songs = data?.items?.map((item) => ssNormalize.song(item as SSSong, server, ''));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: songs,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const musicFolderList = (
|
|
||||||
data: RawMusicFolderListResponse | undefined,
|
|
||||||
server: ServerListItem | null,
|
|
||||||
) => {
|
|
||||||
let musicFolders;
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
musicFolders = (data as JFMusicFolderList)?.map((item) => ({
|
|
||||||
id: String(item.Id),
|
|
||||||
name: item.Name,
|
|
||||||
}));
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
musicFolders = (data as SSMusicFolderList)?.map((item) => ({
|
|
||||||
id: String(item.id),
|
|
||||||
name: item.name,
|
|
||||||
}));
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
musicFolders = (data as SSMusicFolderList)?.map((item) => ({
|
|
||||||
id: String(item.id),
|
|
||||||
name: item.name,
|
|
||||||
}));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return musicFolders;
|
|
||||||
};
|
|
||||||
|
|
||||||
const genreList = (data: RawGenreListResponse | undefined, server: ServerListItem | null) => {
|
|
||||||
let genres;
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
genres = (data as JFGenreList)?.Items.map((item) => ({
|
|
||||||
id: String(item.Id),
|
|
||||||
name: item.Name,
|
|
||||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
genres = (data as NDGenreList)
|
|
||||||
?.map((item) => ({
|
|
||||||
id: String(item.id),
|
|
||||||
name: item.name,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
genres = (data as SSGenreList)
|
|
||||||
?.map((item) => ({
|
|
||||||
id: item.value,
|
|
||||||
name: item.value,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return genres;
|
|
||||||
};
|
|
||||||
|
|
||||||
const albumArtistDetail = (
|
|
||||||
data: RawAlbumArtistDetailResponse | undefined,
|
|
||||||
server: ServerListItem | null,
|
|
||||||
): AlbumArtist | undefined => {
|
|
||||||
let albumArtist: AlbumArtist | undefined;
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
albumArtist = jfNormalize.albumArtist(data as JFAlbumArtist, server);
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
albumArtist = ndNormalize.albumArtist(data as NDAlbumArtist, server);
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return albumArtist;
|
|
||||||
};
|
|
||||||
|
|
||||||
const albumArtistList = (
|
|
||||||
data: RawAlbumArtistListResponse | undefined,
|
|
||||||
server: ServerListItem | null,
|
|
||||||
) => {
|
|
||||||
let albumArtists;
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
albumArtists = data?.items.map((item) =>
|
|
||||||
jfNormalize.albumArtist(item as JFAlbumArtist, server),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
albumArtists = data?.items.map((item) =>
|
|
||||||
ndNormalize.albumArtist(item as NDAlbumArtist, server),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: albumArtists,
|
|
||||||
startIndex: data?.startIndex,
|
|
||||||
totalRecordCount: data?.totalRecordCount,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const playlistList = (data: RawPlaylistListResponse | undefined, server: ServerListItem | null) => {
|
|
||||||
let playlists;
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
playlists = data?.items.map((item) => jfNormalize.playlist(item as JFPlaylist, server));
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
playlists = data?.items.map((item) => ndNormalize.playlist(item as NDPlaylist, server));
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: playlists,
|
|
||||||
startIndex: data?.startIndex,
|
|
||||||
totalRecordCount: data?.totalRecordCount,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const playlistDetail = (
|
|
||||||
data: RawPlaylistDetailResponse | undefined,
|
|
||||||
server: ServerListItem | null,
|
|
||||||
) => {
|
|
||||||
let playlist;
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
playlist = jfNormalize.playlist(data as JFPlaylist, server);
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
playlist = ndNormalize.playlist(data as NDPlaylist, server);
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return playlist;
|
|
||||||
};
|
|
||||||
|
|
||||||
const userList = (data: RawUserListResponse | undefined, server: ServerListItem | null) => {
|
|
||||||
let users;
|
|
||||||
switch (server?.type) {
|
|
||||||
case 'jellyfin':
|
|
||||||
break;
|
|
||||||
case 'navidrome':
|
|
||||||
users = data?.items.map((item) => ndNormalize.user(item as NDUser));
|
|
||||||
break;
|
|
||||||
case 'subsonic':
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: users,
|
|
||||||
startIndex: data?.startIndex,
|
|
||||||
totalRecordCount: data?.totalRecordCount,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const normalize = {
|
|
||||||
albumArtistDetail,
|
|
||||||
albumArtistList,
|
|
||||||
albumDetail,
|
|
||||||
albumList,
|
|
||||||
genreList,
|
|
||||||
musicFolderList,
|
|
||||||
playlistDetail,
|
|
||||||
playlistList,
|
|
||||||
songList,
|
|
||||||
topSongList,
|
|
||||||
userList,
|
|
||||||
};
|
|
|
@ -1,497 +0,0 @@
|
||||||
import ky from 'ky';
|
|
||||||
import md5 from 'md5';
|
|
||||||
import { parseSearchParams, randomString } from '/@/renderer/utils';
|
|
||||||
import type {
|
|
||||||
SSAlbumListResponse,
|
|
||||||
SSAlbumDetailResponse,
|
|
||||||
SSArtistIndex,
|
|
||||||
SSAlbumArtistList,
|
|
||||||
SSAlbumArtistListResponse,
|
|
||||||
SSGenreListResponse,
|
|
||||||
SSMusicFolderList,
|
|
||||||
SSMusicFolderListResponse,
|
|
||||||
SSGenreList,
|
|
||||||
SSAlbumDetail,
|
|
||||||
SSAlbumList,
|
|
||||||
SSAlbumArtistDetail,
|
|
||||||
SSAlbumArtistDetailResponse,
|
|
||||||
SSFavoriteParams,
|
|
||||||
SSRatingParams,
|
|
||||||
SSAlbumArtistDetailParams,
|
|
||||||
SSAlbumArtistListParams,
|
|
||||||
SSTopSongListParams,
|
|
||||||
SSTopSongListResponse,
|
|
||||||
SSArtistInfoParams,
|
|
||||||
SSArtistInfoResponse,
|
|
||||||
SSArtistInfo,
|
|
||||||
SSSong,
|
|
||||||
SSTopSongList,
|
|
||||||
SSScrobbleParams,
|
|
||||||
} from '/@/renderer/api/subsonic.types';
|
|
||||||
import {
|
|
||||||
AlbumArtistDetailArgs,
|
|
||||||
AlbumArtistListArgs,
|
|
||||||
AlbumDetailArgs,
|
|
||||||
AlbumListArgs,
|
|
||||||
ArtistInfoArgs,
|
|
||||||
AuthenticationResponse,
|
|
||||||
FavoriteArgs,
|
|
||||||
FavoriteResponse,
|
|
||||||
GenreListArgs,
|
|
||||||
LibraryItem,
|
|
||||||
MusicFolderListArgs,
|
|
||||||
QueueSong,
|
|
||||||
RatingArgs,
|
|
||||||
RatingResponse,
|
|
||||||
RawScrobbleResponse,
|
|
||||||
ScrobbleArgs,
|
|
||||||
ServerListItem,
|
|
||||||
ServerType,
|
|
||||||
TopSongListArgs,
|
|
||||||
} from '/@/renderer/api/types';
|
|
||||||
import { toast } from '/@/renderer/components/toast';
|
|
||||||
import { nanoid } from 'nanoid/non-secure';
|
|
||||||
|
|
||||||
const IGNORE_CORS = localStorage.getItem('IGNORE_CORS') === 'true';
|
|
||||||
|
|
||||||
const getCoverArtUrl = (args: {
|
|
||||||
baseUrl: string;
|
|
||||||
coverArtId: string;
|
|
||||||
credential: string;
|
|
||||||
size: number;
|
|
||||||
}) => {
|
|
||||||
const size = args.size ? args.size : 150;
|
|
||||||
|
|
||||||
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
`${args.baseUrl}/rest/getCoverArt.view` +
|
|
||||||
`?id=${args.coverArtId}` +
|
|
||||||
`&${args.credential}` +
|
|
||||||
'&v=1.13.0' +
|
|
||||||
'&c=feishin' +
|
|
||||||
`&size=${size}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const api = ky.create({
|
|
||||||
hooks: {
|
|
||||||
afterResponse: [
|
|
||||||
async (_request, _options, response) => {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data['subsonic-response'].status !== 'ok') {
|
|
||||||
// Suppress code related to non-linked lastfm or spotify from Navidrome
|
|
||||||
if (data['subsonic-response'].error.code !== 0) {
|
|
||||||
toast.error({
|
|
||||||
message: data['subsonic-response'].error.message,
|
|
||||||
title: 'Issue from Subsonic API',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(data['subsonic-response']), { status: 200 });
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
mode: IGNORE_CORS ? 'cors' : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getDefaultParams = (server: ServerListItem | null) => {
|
|
||||||
if (!server) return {};
|
|
||||||
|
|
||||||
const authParams = server.credential.split(/&?\w=/gm);
|
|
||||||
|
|
||||||
const params: Record<string, string> = {
|
|
||||||
c: 'Feishin',
|
|
||||||
f: 'json',
|
|
||||||
u: server.username,
|
|
||||||
v: '1.13.0',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (authParams?.length === 4) {
|
|
||||||
params.s = authParams[2];
|
|
||||||
params.t = authParams[3];
|
|
||||||
} else if (authParams?.length === 3) {
|
|
||||||
params.p = authParams[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
return params;
|
|
||||||
};
|
|
||||||
|
|
||||||
const authenticate = async (
|
|
||||||
url: string,
|
|
||||||
body: {
|
|
||||||
legacy?: boolean;
|
|
||||||
password: string;
|
|
||||||
username: string;
|
|
||||||
},
|
|
||||||
): Promise<AuthenticationResponse> => {
|
|
||||||
let credential;
|
|
||||||
const cleanServerUrl = url.replace(/\/$/, '');
|
|
||||||
|
|
||||||
if (body.legacy) {
|
|
||||||
credential = `u=${body.username}&p=${body.password}`;
|
|
||||||
} else {
|
|
||||||
const salt = randomString(12);
|
|
||||||
const hash = md5(body.password + salt);
|
|
||||||
credential = `u=${body.username}&s=${salt}&t=${hash}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ky.get(`${cleanServerUrl}/rest/ping.view?v=1.13.0&c=Feishin&f=json&${credential}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
credential,
|
|
||||||
userId: null,
|
|
||||||
username: body.username,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMusicFolderList = async (args: MusicFolderListArgs): Promise<SSMusicFolderList> => {
|
|
||||||
const { signal, server } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.get('rest/getMusicFolders.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: defaultParams,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<SSMusicFolderListResponse>();
|
|
||||||
|
|
||||||
return data.musicFolders.musicFolder;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAlbumArtistDetail = async (
|
|
||||||
args: AlbumArtistDetailArgs,
|
|
||||||
): Promise<SSAlbumArtistDetail> => {
|
|
||||||
const { server, signal, query } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
const searchParams: SSAlbumArtistDetailParams = {
|
|
||||||
id: query.id,
|
|
||||||
...defaultParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.get('/getArtist.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<SSAlbumArtistDetailResponse>();
|
|
||||||
|
|
||||||
return data.artist;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArtistList> => {
|
|
||||||
const { signal, server, query } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
const searchParams: SSAlbumArtistListParams = {
|
|
||||||
musicFolderId: query.musicFolderId,
|
|
||||||
...defaultParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.get('rest/getArtists.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<SSAlbumArtistListResponse>();
|
|
||||||
|
|
||||||
const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: artists,
|
|
||||||
startIndex: query.startIndex,
|
|
||||||
totalRecordCount: null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
|
|
||||||
const { server, signal } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.get('rest/getGenres.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: defaultParams,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<SSGenreListResponse>();
|
|
||||||
|
|
||||||
return data.genres.genre;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> => {
|
|
||||||
const { server, query, signal } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
const searchParams = {
|
|
||||||
id: query.id,
|
|
||||||
...defaultParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.get('rest/getAlbum.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<SSAlbumDetailResponse>();
|
|
||||||
|
|
||||||
const { song: songs, ...dataWithoutSong } = data.album;
|
|
||||||
return { ...dataWithoutSong, songs };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => {
|
|
||||||
const { server, query, signal } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
const searchParams = {
|
|
||||||
...defaultParams,
|
|
||||||
};
|
|
||||||
const data = await api
|
|
||||||
.get('rest/getAlbumList2.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<SSAlbumListResponse>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: data.albumList2.album,
|
|
||||||
startIndex: query.startIndex,
|
|
||||||
totalRecordCount: null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
|
||||||
const { server, query, signal } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
for (const id of query.id) {
|
|
||||||
const searchParams: SSFavoriteParams = {
|
|
||||||
albumId: query.type === LibraryItem.ALBUM ? id : undefined,
|
|
||||||
artistId: query.type === LibraryItem.ALBUM_ARTIST ? id : undefined,
|
|
||||||
id: query.type === LibraryItem.SONG ? id : undefined,
|
|
||||||
...defaultParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
await api.get('rest/star.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
// .json<SSFavoriteResponse>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: query.id,
|
|
||||||
type: query.type,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
|
||||||
const { server, query, signal } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
for (const id of query.id) {
|
|
||||||
const searchParams: SSFavoriteParams = {
|
|
||||||
albumId: query.type === LibraryItem.ALBUM ? id : undefined,
|
|
||||||
artistId: query.type === LibraryItem.ALBUM_ARTIST ? id : undefined,
|
|
||||||
id: query.type === LibraryItem.SONG ? id : undefined,
|
|
||||||
...defaultParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
await api.get('rest/unstar.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
// .json<SSFavoriteResponse>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: query.id,
|
|
||||||
type: query.type,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateRating = async (args: RatingArgs): Promise<RatingResponse> => {
|
|
||||||
const { server, query, signal } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
const itemIds = query.item.map((item) => item.id);
|
|
||||||
|
|
||||||
for (const id of itemIds) {
|
|
||||||
const searchParams: SSRatingParams = {
|
|
||||||
id,
|
|
||||||
rating: query.rating,
|
|
||||||
...defaultParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
await api.get('rest/setRating.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTopSongList = async (args: TopSongListArgs): Promise<SSTopSongList> => {
|
|
||||||
const { signal, server, query } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
const searchParams: SSTopSongListParams = {
|
|
||||||
artist: query.artist,
|
|
||||||
count: query.limit,
|
|
||||||
...defaultParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.get('rest/getTopSongs.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams: parseSearchParams(searchParams),
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<SSTopSongListResponse>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: data?.topSongs?.song,
|
|
||||||
startIndex: 0,
|
|
||||||
totalRecordCount: data?.topSongs?.song?.length || 0,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getArtistInfo = async (args: ArtistInfoArgs): Promise<SSArtistInfo> => {
|
|
||||||
const { signal, server, query } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
const searchParams: SSArtistInfoParams = {
|
|
||||||
count: query.limit,
|
|
||||||
id: query.artistId,
|
|
||||||
...defaultParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await api
|
|
||||||
.get('rest/getArtistInfo2.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams,
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
.json<SSArtistInfoResponse>();
|
|
||||||
|
|
||||||
return data.artistInfo2;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrobble = async (args: ScrobbleArgs): Promise<RawScrobbleResponse> => {
|
|
||||||
const { signal, server, query } = args;
|
|
||||||
const defaultParams = getDefaultParams(server);
|
|
||||||
|
|
||||||
const searchParams: SSScrobbleParams = {
|
|
||||||
id: query.id,
|
|
||||||
submission: query.submission,
|
|
||||||
...defaultParams,
|
|
||||||
};
|
|
||||||
|
|
||||||
await api.get('rest/scrobble.view', {
|
|
||||||
prefixUrl: server?.url,
|
|
||||||
searchParams,
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeSong = (item: SSSong, server: ServerListItem, deviceId: string): QueueSong => {
|
|
||||||
const imageUrl =
|
|
||||||
getCoverArtUrl({
|
|
||||||
baseUrl: server.url,
|
|
||||||
coverArtId: item.coverArt,
|
|
||||||
credential: server.credential,
|
|
||||||
size: 300,
|
|
||||||
}) || null;
|
|
||||||
|
|
||||||
const streamUrl = `${server.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server.credential}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
album: item.album,
|
|
||||||
albumArtists: [
|
|
||||||
{
|
|
||||||
id: item.artistId || '',
|
|
||||||
imageUrl: null,
|
|
||||||
name: item.artist,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
albumId: item.albumId,
|
|
||||||
artistName: item.artist,
|
|
||||||
artists: [
|
|
||||||
{
|
|
||||||
id: item.artistId || '',
|
|
||||||
imageUrl: null,
|
|
||||||
name: item.artist,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
bitRate: item.bitRate,
|
|
||||||
bpm: null,
|
|
||||||
channels: null,
|
|
||||||
comment: null,
|
|
||||||
compilation: null,
|
|
||||||
container: item.contentType,
|
|
||||||
createdAt: item.created,
|
|
||||||
discNumber: item.discNumber || 1,
|
|
||||||
duration: item.duration,
|
|
||||||
genres: [
|
|
||||||
{
|
|
||||||
id: item.genre,
|
|
||||||
name: item.genre,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
id: item.id,
|
|
||||||
imagePlaceholderUrl: null,
|
|
||||||
imageUrl,
|
|
||||||
itemType: LibraryItem.SONG,
|
|
||||||
lastPlayedAt: null,
|
|
||||||
name: item.title,
|
|
||||||
path: item.path,
|
|
||||||
playCount: item?.playCount || 0,
|
|
||||||
releaseDate: null,
|
|
||||||
releaseYear: item.year ? String(item.year) : null,
|
|
||||||
serverId: server.id,
|
|
||||||
serverType: ServerType.SUBSONIC,
|
|
||||||
size: item.size,
|
|
||||||
streamUrl,
|
|
||||||
trackNumber: item.track,
|
|
||||||
uniqueId: nanoid(),
|
|
||||||
updatedAt: '',
|
|
||||||
userFavorite: item.starred || false,
|
|
||||||
userRating: item.userRating || null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const subsonicApi = {
|
|
||||||
authenticate,
|
|
||||||
createFavorite,
|
|
||||||
deleteFavorite,
|
|
||||||
getAlbumArtistDetail,
|
|
||||||
getAlbumArtistList,
|
|
||||||
getAlbumDetail,
|
|
||||||
getAlbumList,
|
|
||||||
getArtistInfo,
|
|
||||||
getCoverArtUrl,
|
|
||||||
getGenreList,
|
|
||||||
getMusicFolderList,
|
|
||||||
getTopSongList,
|
|
||||||
scrobble,
|
|
||||||
updateRating,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ssNormalize = {
|
|
||||||
song: normalizeSong,
|
|
||||||
};
|
|
Reference in a new issue