Lint all files

This commit is contained in:
jeffvli 2023-07-01 19:10:05 -07:00
parent 22af76b4d6
commit 30e52ebb54
334 changed files with 76519 additions and 75932 deletions

View file

@ -1,53 +1,53 @@
import { useAuthStore } from '/@/renderer/store';
import { toast } from '/@/renderer/components/toast/index';
import type {
AlbumDetailArgs,
AlbumListArgs,
SongListArgs,
SongDetailArgs,
AlbumArtistDetailArgs,
AlbumArtistListArgs,
SetRatingArgs,
GenreListArgs,
CreatePlaylistArgs,
DeletePlaylistArgs,
PlaylistDetailArgs,
PlaylistListArgs,
MusicFolderListArgs,
PlaylistSongListArgs,
ArtistListArgs,
UpdatePlaylistArgs,
UserListArgs,
FavoriteArgs,
TopSongListArgs,
AddToPlaylistArgs,
AddToPlaylistResponse,
RemoveFromPlaylistArgs,
RemoveFromPlaylistResponse,
ScrobbleArgs,
ScrobbleResponse,
AlbumArtistDetailResponse,
FavoriteResponse,
CreatePlaylistResponse,
AlbumArtistListResponse,
AlbumDetailResponse,
AlbumListResponse,
ArtistListResponse,
GenreListResponse,
MusicFolderListResponse,
PlaylistDetailResponse,
PlaylistListResponse,
RatingResponse,
SongDetailResponse,
SongListResponse,
TopSongListResponse,
UpdatePlaylistResponse,
UserListResponse,
AuthenticationResponse,
SearchArgs,
SearchResponse,
LyricsArgs,
LyricsResponse,
AlbumDetailArgs,
AlbumListArgs,
SongListArgs,
SongDetailArgs,
AlbumArtistDetailArgs,
AlbumArtistListArgs,
SetRatingArgs,
GenreListArgs,
CreatePlaylistArgs,
DeletePlaylistArgs,
PlaylistDetailArgs,
PlaylistListArgs,
MusicFolderListArgs,
PlaylistSongListArgs,
ArtistListArgs,
UpdatePlaylistArgs,
UserListArgs,
FavoriteArgs,
TopSongListArgs,
AddToPlaylistArgs,
AddToPlaylistResponse,
RemoveFromPlaylistArgs,
RemoveFromPlaylistResponse,
ScrobbleArgs,
ScrobbleResponse,
AlbumArtistDetailResponse,
FavoriteResponse,
CreatePlaylistResponse,
AlbumArtistListResponse,
AlbumDetailResponse,
AlbumListResponse,
ArtistListResponse,
GenreListResponse,
MusicFolderListResponse,
PlaylistDetailResponse,
PlaylistListResponse,
RatingResponse,
SongDetailResponse,
SongListResponse,
TopSongListResponse,
UpdatePlaylistResponse,
UserListResponse,
AuthenticationResponse,
SearchArgs,
SearchResponse,
LyricsArgs,
LyricsResponse,
} from '/@/renderer/api/types';
import { ServerType } from '/@/renderer/types';
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
@ -56,436 +56,445 @@ import { ssController } from '/@/renderer/api/subsonic/subsonic-controller';
import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller';
export type ControllerEndpoint = Partial<{
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
authenticate: (
url: string,
body: { password: string; username: string },
) => Promise<AuthenticationResponse>;
clearPlaylist: () => void;
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
getArtistDetail: () => void;
getArtistInfo: (args: any) => void;
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
getFavoritesList: () => void;
getFolderItemList: () => void;
getFolderList: () => void;
getFolderSongs: () => void;
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
getLyrics: (args: LyricsArgs) => Promise<LyricsResponse>;
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
search: (args: SearchArgs) => Promise<SearchResponse>;
setRating: (args: SetRatingArgs) => Promise<RatingResponse>;
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
authenticate: (
url: string,
body: { password: string; username: string },
) => Promise<AuthenticationResponse>;
clearPlaylist: () => void;
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
getArtistDetail: () => void;
getArtistInfo: (args: any) => void;
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
getFavoritesList: () => void;
getFolderItemList: () => void;
getFolderList: () => void;
getFolderSongs: () => void;
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
getLyrics: (args: LyricsArgs) => Promise<LyricsResponse>;
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
search: (args: SearchArgs) => Promise<SearchResponse>;
setRating: (args: SetRatingArgs) => Promise<RatingResponse>;
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
}>;
type ApiController = {
jellyfin: ControllerEndpoint;
navidrome: ControllerEndpoint;
subsonic: ControllerEndpoint;
jellyfin: ControllerEndpoint;
navidrome: ControllerEndpoint;
subsonic: ControllerEndpoint;
};
const endpoints: ApiController = {
jellyfin: {
addToPlaylist: jfController.addToPlaylist,
authenticate: jfController.authenticate,
clearPlaylist: undefined,
createFavorite: jfController.createFavorite,
createPlaylist: jfController.createPlaylist,
deleteFavorite: jfController.deleteFavorite,
deletePlaylist: jfController.deletePlaylist,
getAlbumArtistDetail: jfController.getAlbumArtistDetail,
getAlbumArtistList: jfController.getAlbumArtistList,
getAlbumDetail: jfController.getAlbumDetail,
getAlbumList: jfController.getAlbumList,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: jfController.getGenreList,
getLyrics: jfController.getLyrics,
getMusicFolderList: jfController.getMusicFolderList,
getPlaylistDetail: jfController.getPlaylistDetail,
getPlaylistList: jfController.getPlaylistList,
getPlaylistSongList: jfController.getPlaylistSongList,
getRandomSongList: jfController.getRandomSongList,
getSongDetail: undefined,
getSongList: jfController.getSongList,
getTopSongs: jfController.getTopSongList,
getUserList: undefined,
removeFromPlaylist: jfController.removeFromPlaylist,
scrobble: jfController.scrobble,
search: jfController.search,
setRating: undefined,
updatePlaylist: jfController.updatePlaylist,
},
navidrome: {
addToPlaylist: ndController.addToPlaylist,
authenticate: ndController.authenticate,
clearPlaylist: undefined,
createFavorite: ssController.createFavorite,
createPlaylist: ndController.createPlaylist,
deleteFavorite: ssController.removeFavorite,
deletePlaylist: ndController.deletePlaylist,
getAlbumArtistDetail: ndController.getAlbumArtistDetail,
getAlbumArtistList: ndController.getAlbumArtistList,
getAlbumDetail: ndController.getAlbumDetail,
getAlbumList: ndController.getAlbumList,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: ndController.getGenreList,
getLyrics: undefined,
getMusicFolderList: ssController.getMusicFolderList,
getPlaylistDetail: ndController.getPlaylistDetail,
getPlaylistList: ndController.getPlaylistList,
getPlaylistSongList: ndController.getPlaylistSongList,
getRandomSongList: ssController.getRandomSongList,
getSongDetail: ndController.getSongDetail,
getSongList: ndController.getSongList,
getTopSongs: ssController.getTopSongList,
getUserList: ndController.getUserList,
removeFromPlaylist: ndController.removeFromPlaylist,
scrobble: ssController.scrobble,
search: ssController.search3,
setRating: ssController.setRating,
updatePlaylist: ndController.updatePlaylist,
},
subsonic: {
authenticate: ssController.authenticate,
clearPlaylist: undefined,
createFavorite: ssController.createFavorite,
createPlaylist: undefined,
deleteFavorite: ssController.removeFavorite,
deletePlaylist: undefined,
getAlbumArtistDetail: undefined,
getAlbumArtistList: undefined,
getAlbumDetail: undefined,
getAlbumList: undefined,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: undefined,
getLyrics: undefined,
getMusicFolderList: ssController.getMusicFolderList,
getPlaylistDetail: undefined,
getPlaylistList: undefined,
getSongDetail: undefined,
getSongList: undefined,
getTopSongs: ssController.getTopSongList,
getUserList: undefined,
scrobble: ssController.scrobble,
search: ssController.search3,
setRating: undefined,
updatePlaylist: undefined,
},
jellyfin: {
addToPlaylist: jfController.addToPlaylist,
authenticate: jfController.authenticate,
clearPlaylist: undefined,
createFavorite: jfController.createFavorite,
createPlaylist: jfController.createPlaylist,
deleteFavorite: jfController.deleteFavorite,
deletePlaylist: jfController.deletePlaylist,
getAlbumArtistDetail: jfController.getAlbumArtistDetail,
getAlbumArtistList: jfController.getAlbumArtistList,
getAlbumDetail: jfController.getAlbumDetail,
getAlbumList: jfController.getAlbumList,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: jfController.getGenreList,
getLyrics: jfController.getLyrics,
getMusicFolderList: jfController.getMusicFolderList,
getPlaylistDetail: jfController.getPlaylistDetail,
getPlaylistList: jfController.getPlaylistList,
getPlaylistSongList: jfController.getPlaylistSongList,
getRandomSongList: jfController.getRandomSongList,
getSongDetail: undefined,
getSongList: jfController.getSongList,
getTopSongs: jfController.getTopSongList,
getUserList: undefined,
removeFromPlaylist: jfController.removeFromPlaylist,
scrobble: jfController.scrobble,
search: jfController.search,
setRating: undefined,
updatePlaylist: jfController.updatePlaylist,
},
navidrome: {
addToPlaylist: ndController.addToPlaylist,
authenticate: ndController.authenticate,
clearPlaylist: undefined,
createFavorite: ssController.createFavorite,
createPlaylist: ndController.createPlaylist,
deleteFavorite: ssController.removeFavorite,
deletePlaylist: ndController.deletePlaylist,
getAlbumArtistDetail: ndController.getAlbumArtistDetail,
getAlbumArtistList: ndController.getAlbumArtistList,
getAlbumDetail: ndController.getAlbumDetail,
getAlbumList: ndController.getAlbumList,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: ndController.getGenreList,
getLyrics: undefined,
getMusicFolderList: ssController.getMusicFolderList,
getPlaylistDetail: ndController.getPlaylistDetail,
getPlaylistList: ndController.getPlaylistList,
getPlaylistSongList: ndController.getPlaylistSongList,
getRandomSongList: ssController.getRandomSongList,
getSongDetail: ndController.getSongDetail,
getSongList: ndController.getSongList,
getTopSongs: ssController.getTopSongList,
getUserList: ndController.getUserList,
removeFromPlaylist: ndController.removeFromPlaylist,
scrobble: ssController.scrobble,
search: ssController.search3,
setRating: ssController.setRating,
updatePlaylist: ndController.updatePlaylist,
},
subsonic: {
authenticate: ssController.authenticate,
clearPlaylist: undefined,
createFavorite: ssController.createFavorite,
createPlaylist: undefined,
deleteFavorite: ssController.removeFavorite,
deletePlaylist: undefined,
getAlbumArtistDetail: undefined,
getAlbumArtistList: undefined,
getAlbumDetail: undefined,
getAlbumList: undefined,
getArtistDetail: undefined,
getArtistInfo: undefined,
getArtistList: undefined,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: undefined,
getLyrics: undefined,
getMusicFolderList: ssController.getMusicFolderList,
getPlaylistDetail: undefined,
getPlaylistList: undefined,
getSongDetail: undefined,
getSongList: undefined,
getTopSongs: ssController.getTopSongList,
getUserList: undefined,
scrobble: ssController.scrobble,
search: ssController.search3,
setRating: undefined,
updatePlaylist: undefined,
},
};
const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => {
const serverType = type || useAuthStore.getState().currentServer?.type;
const serverType = type || useAuthStore.getState().currentServer?.type;
if (!serverType) {
toast.error({ message: 'No server selected', title: 'Unable to route request' });
throw new Error(`No server selected`);
}
if (!serverType) {
toast.error({ message: 'No server selected', title: 'Unable to route request' });
throw new Error(`No server selected`);
}
const controllerFn = endpoints?.[serverType]?.[endpoint];
const controllerFn = endpoints?.[serverType]?.[endpoint];
if (typeof controllerFn !== 'function') {
toast.error({
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
title: 'Unable to route request',
});
if (typeof controllerFn !== 'function') {
toast.error({
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
title: 'Unable to route request',
});
throw new Error(`Endpoint ${endpoint} is not implemented for ${serverType}`);
}
throw new Error(`Endpoint ${endpoint} is not implemented for ${serverType}`);
}
return endpoints[serverType][endpoint];
return endpoints[serverType][endpoint];
};
const authenticate = async (
url: string,
body: { legacy?: boolean; password: string; username: string },
type: ServerType,
url: string,
body: { legacy?: boolean; password: string; username: string },
type: ServerType,
) => {
return (apiController('authenticate', type) as ControllerEndpoint['authenticate'])?.(url, body);
return (apiController('authenticate', type) as ControllerEndpoint['authenticate'])?.(url, body);
};
const getAlbumList = async (args: AlbumListArgs) => {
return (
apiController(
'getAlbumList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumList']
)?.(args);
return (
apiController(
'getAlbumList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumList']
)?.(args);
};
const getAlbumDetail = async (args: AlbumDetailArgs) => {
return (
apiController(
'getAlbumDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumDetail']
)?.(args);
return (
apiController(
'getAlbumDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumDetail']
)?.(args);
};
const getSongList = async (args: SongListArgs) => {
return (
apiController(
'getSongList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getSongList']
)?.(args);
return (
apiController(
'getSongList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getSongList']
)?.(args);
};
const getSongDetail = async (args: SongDetailArgs) => {
return (
apiController(
'getSongDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getSongDetail']
)?.(args);
return (
apiController(
'getSongDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getSongDetail']
)?.(args);
};
const getMusicFolderList = async (args: MusicFolderListArgs) => {
return (
apiController(
'getMusicFolderList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getMusicFolderList']
)?.(args);
return (
apiController(
'getMusicFolderList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getMusicFolderList']
)?.(args);
};
const getGenreList = async (args: GenreListArgs) => {
return (
apiController(
'getGenreList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getGenreList']
)?.(args);
return (
apiController(
'getGenreList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getGenreList']
)?.(args);
};
const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs) => {
return (
apiController(
'getAlbumArtistDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumArtistDetail']
)?.(args);
return (
apiController(
'getAlbumArtistDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumArtistDetail']
)?.(args);
};
const getAlbumArtistList = async (args: AlbumArtistListArgs) => {
return (
apiController(
'getAlbumArtistList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumArtistList']
)?.(args);
return (
apiController(
'getAlbumArtistList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getAlbumArtistList']
)?.(args);
};
const getArtistList = async (args: ArtistListArgs) => {
return (
apiController(
'getArtistList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getArtistList']
)?.(args);
return (
apiController(
'getArtistList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getArtistList']
)?.(args);
};
const getPlaylistList = async (args: PlaylistListArgs) => {
return (
apiController(
'getPlaylistList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getPlaylistList']
)?.(args);
return (
apiController(
'getPlaylistList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getPlaylistList']
)?.(args);
};
const createPlaylist = async (args: CreatePlaylistArgs) => {
return (
apiController(
'createPlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['createPlaylist']
)?.(args);
return (
apiController(
'createPlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['createPlaylist']
)?.(args);
};
const updatePlaylist = async (args: UpdatePlaylistArgs) => {
return (
apiController(
'updatePlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['updatePlaylist']
)?.(args);
return (
apiController(
'updatePlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['updatePlaylist']
)?.(args);
};
const deletePlaylist = async (args: DeletePlaylistArgs) => {
return (
apiController(
'deletePlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['deletePlaylist']
)?.(args);
return (
apiController(
'deletePlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['deletePlaylist']
)?.(args);
};
const addToPlaylist = async (args: AddToPlaylistArgs) => {
return (
apiController(
'addToPlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['addToPlaylist']
)?.(args);
return (
apiController(
'addToPlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['addToPlaylist']
)?.(args);
};
const removeFromPlaylist = async (args: RemoveFromPlaylistArgs) => {
return (
apiController(
'removeFromPlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['removeFromPlaylist']
)?.(args);
return (
apiController(
'removeFromPlaylist',
args.apiClientProps.server?.type,
) as ControllerEndpoint['removeFromPlaylist']
)?.(args);
};
const getPlaylistDetail = async (args: PlaylistDetailArgs) => {
return (
apiController(
'getPlaylistDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getPlaylistDetail']
)?.(args);
return (
apiController(
'getPlaylistDetail',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getPlaylistDetail']
)?.(args);
};
const getPlaylistSongList = async (args: PlaylistSongListArgs) => {
return (
apiController(
'getPlaylistSongList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getPlaylistSongList']
)?.(args);
return (
apiController(
'getPlaylistSongList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getPlaylistSongList']
)?.(args);
};
const getUserList = async (args: UserListArgs) => {
return (
apiController(
'getUserList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getUserList']
)?.(args);
return (
apiController(
'getUserList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getUserList']
)?.(args);
};
const createFavorite = async (args: FavoriteArgs) => {
return (
apiController(
'createFavorite',
args.apiClientProps.server?.type,
) as ControllerEndpoint['createFavorite']
)?.(args);
return (
apiController(
'createFavorite',
args.apiClientProps.server?.type,
) as ControllerEndpoint['createFavorite']
)?.(args);
};
const deleteFavorite = async (args: FavoriteArgs) => {
return (
apiController(
'deleteFavorite',
args.apiClientProps.server?.type,
) as ControllerEndpoint['deleteFavorite']
)?.(args);
return (
apiController(
'deleteFavorite',
args.apiClientProps.server?.type,
) as ControllerEndpoint['deleteFavorite']
)?.(args);
};
const updateRating = async (args: SetRatingArgs) => {
return (
apiController('setRating', args.apiClientProps.server?.type) as ControllerEndpoint['setRating']
)?.(args);
return (
apiController(
'setRating',
args.apiClientProps.server?.type,
) as ControllerEndpoint['setRating']
)?.(args);
};
const getTopSongList = async (args: TopSongListArgs) => {
return (
apiController(
'getTopSongs',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getTopSongs']
)?.(args);
return (
apiController(
'getTopSongs',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getTopSongs']
)?.(args);
};
const scrobble = async (args: ScrobbleArgs) => {
return (
apiController('scrobble', args.apiClientProps.server?.type) as ControllerEndpoint['scrobble']
)?.(args);
return (
apiController(
'scrobble',
args.apiClientProps.server?.type,
) as ControllerEndpoint['scrobble']
)?.(args);
};
const search = async (args: SearchArgs) => {
return (
apiController('search', args.apiClientProps.server?.type) as ControllerEndpoint['search']
)?.(args);
return (
apiController('search', args.apiClientProps.server?.type) as ControllerEndpoint['search']
)?.(args);
};
const getRandomSongList = async (args: RandomSongListArgs) => {
return (
apiController(
'getRandomSongList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getRandomSongList']
)?.(args);
return (
apiController(
'getRandomSongList',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getRandomSongList']
)?.(args);
};
const getLyrics = async (args: LyricsArgs) => {
return (
apiController('getLyrics', args.apiClientProps.server?.type) as ControllerEndpoint['getLyrics']
)?.(args);
return (
apiController(
'getLyrics',
args.apiClientProps.server?.type,
) as ControllerEndpoint['getLyrics']
)?.(args);
};
export const controller = {
addToPlaylist,
authenticate,
createFavorite,
createPlaylist,
deleteFavorite,
deletePlaylist,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumDetail,
getAlbumList,
getArtistList,
getGenreList,
getLyrics,
getMusicFolderList,
getPlaylistDetail,
getPlaylistList,
getPlaylistSongList,
getRandomSongList,
getSongDetail,
getSongList,
getTopSongList,
getUserList,
removeFromPlaylist,
scrobble,
search,
updatePlaylist,
updateRating,
addToPlaylist,
authenticate,
createFavorite,
createPlaylist,
deleteFavorite,
deletePlaylist,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumDetail,
getAlbumList,
getArtistList,
getGenreList,
getLyrics,
getMusicFolderList,
getPlaylistDetail,
getPlaylistList,
getPlaylistSongList,
getRandomSongList,
getSongDetail,
getSongList,
getTopSongList,
getUserList,
removeFromPlaylist,
scrobble,
search,
updatePlaylist,
updateRating,
};

View file

@ -1,5 +1,5 @@
import { controller } from '/@/renderer/api/controller';
export const api = {
controller,
controller,
};

File diff suppressed because it is too large Load diff

View file

@ -11,340 +11,340 @@ import { authenticationFailure } from '/@/renderer/api/utils';
const c = initContract();
export const contract = c.router({
addToPlaylist: {
body: z.null(),
method: 'POST',
path: 'playlists/:id/items',
query: jfType._parameters.addToPlaylist,
responses: {
204: jfType._response.addToPlaylist,
400: jfType._response.error,
addToPlaylist: {
body: z.null(),
method: 'POST',
path: 'playlists/:id/items',
query: jfType._parameters.addToPlaylist,
responses: {
204: jfType._response.addToPlaylist,
400: jfType._response.error,
},
},
},
authenticate: {
body: jfType._parameters.authenticate,
headers: z.object({
'X-Emby-Authorization': z.string(),
}),
method: 'POST',
path: 'users/authenticatebyname',
responses: {
200: jfType._response.authenticate,
400: jfType._response.error,
authenticate: {
body: jfType._parameters.authenticate,
headers: z.object({
'X-Emby-Authorization': z.string(),
}),
method: 'POST',
path: 'users/authenticatebyname',
responses: {
200: jfType._response.authenticate,
400: jfType._response.error,
},
},
},
createFavorite: {
body: jfType._parameters.favorite,
method: 'POST',
path: 'users/:userId/favoriteitems/:id',
responses: {
200: jfType._response.favorite,
400: jfType._response.error,
createFavorite: {
body: jfType._parameters.favorite,
method: 'POST',
path: 'users/:userId/favoriteitems/:id',
responses: {
200: jfType._response.favorite,
400: jfType._response.error,
},
},
},
createPlaylist: {
body: jfType._parameters.createPlaylist,
method: 'POST',
path: 'playlists',
responses: {
200: jfType._response.createPlaylist,
400: jfType._response.error,
createPlaylist: {
body: jfType._parameters.createPlaylist,
method: 'POST',
path: 'playlists',
responses: {
200: jfType._response.createPlaylist,
400: jfType._response.error,
},
},
},
deletePlaylist: {
body: null,
method: 'DELETE',
path: 'items/:id',
responses: {
204: jfType._response.deletePlaylist,
400: jfType._response.error,
deletePlaylist: {
body: null,
method: 'DELETE',
path: 'items/:id',
responses: {
204: jfType._response.deletePlaylist,
400: jfType._response.error,
},
},
},
getAlbumArtistDetail: {
method: 'GET',
path: 'users/:userId/items/:id',
query: jfType._parameters.albumArtistDetail,
responses: {
200: jfType._response.albumArtist,
400: jfType._response.error,
getAlbumArtistDetail: {
method: 'GET',
path: 'users/:userId/items/:id',
query: jfType._parameters.albumArtistDetail,
responses: {
200: jfType._response.albumArtist,
400: jfType._response.error,
},
},
},
getAlbumArtistList: {
method: 'GET',
path: 'artists/albumArtists',
query: jfType._parameters.albumArtistList,
responses: {
200: jfType._response.albumArtistList,
400: jfType._response.error,
getAlbumArtistList: {
method: 'GET',
path: 'artists/albumArtists',
query: jfType._parameters.albumArtistList,
responses: {
200: jfType._response.albumArtistList,
400: jfType._response.error,
},
},
},
getAlbumDetail: {
method: 'GET',
path: 'users/:userId/items/:id',
query: jfType._parameters.albumDetail,
responses: {
200: jfType._response.album,
400: jfType._response.error,
getAlbumDetail: {
method: 'GET',
path: 'users/:userId/items/:id',
query: jfType._parameters.albumDetail,
responses: {
200: jfType._response.album,
400: jfType._response.error,
},
},
},
getAlbumList: {
method: 'GET',
path: 'users/:userId/items',
query: jfType._parameters.albumList,
responses: {
200: jfType._response.albumList,
400: jfType._response.error,
getAlbumList: {
method: 'GET',
path: 'users/:userId/items',
query: jfType._parameters.albumList,
responses: {
200: jfType._response.albumList,
400: jfType._response.error,
},
},
},
getArtistList: {
method: 'GET',
path: 'artists',
query: jfType._parameters.albumArtistList,
responses: {
200: jfType._response.albumArtistList,
400: jfType._response.error,
getArtistList: {
method: 'GET',
path: 'artists',
query: jfType._parameters.albumArtistList,
responses: {
200: jfType._response.albumArtistList,
400: jfType._response.error,
},
},
},
getGenreList: {
method: 'GET',
path: 'genres',
responses: {
200: jfType._response.genreList,
400: jfType._response.error,
getGenreList: {
method: 'GET',
path: 'genres',
responses: {
200: jfType._response.genreList,
400: jfType._response.error,
},
},
},
getMusicFolderList: {
method: 'GET',
path: 'users/:userId/items',
responses: {
200: jfType._response.musicFolderList,
400: jfType._response.error,
getMusicFolderList: {
method: 'GET',
path: 'users/:userId/items',
responses: {
200: jfType._response.musicFolderList,
400: jfType._response.error,
},
},
},
getPlaylistDetail: {
method: 'GET',
path: 'users/:userId/items/:id',
query: jfType._parameters.playlistDetail,
responses: {
200: jfType._response.playlist,
400: jfType._response.error,
getPlaylistDetail: {
method: 'GET',
path: 'users/:userId/items/:id',
query: jfType._parameters.playlistDetail,
responses: {
200: jfType._response.playlist,
400: jfType._response.error,
},
},
},
getPlaylistList: {
method: 'GET',
path: 'users/:userId/items',
query: jfType._parameters.playlistList,
responses: {
200: jfType._response.playlistList,
400: jfType._response.error,
getPlaylistList: {
method: 'GET',
path: 'users/:userId/items',
query: jfType._parameters.playlistList,
responses: {
200: jfType._response.playlistList,
400: jfType._response.error,
},
},
},
getPlaylistSongList: {
method: 'GET',
path: 'playlists/:id/items',
query: jfType._parameters.songList,
responses: {
200: jfType._response.playlistSongList,
400: jfType._response.error,
getPlaylistSongList: {
method: 'GET',
path: 'playlists/:id/items',
query: jfType._parameters.songList,
responses: {
200: jfType._response.playlistSongList,
400: jfType._response.error,
},
},
},
getSimilarArtistList: {
method: 'GET',
path: 'artists/:id/similar',
query: jfType._parameters.similarArtistList,
responses: {
200: jfType._response.albumArtistList,
400: jfType._response.error,
getSimilarArtistList: {
method: 'GET',
path: 'artists/:id/similar',
query: jfType._parameters.similarArtistList,
responses: {
200: jfType._response.albumArtistList,
400: jfType._response.error,
},
},
},
getSongDetail: {
method: 'GET',
path: 'song/:id',
responses: {
200: jfType._response.song,
400: jfType._response.error,
getSongDetail: {
method: 'GET',
path: 'song/:id',
responses: {
200: jfType._response.song,
400: jfType._response.error,
},
},
},
getSongList: {
method: 'GET',
path: 'users/:userId/items',
query: jfType._parameters.songList,
responses: {
200: jfType._response.songList,
400: jfType._response.error,
getSongList: {
method: 'GET',
path: 'users/:userId/items',
query: jfType._parameters.songList,
responses: {
200: jfType._response.songList,
400: jfType._response.error,
},
},
},
getSongLyrics: {
method: 'GET',
path: 'users/:userId/Items/:id/Lyrics',
responses: {
200: jfType._response.lyrics,
404: jfType._response.error,
getSongLyrics: {
method: 'GET',
path: 'users/:userId/Items/:id/Lyrics',
responses: {
200: jfType._response.lyrics,
404: jfType._response.error,
},
},
},
getTopSongsList: {
method: 'GET',
path: 'users/:userId/items',
query: jfType._parameters.songList,
responses: {
200: jfType._response.topSongsList,
400: jfType._response.error,
getTopSongsList: {
method: 'GET',
path: 'users/:userId/items',
query: jfType._parameters.songList,
responses: {
200: jfType._response.topSongsList,
400: jfType._response.error,
},
},
},
removeFavorite: {
body: jfType._parameters.favorite,
method: 'DELETE',
path: 'users/:userId/favoriteitems/:id',
responses: {
200: jfType._response.favorite,
400: jfType._response.error,
removeFavorite: {
body: jfType._parameters.favorite,
method: 'DELETE',
path: 'users/:userId/favoriteitems/:id',
responses: {
200: jfType._response.favorite,
400: jfType._response.error,
},
},
},
removeFromPlaylist: {
body: null,
method: 'DELETE',
path: 'playlists/:id/items',
query: jfType._parameters.removeFromPlaylist,
responses: {
200: jfType._response.removeFromPlaylist,
400: jfType._response.error,
removeFromPlaylist: {
body: null,
method: 'DELETE',
path: 'playlists/:id/items',
query: jfType._parameters.removeFromPlaylist,
responses: {
200: jfType._response.removeFromPlaylist,
400: jfType._response.error,
},
},
},
scrobblePlaying: {
body: jfType._parameters.scrobble,
method: 'POST',
path: 'sessions/playing',
responses: {
200: jfType._response.scrobble,
400: jfType._response.error,
scrobblePlaying: {
body: jfType._parameters.scrobble,
method: 'POST',
path: 'sessions/playing',
responses: {
200: jfType._response.scrobble,
400: jfType._response.error,
},
},
},
scrobbleProgress: {
body: jfType._parameters.scrobble,
method: 'POST',
path: 'sessions/playing/progress',
responses: {
200: jfType._response.scrobble,
400: jfType._response.error,
scrobbleProgress: {
body: jfType._parameters.scrobble,
method: 'POST',
path: 'sessions/playing/progress',
responses: {
200: jfType._response.scrobble,
400: jfType._response.error,
},
},
},
scrobbleStopped: {
body: jfType._parameters.scrobble,
method: 'POST',
path: 'sessions/playing/stopped',
responses: {
200: jfType._response.scrobble,
400: jfType._response.error,
scrobbleStopped: {
body: jfType._parameters.scrobble,
method: 'POST',
path: 'sessions/playing/stopped',
responses: {
200: jfType._response.scrobble,
400: jfType._response.error,
},
},
},
search: {
method: 'GET',
path: 'users/:userId/items',
query: jfType._parameters.search,
responses: {
200: jfType._response.search,
400: jfType._response.error,
search: {
method: 'GET',
path: 'users/:userId/items',
query: jfType._parameters.search,
responses: {
200: jfType._response.search,
400: jfType._response.error,
},
},
},
updatePlaylist: {
body: jfType._parameters.updatePlaylist,
method: 'PUT',
path: 'items/:id',
responses: {
200: jfType._response.updatePlaylist,
400: jfType._response.error,
updatePlaylist: {
body: jfType._parameters.updatePlaylist,
method: 'PUT',
path: 'items/:id',
responses: {
200: jfType._response.updatePlaylist,
400: jfType._response.error,
},
},
},
});
const axiosClient = axios.create({});
axiosClient.defaults.paramsSerializer = (params) => {
return qs.stringify(params, { arrayFormat: 'repeat' });
return qs.stringify(params, { arrayFormat: 'repeat' });
};
axiosClient.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response && error.response.status === 401) {
const currentServer = useAuthStore.getState().currentServer;
(response) => {
return response;
},
(error) => {
if (error.response && error.response.status === 401) {
const currentServer = useAuthStore.getState().currentServer;
authenticationFailure(currentServer);
}
authenticationFailure(currentServer);
}
return Promise.reject(error);
},
return Promise.reject(error);
},
);
const parsePath = (fullPath: string) => {
const [path, params] = fullPath.split('?');
const [path, params] = fullPath.split('?');
const parsedParams = qs.parse(params);
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
const parsedParams = qs.parse(params);
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
return {
params: notNilParams,
path,
};
return {
params: notNilParams,
path,
};
};
export const jfApiClient = (args: {
server: ServerListItem | null;
signal?: AbortSignal;
url?: string;
server: ServerListItem | null;
signal?: AbortSignal;
url?: string;
}) => {
const { server, url, signal } = args;
const { server, url, signal } = args;
return initClient(contract, {
api: async ({ path, method, headers, body }) => {
let baseUrl: string | undefined;
let token: string | undefined;
return initClient(contract, {
api: async ({ path, method, headers, body }) => {
let baseUrl: string | undefined;
let token: string | undefined;
const { params, path: api } = parsePath(path);
const { params, path: api } = parsePath(path);
if (server) {
baseUrl = `${server?.url}`;
token = server?.credential;
} else {
baseUrl = url;
}
if (server) {
baseUrl = `${server?.url}`;
token = server?.credential;
} else {
baseUrl = url;
}
try {
const result = await axiosClient.request({
data: body,
headers: {
...headers,
...(token && { 'X-MediaBrowser-Token': token }),
},
method: method as Method,
params,
signal,
url: `${baseUrl}/${api}`,
});
return {
body: result.data,
headers: result.headers as any,
status: result.status,
};
} catch (e: Error | AxiosError | any) {
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return {
body: response?.data,
headers: response?.headers as any,
status: response.status,
};
}
throw e;
}
},
baseHeaders: {
'Content-Type': 'application/json',
},
baseUrl: '',
jsonQuery: false,
});
try {
const result = await axiosClient.request({
data: body,
headers: {
...headers,
...(token && { 'X-MediaBrowser-Token': token }),
},
method: method as Method,
params,
signal,
url: `${baseUrl}/${api}`,
});
return {
body: result.data,
headers: result.headers as any,
status: result.status,
};
} catch (e: Error | AxiosError | any) {
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return {
body: response?.data,
headers: response?.headers as any,
status: response.status,
};
}
throw e;
}
},
baseHeaders: {
'Content-Type': 'application/json',
},
baseUrl: '',
jsonQuery: false,
});
};

File diff suppressed because it is too large Load diff

View file

@ -3,312 +3,316 @@ import { z } from 'zod';
import { JFAlbum, JFPlaylist, JFMusicFolder, JFGenre } from '/@/renderer/api/jellyfin.types';
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import {
Song,
LibraryItem,
Album,
AlbumArtist,
Playlist,
MusicFolder,
Genre,
Song,
LibraryItem,
Album,
AlbumArtist,
Playlist,
MusicFolder,
Genre,
} from '/@/renderer/api/types';
import { ServerListItem, ServerType } from '/@/renderer/types';
const getStreamUrl = (args: {
container?: string;
deviceId: string;
eTag?: string;
id: string;
mediaSourceId?: string;
server: ServerListItem | null;
container?: string;
deviceId: string;
eTag?: string;
id: string;
mediaSourceId?: string;
server: ServerListItem | null;
}) => {
const { id, server, deviceId } = args;
const { id, server, deviceId } = args;
return (
`${server?.url}/audio` +
`/${id}/universal` +
`?userId=${server?.userId}` +
`&deviceId=${deviceId}` +
'&audioCodec=aac' +
`&api_key=${server?.credential}` +
`&playSessionId=${deviceId}` +
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
'&transcodingContainer=ts' +
'&transcodingProtocol=hls'
);
return (
`${server?.url}/audio` +
`/${id}/universal` +
`?userId=${server?.userId}` +
`&deviceId=${deviceId}` +
'&audioCodec=aac' +
`&api_key=${server?.credential}` +
`&playSessionId=${deviceId}` +
'&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' +
'&transcodingContainer=ts' +
'&transcodingProtocol=hls'
);
};
const getAlbumArtistCoverArtUrl = (args: {
baseUrl: string;
item: z.infer<typeof jfType._response.albumArtist>;
size: number;
baseUrl: string;
item: z.infer<typeof jfType._response.albumArtist>;
size: number;
}) => {
const size = args.size ? args.size : 300;
const size = args.size ? args.size : 300;
if (!args.item.ImageTags?.Primary) {
return null;
}
if (!args.item.ImageTags?.Primary) {
return null;
}
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
};
const getAlbumCoverArtUrl = (args: { baseUrl: string; item: JFAlbum; size: number }) => {
const size = args.size ? args.size : 300;
const size = args.size ? args.size : 300;
if (!args.item.ImageTags?.Primary && !args.item?.AlbumPrimaryImageTag) {
return null;
}
if (!args.item.ImageTags?.Primary && !args.item?.AlbumPrimaryImageTag) {
return null;
}
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
};
const getSongCoverArtUrl = (args: {
baseUrl: string;
item: z.infer<typeof jfType._response.song>;
size: number;
baseUrl: string;
item: z.infer<typeof jfType._response.song>;
size: number;
}) => {
const size = args.size ? args.size : 100;
const size = args.size ? args.size : 100;
if (args.item.ImageTags.Primary) {
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
}
if (args.item.ImageTags.Primary) {
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
}
if (args.item?.AlbumPrimaryImageTag) {
// Fall back to album art if no image embedded
return (
`${args.baseUrl}/Items` +
`/${args.item?.AlbumId}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
}
if (args.item?.AlbumPrimaryImageTag) {
// Fall back to album art if no image embedded
return (
`${args.baseUrl}/Items` +
`/${args.item?.AlbumId}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
}
return null;
return null;
};
const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size: number }) => {
const size = args.size ? args.size : 300;
const size = args.size ? args.size : 300;
if (!args.item.ImageTags?.Primary) {
return null;
}
if (!args.item.ImageTags?.Primary) {
return null;
}
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
return (
`${args.baseUrl}/Items` +
`/${args.item.Id}` +
'/Images/Primary' +
`?width=${size}&height=${size}` +
'&quality=96'
);
};
const normalizeSong = (
item: z.infer<typeof jfType._response.song>,
server: ServerListItem | null,
deviceId: string,
imageSize?: number,
item: z.infer<typeof jfType._response.song>,
server: ServerListItem | null,
deviceId: string,
imageSize?: number,
): Song => {
return {
album: item.Album,
albumArtists: item.AlbumArtists?.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})),
albumId: item.AlbumId,
artistName: item?.ArtistItems?.[0]?.Name,
artists: item?.ArtistItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})),
bitRate: item.MediaSources && Number(Math.trunc(item.MediaSources[0]?.Bitrate / 1000)),
bpm: null,
channels: null,
comment: null,
compilation: null,
container: (item.MediaSources && item.MediaSources[0]?.Container) || null,
createdAt: item.DateCreated,
discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1,
duration: item.RunTimeTicks / 10000000,
genres: item.GenreItems.map((entry: any) => ({ id: entry.Id, name: entry.Name })),
id: item.Id,
imagePlaceholderUrl: null,
imageUrl: getSongCoverArtUrl({ baseUrl: server?.url || '', item, size: imageSize || 100 }),
itemType: LibraryItem.SONG,
lastPlayedAt: null,
lyrics: null,
name: item.Name,
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
playCount: (item.UserData && item.UserData.PlayCount) || 0,
playlistItemId: item.PlaylistItemId,
// releaseDate: (item.ProductionYear && new Date(item.ProductionYear, 0, 1).toISOString()) || null,
releaseDate: null,
releaseYear: item.ProductionYear ? String(item.ProductionYear) : null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: item.MediaSources && item.MediaSources[0]?.Size,
streamUrl: getStreamUrl({
container: item.MediaSources?.[0]?.Container,
deviceId,
eTag: item.MediaSources?.[0]?.ETag,
id: item.Id,
mediaSourceId: item.MediaSources?.[0]?.Id,
server,
}),
trackNumber: item.IndexNumber,
uniqueId: nanoid(),
updatedAt: item.DateCreated,
userFavorite: (item.UserData && item.UserData.IsFavorite) || false,
userRating: null,
};
return {
album: item.Album,
albumArtists: item.AlbumArtists?.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})),
albumId: item.AlbumId,
artistName: item?.ArtistItems?.[0]?.Name,
artists: item?.ArtistItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})),
bitRate: item.MediaSources && Number(Math.trunc(item.MediaSources[0]?.Bitrate / 1000)),
bpm: null,
channels: null,
comment: null,
compilation: null,
container: (item.MediaSources && item.MediaSources[0]?.Container) || null,
createdAt: item.DateCreated,
discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1,
duration: item.RunTimeTicks / 10000000,
genres: item.GenreItems.map((entry: any) => ({ id: entry.Id, name: entry.Name })),
id: item.Id,
imagePlaceholderUrl: null,
imageUrl: getSongCoverArtUrl({ baseUrl: server?.url || '', item, size: imageSize || 100 }),
itemType: LibraryItem.SONG,
lastPlayedAt: null,
lyrics: null,
name: item.Name,
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
playCount: (item.UserData && item.UserData.PlayCount) || 0,
playlistItemId: item.PlaylistItemId,
// releaseDate: (item.ProductionYear && new Date(item.ProductionYear, 0, 1).toISOString()) || null,
releaseDate: null,
releaseYear: item.ProductionYear ? String(item.ProductionYear) : null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: item.MediaSources && item.MediaSources[0]?.Size,
streamUrl: getStreamUrl({
container: item.MediaSources?.[0]?.Container,
deviceId,
eTag: item.MediaSources?.[0]?.ETag,
id: item.Id,
mediaSourceId: item.MediaSources?.[0]?.Id,
server,
}),
trackNumber: item.IndexNumber,
uniqueId: nanoid(),
updatedAt: item.DateCreated,
userFavorite: (item.UserData && item.UserData.IsFavorite) || false,
userRating: null,
};
};
const normalizeAlbum = (
item: z.infer<typeof jfType._response.album>,
server: ServerListItem | null,
imageSize?: number,
item: z.infer<typeof jfType._response.album>,
server: ServerListItem | null,
imageSize?: number,
): Album => {
return {
albumArtists:
item.AlbumArtists.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})) || [],
artists: item.ArtistItems?.map((entry) => ({ id: entry.Id, imageUrl: null, name: entry.Name })),
backdropImageUrl: null,
createdAt: item.DateCreated,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
id: item.Id,
imagePlaceholderUrl: null,
imageUrl: getAlbumCoverArtUrl({
baseUrl: server?.url || '',
item,
size: imageSize || 300,
}),
isCompilation: null,
itemType: LibraryItem.ALBUM,
lastPlayedAt: null,
name: item.Name,
playCount: item.UserData?.PlayCount || 0,
releaseDate: item.PremiereDate?.split('T')[0] || null,
releaseYear: item.ProductionYear || null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: null,
songCount: item?.ChildCount || null,
songs: item.Songs?.map((song) => normalizeSong(song, server, '', imageSize)),
uniqueId: nanoid(),
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
};
return {
albumArtists:
item.AlbumArtists.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})) || [],
artists: item.ArtistItems?.map((entry) => ({
id: entry.Id,
imageUrl: null,
name: entry.Name,
})),
backdropImageUrl: null,
createdAt: item.DateCreated,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
id: item.Id,
imagePlaceholderUrl: null,
imageUrl: getAlbumCoverArtUrl({
baseUrl: server?.url || '',
item,
size: imageSize || 300,
}),
isCompilation: null,
itemType: LibraryItem.ALBUM,
lastPlayedAt: null,
name: item.Name,
playCount: item.UserData?.PlayCount || 0,
releaseDate: item.PremiereDate?.split('T')[0] || null,
releaseYear: item.ProductionYear || null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: null,
songCount: item?.ChildCount || null,
songs: item.Songs?.map((song) => normalizeSong(song, server, '', imageSize)),
uniqueId: nanoid(),
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
};
};
const normalizeAlbumArtist = (
item: z.infer<typeof jfType._response.albumArtist> & {
similarArtists?: z.infer<typeof jfType._response.albumArtistList>;
},
server: ServerListItem | null,
imageSize?: number,
item: z.infer<typeof jfType._response.albumArtist> & {
similarArtists?: z.infer<typeof jfType._response.albumArtistList>;
},
server: ServerListItem | null,
imageSize?: number,
): AlbumArtist => {
const similarArtists =
item.similarArtists?.Items?.filter((entry) => entry.Name !== 'Various Artists').map(
(entry) => ({
id: entry.Id,
imageUrl: getAlbumArtistCoverArtUrl({
baseUrl: server?.url || '',
item: entry,
size: imageSize || 300,
}),
name: entry.Name,
}),
) || [];
const similarArtists =
item.similarArtists?.Items?.filter((entry) => entry.Name !== 'Various Artists').map(
(entry) => ({
id: entry.Id,
imageUrl: getAlbumArtistCoverArtUrl({
baseUrl: server?.url || '',
item: entry,
size: imageSize || 300,
}),
name: entry.Name,
}),
) || [];
return {
albumCount: null,
backgroundImageUrl: null,
biography: item.Overview || null,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
id: item.Id,
imageUrl: getAlbumArtistCoverArtUrl({
baseUrl: server?.url || '',
item,
size: imageSize || 300,
}),
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: null,
name: item.Name,
playCount: item.UserData?.PlayCount || 0,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
similarArtists,
songCount: null,
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
};
return {
albumCount: null,
backgroundImageUrl: null,
biography: item.Overview || null,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
id: item.Id,
imageUrl: getAlbumArtistCoverArtUrl({
baseUrl: server?.url || '',
item,
size: imageSize || 300,
}),
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: null,
name: item.Name,
playCount: item.UserData?.PlayCount || 0,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
similarArtists,
songCount: null,
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
};
};
const normalizePlaylist = (
item: z.infer<typeof jfType._response.playlist>,
server: ServerListItem | null,
imageSize?: number,
item: z.infer<typeof jfType._response.playlist>,
server: ServerListItem | null,
imageSize?: number,
): Playlist => {
const imageUrl = getPlaylistCoverArtUrl({
baseUrl: server?.url || '',
item,
size: imageSize || 300,
});
const imageUrl = getPlaylistCoverArtUrl({
baseUrl: server?.url || '',
item,
size: imageSize || 300,
});
const imagePlaceholderUrl = null;
const imagePlaceholderUrl = null;
return {
description: item.Overview || null,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
id: item.Id,
imagePlaceholderUrl,
imageUrl: imageUrl || null,
itemType: LibraryItem.PLAYLIST,
name: item.Name,
owner: null,
ownerId: null,
public: null,
rules: null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: null,
songCount: item?.ChildCount || null,
sync: null,
};
return {
description: item.Overview || null,
duration: item.RunTimeTicks / 10000,
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
id: item.Id,
imagePlaceholderUrl,
imageUrl: imageUrl || null,
itemType: LibraryItem.PLAYLIST,
name: item.Name,
owner: null,
ownerId: null,
public: null,
rules: null,
serverId: server?.id || '',
serverType: ServerType.JELLYFIN,
size: null,
songCount: item?.ChildCount || null,
sync: null,
};
};
const normalizeMusicFolder = (item: JFMusicFolder): MusicFolder => {
return {
id: item.Id,
name: item.Name,
};
return {
id: item.Id,
name: item.Name,
};
};
// const normalizeArtist = (item: any) => {
@ -332,12 +336,12 @@ const normalizeMusicFolder = (item: JFMusicFolder): MusicFolder => {
// };
const normalizeGenre = (item: JFGenre): Genre => {
return {
albumCount: undefined,
id: item.Id,
name: item.Name,
songCount: undefined,
};
return {
albumCount: undefined,
id: item.Id,
name: item.Name,
songCount: undefined,
};
};
// const normalizeFolder = (item: any) => {
@ -360,10 +364,10 @@ const normalizeGenre = (item: JFGenre): Genre => {
// };
export const jfNormalize = {
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
genre: normalizeGenre,
musicFolder: normalizeMusicFolder,
playlist: normalizePlaylist,
song: normalizeSong,
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
genre: normalizeGenre,
musicFolder: normalizeMusicFolder,
playlist: normalizePlaylist,
song: normalizeSong,
};

File diff suppressed because it is too large Load diff

View file

@ -1,143 +1,143 @@
import { SSArtistInfo } from '/@/renderer/api/subsonic.types';
export type NDAuthenticate = {
id: string;
isAdmin: boolean;
name: string;
subsonicSalt: string;
subsonicToken: string;
token: string;
username: string;
id: string;
isAdmin: boolean;
name: string;
subsonicSalt: string;
subsonicToken: string;
token: string;
username: string;
};
export type NDUser = {
createdAt: string;
email: string;
id: string;
isAdmin: boolean;
lastAccessAt: string;
lastLoginAt: string;
name: string;
updatedAt: string;
userName: string;
createdAt: string;
email: string;
id: string;
isAdmin: boolean;
lastAccessAt: string;
lastLoginAt: string;
name: string;
updatedAt: string;
userName: string;
};
export type NDGenre = {
id: string;
name: string;
id: string;
name: string;
};
export type NDAlbum = {
albumArtist: string;
albumArtistId: string;
allArtistIds: string;
artist: string;
artistId: string;
compilation: boolean;
coverArtId?: string; // Removed after v0.48.0
coverArtPath?: string; // Removed after v0.48.0
createdAt: string;
duration: number;
fullText: string;
genre: string;
genres: NDGenre[];
id: string;
maxYear: number;
mbzAlbumArtistId: string;
mbzAlbumId: string;
minYear: number;
name: string;
orderAlbumArtistName: string;
orderAlbumName: string;
playCount: number;
playDate: string;
rating: number;
size: number;
songCount: number;
sortAlbumArtistName: string;
sortArtistName: string;
starred: boolean;
starredAt: string;
updatedAt: string;
albumArtist: string;
albumArtistId: string;
allArtistIds: string;
artist: string;
artistId: string;
compilation: boolean;
coverArtId?: string; // Removed after v0.48.0
coverArtPath?: string; // Removed after v0.48.0
createdAt: string;
duration: number;
fullText: string;
genre: string;
genres: NDGenre[];
id: string;
maxYear: number;
mbzAlbumArtistId: string;
mbzAlbumId: string;
minYear: number;
name: string;
orderAlbumArtistName: string;
orderAlbumName: string;
playCount: number;
playDate: string;
rating: number;
size: number;
songCount: number;
sortAlbumArtistName: string;
sortArtistName: string;
starred: boolean;
starredAt: string;
updatedAt: string;
} & { songs?: NDSong[] };
export type NDSong = {
album: string;
albumArtist: string;
albumArtistId: string;
albumId: string;
artist: string;
artistId: string;
bitRate: number;
bookmarkPosition: number;
bpm?: number;
channels?: number;
comment?: string;
compilation: boolean;
createdAt: string;
discNumber: number;
duration: number;
fullText: string;
genre: string;
genres: NDGenre[];
hasCoverArt: boolean;
id: string;
lyrics?: string;
mbzAlbumArtistId: string;
mbzAlbumId: string;
mbzArtistId: string;
mbzTrackId: string;
orderAlbumArtistName: string;
orderAlbumName: string;
orderArtistName: string;
orderTitle: string;
path: string;
playCount: number;
playDate: string;
rating: number;
size: number;
sortAlbumArtistName: string;
sortArtistName: string;
starred: boolean;
starredAt: string;
suffix: string;
title: string;
trackNumber: number;
updatedAt: string;
year: number;
album: string;
albumArtist: string;
albumArtistId: string;
albumId: string;
artist: string;
artistId: string;
bitRate: number;
bookmarkPosition: number;
bpm?: number;
channels?: number;
comment?: string;
compilation: boolean;
createdAt: string;
discNumber: number;
duration: number;
fullText: string;
genre: string;
genres: NDGenre[];
hasCoverArt: boolean;
id: string;
lyrics?: string;
mbzAlbumArtistId: string;
mbzAlbumId: string;
mbzArtistId: string;
mbzTrackId: string;
orderAlbumArtistName: string;
orderAlbumName: string;
orderArtistName: string;
orderTitle: string;
path: string;
playCount: number;
playDate: string;
rating: number;
size: number;
sortAlbumArtistName: string;
sortArtistName: string;
starred: boolean;
starredAt: string;
suffix: string;
title: string;
trackNumber: number;
updatedAt: string;
year: number;
};
export type NDAlbumArtist = {
albumCount: number;
biography: string;
externalInfoUpdatedAt: string;
externalUrl: string;
fullText: string;
genres: NDGenre[];
id: string;
largeImageUrl?: string;
mbzArtistId: string;
mediumImageUrl?: string;
name: string;
orderArtistName: string;
playCount: number;
playDate: string;
rating: number;
size: number;
smallImageUrl?: string;
songCount: number;
starred: boolean;
starredAt: string;
albumCount: number;
biography: string;
externalInfoUpdatedAt: string;
externalUrl: string;
fullText: string;
genres: NDGenre[];
id: string;
largeImageUrl?: string;
mbzArtistId: string;
mediumImageUrl?: string;
name: string;
orderArtistName: string;
playCount: number;
playDate: string;
rating: number;
size: number;
smallImageUrl?: string;
songCount: number;
starred: boolean;
starredAt: string;
} & {
similarArtists?: SSArtistInfo['similarArtist'];
similarArtists?: SSArtistInfo['similarArtist'];
};
export type NDAuthenticationResponse = NDAuthenticate;
export type NDAlbumArtistList = {
items: NDAlbumArtist[];
startIndex: number;
totalRecordCount: number;
items: NDAlbumArtist[];
startIndex: number;
totalRecordCount: number;
};
export type NDAlbumArtistDetail = NDAlbumArtist;
@ -155,9 +155,9 @@ export type NDAlbumDetail = NDAlbum & { songs?: NDSongListResponse };
export type NDAlbumListResponse = NDAlbum[];
export type NDAlbumList = {
items: NDAlbum[];
startIndex: number;
totalRecordCount: number;
items: NDAlbum[];
startIndex: number;
totalRecordCount: number;
};
export type NDSongDetail = NDSong;
@ -167,142 +167,142 @@ export type NDSongDetailResponse = NDSong;
export type NDSongListResponse = NDSong[];
export type NDSongList = {
items: NDSong[];
startIndex: number;
totalRecordCount: number;
items: NDSong[];
startIndex: number;
totalRecordCount: number;
};
export type NDArtistListResponse = NDAlbumArtist[];
export type NDPagination = {
_end?: number;
_start?: number;
_end?: number;
_start?: number;
};
export enum NDSortOrder {
ASC = 'ASC',
DESC = 'DESC',
ASC = 'ASC',
DESC = 'DESC',
}
export type NDOrder = {
_order?: NDSortOrder;
_order?: NDSortOrder;
};
export enum NDGenreListSort {
NAME = 'name',
NAME = 'name',
}
export type NDGenreListParams = {
_sort?: NDGenreListSort;
id?: string;
_sort?: NDGenreListSort;
id?: string;
} & NDPagination &
NDOrder;
NDOrder;
export enum NDAlbumListSort {
ALBUM_ARTIST = 'albumArtist',
ARTIST = 'artist',
DURATION = 'duration',
NAME = 'name',
PLAY_COUNT = 'playCount',
PLAY_DATE = 'play_date',
RANDOM = 'random',
RATING = 'rating',
RECENTLY_ADDED = 'recently_added',
SONG_COUNT = 'songCount',
STARRED = 'starred',
YEAR = 'max_year',
ALBUM_ARTIST = 'albumArtist',
ARTIST = 'artist',
DURATION = 'duration',
NAME = 'name',
PLAY_COUNT = 'playCount',
PLAY_DATE = 'play_date',
RANDOM = 'random',
RATING = 'rating',
RECENTLY_ADDED = 'recently_added',
SONG_COUNT = 'songCount',
STARRED = 'starred',
YEAR = 'max_year',
}
export type NDAlbumListParams = {
_sort?: NDAlbumListSort;
album_id?: string;
artist_id?: string;
compilation?: boolean;
genre_id?: string;
has_rating?: boolean;
id?: string;
name?: string;
recently_played?: boolean;
starred?: boolean;
year?: number;
_sort?: NDAlbumListSort;
album_id?: string;
artist_id?: string;
compilation?: boolean;
genre_id?: string;
has_rating?: boolean;
id?: string;
name?: string;
recently_played?: boolean;
starred?: boolean;
year?: number;
} & NDPagination &
NDOrder;
NDOrder;
export enum NDSongListSort {
ALBUM = 'album, order_album_artist_name, disc_number, track_number, title',
ALBUM_ARTIST = 'order_album_artist_name, album, disc_number, track_number, title',
ALBUM_SONGS = 'album, discNumber, trackNumber',
ARTIST = 'artist',
BPM = 'bpm',
CHANNELS = 'channels',
COMMENT = 'comment',
DURATION = 'duration',
FAVORITED = 'starred ASC, starredAt ASC',
GENRE = 'genre',
ID = 'id',
PLAY_COUNT = 'playCount',
PLAY_DATE = 'playDate',
RATING = 'rating',
RECENTLY_ADDED = 'createdAt',
TITLE = 'title',
TRACK = 'track',
YEAR = 'year, album, discNumber, trackNumber',
ALBUM = 'album, order_album_artist_name, disc_number, track_number, title',
ALBUM_ARTIST = 'order_album_artist_name, album, disc_number, track_number, title',
ALBUM_SONGS = 'album, discNumber, trackNumber',
ARTIST = 'artist',
BPM = 'bpm',
CHANNELS = 'channels',
COMMENT = 'comment',
DURATION = 'duration',
FAVORITED = 'starred ASC, starredAt ASC',
GENRE = 'genre',
ID = 'id',
PLAY_COUNT = 'playCount',
PLAY_DATE = 'playDate',
RATING = 'rating',
RECENTLY_ADDED = 'createdAt',
TITLE = 'title',
TRACK = 'track',
YEAR = 'year, album, discNumber, trackNumber',
}
export type NDSongListParams = {
_sort?: NDSongListSort;
album_id?: string[];
artist_id?: string[];
genre_id?: string;
starred?: boolean;
_sort?: NDSongListSort;
album_id?: string[];
artist_id?: string[];
genre_id?: string;
starred?: boolean;
} & NDPagination &
NDOrder;
NDOrder;
export enum NDAlbumArtistListSort {
ALBUM_COUNT = 'albumCount',
FAVORITED = 'starred ASC, starredAt ASC',
NAME = 'name',
PLAY_COUNT = 'playCount',
RATING = 'rating',
SONG_COUNT = 'songCount',
ALBUM_COUNT = 'albumCount',
FAVORITED = 'starred ASC, starredAt ASC',
NAME = 'name',
PLAY_COUNT = 'playCount',
RATING = 'rating',
SONG_COUNT = 'songCount',
}
export type NDAlbumArtistListParams = {
_sort?: NDAlbumArtistListSort;
genre_id?: string;
starred?: boolean;
_sort?: NDAlbumArtistListSort;
genre_id?: string;
starred?: boolean;
} & NDPagination &
NDOrder;
NDOrder;
export type NDAddToPlaylistResponse = {
added: number;
added: number;
};
export type NDAddToPlaylistBody = {
ids: string[];
ids: string[];
};
export type NDAddToPlaylist = null;
export type NDRemoveFromPlaylistResponse = {
ids: string[];
ids: string[];
};
export type NDRemoveFromPlaylistParams = {
id: string[];
id: string[];
};
export type NDRemoveFromPlaylist = null;
export type NDCreatePlaylistParams = {
comment?: string;
name: string;
public?: boolean;
rules?: Record<string, any> | null;
comment?: string;
name: string;
public?: boolean;
rules?: Record<string, any> | null;
};
export type NDCreatePlaylistResponse = {
id: string;
id: string;
};
export type NDCreatePlaylist = NDCreatePlaylistResponse;
@ -312,7 +312,7 @@ export type NDUpdatePlaylistParams = Partial<NDPlaylist>;
export type NDUpdatePlaylistResponse = NDPlaylist;
export type NDDeletePlaylistParams = {
id: string;
id: string;
};
export type NDDeletePlaylistResponse = null;
@ -320,21 +320,21 @@ 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: Record<string, any> | null;
size: number;
songCount: number;
sync: boolean;
updatedAt: string;
comment: string;
createdAt: string;
duration: number;
evaluatedAt: string;
id: string;
name: string;
ownerId: string;
ownerName: string;
path: string;
public: boolean;
rules: Record<string, any> | null;
size: number;
songCount: number;
sync: boolean;
updatedAt: string;
};
export type NDPlaylistDetail = NDPlaylist;
@ -342,125 +342,125 @@ export type NDPlaylistDetail = NDPlaylist;
export type NDPlaylistDetailResponse = NDPlaylist;
export type NDPlaylistList = {
items: NDPlaylist[];
startIndex: number;
totalRecordCount: number;
items: NDPlaylist[];
startIndex: number;
totalRecordCount: number;
};
export type NDPlaylistListResponse = NDPlaylist[];
export enum NDPlaylistListSort {
DURATION = 'duration',
NAME = 'name',
OWNER = 'ownerName',
PUBLIC = 'public',
SONG_COUNT = 'songCount',
UPDATED_AT = 'updatedAt',
DURATION = 'duration',
NAME = 'name',
OWNER = 'ownerName',
PUBLIC = 'public',
SONG_COUNT = 'songCount',
UPDATED_AT = 'updatedAt',
}
export type NDPlaylistListParams = {
_sort?: NDPlaylistListSort;
owner_id?: string;
_sort?: NDPlaylistListSort;
owner_id?: string;
} & NDPagination &
NDOrder;
NDOrder;
export type NDPlaylistSong = NDSong & {
mediaFileId: string;
playlistId: string;
mediaFileId: string;
playlistId: string;
};
export type NDPlaylistSongListResponse = NDPlaylistSong[];
export type NDPlaylistSongList = {
items: NDPlaylistSong[];
startIndex: number;
totalRecordCount: number;
items: NDPlaylistSong[];
startIndex: number;
totalRecordCount: number;
};
export const NDSongQueryFields = [
{ label: 'Album', type: 'string', value: 'album' },
{ label: 'Album Artist', type: 'string', value: 'albumartist' },
{ label: 'Album Comment', type: 'string', value: 'albumcomment' },
{ label: 'Album Type', type: 'string', value: 'albumtype' },
{ label: 'Artist', type: 'string', value: 'artist' },
{ label: 'Bitrate', type: 'number', value: 'bitrate' },
{ label: 'BPM', type: 'number', value: 'bpm' },
{ label: 'Catalog Number', type: 'string', value: 'catalognumber' },
{ label: 'Channels', type: 'number', value: 'channels' },
{ label: 'Comment', type: 'string', value: 'comment' },
{ label: 'Date Added', type: 'date', value: 'dateadded' },
{ label: 'Date Favorited', type: 'date', value: 'dateloved' },
{ label: 'Date Last Played', type: 'date', value: 'lastplayed' },
{ label: 'Date Modified', type: 'date', value: 'datemodified' },
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
{ label: 'Disc Number', type: 'number', value: 'discnumber' },
{ label: 'Duration', type: 'number', value: 'duration' },
{ label: 'File Path', type: 'string', value: 'filepath' },
{ label: 'File Type', type: 'string', value: 'filetype' },
{ label: 'Genre', type: 'string', value: 'genre' },
{ label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' },
{ label: 'Is Compilation', type: 'boolean', value: 'compilation' },
{ label: 'Is Favorite', type: 'boolean', value: 'loved' },
{ label: 'Lyrics', type: 'string', value: 'lyrics' },
{ label: 'Name', type: 'string', value: 'title' },
{ label: 'Play Count', type: 'number', value: 'playcount' },
{ label: 'Rating', type: 'number', value: 'rating' },
{ label: 'Size', type: 'number', value: 'size' },
{ label: 'Sort Album', type: 'string', value: 'sortalbum' },
{ label: 'Sort Album Artist', type: 'string', value: 'sortalbumartist' },
{ label: 'Sort Artist', type: 'string', value: 'sortartist' },
{ label: 'Sort Name', type: 'string', value: 'sorttitle' },
{ label: 'Track Number', type: 'number', value: 'tracknumber' },
{ label: 'Year', type: 'number', value: 'year' },
{ label: 'Album', type: 'string', value: 'album' },
{ label: 'Album Artist', type: 'string', value: 'albumartist' },
{ label: 'Album Comment', type: 'string', value: 'albumcomment' },
{ label: 'Album Type', type: 'string', value: 'albumtype' },
{ label: 'Artist', type: 'string', value: 'artist' },
{ label: 'Bitrate', type: 'number', value: 'bitrate' },
{ label: 'BPM', type: 'number', value: 'bpm' },
{ label: 'Catalog Number', type: 'string', value: 'catalognumber' },
{ label: 'Channels', type: 'number', value: 'channels' },
{ label: 'Comment', type: 'string', value: 'comment' },
{ label: 'Date Added', type: 'date', value: 'dateadded' },
{ label: 'Date Favorited', type: 'date', value: 'dateloved' },
{ label: 'Date Last Played', type: 'date', value: 'lastplayed' },
{ label: 'Date Modified', type: 'date', value: 'datemodified' },
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
{ label: 'Disc Number', type: 'number', value: 'discnumber' },
{ label: 'Duration', type: 'number', value: 'duration' },
{ label: 'File Path', type: 'string', value: 'filepath' },
{ label: 'File Type', type: 'string', value: 'filetype' },
{ label: 'Genre', type: 'string', value: 'genre' },
{ label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' },
{ label: 'Is Compilation', type: 'boolean', value: 'compilation' },
{ label: 'Is Favorite', type: 'boolean', value: 'loved' },
{ label: 'Lyrics', type: 'string', value: 'lyrics' },
{ label: 'Name', type: 'string', value: 'title' },
{ label: 'Play Count', type: 'number', value: 'playcount' },
{ label: 'Rating', type: 'number', value: 'rating' },
{ label: 'Size', type: 'number', value: 'size' },
{ label: 'Sort Album', type: 'string', value: 'sortalbum' },
{ label: 'Sort Album Artist', type: 'string', value: 'sortalbumartist' },
{ label: 'Sort Artist', type: 'string', value: 'sortartist' },
{ label: 'Sort Name', type: 'string', value: 'sorttitle' },
{ label: 'Track Number', type: 'number', value: 'tracknumber' },
{ label: 'Year', type: 'number', value: 'year' },
];
export const NDSongQueryDateOperators = [
{ label: 'is', value: 'is' },
{ label: 'is not', value: 'isNot' },
{ label: 'is before', value: 'before' },
{ label: 'is after', value: 'after' },
{ label: 'is in the last', value: 'inTheLast' },
{ label: 'is not in the last', value: 'notInTheLast' },
{ label: 'is in the range', value: 'inTheRange' },
{ label: 'is', value: 'is' },
{ label: 'is not', value: 'isNot' },
{ label: 'is before', value: 'before' },
{ label: 'is after', value: 'after' },
{ label: 'is in the last', value: 'inTheLast' },
{ label: 'is not in the last', value: 'notInTheLast' },
{ label: 'is in the range', value: 'inTheRange' },
];
export const NDSongQueryStringOperators = [
{ label: 'is', value: 'is' },
{ label: 'is not', value: 'isNot' },
{ label: 'contains', value: 'contains' },
{ label: 'does not contain', value: 'notContains' },
{ label: 'starts with', value: 'startsWith' },
{ label: 'ends with', value: 'endsWith' },
{ label: 'is', value: 'is' },
{ label: 'is not', value: 'isNot' },
{ label: 'contains', value: 'contains' },
{ label: 'does not contain', value: 'notContains' },
{ label: 'starts with', value: 'startsWith' },
{ label: 'ends with', value: 'endsWith' },
];
export const NDSongQueryBooleanOperators = [
{ label: 'is', value: 'is' },
{ label: 'is not', value: 'isNot' },
{ label: 'is', value: 'is' },
{ label: 'is not', value: 'isNot' },
];
export const NDSongQueryNumberOperators = [
{ label: 'is', value: 'is' },
{ label: 'is not', value: 'isNot' },
{ label: 'contains', value: 'contains' },
{ label: 'does not contain', value: 'notContains' },
{ label: 'is greater than', value: 'gt' },
{ label: 'is less than', value: 'lt' },
{ label: 'is in the range', value: 'inTheRange' },
{ label: 'is', value: 'is' },
{ label: 'is not', value: 'isNot' },
{ label: 'contains', value: 'contains' },
{ label: 'does not contain', value: 'notContains' },
{ label: 'is greater than', value: 'gt' },
{ label: 'is less than', value: 'lt' },
{ label: 'is in the range', value: 'inTheRange' },
];
export type NDUserListParams = {
_sort?: NDUserListSort;
_sort?: NDUserListSort;
} & NDPagination &
NDOrder;
NDOrder;
export type NDUserListResponse = NDUser[];
export type NDUserList = {
items: NDUser[];
startIndex: number;
totalRecordCount: number;
items: NDUser[];
startIndex: number;
totalRecordCount: number;
};
export enum NDUserListSort {
NAME = 'name',
NAME = 'name',
}

View file

@ -15,187 +15,188 @@ const localSettings = isElectron() ? window.electron.localSettings : null;
const c = initContract();
export const contract = c.router({
addToPlaylist: {
body: ndType._parameters.addToPlaylist,
method: 'POST',
path: 'playlist/:id/tracks',
responses: {
200: resultWithHeaders(ndType._response.addToPlaylist),
500: resultWithHeaders(ndType._response.error),
addToPlaylist: {
body: ndType._parameters.addToPlaylist,
method: 'POST',
path: 'playlist/:id/tracks',
responses: {
200: resultWithHeaders(ndType._response.addToPlaylist),
500: resultWithHeaders(ndType._response.error),
},
},
},
authenticate: {
body: ndType._parameters.authenticate,
method: 'POST',
path: 'auth/login',
responses: {
200: resultWithHeaders(ndType._response.authenticate),
500: resultWithHeaders(ndType._response.error),
authenticate: {
body: ndType._parameters.authenticate,
method: 'POST',
path: 'auth/login',
responses: {
200: resultWithHeaders(ndType._response.authenticate),
500: resultWithHeaders(ndType._response.error),
},
},
},
createPlaylist: {
body: ndType._parameters.createPlaylist,
method: 'POST',
path: 'playlist',
responses: {
200: resultWithHeaders(ndType._response.createPlaylist),
500: resultWithHeaders(ndType._response.error),
createPlaylist: {
body: ndType._parameters.createPlaylist,
method: 'POST',
path: 'playlist',
responses: {
200: resultWithHeaders(ndType._response.createPlaylist),
500: resultWithHeaders(ndType._response.error),
},
},
},
deletePlaylist: {
body: null,
method: 'DELETE',
path: 'playlist/:id',
responses: {
200: resultWithHeaders(ndType._response.deletePlaylist),
500: resultWithHeaders(ndType._response.error),
deletePlaylist: {
body: null,
method: 'DELETE',
path: 'playlist/:id',
responses: {
200: resultWithHeaders(ndType._response.deletePlaylist),
500: resultWithHeaders(ndType._response.error),
},
},
},
getAlbumArtistDetail: {
method: 'GET',
path: 'artist/:id',
responses: {
200: resultWithHeaders(ndType._response.albumArtist),
500: resultWithHeaders(ndType._response.error),
getAlbumArtistDetail: {
method: 'GET',
path: 'artist/:id',
responses: {
200: resultWithHeaders(ndType._response.albumArtist),
500: resultWithHeaders(ndType._response.error),
},
},
},
getAlbumArtistList: {
method: 'GET',
path: 'artist',
query: ndType._parameters.albumArtistList,
responses: {
200: resultWithHeaders(ndType._response.albumArtistList),
500: resultWithHeaders(ndType._response.error),
getAlbumArtistList: {
method: 'GET',
path: 'artist',
query: ndType._parameters.albumArtistList,
responses: {
200: resultWithHeaders(ndType._response.albumArtistList),
500: resultWithHeaders(ndType._response.error),
},
},
},
getAlbumDetail: {
method: 'GET',
path: 'album/:id',
responses: {
200: resultWithHeaders(ndType._response.album),
500: resultWithHeaders(ndType._response.error),
getAlbumDetail: {
method: 'GET',
path: 'album/:id',
responses: {
200: resultWithHeaders(ndType._response.album),
500: resultWithHeaders(ndType._response.error),
},
},
},
getAlbumList: {
method: 'GET',
path: 'album',
query: ndType._parameters.albumList,
responses: {
200: resultWithHeaders(ndType._response.albumList),
500: resultWithHeaders(ndType._response.error),
getAlbumList: {
method: 'GET',
path: 'album',
query: ndType._parameters.albumList,
responses: {
200: resultWithHeaders(ndType._response.albumList),
500: resultWithHeaders(ndType._response.error),
},
},
},
getGenreList: {
method: 'GET',
path: 'genre',
responses: {
200: resultWithHeaders(ndType._response.genreList),
500: resultWithHeaders(ndType._response.error),
getGenreList: {
method: 'GET',
path: 'genre',
responses: {
200: resultWithHeaders(ndType._response.genreList),
500: resultWithHeaders(ndType._response.error),
},
},
},
getPlaylistDetail: {
method: 'GET',
path: 'playlist/:id',
responses: {
200: resultWithHeaders(ndType._response.playlist),
500: resultWithHeaders(ndType._response.error),
getPlaylistDetail: {
method: 'GET',
path: 'playlist/:id',
responses: {
200: resultWithHeaders(ndType._response.playlist),
500: resultWithHeaders(ndType._response.error),
},
},
},
getPlaylistList: {
method: 'GET',
path: 'playlist',
query: ndType._parameters.playlistList,
responses: {
200: resultWithHeaders(ndType._response.playlistList),
500: resultWithHeaders(ndType._response.error),
getPlaylistList: {
method: 'GET',
path: 'playlist',
query: ndType._parameters.playlistList,
responses: {
200: resultWithHeaders(ndType._response.playlistList),
500: resultWithHeaders(ndType._response.error),
},
},
},
getPlaylistSongList: {
method: 'GET',
path: 'playlist/:id/tracks',
query: ndType._parameters.songList,
responses: {
200: resultWithHeaders(ndType._response.playlistSongList),
500: resultWithHeaders(ndType._response.error),
getPlaylistSongList: {
method: 'GET',
path: 'playlist/:id/tracks',
query: ndType._parameters.songList,
responses: {
200: resultWithHeaders(ndType._response.playlistSongList),
500: resultWithHeaders(ndType._response.error),
},
},
},
getSongDetail: {
method: 'GET',
path: 'song/:id',
responses: {
200: resultWithHeaders(ndType._response.song),
500: resultWithHeaders(ndType._response.error),
getSongDetail: {
method: 'GET',
path: 'song/:id',
responses: {
200: resultWithHeaders(ndType._response.song),
500: resultWithHeaders(ndType._response.error),
},
},
},
getSongList: {
method: 'GET',
path: 'song',
query: ndType._parameters.songList,
responses: {
200: resultWithHeaders(ndType._response.songList),
500: resultWithHeaders(ndType._response.error),
getSongList: {
method: 'GET',
path: 'song',
query: ndType._parameters.songList,
responses: {
200: resultWithHeaders(ndType._response.songList),
500: resultWithHeaders(ndType._response.error),
},
},
},
getUserList: {
method: 'GET',
path: 'user',
query: ndType._parameters.userList,
responses: {
200: resultWithHeaders(ndType._response.userList),
500: resultWithHeaders(ndType._response.error),
getUserList: {
method: 'GET',
path: 'user',
query: ndType._parameters.userList,
responses: {
200: resultWithHeaders(ndType._response.userList),
500: resultWithHeaders(ndType._response.error),
},
},
},
removeFromPlaylist: {
body: null,
method: 'DELETE',
path: 'playlist/:id/tracks',
query: ndType._parameters.removeFromPlaylist,
responses: {
200: resultWithHeaders(ndType._response.removeFromPlaylist),
500: resultWithHeaders(ndType._response.error),
removeFromPlaylist: {
body: null,
method: 'DELETE',
path: 'playlist/:id/tracks',
query: ndType._parameters.removeFromPlaylist,
responses: {
200: resultWithHeaders(ndType._response.removeFromPlaylist),
500: resultWithHeaders(ndType._response.error),
},
},
},
updatePlaylist: {
body: ndType._parameters.updatePlaylist,
method: 'PUT',
path: 'playlist/:id',
responses: {
200: resultWithHeaders(ndType._response.updatePlaylist),
500: resultWithHeaders(ndType._response.error),
updatePlaylist: {
body: ndType._parameters.updatePlaylist,
method: 'PUT',
path: 'playlist/:id',
responses: {
200: resultWithHeaders(ndType._response.updatePlaylist),
500: resultWithHeaders(ndType._response.error),
},
},
},
});
const axiosClient = axios.create({});
axiosClient.defaults.paramsSerializer = (params) => {
return qs.stringify(params, { arrayFormat: 'repeat' });
return qs.stringify(params, { arrayFormat: 'repeat' });
};
const parsePath = (fullPath: string) => {
const [path, params] = fullPath.split('?');
const [path, params] = fullPath.split('?');
const parsedParams = qs.parse(params);
const parsedParams = qs.parse(params);
// Convert indexed object to array
const newParams: Record<string, any> = {};
Object.keys(parsedParams).forEach((key) => {
const isIndexedArrayObject =
typeof parsedParams[key] === 'object' && Object.keys(parsedParams[key] || {}).includes('0');
// Convert indexed object to array
const newParams: Record<string, any> = {};
Object.keys(parsedParams).forEach((key) => {
const isIndexedArrayObject =
typeof parsedParams[key] === 'object' &&
Object.keys(parsedParams[key] || {}).includes('0');
if (!isIndexedArrayObject) {
newParams[key] = parsedParams[key];
} else {
newParams[key] = Object.values(parsedParams[key] || {});
}
});
if (!isIndexedArrayObject) {
newParams[key] = parsedParams[key];
} else {
newParams[key] = Object.values(parsedParams[key] || {});
}
});
const notNilParams = omitBy(newParams, (value) => value === 'undefined' || value === 'null');
const notNilParams = omitBy(newParams, (value) => value === 'undefined' || value === 'null');
return {
params: notNilParams,
path,
};
return {
params: notNilParams,
path,
};
};
let authSuccess = true;
@ -205,184 +206,186 @@ const RETRY_DELAY_MS = 1000;
const MAX_RETRIES = 5;
const waitForResult = async (count = 0): Promise<void> => {
return new Promise((resolve) => {
if (count === MAX_RETRIES || !shouldDelay) resolve();
return new Promise((resolve) => {
if (count === MAX_RETRIES || !shouldDelay) resolve();
setTimeout(() => {
waitForResult(count + 1)
.then(resolve)
.catch(resolve);
}, RETRY_DELAY_MS);
});
setTimeout(() => {
waitForResult(count + 1)
.then(resolve)
.catch(resolve);
}, RETRY_DELAY_MS);
});
};
const limitedFail = debounce(authenticationFailure, RETRY_DELAY_MS);
const TIMEOUT_ERROR = Error();
axiosClient.interceptors.response.use(
(response) => {
const serverId = useAuthStore.getState().currentServer?.id;
(response) => {
const serverId = useAuthStore.getState().currentServer?.id;
if (serverId) {
const headerCredential = response.headers['x-nd-authorization'] as string | undefined;
if (serverId) {
const headerCredential = response.headers['x-nd-authorization'] as string | undefined;
if (headerCredential) {
useAuthStore.getState().actions.updateServer(serverId, {
ndCredential: headerCredential,
});
}
}
if (headerCredential) {
useAuthStore.getState().actions.updateServer(serverId, {
ndCredential: headerCredential,
});
}
}
authSuccess = true;
authSuccess = true;
return response;
},
(error) => {
if (error.response && error.response.status === 401) {
const currentServer = useAuthStore.getState().currentServer;
return response;
},
(error) => {
if (error.response && error.response.status === 401) {
const currentServer = useAuthStore.getState().currentServer;
if (localSettings && currentServer?.savePassword) {
// eslint-disable-next-line promise/no-promise-in-callback
return localSettings
.passwordGet(currentServer.id)
.then(async (password: string | null) => {
authSuccess = false;
if (localSettings && currentServer?.savePassword) {
// eslint-disable-next-line promise/no-promise-in-callback
return localSettings
.passwordGet(currentServer.id)
.then(async (password: string | null) => {
authSuccess = false;
if (password === null) {
throw error;
if (password === null) {
throw error;
}
if (shouldDelay) {
await waitForResult();
// Hopefully the delay was sufficient for authentication.
// Otherwise, it will require manual intervention
if (authSuccess) {
return axiosClient.request(error.config);
}
throw error;
}
shouldDelay = true;
// Do not use axiosClient. Instead, manually make a post
const res = await axios.post(`${currentServer.url}/auth/login`, {
password,
username: currentServer.username,
});
if (res.status === 429) {
toast.error({
message:
'you have exceeded the number of allowed login requests. Please wait before logging, or consider tweaking AuthRequestLimit',
title: 'Your session has expired.',
});
const serverId = currentServer.id;
useAuthStore
.getState()
.actions.updateServer(serverId, { ndCredential: undefined });
useAuthStore.getState().actions.setCurrentServer(null);
// special error to prevent sending a second message, and stop other messages that could be enqueued
limitedFail.cancel();
throw TIMEOUT_ERROR;
}
if (res.status !== 200) {
throw new Error('Failed to authenticate');
}
const newCredential = res.data.token;
const subsonicCredential = `u=${currentServer.username}&s=${res.data.subsonicSalt}&t=${res.data.subsonicToken}`;
useAuthStore.getState().actions.updateServer(currentServer.id, {
credential: subsonicCredential,
ndCredential: newCredential,
});
error.config.headers['x-nd-authorization'] = `Bearer ${newCredential}`;
authSuccess = true;
return axiosClient.request(error.config);
})
.catch((newError: any) => {
if (newError !== TIMEOUT_ERROR) {
console.error('Error when trying to reauthenticate: ', newError);
limitedFail(currentServer);
}
// make sure to pass the error so axios will error later on
throw newError;
})
.finally(() => {
shouldDelay = false;
});
}
if (shouldDelay) {
await waitForResult();
limitedFail(currentServer);
}
// Hopefully the delay was sufficient for authentication.
// Otherwise, it will require manual intervention
if (authSuccess) {
return axiosClient.request(error.config);
}
throw error;
}
shouldDelay = true;
// Do not use axiosClient. Instead, manually make a post
const res = await axios.post(`${currentServer.url}/auth/login`, {
password,
username: currentServer.username,
});
if (res.status === 429) {
toast.error({
message:
'you have exceeded the number of allowed login requests. Please wait before logging, or consider tweaking AuthRequestLimit',
title: 'Your session has expired.',
});
const serverId = currentServer.id;
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
useAuthStore.getState().actions.setCurrentServer(null);
// special error to prevent sending a second message, and stop other messages that could be enqueued
limitedFail.cancel();
throw TIMEOUT_ERROR;
}
if (res.status !== 200) {
throw new Error('Failed to authenticate');
}
const newCredential = res.data.token;
const subsonicCredential = `u=${currentServer.username}&s=${res.data.subsonicSalt}&t=${res.data.subsonicToken}`;
useAuthStore.getState().actions.updateServer(currentServer.id, {
credential: subsonicCredential,
ndCredential: newCredential,
});
error.config.headers['x-nd-authorization'] = `Bearer ${newCredential}`;
authSuccess = true;
return axiosClient.request(error.config);
})
.catch((newError: any) => {
if (newError !== TIMEOUT_ERROR) {
console.error('Error when trying to reauthenticate: ', newError);
limitedFail(currentServer);
}
// make sure to pass the error so axios will error later on
throw newError;
})
.finally(() => {
shouldDelay = false;
});
}
limitedFail(currentServer);
}
return Promise.reject(error);
},
return Promise.reject(error);
},
);
export const ndApiClient = (args: {
server: ServerListItem | null;
signal?: AbortSignal;
url?: string;
server: ServerListItem | null;
signal?: AbortSignal;
url?: string;
}) => {
const { server, url, signal } = args;
const { server, url, signal } = args;
return initClient(contract, {
api: async ({ path, method, headers, body }) => {
let baseUrl: string | undefined;
let token: string | undefined;
return initClient(contract, {
api: async ({ path, method, headers, body }) => {
let baseUrl: string | undefined;
let token: string | undefined;
const { params, path: api } = parsePath(path);
const { params, path: api } = parsePath(path);
if (server) {
baseUrl = `${server?.url}/api`;
token = server?.ndCredential;
} else {
baseUrl = url;
}
if (server) {
baseUrl = `${server?.url}/api`;
token = server?.ndCredential;
} else {
baseUrl = url;
}
try {
if (shouldDelay) await waitForResult();
try {
if (shouldDelay) await waitForResult();
const result = await axiosClient.request({
data: body,
headers: {
...headers,
...(token && { 'x-nd-authorization': `Bearer ${token}` }),
},
method: method as Method,
params,
signal,
url: `${baseUrl}/${api}`,
});
return {
body: { data: result.data, headers: result.headers },
headers: result.headers as any,
status: result.status,
};
} catch (e: Error | AxiosError | any) {
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return {
body: { data: response.data, headers: response.headers },
headers: response.headers as any,
status: response.status,
};
}
throw e;
}
},
baseHeaders: {
'Content-Type': 'application/json',
},
baseUrl: '',
jsonQuery: false,
});
const result = await axiosClient.request({
data: body,
headers: {
...headers,
...(token && { 'x-nd-authorization': `Bearer ${token}` }),
},
method: method as Method,
params,
signal,
url: `${baseUrl}/${api}`,
});
return {
body: { data: result.data, headers: result.headers },
headers: result.headers as any,
status: result.status,
};
} catch (e: Error | AxiosError | any) {
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return {
body: { data: response.data, headers: response.headers },
headers: response.headers as any,
status: response.status,
};
}
throw e;
}
},
baseHeaders: {
'Content-Type': 'application/json',
},
baseUrl: '',
jsonQuery: false,
});
};

View file

@ -1,43 +1,43 @@
import {
AlbumArtistDetailArgs,
AlbumArtistDetailResponse,
AddToPlaylistArgs,
AddToPlaylistResponse,
CreatePlaylistResponse,
CreatePlaylistArgs,
DeletePlaylistArgs,
DeletePlaylistResponse,
AlbumArtistListResponse,
AlbumArtistListArgs,
albumArtistListSortMap,
sortOrderMap,
AuthenticationResponse,
UserListResponse,
UserListArgs,
userListSortMap,
GenreListArgs,
GenreListResponse,
AlbumDetailResponse,
AlbumDetailArgs,
AlbumListArgs,
albumListSortMap,
AlbumListResponse,
SongListResponse,
SongListArgs,
songListSortMap,
SongDetailResponse,
SongDetailArgs,
UpdatePlaylistArgs,
UpdatePlaylistResponse,
PlaylistListResponse,
PlaylistDetailArgs,
PlaylistListArgs,
playlistListSortMap,
PlaylistDetailResponse,
PlaylistSongListArgs,
PlaylistSongListResponse,
RemoveFromPlaylistResponse,
RemoveFromPlaylistArgs,
AlbumArtistDetailArgs,
AlbumArtistDetailResponse,
AddToPlaylistArgs,
AddToPlaylistResponse,
CreatePlaylistResponse,
CreatePlaylistArgs,
DeletePlaylistArgs,
DeletePlaylistResponse,
AlbumArtistListResponse,
AlbumArtistListArgs,
albumArtistListSortMap,
sortOrderMap,
AuthenticationResponse,
UserListResponse,
UserListArgs,
userListSortMap,
GenreListArgs,
GenreListResponse,
AlbumDetailResponse,
AlbumDetailArgs,
AlbumListArgs,
albumListSortMap,
AlbumListResponse,
SongListResponse,
SongListArgs,
songListSortMap,
SongDetailResponse,
SongDetailArgs,
UpdatePlaylistArgs,
UpdatePlaylistResponse,
PlaylistListResponse,
PlaylistDetailArgs,
PlaylistListArgs,
playlistListSortMap,
PlaylistDetailResponse,
PlaylistSongListArgs,
PlaylistSongListResponse,
RemoveFromPlaylistResponse,
RemoveFromPlaylistArgs,
} from '../types';
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
@ -45,428 +45,430 @@ import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
const authenticate = async (
url: string,
body: { password: string; username: string },
url: string,
body: { password: string; username: string },
): Promise<AuthenticationResponse> => {
const cleanServerUrl = url.replace(/\/$/, '');
const cleanServerUrl = url.replace(/\/$/, '');
const res = await ndApiClient({ server: null, url: cleanServerUrl }).authenticate({
body: {
password: body.password,
username: body.username,
},
});
const res = await ndApiClient({ server: null, url: cleanServerUrl }).authenticate({
body: {
password: body.password,
username: body.username,
},
});
if (res.status !== 200) {
throw new Error('Failed to authenticate');
}
if (res.status !== 200) {
throw new Error('Failed to authenticate');
}
return {
credential: `u=${body.username}&s=${res.body.data.subsonicSalt}&t=${res.body.data.subsonicToken}`,
ndCredential: res.body.data.token,
userId: res.body.data.id,
username: res.body.data.username,
};
return {
credential: `u=${body.username}&s=${res.body.data.subsonicSalt}&t=${res.body.data.subsonicToken}`,
ndCredential: res.body.data.token,
userId: res.body.data.id,
username: res.body.data.username,
};
};
const getUserList = async (args: UserListArgs): Promise<UserListResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getUserList({
query: {
_end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: userListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
...query._custom?.navidrome,
},
});
const res = await ndApiClient(apiClientProps).getUserList({
query: {
_end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: userListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
...query._custom?.navidrome,
},
});
if (res.status !== 200) {
throw new Error('Failed to get user list');
}
if (res.status !== 200) {
throw new Error('Failed to get user list');
}
return {
items: res.body.data.map((user) => ndNormalize.user(user)),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
return {
items: res.body.data.map((user) => ndNormalize.user(user)),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
};
const getGenreList = async (args: GenreListArgs): Promise<GenreListResponse> => {
const { apiClientProps } = args;
const { apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getGenreList({});
const res = await ndApiClient(apiClientProps).getGenreList({});
if (res.status !== 200) {
throw new Error('Failed to get genre list');
}
if (res.status !== 200) {
throw new Error('Failed to get genre list');
}
return {
items: res.body.data,
startIndex: 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
return {
items: res.body.data,
startIndex: 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
};
const getAlbumArtistDetail = async (
args: AlbumArtistDetailArgs,
args: AlbumArtistDetailArgs,
): Promise<AlbumArtistDetailResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({
params: {
id: query.id,
},
});
const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({
params: {
id: query.id,
},
});
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
query: {
count: 10,
id: query.id,
},
});
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
query: {
count: 10,
id: query.id,
},
});
if (res.status !== 200) {
throw new Error('Failed to get album artist detail');
}
if (res.status !== 200) {
throw new Error('Failed to get album artist detail');
}
if (!apiClientProps.server) {
throw new Error('Server is required');
}
if (!apiClientProps.server) {
throw new Error('Server is required');
}
return ndNormalize.albumArtist(
{
...res.body.data,
...(artistInfoRes.status === 200 && {
similarArtists: artistInfoRes.body.artistInfo.similarArtist,
...(!res.body.data.largeImageUrl && {
largeImageUrl: artistInfoRes.body.artistInfo.largeImageUrl,
}),
...(!res.body.data.mediumImageUrl && {
largeImageUrl: artistInfoRes.body.artistInfo.mediumImageUrl,
}),
...(!res.body.data.smallImageUrl && {
largeImageUrl: artistInfoRes.body.artistInfo.smallImageUrl,
}),
}),
},
apiClientProps.server,
);
return ndNormalize.albumArtist(
{
...res.body.data,
...(artistInfoRes.status === 200 && {
similarArtists: artistInfoRes.body.artistInfo.similarArtist,
...(!res.body.data.largeImageUrl && {
largeImageUrl: artistInfoRes.body.artistInfo.largeImageUrl,
}),
...(!res.body.data.mediumImageUrl && {
largeImageUrl: artistInfoRes.body.artistInfo.mediumImageUrl,
}),
...(!res.body.data.smallImageUrl && {
largeImageUrl: artistInfoRes.body.artistInfo.smallImageUrl,
}),
}),
},
apiClientProps.server,
);
};
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<AlbumArtistListResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
query: {
_end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumArtistListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
name: query.searchTerm,
...query._custom?.navidrome,
},
});
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
query: {
_end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumArtistListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
name: query.searchTerm,
...query._custom?.navidrome,
},
});
if (res.status !== 200) {
throw new Error('Failed to get album artist list');
}
if (res.status !== 200) {
throw new Error('Failed to get album artist list');
}
return {
items: res.body.data.map((albumArtist) =>
ndNormalize.albumArtist(albumArtist, apiClientProps.server),
),
startIndex: query.startIndex,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
return {
items: res.body.data.map((albumArtist) =>
ndNormalize.albumArtist(albumArtist, apiClientProps.server),
),
startIndex: query.startIndex,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
};
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<AlbumDetailResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({
params: {
id: query.id,
},
});
const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({
params: {
id: query.id,
},
});
const songsData = await ndApiClient(apiClientProps).getSongList({
query: {
_end: 0,
_order: 'ASC',
_sort: 'album',
_start: 0,
album_id: [query.id],
},
});
const songsData = await ndApiClient(apiClientProps).getSongList({
query: {
_end: 0,
_order: 'ASC',
_sort: 'album',
_start: 0,
album_id: [query.id],
},
});
if (albumRes.status !== 200 || songsData.status !== 200) {
throw new Error('Failed to get album detail');
}
if (albumRes.status !== 200 || songsData.status !== 200) {
throw new Error('Failed to get album detail');
}
return ndNormalize.album(
{ ...albumRes.body.data, songs: songsData.body.data },
apiClientProps.server,
);
return ndNormalize.album(
{ ...albumRes.body.data, songs: songsData.body.data },
apiClientProps.server,
);
};
const getAlbumList = async (args: AlbumListArgs): Promise<AlbumListResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getAlbumList({
query: {
_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._custom?.navidrome,
},
});
const res = await ndApiClient(apiClientProps).getAlbumList({
query: {
_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._custom?.navidrome,
},
});
if (res.status !== 200) {
throw new Error('Failed to get album list');
}
if (res.status !== 200) {
throw new Error('Failed to get album list');
}
return {
items: res.body.data.map((album) => ndNormalize.album(album, apiClientProps.server)),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
return {
items: res.body.data.map((album) => ndNormalize.album(album, apiClientProps.server)),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
};
const getSongList = async (args: SongListArgs): Promise<SongListResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getSongList({
query: {
_end: query.startIndex + (query.limit || -1),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: songListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
album_artist_id: query.artistIds,
album_id: query.albumIds,
title: query.searchTerm,
...query._custom?.navidrome,
},
});
const res = await ndApiClient(apiClientProps).getSongList({
query: {
_end: query.startIndex + (query.limit || -1),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: songListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
album_artist_id: query.artistIds,
album_id: query.albumIds,
title: query.searchTerm,
...query._custom?.navidrome,
},
});
if (res.status !== 200) {
throw new Error('Failed to get song list');
}
if (res.status !== 200) {
throw new Error('Failed to get song list');
}
return {
items: res.body.data.map((song) => ndNormalize.song(song, apiClientProps.server, '')),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
return {
items: res.body.data.map((song) => ndNormalize.song(song, apiClientProps.server, '')),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
};
const getSongDetail = async (args: SongDetailArgs): Promise<SongDetailResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getSongDetail({
params: {
id: query.id,
},
});
const res = await ndApiClient(apiClientProps).getSongDetail({
params: {
id: query.id,
},
});
if (res.status !== 200) {
throw new Error('Failed to get song detail');
}
if (res.status !== 200) {
throw new Error('Failed to get song detail');
}
return ndNormalize.song(res.body.data, apiClientProps.server, '');
return ndNormalize.song(res.body.data, apiClientProps.server, '');
};
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
const { body, apiClientProps } = args;
const { body, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).createPlaylist({
body: {
comment: body.comment,
name: body.name,
public: body._custom?.navidrome?.public,
rules: body._custom?.navidrome?.rules,
sync: body._custom?.navidrome?.sync,
},
});
const res = await ndApiClient(apiClientProps).createPlaylist({
body: {
comment: body.comment,
name: body.name,
public: body._custom?.navidrome?.public,
rules: body._custom?.navidrome?.rules,
sync: body._custom?.navidrome?.sync,
},
});
if (res.status !== 200) {
throw new Error('Failed to create playlist');
}
if (res.status !== 200) {
throw new Error('Failed to create playlist');
}
return {
id: res.body.data.id,
};
return {
id: res.body.data.id,
};
};
const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistResponse> => {
const { query, body, apiClientProps } = args;
const { query, body, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).updatePlaylist({
body: {
comment: body.comment || '',
name: body.name,
public: body._custom?.navidrome?.public || false,
rules: body._custom?.navidrome?.rules ? body._custom.navidrome.rules : undefined,
sync: body._custom?.navidrome?.sync || undefined,
},
params: {
id: query.id,
},
});
const res = await ndApiClient(apiClientProps).updatePlaylist({
body: {
comment: body.comment || '',
name: body.name,
public: body._custom?.navidrome?.public || false,
rules: body._custom?.navidrome?.rules ? body._custom.navidrome.rules : undefined,
sync: body._custom?.navidrome?.sync || undefined,
},
params: {
id: query.id,
},
});
if (res.status !== 200) {
throw new Error('Failed to update playlist');
}
if (res.status !== 200) {
throw new Error('Failed to update playlist');
}
return null;
return null;
};
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<DeletePlaylistResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).deletePlaylist({
body: null,
params: {
id: query.id,
},
});
const res = await ndApiClient(apiClientProps).deletePlaylist({
body: null,
params: {
id: query.id,
},
});
if (res.status !== 200) {
throw new Error('Failed to delete playlist');
}
if (res.status !== 200) {
throw new Error('Failed to delete playlist');
}
return null;
return null;
};
const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getPlaylistList({
query: {
_end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
_start: query.startIndex,
...query._custom?.navidrome,
},
});
const res = await ndApiClient(apiClientProps).getPlaylistList({
query: {
_end: query.startIndex + (query.limit || 0),
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
_start: query.startIndex,
...query._custom?.navidrome,
},
});
if (res.status !== 200) {
throw new Error('Failed to get playlist list');
}
if (res.status !== 200) {
throw new Error('Failed to get playlist list');
}
return {
items: res.body.data.map((item) => ndNormalize.playlist(item, apiClientProps.server)),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
return {
items: res.body.data.map((item) => ndNormalize.playlist(item, apiClientProps.server)),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
};
const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise<PlaylistDetailResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getPlaylistDetail({
params: {
id: query.id,
},
});
const res = await ndApiClient(apiClientProps).getPlaylistDetail({
params: {
id: query.id,
},
});
if (res.status !== 200) {
throw new Error('Failed to get playlist detail');
}
if (res.status !== 200) {
throw new Error('Failed to get playlist detail');
}
return ndNormalize.playlist(res.body.data, apiClientProps.server);
return ndNormalize.playlist(res.body.data, apiClientProps.server);
};
const getPlaylistSongList = async (
args: PlaylistSongListArgs,
args: PlaylistSongListArgs,
): Promise<PlaylistSongListResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).getPlaylistSongList({
params: {
id: query.id,
},
query: {
_end: query.startIndex + (query.limit || 0),
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC',
_sort: query.sortBy ? songListSortMap.navidrome[query.sortBy] : ndType._enum.songList.ID,
_start: query.startIndex,
},
});
const res = await ndApiClient(apiClientProps).getPlaylistSongList({
params: {
id: query.id,
},
query: {
_end: query.startIndex + (query.limit || 0),
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC',
_sort: query.sortBy
? songListSortMap.navidrome[query.sortBy]
: ndType._enum.songList.ID,
_start: query.startIndex,
},
});
if (res.status !== 200) {
throw new Error('Failed to get playlist song list');
}
if (res.status !== 200) {
throw new Error('Failed to get playlist song list');
}
return {
items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server, '')),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
return {
items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server, '')),
startIndex: query?.startIndex || 0,
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
};
};
const addToPlaylist = async (args: AddToPlaylistArgs): Promise<AddToPlaylistResponse> => {
const { body, query, apiClientProps } = args;
const { body, query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).addToPlaylist({
body: {
ids: body.songId,
},
params: {
id: query.id,
},
});
const res = await ndApiClient(apiClientProps).addToPlaylist({
body: {
ids: body.songId,
},
params: {
id: query.id,
},
});
if (res.status !== 200) {
throw new Error('Failed to add to playlist');
}
if (res.status !== 200) {
throw new Error('Failed to add to playlist');
}
return null;
return null;
};
const removeFromPlaylist = async (
args: RemoveFromPlaylistArgs,
args: RemoveFromPlaylistArgs,
): Promise<RemoveFromPlaylistResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
body: null,
params: {
id: query.id,
},
query: {
id: query.songId,
},
});
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
body: null,
params: {
id: query.id,
},
query: {
id: query.songId,
},
});
if (res.status !== 200) {
throw new Error('Failed to remove from playlist');
}
if (res.status !== 200) {
throw new Error('Failed to remove from playlist');
}
return null;
return null;
};
export const ndController = {
addToPlaylist,
authenticate,
createPlaylist,
deletePlaylist,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumDetail,
getAlbumList,
getGenreList,
getPlaylistDetail,
getPlaylistList,
getPlaylistSongList,
getSongDetail,
getSongList,
getUserList,
removeFromPlaylist,
updatePlaylist,
addToPlaylist,
authenticate,
createPlaylist,
deletePlaylist,
getAlbumArtistDetail,
getAlbumArtistList,
getAlbumDetail,
getAlbumList,
getGenreList,
getPlaylistDetail,
getPlaylistList,
getPlaylistSongList,
getSongDetail,
getSongList,
getUserList,
removeFromPlaylist,
updatePlaylist,
};

View file

@ -6,226 +6,226 @@ import { ndType } from './navidrome-types';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
const getCoverArtUrl = (args: {
baseUrl: string | undefined;
coverArtId: string;
credential: string | undefined;
size: number;
baseUrl: string | undefined;
coverArtId: string;
credential: string | undefined;
size: number;
}) => {
const size = args.size ? args.size : 250;
const size = args.size ? args.size : 250;
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) {
return null;
}
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}`
);
return (
`${args.baseUrl}/rest/getCoverArt.view` +
`?id=${args.coverArtId}` +
`&${args.credential}` +
'&v=1.13.0' +
'&c=feishin' +
`&size=${size}`
);
};
const normalizeSong = (
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
server: ServerListItem | null,
deviceId: string,
imageSize?: number,
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
server: ServerListItem | null,
deviceId: string,
imageSize?: number,
): Song => {
let id;
let playlistItemId;
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;
}
// 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 imageUrl = getCoverArtUrl({
baseUrl: server?.url,
coverArtId: id,
credential: server?.credential,
size: imageSize || 100,
});
const imagePlaceholderUrl = null;
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,
lyrics: item.lyrics ? item.lyrics : null,
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 || 'unknown',
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,
};
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,
lyrics: item.lyrics ? item.lyrics : null,
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 || 'unknown',
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: z.infer<typeof ndType._response.album> & {
songs?: z.infer<typeof ndType._response.songList>;
},
server: ServerListItem | null,
imageSize?: number,
item: z.infer<typeof ndType._response.album> & {
songs?: z.infer<typeof ndType._response.songList>;
},
server: ServerListItem | null,
imageSize?: number,
): Album => {
const imageUrl = getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArtId || item.id,
credential: server?.credential,
size: imageSize || 300,
});
const imageUrl = getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArtId || item.id,
credential: server?.credential,
size: imageSize || 300,
});
const imagePlaceholderUrl = null;
const imagePlaceholderUrl = null;
const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || 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 || 'unknown',
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 || 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 || 'unknown',
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 || null,
};
};
const normalizeAlbumArtist = (
item: z.infer<typeof ndType._response.albumArtist> & {
similarArtists?: z.infer<typeof ssType._response.artistInfo>['artistInfo']['similarArtist'];
},
server: ServerListItem | null,
item: z.infer<typeof ndType._response.albumArtist> & {
similarArtists?: z.infer<typeof ssType._response.artistInfo>['artistInfo']['similarArtist'];
},
server: ServerListItem | null,
): AlbumArtist => {
const imageUrl =
item.largeImageUrl === '/app/artist-placeholder.webp' ? null : item.largeImageUrl;
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 || 'unknown',
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,
};
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 || 'unknown',
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: z.infer<typeof ndType._response.playlist>,
server: ServerListItem | null,
imageSize?: number,
item: z.infer<typeof ndType._response.playlist>,
server: ServerListItem | null,
imageSize?: number,
): Playlist => {
const imageUrl = getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.id,
credential: server?.credential,
size: imageSize || 300,
});
const imageUrl = getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.id,
credential: server?.credential,
size: imageSize || 300,
});
const imagePlaceholderUrl = null;
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 || 'unknown',
serverType: ServerType.NAVIDROME,
size: item.size,
songCount: item.songCount,
sync: item.sync,
};
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 || 'unknown',
serverType: ServerType.NAVIDROME,
size: item.size,
songCount: item.songCount,
sync: item.sync,
};
};
const normalizeUser = (item: z.infer<typeof ndType._response.user>): User => {
return {
createdAt: item.createdAt,
email: item.email || null,
id: item.id,
isAdmin: item.isAdmin,
lastLoginAt: item.lastLoginAt,
name: item.userName,
updatedAt: item.updatedAt,
};
return {
createdAt: item.createdAt,
email: item.email || null,
id: item.id,
isAdmin: item.isAdmin,
lastLoginAt: item.lastLoginAt,
name: item.userName,
updatedAt: item.updatedAt,
};
};
export const ndNormalize = {
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
playlist: normalizePlaylist,
song: normalizeSong,
user: normalizeUser,
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
playlist: normalizePlaylist,
song: normalizeSong,
user: normalizeUser,
};

View file

@ -5,294 +5,294 @@ const sortOrderValues = ['ASC', 'DESC'] as const;
const error = z.string();
const paginationParameters = z.object({
_end: z.number().optional(),
_order: z.enum(sortOrderValues),
_start: z.number().optional(),
_end: z.number().optional(),
_order: z.enum(sortOrderValues),
_start: z.number().optional(),
});
const authenticate = z.object({
id: z.string(),
isAdmin: z.boolean(),
name: z.string(),
subsonicSalt: z.string(),
subsonicToken: z.string(),
token: z.string(),
username: z.string(),
id: z.string(),
isAdmin: z.boolean(),
name: z.string(),
subsonicSalt: z.string(),
subsonicToken: z.string(),
token: z.string(),
username: z.string(),
});
const authenticateParameters = z.object({
password: z.string(),
username: z.string(),
password: z.string(),
username: z.string(),
});
const user = z.object({
createdAt: z.string(),
email: z.string().optional(),
id: z.string(),
isAdmin: z.boolean(),
lastAccessAt: z.string(),
lastLoginAt: z.string(),
name: z.string(),
updatedAt: z.string(),
userName: z.string(),
createdAt: z.string(),
email: z.string().optional(),
id: z.string(),
isAdmin: z.boolean(),
lastAccessAt: z.string(),
lastLoginAt: z.string(),
name: z.string(),
updatedAt: z.string(),
userName: z.string(),
});
const userList = z.array(user);
const ndUserListSort = {
NAME: 'name',
NAME: 'name',
} as const;
const userListParameters = paginationParameters.extend({
_sort: z.nativeEnum(ndUserListSort).optional(),
_sort: z.nativeEnum(ndUserListSort).optional(),
});
const genre = z.object({
id: z.string(),
name: z.string(),
id: z.string(),
name: z.string(),
});
const genreList = z.array(genre);
const albumArtist = z.object({
albumCount: z.number(),
biography: z.string(),
externalInfoUpdatedAt: z.string(),
externalUrl: z.string(),
fullText: z.string(),
genres: z.array(genre),
id: z.string(),
largeImageUrl: z.string().optional(),
mbzArtistId: z.string().optional(),
mediumImageUrl: z.string().optional(),
name: z.string(),
orderArtistName: z.string(),
playCount: z.number(),
playDate: z.string(),
rating: z.number(),
size: z.number(),
smallImageUrl: z.string().optional(),
songCount: z.number(),
starred: z.boolean(),
starredAt: z.string(),
albumCount: z.number(),
biography: z.string(),
externalInfoUpdatedAt: z.string(),
externalUrl: z.string(),
fullText: z.string(),
genres: z.array(genre),
id: z.string(),
largeImageUrl: z.string().optional(),
mbzArtistId: z.string().optional(),
mediumImageUrl: z.string().optional(),
name: z.string(),
orderArtistName: z.string(),
playCount: z.number(),
playDate: z.string(),
rating: z.number(),
size: z.number(),
smallImageUrl: z.string().optional(),
songCount: z.number(),
starred: z.boolean(),
starredAt: z.string(),
});
const albumArtistList = z.array(albumArtist);
const ndAlbumArtistListSort = {
ALBUM_COUNT: 'albumCount',
FAVORITED: 'starred ASC, starredAt ASC',
NAME: 'name',
PLAY_COUNT: 'playCount',
RATING: 'rating',
SONG_COUNT: 'songCount',
ALBUM_COUNT: 'albumCount',
FAVORITED: 'starred ASC, starredAt ASC',
NAME: 'name',
PLAY_COUNT: 'playCount',
RATING: 'rating',
SONG_COUNT: 'songCount',
} as const;
const albumArtistListParameters = paginationParameters.extend({
_sort: z.nativeEnum(ndAlbumArtistListSort).optional(),
genre_id: z.string().optional(),
name: z.string().optional(),
starred: z.boolean().optional(),
_sort: z.nativeEnum(ndAlbumArtistListSort).optional(),
genre_id: z.string().optional(),
name: z.string().optional(),
starred: z.boolean().optional(),
});
const album = z.object({
albumArtist: z.string(),
albumArtistId: z.string(),
allArtistIds: z.string(),
artist: z.string(),
artistId: z.string(),
compilation: z.boolean(),
coverArtId: z.string().optional(), // Removed after v0.48.0
coverArtPath: z.string().optional(), // Removed after v0.48.0
createdAt: z.string(),
duration: z.number(),
fullText: z.string(),
genre: z.string(),
genres: z.array(genre),
id: z.string(),
maxYear: z.number(),
mbzAlbumArtistId: z.string().optional(),
mbzAlbumId: z.string().optional(),
minYear: z.number(),
name: z.string(),
orderAlbumArtistName: z.string(),
orderAlbumName: z.string(),
playCount: z.number(),
playDate: z.string(),
rating: z.number().optional(),
size: z.number(),
songCount: z.number(),
sortAlbumArtistName: z.string(),
sortArtistName: z.string(),
starred: z.boolean(),
starredAt: z.string().optional(),
updatedAt: z.string(),
albumArtist: z.string(),
albumArtistId: z.string(),
allArtistIds: z.string(),
artist: z.string(),
artistId: z.string(),
compilation: z.boolean(),
coverArtId: z.string().optional(), // Removed after v0.48.0
coverArtPath: z.string().optional(), // Removed after v0.48.0
createdAt: z.string(),
duration: z.number(),
fullText: z.string(),
genre: z.string(),
genres: z.array(genre),
id: z.string(),
maxYear: z.number(),
mbzAlbumArtistId: z.string().optional(),
mbzAlbumId: z.string().optional(),
minYear: z.number(),
name: z.string(),
orderAlbumArtistName: z.string(),
orderAlbumName: z.string(),
playCount: z.number(),
playDate: z.string(),
rating: z.number().optional(),
size: z.number(),
songCount: z.number(),
sortAlbumArtistName: z.string(),
sortArtistName: z.string(),
starred: z.boolean(),
starredAt: z.string().optional(),
updatedAt: z.string(),
});
const albumList = z.array(album);
const ndAlbumListSort = {
ALBUM_ARTIST: 'albumArtist',
ARTIST: 'artist',
DURATION: 'duration',
NAME: 'name',
PLAY_COUNT: 'playCount',
PLAY_DATE: 'play_date',
RANDOM: 'random',
RATING: 'rating',
RECENTLY_ADDED: 'recently_added',
SONG_COUNT: 'songCount',
STARRED: 'starred',
YEAR: 'max_year',
ALBUM_ARTIST: 'albumArtist',
ARTIST: 'artist',
DURATION: 'duration',
NAME: 'name',
PLAY_COUNT: 'playCount',
PLAY_DATE: 'play_date',
RANDOM: 'random',
RATING: 'rating',
RECENTLY_ADDED: 'recently_added',
SONG_COUNT: 'songCount',
STARRED: 'starred',
YEAR: 'max_year',
} as const;
const albumListParameters = paginationParameters.extend({
_sort: z.nativeEnum(ndAlbumListSort).optional(),
album_id: z.string().optional(),
artist_id: z.string().optional(),
compilation: z.boolean().optional(),
genre_id: z.string().optional(),
has_rating: z.boolean().optional(),
id: z.string().optional(),
name: z.string().optional(),
recently_added: z.boolean().optional(),
recently_played: z.boolean().optional(),
starred: z.boolean().optional(),
year: z.number().optional(),
_sort: z.nativeEnum(ndAlbumListSort).optional(),
album_id: z.string().optional(),
artist_id: z.string().optional(),
compilation: z.boolean().optional(),
genre_id: z.string().optional(),
has_rating: z.boolean().optional(),
id: z.string().optional(),
name: z.string().optional(),
recently_added: z.boolean().optional(),
recently_played: z.boolean().optional(),
starred: z.boolean().optional(),
year: z.number().optional(),
});
const song = z.object({
album: z.string(),
albumArtist: z.string(),
albumArtistId: z.string(),
albumId: z.string(),
artist: z.string(),
artistId: z.string(),
bitRate: z.number(),
bookmarkPosition: z.number(),
bpm: z.number().optional(),
channels: z.number().optional(),
comment: z.string().optional(),
compilation: z.boolean(),
createdAt: z.string(),
discNumber: z.number(),
duration: z.number(),
fullText: z.string(),
genre: z.string(),
genres: z.array(genre),
hasCoverArt: z.boolean(),
id: z.string(),
lyrics: z.string().optional(),
mbzAlbumArtistId: z.string().optional(),
mbzAlbumId: z.string().optional(),
mbzArtistId: z.string().optional(),
mbzTrackId: z.string().optional(),
orderAlbumArtistName: z.string(),
orderAlbumName: z.string(),
orderArtistName: z.string(),
orderTitle: z.string(),
path: z.string(),
playCount: z.number(),
playDate: z.string(),
rating: z.number().optional(),
size: z.number(),
sortAlbumArtistName: z.string(),
sortArtistName: z.string(),
starred: z.boolean(),
starredAt: z.string().optional(),
suffix: z.string(),
title: z.string(),
trackNumber: z.number(),
updatedAt: z.string(),
year: z.number(),
album: z.string(),
albumArtist: z.string(),
albumArtistId: z.string(),
albumId: z.string(),
artist: z.string(),
artistId: z.string(),
bitRate: z.number(),
bookmarkPosition: z.number(),
bpm: z.number().optional(),
channels: z.number().optional(),
comment: z.string().optional(),
compilation: z.boolean(),
createdAt: z.string(),
discNumber: z.number(),
duration: z.number(),
fullText: z.string(),
genre: z.string(),
genres: z.array(genre),
hasCoverArt: z.boolean(),
id: z.string(),
lyrics: z.string().optional(),
mbzAlbumArtistId: z.string().optional(),
mbzAlbumId: z.string().optional(),
mbzArtistId: z.string().optional(),
mbzTrackId: z.string().optional(),
orderAlbumArtistName: z.string(),
orderAlbumName: z.string(),
orderArtistName: z.string(),
orderTitle: z.string(),
path: z.string(),
playCount: z.number(),
playDate: z.string(),
rating: z.number().optional(),
size: z.number(),
sortAlbumArtistName: z.string(),
sortArtistName: z.string(),
starred: z.boolean(),
starredAt: z.string().optional(),
suffix: z.string(),
title: z.string(),
trackNumber: z.number(),
updatedAt: z.string(),
year: z.number(),
});
const songList = z.array(song);
const ndSongListSort = {
ALBUM: 'album, order_album_artist_name, disc_number, track_number, title',
ALBUM_ARTIST: 'order_album_artist_name, album, disc_number, track_number, title',
ALBUM_SONGS: 'album, discNumber, trackNumber',
ARTIST: 'artist',
BPM: 'bpm',
CHANNELS: 'channels',
COMMENT: 'comment',
DURATION: 'duration',
FAVORITED: 'starred ASC, starredAt ASC',
GENRE: 'genre',
ID: 'id',
PLAY_COUNT: 'playCount',
PLAY_DATE: 'playDate',
RATING: 'rating',
RECENTLY_ADDED: 'createdAt',
TITLE: 'title',
TRACK: 'track',
YEAR: 'year, album, discNumber, trackNumber',
ALBUM: 'album, order_album_artist_name, disc_number, track_number, title',
ALBUM_ARTIST: 'order_album_artist_name, album, disc_number, track_number, title',
ALBUM_SONGS: 'album, discNumber, trackNumber',
ARTIST: 'artist',
BPM: 'bpm',
CHANNELS: 'channels',
COMMENT: 'comment',
DURATION: 'duration',
FAVORITED: 'starred ASC, starredAt ASC',
GENRE: 'genre',
ID: 'id',
PLAY_COUNT: 'playCount',
PLAY_DATE: 'playDate',
RATING: 'rating',
RECENTLY_ADDED: 'createdAt',
TITLE: 'title',
TRACK: 'track',
YEAR: 'year, album, discNumber, trackNumber',
};
const songListParameters = paginationParameters.extend({
_sort: z.nativeEnum(ndSongListSort).optional(),
album_artist_id: z.array(z.string()).optional(),
album_id: z.array(z.string()).optional(),
artist_id: z.array(z.string()).optional(),
genre_id: z.string().optional(),
starred: z.boolean().optional(),
title: z.string().optional(),
year: z.number().optional(),
_sort: z.nativeEnum(ndSongListSort).optional(),
album_artist_id: z.array(z.string()).optional(),
album_id: z.array(z.string()).optional(),
artist_id: z.array(z.string()).optional(),
genre_id: z.string().optional(),
starred: z.boolean().optional(),
title: z.string().optional(),
year: z.number().optional(),
});
const playlist = z.object({
comment: z.string(),
createdAt: z.string(),
duration: z.number(),
evaluatedAt: z.string(),
id: z.string(),
name: z.string(),
ownerId: z.string(),
ownerName: z.string(),
path: z.string(),
public: z.boolean(),
rules: z.record(z.string(), z.any()),
size: z.number(),
songCount: z.number(),
sync: z.boolean(),
updatedAt: z.string(),
comment: z.string(),
createdAt: z.string(),
duration: z.number(),
evaluatedAt: z.string(),
id: z.string(),
name: z.string(),
ownerId: z.string(),
ownerName: z.string(),
path: z.string(),
public: z.boolean(),
rules: z.record(z.string(), z.any()),
size: z.number(),
songCount: z.number(),
sync: z.boolean(),
updatedAt: z.string(),
});
const playlistList = z.array(playlist);
const ndPlaylistListSort = {
DURATION: 'duration',
NAME: 'name',
OWNER: 'ownerName',
PUBLIC: 'public',
SONG_COUNT: 'songCount',
UPDATED_AT: 'updatedAt',
DURATION: 'duration',
NAME: 'name',
OWNER: 'ownerName',
PUBLIC: 'public',
SONG_COUNT: 'songCount',
UPDATED_AT: 'updatedAt',
} as const;
const playlistListParameters = paginationParameters.extend({
_sort: z.nativeEnum(ndPlaylistListSort).optional(),
owner_id: z.string().optional(),
smart: z.boolean().optional(),
_sort: z.nativeEnum(ndPlaylistListSort).optional(),
owner_id: z.string().optional(),
smart: z.boolean().optional(),
});
const playlistSong = song.extend({
mediaFileId: z.string(),
playlistId: z.string(),
mediaFileId: z.string(),
playlistId: z.string(),
});
const playlistSongList = z.array(playlistSong);
const createPlaylist = playlist.pick({
id: true,
id: true,
});
const createPlaylistParameters = z.object({
comment: z.string().optional(),
name: z.string(),
public: z.boolean().optional(),
rules: z.record(z.any()).optional(),
sync: z.boolean().optional(),
comment: z.string().optional(),
name: z.string(),
public: z.boolean().optional(),
rules: z.record(z.any()).optional(),
sync: z.boolean().optional(),
});
const updatePlaylist = playlist;
@ -302,62 +302,62 @@ const updatePlaylistParameters = createPlaylistParameters.partial();
const deletePlaylist = z.null();
const addToPlaylist = z.object({
added: z.number(),
added: z.number(),
});
const addToPlaylistParameters = z.object({
ids: z.array(z.string()),
ids: z.array(z.string()),
});
const removeFromPlaylist = z.object({
ids: z.array(z.string()),
ids: z.array(z.string()),
});
const removeFromPlaylistParameters = z.object({
id: z.array(z.string()),
id: z.array(z.string()),
});
export const ndType = {
_enum: {
albumArtistList: ndAlbumArtistListSort,
albumList: ndAlbumListSort,
playlistList: ndPlaylistListSort,
songList: ndSongListSort,
userList: ndUserListSort,
},
_parameters: {
addToPlaylist: addToPlaylistParameters,
albumArtistList: albumArtistListParameters,
albumList: albumListParameters,
authenticate: authenticateParameters,
createPlaylist: createPlaylistParameters,
playlistList: playlistListParameters,
removeFromPlaylist: removeFromPlaylistParameters,
songList: songListParameters,
updatePlaylist: updatePlaylistParameters,
userList: userListParameters,
},
_response: {
addToPlaylist,
album,
albumArtist,
albumArtistList,
albumList,
authenticate,
createPlaylist,
deletePlaylist,
error,
genre,
genreList,
playlist,
playlistList,
playlistSong,
playlistSongList,
removeFromPlaylist,
song,
songList,
updatePlaylist,
user,
userList,
},
_enum: {
albumArtistList: ndAlbumArtistListSort,
albumList: ndAlbumListSort,
playlistList: ndPlaylistListSort,
songList: ndSongListSort,
userList: ndUserListSort,
},
_parameters: {
addToPlaylist: addToPlaylistParameters,
albumArtistList: albumArtistListParameters,
albumList: albumListParameters,
authenticate: authenticateParameters,
createPlaylist: createPlaylistParameters,
playlistList: playlistListParameters,
removeFromPlaylist: removeFromPlaylistParameters,
songList: songListParameters,
updatePlaylist: updatePlaylistParameters,
userList: userListParameters,
},
_response: {
addToPlaylist,
album,
albumArtist,
albumArtistList,
albumList,
authenticate,
createPlaylist,
deletePlaylist,
error,
genre,
genreList,
playlist,
playlistList,
playlistSong,
playlistSongList,
removeFromPlaylist,
song,
songList,
updatePlaylist,
user,
userList,
},
};

View file

@ -1,132 +1,132 @@
import { QueryFunctionContext } from '@tanstack/react-query';
import { LyricSource } from './types';
import type {
AlbumListQuery,
SongListQuery,
AlbumDetailQuery,
AlbumArtistListQuery,
ArtistListQuery,
PlaylistListQuery,
PlaylistDetailQuery,
PlaylistSongListQuery,
UserListQuery,
AlbumArtistDetailQuery,
TopSongListQuery,
SearchQuery,
SongDetailQuery,
RandomSongListQuery,
LyricsQuery,
LyricSearchQuery,
AlbumListQuery,
SongListQuery,
AlbumDetailQuery,
AlbumArtistListQuery,
ArtistListQuery,
PlaylistListQuery,
PlaylistDetailQuery,
PlaylistSongListQuery,
UserListQuery,
AlbumArtistDetailQuery,
TopSongListQuery,
SearchQuery,
SongDetailQuery,
RandomSongListQuery,
LyricsQuery,
LyricSearchQuery,
} from './types';
export const queryKeys: Record<
string,
Record<string, (...props: any) => QueryFunctionContext['queryKey']>
string,
Record<string, (...props: any) => QueryFunctionContext['queryKey']>
> = {
albumArtists: {
detail: (serverId: string, query?: AlbumArtistDetailQuery) => {
if (query) return [serverId, 'albumArtists', 'detail', query] as const;
return [serverId, 'albumArtists', 'detail'] as const;
albumArtists: {
detail: (serverId: string, query?: AlbumArtistDetailQuery) => {
if (query) return [serverId, 'albumArtists', 'detail', query] as const;
return [serverId, 'albumArtists', 'detail'] as const;
},
list: (serverId: string, query?: AlbumArtistListQuery) => {
if (query) return [serverId, 'albumArtists', 'list', query] as const;
return [serverId, 'albumArtists', 'list'] as const;
},
root: (serverId: string) => [serverId, 'albumArtists'] as const,
topSongs: (serverId: string, query?: TopSongListQuery) => {
if (query) return [serverId, 'albumArtists', 'topSongs', query] as const;
return [serverId, 'albumArtists', 'topSongs'] as const;
},
},
list: (serverId: string, query?: AlbumArtistListQuery) => {
if (query) return [serverId, 'albumArtists', 'list', query] as const;
return [serverId, 'albumArtists', 'list'] as const;
albums: {
detail: (serverId: string, query?: AlbumDetailQuery) =>
[serverId, 'albums', 'detail', query] as const,
list: (serverId: string, query?: AlbumListQuery) => {
if (query) return [serverId, 'albums', 'list', query] as const;
return [serverId, 'albums', 'list'] as const;
},
root: (serverId: string) => [serverId, 'albums'],
serverRoot: (serverId: string) => [serverId, 'albums'],
songs: (serverId: string, query: SongListQuery) =>
[serverId, 'albums', 'songs', query] as const,
},
root: (serverId: string) => [serverId, 'albumArtists'] as const,
topSongs: (serverId: string, query?: TopSongListQuery) => {
if (query) return [serverId, 'albumArtists', 'topSongs', query] as const;
return [serverId, 'albumArtists', 'topSongs'] as const;
artists: {
list: (serverId: string, query?: ArtistListQuery) => {
if (query) return [serverId, 'artists', 'list', query] as const;
return [serverId, 'artists', 'list'] as const;
},
root: (serverId: string) => [serverId, 'artists'] as const,
},
},
albums: {
detail: (serverId: string, query?: AlbumDetailQuery) =>
[serverId, 'albums', 'detail', query] as const,
list: (serverId: string, query?: AlbumListQuery) => {
if (query) return [serverId, 'albums', 'list', query] as const;
return [serverId, 'albums', 'list'] as const;
genres: {
list: (serverId: string) => [serverId, 'genres', 'list'] as const,
root: (serverId: string) => [serverId, 'genres'] as const,
},
root: (serverId: string) => [serverId, 'albums'],
serverRoot: (serverId: string) => [serverId, 'albums'],
songs: (serverId: string, query: SongListQuery) =>
[serverId, 'albums', 'songs', query] as const,
},
artists: {
list: (serverId: string, query?: ArtistListQuery) => {
if (query) return [serverId, 'artists', 'list', query] as const;
return [serverId, 'artists', 'list'] as const;
musicFolders: {
list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const,
},
root: (serverId: string) => [serverId, 'artists'] as const,
},
genres: {
list: (serverId: string) => [serverId, 'genres', 'list'] as const,
root: (serverId: string) => [serverId, 'genres'] as const,
},
musicFolders: {
list: (serverId: string) => [serverId, 'musicFolders', 'list'] as const,
},
playlists: {
detail: (serverId: string, id?: string, query?: PlaylistDetailQuery) => {
if (query) return [serverId, 'playlists', id, 'detail', query] as const;
if (id) return [serverId, 'playlists', id, 'detail'] as const;
return [serverId, 'playlists', 'detail'] as const;
playlists: {
detail: (serverId: string, id?: string, query?: PlaylistDetailQuery) => {
if (query) return [serverId, 'playlists', id, 'detail', query] as const;
if (id) return [serverId, 'playlists', id, 'detail'] as const;
return [serverId, 'playlists', 'detail'] as const;
},
detailSongList: (serverId: string, id: string, query?: PlaylistSongListQuery) => {
if (query) return [serverId, 'playlists', id, 'detailSongList', query] as const;
if (id) return [serverId, 'playlists', id, 'detailSongList'] as const;
return [serverId, 'playlists', 'detailSongList'] as const;
},
list: (serverId: string, query?: PlaylistListQuery) => {
if (query) return [serverId, 'playlists', 'list', query] as const;
return [serverId, 'playlists', 'list'] as const;
},
root: (serverId: string) => [serverId, 'playlists'] as const,
songList: (serverId: string, id?: string, query?: PlaylistSongListQuery) => {
if (query && id) return [serverId, 'playlists', id, 'songList', query] as const;
if (id) return [serverId, 'playlists', id, 'songList'] as const;
return [serverId, 'playlists', 'songList'] as const;
},
},
detailSongList: (serverId: string, id: string, query?: PlaylistSongListQuery) => {
if (query) return [serverId, 'playlists', id, 'detailSongList', query] as const;
if (id) return [serverId, 'playlists', id, 'detailSongList'] as const;
return [serverId, 'playlists', 'detailSongList'] as const;
search: {
list: (serverId: string, query?: SearchQuery) => {
if (query) return [serverId, 'search', 'list', query] as const;
return [serverId, 'search', 'list'] as const;
},
root: (serverId: string) => [serverId, 'search'] as const,
},
list: (serverId: string, query?: PlaylistListQuery) => {
if (query) return [serverId, 'playlists', 'list', query] as const;
return [serverId, 'playlists', 'list'] as const;
server: {
root: (serverId: string) => [serverId] as const,
},
root: (serverId: string) => [serverId, 'playlists'] as const,
songList: (serverId: string, id?: string, query?: PlaylistSongListQuery) => {
if (query && id) return [serverId, 'playlists', id, 'songList', query] as const;
if (id) return [serverId, 'playlists', id, 'songList'] as const;
return [serverId, 'playlists', 'songList'] as const;
songs: {
detail: (serverId: string, query?: SongDetailQuery) => {
if (query) return [serverId, 'songs', 'detail', query] as const;
return [serverId, 'songs', 'detail'] as const;
},
list: (serverId: string, query?: SongListQuery) => {
if (query) return [serverId, 'songs', 'list', query] as const;
return [serverId, 'songs', 'list'] as const;
},
lyrics: (serverId: string, query?: LyricsQuery) => {
if (query) return [serverId, 'song', 'lyrics', query] as const;
return [serverId, 'song', 'lyrics'] as const;
},
lyricsByRemoteId: (searchQuery: { remoteSongId: string; remoteSource: LyricSource }) => {
return ['song', 'lyrics', 'remote', searchQuery] as const;
},
lyricsSearch: (query?: LyricSearchQuery) => {
if (query) return ['lyrics', 'search', query] as const;
return ['lyrics', 'search'] as const;
},
randomSongList: (serverId: string, query?: RandomSongListQuery) => {
if (query) return [serverId, 'songs', 'randomSongList', query] as const;
return [serverId, 'songs', 'randomSongList'] as const;
},
root: (serverId: string) => [serverId, 'songs'] as const,
},
},
search: {
list: (serverId: string, query?: SearchQuery) => {
if (query) return [serverId, 'search', 'list', query] as const;
return [serverId, 'search', 'list'] as const;
users: {
list: (serverId: string, query?: UserListQuery) => {
if (query) return [serverId, 'users', 'list', query] as const;
return [serverId, 'users', 'list'] as const;
},
root: (serverId: string) => [serverId, 'users'] as const,
},
root: (serverId: string) => [serverId, 'search'] as const,
},
server: {
root: (serverId: string) => [serverId] as const,
},
songs: {
detail: (serverId: string, query?: SongDetailQuery) => {
if (query) return [serverId, 'songs', 'detail', query] as const;
return [serverId, 'songs', 'detail'] as const;
},
list: (serverId: string, query?: SongListQuery) => {
if (query) return [serverId, 'songs', 'list', query] as const;
return [serverId, 'songs', 'list'] as const;
},
lyrics: (serverId: string, query?: LyricsQuery) => {
if (query) return [serverId, 'song', 'lyrics', query] as const;
return [serverId, 'song', 'lyrics'] as const;
},
lyricsByRemoteId: (searchQuery: { remoteSongId: string; remoteSource: LyricSource }) => {
return ['song', 'lyrics', 'remote', searchQuery] as const;
},
lyricsSearch: (query?: LyricSearchQuery) => {
if (query) return ['lyrics', 'search', query] as const;
return ['lyrics', 'search'] as const;
},
randomSongList: (serverId: string, query?: RandomSongListQuery) => {
if (query) return [serverId, 'songs', 'randomSongList', query] as const;
return [serverId, 'songs', 'randomSongList'] as const;
},
root: (serverId: string) => [serverId, 'songs'] as const,
},
users: {
list: (serverId: string, query?: UserListQuery) => {
if (query) return [serverId, 'users', 'list', query] as const;
return [serverId, 'users', 'list'] as const;
},
root: (serverId: string) => [serverId, 'users'] as const,
},
};

View file

@ -1,190 +1,190 @@
export type SSBaseResponse = {
serverVersion?: 'string';
status: 'string';
type?: 'string';
version: 'string';
serverVersion?: 'string';
status: 'string';
type?: 'string';
version: 'string';
};
export type SSMusicFolderList = SSMusicFolder[];
export type SSMusicFolderListResponse = {
musicFolders: {
musicFolder: SSMusicFolder[];
};
musicFolders: {
musicFolder: SSMusicFolder[];
};
};
export type SSGenreList = SSGenre[];
export type SSGenreListResponse = {
genres: {
genre: SSGenre[];
};
genres: {
genre: SSGenre[];
};
};
export type SSAlbumArtistDetailParams = {
id: string;
id: string;
};
export type SSAlbumArtistDetail = SSAlbumArtistListEntry & { album: SSAlbumListEntry[] };
export type SSAlbumArtistDetailResponse = {
artist: SSAlbumArtistListEntry & {
album: SSAlbumListEntry[];
};
artist: SSAlbumArtistListEntry & {
album: SSAlbumListEntry[];
};
};
export type SSAlbumArtistList = {
items: SSAlbumArtistListEntry[];
startIndex: number;
totalRecordCount: number | null;
items: SSAlbumArtistListEntry[];
startIndex: number;
totalRecordCount: number | null;
};
export type SSAlbumArtistListResponse = {
artists: {
ignoredArticles: string;
index: SSArtistIndex[];
lastModified: number;
};
artists: {
ignoredArticles: string;
index: SSArtistIndex[];
lastModified: number;
};
};
export type SSAlbumList = {
items: SSAlbumListEntry[];
startIndex: number;
totalRecordCount: number | null;
items: SSAlbumListEntry[];
startIndex: number;
totalRecordCount: number | null;
};
export type SSAlbumListResponse = {
albumList2: {
album: SSAlbumListEntry[];
};
albumList2: {
album: SSAlbumListEntry[];
};
};
export type SSAlbumDetail = Omit<SSAlbum, 'song'> & { songs: SSSong[] };
export type SSAlbumDetailResponse = {
album: SSAlbum;
album: SSAlbum;
};
export type SSArtistInfoParams = {
count?: number;
id: string;
includeNotPresent?: boolean;
count?: number;
id: string;
includeNotPresent?: boolean;
};
export type SSArtistInfoResponse = {
artistInfo2: SSArtistInfo;
artistInfo2: SSArtistInfo;
};
export type SSArtistInfo = {
biography: string;
largeImageUrl?: string;
lastFmUrl?: string;
mediumImageUrl?: string;
musicBrainzId?: string;
similarArtist?: {
biography: string;
largeImageUrl?: string;
lastFmUrl?: string;
mediumImageUrl?: string;
musicBrainzId?: string;
similarArtist?: {
albumCount: string;
artistImageUrl?: string;
coverArt?: string;
id: string;
name: string;
}[];
smallImageUrl?: string;
};
export type SSMusicFolder = {
id: number;
name: string;
};
export type SSGenre = {
albumCount?: number;
songCount?: number;
value: string;
};
export type SSArtistIndex = {
artist: SSAlbumArtistListEntry[];
name: string;
};
export type SSAlbumArtistListEntry = {
albumCount: string;
artistImageUrl?: string;
coverArt?: string;
id: string;
name: string;
}[];
smallImageUrl?: string;
};
export type SSMusicFolder = {
id: number;
name: string;
};
export type SSGenre = {
albumCount?: number;
songCount?: number;
value: string;
};
export type SSArtistIndex = {
artist: SSAlbumArtistListEntry[];
name: string;
};
export type SSAlbumArtistListEntry = {
albumCount: string;
artistImageUrl?: string;
coverArt?: string;
id: string;
name: string;
};
export type SSAlbumListEntry = {
album: string;
artist: string;
artistId: string;
coverArt: string;
created: string;
duration: number;
genre?: string;
id: string;
isDir: boolean;
isVideo: boolean;
name: string;
parent: string;
songCount: number;
starred?: boolean;
title: string;
userRating?: number;
year: number;
album: string;
artist: string;
artistId: string;
coverArt: string;
created: string;
duration: number;
genre?: string;
id: string;
isDir: boolean;
isVideo: boolean;
name: string;
parent: string;
songCount: number;
starred?: boolean;
title: string;
userRating?: number;
year: number;
};
export type SSAlbum = {
song: SSSong[];
song: SSSong[];
} & SSAlbumListEntry;
export type SSSong = {
album: string;
albumId: string;
artist: string;
artistId?: string;
bitRate: number;
contentType: string;
coverArt: string;
created: string;
discNumber?: number;
duration: number;
genre: string;
id: string;
isDir: boolean;
isVideo: boolean;
parent: string;
path: string;
playCount: number;
size: number;
starred?: boolean;
suffix: string;
title: string;
track: number;
type: string;
userRating?: number;
year: number;
album: string;
albumId: string;
artist: string;
artistId?: string;
bitRate: number;
contentType: string;
coverArt: string;
created: string;
discNumber?: number;
duration: number;
genre: string;
id: string;
isDir: boolean;
isVideo: boolean;
parent: string;
path: string;
playCount: number;
size: number;
starred?: boolean;
suffix: string;
title: string;
track: number;
type: string;
userRating?: number;
year: number;
};
export type SSAlbumListParams = {
fromYear?: number;
genre?: string;
musicFolderId?: string;
offset?: number;
size?: number;
toYear?: number;
type: string;
fromYear?: number;
genre?: string;
musicFolderId?: string;
offset?: number;
size?: number;
toYear?: number;
type: string;
};
export type SSAlbumArtistListParams = {
musicFolderId?: string;
musicFolderId?: string;
};
export type SSFavoriteParams = {
albumId?: string;
artistId?: string;
id?: string;
albumId?: string;
artistId?: string;
id?: string;
};
export type SSFavorite = null;
@ -192,8 +192,8 @@ export type SSFavorite = null;
export type SSFavoriteResponse = null;
export type SSRatingParams = {
id: string;
rating: number;
id: string;
rating: number;
};
export type SSRating = null;
@ -201,24 +201,24 @@ export type SSRating = null;
export type SSRatingResponse = null;
export type SSTopSongListParams = {
artist: string;
count?: number;
artist: string;
count?: number;
};
export type SSTopSongListResponse = {
topSongs: {
song: SSSong[];
};
topSongs: {
song: SSSong[];
};
};
export type SSTopSongList = {
items: SSSong[];
startIndex: number;
totalRecordCount: number | null;
items: SSSong[];
startIndex: number;
totalRecordCount: number | null;
};
export type SSScrobbleParams = {
id: string;
submission?: boolean;
time?: number;
id: string;
submission?: boolean;
time?: number;
};

View file

@ -10,196 +10,198 @@ import { toast } from '/@/renderer/components/toast/index';
const c = initContract();
export const contract = c.router({
authenticate: {
method: 'GET',
path: 'ping.view',
query: ssType._parameters.authenticate,
responses: {
200: ssType._response.authenticate,
authenticate: {
method: 'GET',
path: 'ping.view',
query: ssType._parameters.authenticate,
responses: {
200: ssType._response.authenticate,
},
},
},
createFavorite: {
method: 'GET',
path: 'star.view',
query: ssType._parameters.createFavorite,
responses: {
200: ssType._response.createFavorite,
createFavorite: {
method: 'GET',
path: 'star.view',
query: ssType._parameters.createFavorite,
responses: {
200: ssType._response.createFavorite,
},
},
},
getArtistInfo: {
method: 'GET',
path: 'getArtistInfo.view',
query: ssType._parameters.artistInfo,
responses: {
200: ssType._response.artistInfo,
getArtistInfo: {
method: 'GET',
path: 'getArtistInfo.view',
query: ssType._parameters.artistInfo,
responses: {
200: ssType._response.artistInfo,
},
},
},
getMusicFolderList: {
method: 'GET',
path: 'getMusicFolders.view',
responses: {
200: ssType._response.musicFolderList,
getMusicFolderList: {
method: 'GET',
path: 'getMusicFolders.view',
responses: {
200: ssType._response.musicFolderList,
},
},
},
getRandomSongList: {
method: 'GET',
path: 'getRandomSongs.view',
query: ssType._parameters.randomSongList,
responses: {
200: ssType._response.randomSongList,
getRandomSongList: {
method: 'GET',
path: 'getRandomSongs.view',
query: ssType._parameters.randomSongList,
responses: {
200: ssType._response.randomSongList,
},
},
},
getTopSongsList: {
method: 'GET',
path: 'getTopSongs.view',
query: ssType._parameters.topSongsList,
responses: {
200: ssType._response.topSongsList,
getTopSongsList: {
method: 'GET',
path: 'getTopSongs.view',
query: ssType._parameters.topSongsList,
responses: {
200: ssType._response.topSongsList,
},
},
},
removeFavorite: {
method: 'GET',
path: 'unstar.view',
query: ssType._parameters.removeFavorite,
responses: {
200: ssType._response.removeFavorite,
removeFavorite: {
method: 'GET',
path: 'unstar.view',
query: ssType._parameters.removeFavorite,
responses: {
200: ssType._response.removeFavorite,
},
},
},
scrobble: {
method: 'GET',
path: 'scrobble.view',
query: ssType._parameters.scrobble,
responses: {
200: ssType._response.scrobble,
scrobble: {
method: 'GET',
path: 'scrobble.view',
query: ssType._parameters.scrobble,
responses: {
200: ssType._response.scrobble,
},
},
},
search3: {
method: 'GET',
path: 'search3.view',
query: ssType._parameters.search3,
responses: {
200: ssType._response.search3,
search3: {
method: 'GET',
path: 'search3.view',
query: ssType._parameters.search3,
responses: {
200: ssType._response.search3,
},
},
},
setRating: {
method: 'GET',
path: 'setRating.view',
query: ssType._parameters.setRating,
responses: {
200: ssType._response.setRating,
setRating: {
method: 'GET',
path: 'setRating.view',
query: ssType._parameters.setRating,
responses: {
200: ssType._response.setRating,
},
},
},
});
const axiosClient = axios.create({});
axiosClient.defaults.paramsSerializer = (params) => {
return qs.stringify(params, { arrayFormat: 'repeat' });
return qs.stringify(params, { arrayFormat: 'repeat' });
};
axiosClient.interceptors.response.use(
(response) => {
const data = response.data;
(response) => {
const data = response.data;
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',
});
}
}
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 response;
},
(error) => {
return Promise.reject(error);
},
return response;
},
(error) => {
return Promise.reject(error);
},
);
const parsePath = (fullPath: string) => {
const [path, params] = fullPath.split('?');
const [path, params] = fullPath.split('?');
const parsedParams = qs.parse(params);
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
const parsedParams = qs.parse(params);
const notNilParams = omitBy(parsedParams, (value) => value === 'undefined' || value === 'null');
return {
params: notNilParams,
path,
};
return {
params: notNilParams,
path,
};
};
export const ssApiClient = (args: {
server: ServerListItem | null;
signal?: AbortSignal;
url?: string;
server: ServerListItem | null;
signal?: AbortSignal;
url?: string;
}) => {
const { server, url, signal } = args;
const { server, url, signal } = args;
return initClient(contract, {
api: async ({ path, method, headers, body }) => {
let baseUrl: string | undefined;
const authParams: Record<string, any> = {};
return initClient(contract, {
api: async ({ path, method, headers, body }) => {
let baseUrl: string | undefined;
const authParams: Record<string, any> = {};
const { params, path: api } = parsePath(path);
const { params, path: api } = parsePath(path);
if (server) {
baseUrl = `${server.url}/rest`;
const token = server.credential;
const params = token.split(/&?\w=/gm);
if (server) {
baseUrl = `${server.url}/rest`;
const token = server.credential;
const params = token.split(/&?\w=/gm);
authParams.u = server.username;
if (params?.length === 4) {
authParams.s = params[2];
authParams.t = params[3];
} else if (params?.length === 3) {
authParams.p = params[2];
}
} else {
baseUrl = url;
}
authParams.u = server.username;
if (params?.length === 4) {
authParams.s = params[2];
authParams.t = params[3];
} else if (params?.length === 3) {
authParams.p = params[2];
}
} else {
baseUrl = url;
}
try {
const result = await axiosClient.request<z.infer<typeof ssType._response.baseResponse>>({
data: body,
headers,
method: method as Method,
params: {
c: 'Feishin',
f: 'json',
v: '1.13.0',
...authParams,
...params,
},
signal,
url: `${baseUrl}/${api}`,
});
try {
const result = await axiosClient.request<
z.infer<typeof ssType._response.baseResponse>
>({
data: body,
headers,
method: method as Method,
params: {
c: 'Feishin',
f: 'json',
v: '1.13.0',
...authParams,
...params,
},
signal,
url: `${baseUrl}/${api}`,
});
return {
body: result.data['subsonic-response'],
headers: result.headers as any,
status: result.status,
};
} catch (e: Error | AxiosError | any) {
console.log('CATCH ERR');
return {
body: result.data['subsonic-response'],
headers: result.headers as any,
status: result.status,
};
} catch (e: Error | AxiosError | any) {
console.log('CATCH ERR');
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return {
body: response?.data,
headers: response.headers as any,
status: response?.status,
};
}
throw e;
}
},
baseHeaders: {
'Content-Type': 'application/json',
},
baseUrl: '',
});
return {
body: response?.data,
headers: response.headers as any,
status: response?.status,
};
}
throw e;
}
},
baseHeaders: {
'Content-Type': 'application/json',
},
baseUrl: '',
});
};

View file

@ -4,91 +4,91 @@ import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import {
ArtistInfoArgs,
AuthenticationResponse,
FavoriteArgs,
FavoriteResponse,
LibraryItem,
MusicFolderListArgs,
MusicFolderListResponse,
SetRatingArgs,
RatingResponse,
ScrobbleArgs,
ScrobbleResponse,
SongListResponse,
TopSongListArgs,
SearchArgs,
SearchResponse,
RandomSongListResponse,
RandomSongListArgs,
ArtistInfoArgs,
AuthenticationResponse,
FavoriteArgs,
FavoriteResponse,
LibraryItem,
MusicFolderListArgs,
MusicFolderListResponse,
SetRatingArgs,
RatingResponse,
ScrobbleArgs,
ScrobbleResponse,
SongListResponse,
TopSongListArgs,
SearchArgs,
SearchResponse,
RandomSongListResponse,
RandomSongListArgs,
} from '/@/renderer/api/types';
import { randomString } from '/@/renderer/utils';
const authenticate = async (
url: string,
body: {
legacy?: boolean;
password: string;
username: string;
},
): Promise<AuthenticationResponse> => {
let credential: string;
let credentialParams: {
p?: string;
s?: string;
t?: string;
u: string;
};
const cleanServerUrl = url.replace(/\/$/, '');
if (body.legacy) {
credential = `u=${body.username}&p=${body.password}`;
credentialParams = {
p: body.password,
u: body.username,
};
} else {
const salt = randomString(12);
const hash = md5(body.password + salt);
credential = `u=${body.username}&s=${salt}&t=${hash}`;
credentialParams = {
s: salt,
t: hash,
u: body.username,
};
}
await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({
query: {
c: 'Feishin',
f: 'json',
v: '1.13.0',
...credentialParams,
url: string,
body: {
legacy?: boolean;
password: string;
username: string;
},
});
): Promise<AuthenticationResponse> => {
let credential: string;
let credentialParams: {
p?: string;
s?: string;
t?: string;
u: string;
};
return {
credential,
userId: null,
username: body.username,
};
const cleanServerUrl = url.replace(/\/$/, '');
if (body.legacy) {
credential = `u=${body.username}&p=${body.password}`;
credentialParams = {
p: body.password,
u: body.username,
};
} else {
const salt = randomString(12);
const hash = md5(body.password + salt);
credential = `u=${body.username}&s=${salt}&t=${hash}`;
credentialParams = {
s: salt,
t: hash,
u: body.username,
};
}
await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({
query: {
c: 'Feishin',
f: 'json',
v: '1.13.0',
...credentialParams,
},
});
return {
credential,
userId: null,
username: body.username,
};
};
const getMusicFolderList = async (args: MusicFolderListArgs): Promise<MusicFolderListResponse> => {
const { apiClientProps } = args;
const { apiClientProps } = args;
const res = await ssApiClient(apiClientProps).getMusicFolderList({});
const res = await ssApiClient(apiClientProps).getMusicFolderList({});
if (res.status !== 200) {
throw new Error('Failed to get music folder list');
}
if (res.status !== 200) {
throw new Error('Failed to get music folder list');
}
return {
items: res.body.musicFolders.musicFolder,
startIndex: 0,
totalRecordCount: res.body.musicFolders.musicFolder.length,
};
return {
items: res.body.musicFolders.musicFolder,
startIndex: 0,
totalRecordCount: res.body.musicFolders.musicFolder.length,
};
};
// export const getAlbumArtistDetail = async (
@ -198,184 +198,185 @@ const getMusicFolderList = async (args: MusicFolderListArgs): Promise<MusicFolde
// };
const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ssApiClient(apiClientProps).createFavorite({
query: {
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
id: query.type === LibraryItem.SONG ? query.id : undefined,
},
});
const res = await ssApiClient(apiClientProps).createFavorite({
query: {
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
id: query.type === LibraryItem.SONG ? query.id : undefined,
},
});
if (res.status !== 200) {
throw new Error('Failed to create favorite');
}
if (res.status !== 200) {
throw new Error('Failed to create favorite');
}
return null;
return null;
};
const removeFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ssApiClient(apiClientProps).removeFavorite({
query: {
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
id: query.type === LibraryItem.SONG ? query.id : undefined,
},
});
const res = await ssApiClient(apiClientProps).removeFavorite({
query: {
albumId: query.type === LibraryItem.ALBUM ? query.id : undefined,
artistId: query.type === LibraryItem.ALBUM_ARTIST ? query.id : undefined,
id: query.type === LibraryItem.SONG ? query.id : undefined,
},
});
if (res.status !== 200) {
throw new Error('Failed to delete favorite');
}
if (res.status !== 200) {
throw new Error('Failed to delete favorite');
}
return null;
return null;
};
const setRating = async (args: SetRatingArgs): Promise<RatingResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const itemIds = query.item.map((item) => item.id);
const itemIds = query.item.map((item) => item.id);
for (const id of itemIds) {
await ssApiClient(apiClientProps).setRating({
query: {
id,
rating: query.rating,
},
});
}
for (const id of itemIds) {
await ssApiClient(apiClientProps).setRating({
query: {
id,
rating: query.rating,
},
});
}
return null;
return null;
};
const getTopSongList = async (args: TopSongListArgs): Promise<SongListResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ssApiClient(apiClientProps).getTopSongsList({
query: {
artist: query.artist,
count: query.limit,
},
});
const res = await ssApiClient(apiClientProps).getTopSongsList({
query: {
artist: query.artist,
count: query.limit,
},
});
if (res.status !== 200) {
throw new Error('Failed to get top songs');
}
if (res.status !== 200) {
throw new Error('Failed to get top songs');
}
return {
items:
res.body.topSongs?.song?.map((song) => ssNormalize.song(song, apiClientProps.server, '')) ||
[],
startIndex: 0,
totalRecordCount: res.body.topSongs?.song?.length || 0,
};
return {
items:
res.body.topSongs?.song?.map((song) =>
ssNormalize.song(song, apiClientProps.server, ''),
) || [],
startIndex: 0,
totalRecordCount: res.body.topSongs?.song?.length || 0,
};
};
const getArtistInfo = async (
args: ArtistInfoArgs,
args: ArtistInfoArgs,
): Promise<z.infer<typeof ssType._response.artistInfo>> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ssApiClient(apiClientProps).getArtistInfo({
query: {
count: query.limit,
id: query.artistId,
},
});
const res = await ssApiClient(apiClientProps).getArtistInfo({
query: {
count: query.limit,
id: query.artistId,
},
});
if (res.status !== 200) {
throw new Error('Failed to get artist info');
}
if (res.status !== 200) {
throw new Error('Failed to get artist info');
}
return res.body;
return res.body;
};
const scrobble = async (args: ScrobbleArgs): Promise<ScrobbleResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ssApiClient(apiClientProps).scrobble({
query: {
id: query.id,
submission: query.submission,
},
});
const res = await ssApiClient(apiClientProps).scrobble({
query: {
id: query.id,
submission: query.submission,
},
});
if (res.status !== 200) {
throw new Error('Failed to scrobble');
}
if (res.status !== 200) {
throw new Error('Failed to scrobble');
}
return null;
return null;
};
const search3 = async (args: SearchArgs): Promise<SearchResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ssApiClient(apiClientProps).search3({
query: {
albumCount: query.albumLimit,
albumOffset: query.albumStartIndex,
artistCount: query.albumArtistLimit,
artistOffset: query.albumArtistStartIndex,
query: query.query,
songCount: query.songLimit,
songOffset: query.songStartIndex,
},
});
const res = await ssApiClient(apiClientProps).search3({
query: {
albumCount: query.albumLimit,
albumOffset: query.albumStartIndex,
artistCount: query.albumArtistLimit,
artistOffset: query.albumArtistStartIndex,
query: query.query,
songCount: query.songLimit,
songOffset: query.songStartIndex,
},
});
if (res.status !== 200) {
throw new Error('Failed to search');
}
if (res.status !== 200) {
throw new Error('Failed to search');
}
return {
albumArtists: res.body.searchResult3?.artist?.map((artist) =>
ssNormalize.albumArtist(artist, apiClientProps.server),
),
albums: res.body.searchResult3?.album?.map((album) =>
ssNormalize.album(album, apiClientProps.server),
),
songs: res.body.searchResult3?.song?.map((song) =>
ssNormalize.song(song, apiClientProps.server, ''),
),
};
return {
albumArtists: res.body.searchResult3?.artist?.map((artist) =>
ssNormalize.albumArtist(artist, apiClientProps.server),
),
albums: res.body.searchResult3?.album?.map((album) =>
ssNormalize.album(album, apiClientProps.server),
),
songs: res.body.searchResult3?.song?.map((song) =>
ssNormalize.song(song, apiClientProps.server, ''),
),
};
};
const getRandomSongList = async (args: RandomSongListArgs): Promise<RandomSongListResponse> => {
const { query, apiClientProps } = args;
const { query, apiClientProps } = args;
const res = await ssApiClient(apiClientProps).getRandomSongList({
query: {
fromYear: query.minYear,
genre: query.genre,
musicFolderId: query.musicFolderId,
size: query.limit,
toYear: query.maxYear,
},
});
const res = await ssApiClient(apiClientProps).getRandomSongList({
query: {
fromYear: query.minYear,
genre: query.genre,
musicFolderId: query.musicFolderId,
size: query.limit,
toYear: query.maxYear,
},
});
if (res.status !== 200) {
throw new Error('Failed to get random songs');
}
if (res.status !== 200) {
throw new Error('Failed to get random songs');
}
return {
items: res.body.randomSongs?.song?.map((song) =>
ssNormalize.song(song, apiClientProps.server, ''),
),
startIndex: 0,
totalRecordCount: res.body.randomSongs?.song?.length || 0,
};
return {
items: res.body.randomSongs?.song?.map((song) =>
ssNormalize.song(song, apiClientProps.server, ''),
),
startIndex: 0,
totalRecordCount: res.body.randomSongs?.song?.length || 0,
};
};
export const ssController = {
authenticate,
createFavorite,
getArtistInfo,
getMusicFolderList,
getRandomSongList,
getTopSongList,
removeFavorite,
scrobble,
search3,
setRating,
authenticate,
createFavorite,
getArtistInfo,
getMusicFolderList,
getRandomSongList,
getTopSongList,
removeFavorite,
scrobble,
search3,
setRating,
};

View file

@ -5,176 +5,178 @@ import { QueueSong, LibraryItem, AlbumArtist, Album } from '/@/renderer/api/type
import { ServerListItem, ServerType } from '/@/renderer/types';
const getCoverArtUrl = (args: {
baseUrl: string | undefined;
coverArtId?: string;
credential: string | undefined;
size: number;
baseUrl: string | undefined;
coverArtId?: string;
credential: string | undefined;
size: number;
}) => {
const size = args.size ? args.size : 250;
const size = args.size ? args.size : 250;
if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) {
return null;
}
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}`
);
return (
`${args.baseUrl}/rest/getCoverArt.view` +
`?id=${args.coverArtId}` +
`&${args.credential}` +
'&v=1.13.0' +
'&c=feishin' +
`&size=${size}`
);
};
const normalizeSong = (
item: z.infer<typeof ssType._response.song>,
server: ServerListItem | null,
deviceId: string,
item: z.infer<typeof ssType._response.song>,
server: ServerListItem | null,
deviceId: string,
): QueueSong => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 100,
}) || null;
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 100,
}) || null;
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`;
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 || 0,
bpm: null,
channels: null,
comment: null,
compilation: null,
container: item.contentType,
createdAt: item.created,
discNumber: item.discNumber || 1,
duration: item.duration || 0,
genres: item.genre
? [
{
id: item.genre,
name: item.genre,
},
]
: [],
id: item.id,
imagePlaceholderUrl: null,
imageUrl,
itemType: LibraryItem.SONG,
lastPlayedAt: null,
lyrics: null,
name: item.title,
path: item.path,
playCount: item?.playCount || 0,
releaseDate: null,
releaseYear: item.year ? String(item.year) : null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: item.size,
streamUrl,
trackNumber: item.track || 1,
uniqueId: nanoid(),
updatedAt: '',
userFavorite: item.starred || false,
userRating: item.userRating || 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 || 0,
bpm: null,
channels: null,
comment: null,
compilation: null,
container: item.contentType,
createdAt: item.created,
discNumber: item.discNumber || 1,
duration: item.duration || 0,
genres: item.genre
? [
{
id: item.genre,
name: item.genre,
},
]
: [],
id: item.id,
imagePlaceholderUrl: null,
imageUrl,
itemType: LibraryItem.SONG,
lastPlayedAt: null,
lyrics: null,
name: item.title,
path: item.path,
playCount: item?.playCount || 0,
releaseDate: null,
releaseYear: item.year ? String(item.year) : null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: item.size,
streamUrl,
trackNumber: item.track || 1,
uniqueId: nanoid(),
updatedAt: '',
userFavorite: item.starred || false,
userRating: item.userRating || null,
};
};
const normalizeAlbumArtist = (
item: z.infer<typeof ssType._response.albumArtist>,
server: ServerListItem | null,
item: z.infer<typeof ssType._response.albumArtist>,
server: ServerListItem | null,
): AlbumArtist => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 100,
}) || null;
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 100,
}) || null;
return {
albumCount: item.albumCount ? Number(item.albumCount) : 0,
backgroundImageUrl: null,
biography: null,
duration: null,
genres: [],
id: item.id,
imageUrl,
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: null,
name: item.name,
playCount: null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
similarArtists: [],
songCount: null,
userFavorite: false,
userRating: null,
};
return {
albumCount: item.albumCount ? Number(item.albumCount) : 0,
backgroundImageUrl: null,
biography: null,
duration: null,
genres: [],
id: item.id,
imageUrl,
itemType: LibraryItem.ALBUM_ARTIST,
lastPlayedAt: null,
name: item.name,
playCount: null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
similarArtists: [],
songCount: null,
userFavorite: false,
userRating: null,
};
};
const normalizeAlbum = (
item: z.infer<typeof ssType._response.album>,
server: ServerListItem | null,
item: z.infer<typeof ssType._response.album>,
server: ServerListItem | null,
): Album => {
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 300,
}) || null;
const imageUrl =
getCoverArtUrl({
baseUrl: server?.url,
coverArtId: item.coverArt,
credential: server?.credential,
size: 300,
}) || null;
return {
albumArtists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
backdropImageUrl: null,
createdAt: item.created,
duration: item.duration,
genres: item.genre ? [{ id: item.genre, name: item.genre }] : [],
id: item.id,
imagePlaceholderUrl: null,
imageUrl,
isCompilation: null,
itemType: LibraryItem.ALBUM,
lastPlayedAt: null,
name: item.name,
playCount: null,
releaseDate: item.year ? new Date(item.year, 0, 1).toISOString() : null,
releaseYear: item.year ? Number(item.year) : null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: null,
songCount: item.songCount,
songs: [],
uniqueId: nanoid(),
updatedAt: item.created,
userFavorite: item.starred || false,
userRating: item.userRating || null,
};
return {
albumArtists: item.artistId
? [{ id: item.artistId, imageUrl: null, name: item.artist }]
: [],
artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
backdropImageUrl: null,
createdAt: item.created,
duration: item.duration,
genres: item.genre ? [{ id: item.genre, name: item.genre }] : [],
id: item.id,
imagePlaceholderUrl: null,
imageUrl,
isCompilation: null,
itemType: LibraryItem.ALBUM,
lastPlayedAt: null,
name: item.name,
playCount: null,
releaseDate: item.year ? new Date(item.year, 0, 1).toISOString() : null,
releaseYear: item.year ? Number(item.year) : null,
serverId: server?.id || 'unknown',
serverType: ServerType.SUBSONIC,
size: null,
songCount: item.songCount,
songs: [],
uniqueId: nanoid(),
updatedAt: item.created,
userFavorite: item.starred || false,
userRating: item.userRating || null,
};
};
export const ssNormalize = {
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
song: normalizeSong,
album: normalizeAlbum,
albumArtist: normalizeAlbumArtist,
song: normalizeSong,
};

View file

@ -1,240 +1,240 @@
import { z } from 'zod';
const baseResponse = z.object({
'subsonic-response': z.object({
status: z.string(),
version: z.string(),
}),
'subsonic-response': z.object({
status: z.string(),
version: z.string(),
}),
});
const authenticate = z.null();
const authenticateParameters = z.object({
c: z.string(),
f: z.string(),
p: z.string().optional(),
s: z.string().optional(),
t: z.string().optional(),
u: z.string(),
v: z.string(),
c: z.string(),
f: z.string(),
p: z.string().optional(),
s: z.string().optional(),
t: z.string().optional(),
u: z.string(),
v: z.string(),
});
const createFavoriteParameters = z.object({
albumId: z.array(z.string()).optional(),
artistId: z.array(z.string()).optional(),
id: z.array(z.string()).optional(),
albumId: z.array(z.string()).optional(),
artistId: z.array(z.string()).optional(),
id: z.array(z.string()).optional(),
});
const createFavorite = z.null();
const removeFavoriteParameters = z.object({
albumId: z.array(z.string()).optional(),
artistId: z.array(z.string()).optional(),
id: z.array(z.string()).optional(),
albumId: z.array(z.string()).optional(),
artistId: z.array(z.string()).optional(),
id: z.array(z.string()).optional(),
});
const removeFavorite = z.null();
const setRatingParameters = z.object({
id: z.string(),
rating: z.number(),
id: z.string(),
rating: z.number(),
});
const setRating = z.null();
const musicFolder = z.object({
id: z.string(),
name: z.string(),
id: z.string(),
name: z.string(),
});
const musicFolderList = z.object({
musicFolders: z.object({
musicFolder: z.array(musicFolder),
}),
musicFolders: z.object({
musicFolder: z.array(musicFolder),
}),
});
const song = z.object({
album: z.string().optional(),
albumId: z.string().optional(),
artist: z.string().optional(),
artistId: z.string().optional(),
averageRating: z.number().optional(),
bitRate: z.number().optional(),
contentType: z.string(),
coverArt: z.string().optional(),
created: z.string(),
discNumber: z.number(),
duration: z.number().optional(),
genre: z.string().optional(),
id: z.string(),
isDir: z.boolean(),
isVideo: z.boolean(),
parent: z.string(),
path: z.string(),
playCount: z.number().optional(),
size: z.number(),
starred: z.boolean().optional(),
suffix: z.string(),
title: z.string(),
track: z.number().optional(),
type: z.string(),
userRating: z.number().optional(),
year: z.number().optional(),
album: z.string().optional(),
albumId: z.string().optional(),
artist: z.string().optional(),
artistId: z.string().optional(),
averageRating: z.number().optional(),
bitRate: z.number().optional(),
contentType: z.string(),
coverArt: z.string().optional(),
created: z.string(),
discNumber: z.number(),
duration: z.number().optional(),
genre: z.string().optional(),
id: z.string(),
isDir: z.boolean(),
isVideo: z.boolean(),
parent: z.string(),
path: z.string(),
playCount: z.number().optional(),
size: z.number(),
starred: z.boolean().optional(),
suffix: z.string(),
title: z.string(),
track: z.number().optional(),
type: z.string(),
userRating: z.number().optional(),
year: z.number().optional(),
});
const album = z.object({
album: z.string(),
artist: z.string(),
artistId: z.string(),
coverArt: z.string(),
created: z.string(),
duration: z.number(),
genre: z.string().optional(),
id: z.string(),
isDir: z.boolean(),
isVideo: z.boolean(),
name: z.string(),
parent: z.string(),
song: z.array(song),
songCount: z.number(),
starred: z.boolean().optional(),
title: z.string(),
userRating: z.number().optional(),
year: z.number().optional(),
album: z.string(),
artist: z.string(),
artistId: z.string(),
coverArt: z.string(),
created: z.string(),
duration: z.number(),
genre: z.string().optional(),
id: z.string(),
isDir: z.boolean(),
isVideo: z.boolean(),
name: z.string(),
parent: z.string(),
song: z.array(song),
songCount: z.number(),
starred: z.boolean().optional(),
title: z.string(),
userRating: z.number().optional(),
year: z.number().optional(),
});
const albumListParameters = z.object({
fromYear: z.number().optional(),
genre: z.string().optional(),
musicFolderId: z.string().optional(),
offset: z.number().optional(),
size: z.number().optional(),
toYear: z.number().optional(),
type: z.string().optional(),
fromYear: z.number().optional(),
genre: z.string().optional(),
musicFolderId: z.string().optional(),
offset: z.number().optional(),
size: z.number().optional(),
toYear: z.number().optional(),
type: z.string().optional(),
});
const albumList = z.array(album.omit({ song: true }));
const albumArtist = z.object({
albumCount: z.string(),
artistImageUrl: z.string().optional(),
coverArt: z.string().optional(),
id: z.string(),
name: z.string(),
albumCount: z.string(),
artistImageUrl: z.string().optional(),
coverArt: z.string().optional(),
id: z.string(),
name: z.string(),
});
const albumArtistList = z.object({
artist: z.array(albumArtist),
name: z.string(),
artist: z.array(albumArtist),
name: z.string(),
});
const artistInfoParameters = z.object({
count: z.number().optional(),
id: z.string(),
includeNotPresent: z.boolean().optional(),
count: z.number().optional(),
id: z.string(),
includeNotPresent: z.boolean().optional(),
});
const artistInfo = z.object({
artistInfo: z.object({
biography: z.string().optional(),
largeImageUrl: z.string().optional(),
lastFmUrl: z.string().optional(),
mediumImageUrl: z.string().optional(),
musicBrainzId: z.string().optional(),
similarArtist: z.array(
z.object({
albumCount: z.string(),
artistImageUrl: z.string().optional(),
coverArt: z.string().optional(),
id: z.string(),
name: z.string(),
}),
),
smallImageUrl: z.string().optional(),
}),
artistInfo: z.object({
biography: z.string().optional(),
largeImageUrl: z.string().optional(),
lastFmUrl: z.string().optional(),
mediumImageUrl: z.string().optional(),
musicBrainzId: z.string().optional(),
similarArtist: z.array(
z.object({
albumCount: z.string(),
artistImageUrl: z.string().optional(),
coverArt: z.string().optional(),
id: z.string(),
name: z.string(),
}),
),
smallImageUrl: z.string().optional(),
}),
});
const topSongsListParameters = z.object({
artist: z.string(), // The name of the artist, not the artist ID
count: z.number().optional(),
artist: z.string(), // The name of the artist, not the artist ID
count: z.number().optional(),
});
const topSongsList = z.object({
topSongs: z.object({
song: z.array(song),
}),
topSongs: z.object({
song: z.array(song),
}),
});
const scrobbleParameters = z.object({
id: z.string(),
submission: z.boolean().optional(),
time: z.number().optional(), // The time (in milliseconds since 1 Jan 1970) at which the song was listened to.
id: z.string(),
submission: z.boolean().optional(),
time: z.number().optional(), // The time (in milliseconds since 1 Jan 1970) at which the song was listened to.
});
const scrobble = z.null();
const search3 = z.object({
searchResult3: z.object({
album: z.array(album),
artist: z.array(albumArtist),
song: z.array(song),
}),
searchResult3: z.object({
album: z.array(album),
artist: z.array(albumArtist),
song: z.array(song),
}),
});
const search3Parameters = z.object({
albumCount: z.number().optional(),
albumOffset: z.number().optional(),
artistCount: z.number().optional(),
artistOffset: z.number().optional(),
musicFolderId: z.string().optional(),
query: z.string().optional(),
songCount: z.number().optional(),
songOffset: z.number().optional(),
albumCount: z.number().optional(),
albumOffset: z.number().optional(),
artistCount: z.number().optional(),
artistOffset: z.number().optional(),
musicFolderId: z.string().optional(),
query: z.string().optional(),
songCount: z.number().optional(),
songOffset: z.number().optional(),
});
const randomSongListParameters = z.object({
fromYear: z.number().optional(),
genre: z.string().optional(),
musicFolderId: z.string().optional(),
size: z.number().optional(),
toYear: z.number().optional(),
fromYear: z.number().optional(),
genre: z.string().optional(),
musicFolderId: z.string().optional(),
size: z.number().optional(),
toYear: z.number().optional(),
});
const randomSongList = z.object({
randomSongs: z.object({
song: z.array(song),
}),
randomSongs: z.object({
song: z.array(song),
}),
});
export const ssType = {
_parameters: {
albumList: albumListParameters,
artistInfo: artistInfoParameters,
authenticate: authenticateParameters,
createFavorite: createFavoriteParameters,
randomSongList: randomSongListParameters,
removeFavorite: removeFavoriteParameters,
scrobble: scrobbleParameters,
search3: search3Parameters,
setRating: setRatingParameters,
topSongsList: topSongsListParameters,
},
_response: {
album,
albumArtist,
albumArtistList,
albumList,
artistInfo,
authenticate,
baseResponse,
createFavorite,
musicFolderList,
randomSongList,
removeFavorite,
scrobble,
search3,
setRating,
song,
topSongsList,
},
_parameters: {
albumList: albumListParameters,
artistInfo: artistInfoParameters,
authenticate: authenticateParameters,
createFavorite: createFavoriteParameters,
randomSongList: randomSongListParameters,
removeFavorite: removeFavoriteParameters,
scrobble: scrobbleParameters,
search3: search3Parameters,
setRating: setRatingParameters,
topSongsList: topSongsListParameters,
},
_response: {
album,
albumArtist,
albumArtistList,
albumList,
artistInfo,
authenticate,
baseResponse,
createFavorite,
musicFolderList,
randomSongList,
removeFavorite,
scrobble,
search3,
setRating,
song,
topSongsList,
},
};

File diff suppressed because it is too large Load diff

View file

@ -6,35 +6,35 @@ import { ServerListItem } from '/@/renderer/types';
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
return z.object({
data: itemSchema,
headers: z.instanceof(AxiosHeaders),
});
return z.object({
data: itemSchema,
headers: z.instanceof(AxiosHeaders),
});
};
export const resultSubsonicBaseResponse = <ItemType extends z.ZodRawShape>(
itemSchema: ItemType,
itemSchema: ItemType,
) => {
return z.object({
'subsonic-response': z
.object({
status: z.string(),
version: z.string(),
})
.extend(itemSchema),
});
return z.object({
'subsonic-response': z
.object({
status: z.string(),
version: z.string(),
})
.extend(itemSchema),
});
};
export const authenticationFailure = (currentServer: ServerListItem | null) => {
toast.error({
message: 'Your session has expired.',
});
toast.error({
message: 'Your session has expired.',
});
if (currentServer) {
const serverId = currentServer.id;
const token = currentServer.ndCredential;
console.log(`token is expired: ${token}`);
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
useAuthStore.getState().actions.setCurrentServer(null);
}
if (currentServer) {
const serverId = currentServer.id;
const token = currentServer.ndCredential;
console.log(`token is expired: ${token}`);
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
useAuthStore.getState().actions.setCurrentServer(null);
}
};