mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Add first iteration of new subsonic controller
This commit is contained in:
+25
-153
@@ -1,100 +1,38 @@
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { toast } from '/@/renderer/components/toast/index';
|
||||
import { RandomSongListArgs } from './types';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
||||
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
|
||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
import type {
|
||||
AlbumDetailArgs,
|
||||
AlbumListArgs,
|
||||
SongListArgs,
|
||||
SongDetailArgs,
|
||||
AddToPlaylistArgs,
|
||||
AlbumArtistDetailArgs,
|
||||
AlbumArtistListArgs,
|
||||
SetRatingArgs,
|
||||
GenreListArgs,
|
||||
AlbumDetailArgs,
|
||||
AlbumListArgs,
|
||||
ArtistListArgs,
|
||||
ControllerEndpoint,
|
||||
CreatePlaylistArgs,
|
||||
DeletePlaylistArgs,
|
||||
FavoriteArgs,
|
||||
GenreListArgs,
|
||||
LyricsArgs,
|
||||
MusicFolderListArgs,
|
||||
PlaylistDetailArgs,
|
||||
PlaylistListArgs,
|
||||
MusicFolderListArgs,
|
||||
PlaylistSongListArgs,
|
||||
ArtistListArgs,
|
||||
RemoveFromPlaylistArgs,
|
||||
ScrobbleArgs,
|
||||
SearchArgs,
|
||||
SetRatingArgs,
|
||||
SongDetailArgs,
|
||||
SongListArgs,
|
||||
TopSongListArgs,
|
||||
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 { toast } from '/@/renderer/components/toast/index';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { ServerType } from '/@/renderer/types';
|
||||
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
|
||||
import { ndController } from '/@/renderer/api/navidrome/navidrome-controller';
|
||||
import { ssController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
|
||||
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>;
|
||||
}>;
|
||||
|
||||
type ApiController = {
|
||||
jellyfin: ControllerEndpoint;
|
||||
@@ -139,74 +77,8 @@ const endpoints: ApiController = {
|
||||
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,
|
||||
},
|
||||
navidrome: NavidromeController,
|
||||
subsonic: SubsonicController,
|
||||
};
|
||||
|
||||
const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => {
|
||||
|
||||
@@ -39,11 +39,13 @@ import {
|
||||
RemoveFromPlaylistResponse,
|
||||
RemoveFromPlaylistArgs,
|
||||
genreListSortMap,
|
||||
ControllerEndpoint,
|
||||
} from '../types';
|
||||
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
|
||||
import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
|
||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { subsonicApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
|
||||
const authenticate = async (
|
||||
url: string,
|
||||
@@ -129,7 +131,7 @@ const getAlbumArtistDetail = async (
|
||||
},
|
||||
});
|
||||
|
||||
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
|
||||
const artistInfoRes = await subsonicApiClient(apiClientProps).getArtistInfo({
|
||||
query: {
|
||||
count: 10,
|
||||
id: query.id,
|
||||
@@ -148,15 +150,16 @@ const getAlbumArtistDetail = async (
|
||||
{
|
||||
...res.body.data,
|
||||
...(artistInfoRes.status === 200 && {
|
||||
similarArtists: artistInfoRes.body.artistInfo.similarArtist,
|
||||
similarArtists: artistInfoRes.body['subsonic-response'].artistInfo.similarArtist,
|
||||
...(!res.body.data.largeImageUrl && {
|
||||
largeImageUrl: artistInfoRes.body.artistInfo.largeImageUrl,
|
||||
largeImageUrl: artistInfoRes.body['subsonic-response'].artistInfo.largeImageUrl,
|
||||
}),
|
||||
...(!res.body.data.mediumImageUrl && {
|
||||
largeImageUrl: artistInfoRes.body.artistInfo.mediumImageUrl,
|
||||
largeImageUrl:
|
||||
artistInfoRes.body['subsonic-response'].artistInfo.mediumImageUrl,
|
||||
}),
|
||||
...(!res.body.data.smallImageUrl && {
|
||||
largeImageUrl: artistInfoRes.body.artistInfo.smallImageUrl,
|
||||
largeImageUrl: artistInfoRes.body['subsonic-response'].artistInfo.smallImageUrl,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
@@ -322,7 +325,7 @@ const updatePlaylist = async (args: UpdatePlaylistArgs): Promise<UpdatePlaylistR
|
||||
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,
|
||||
sync: body._custom?.navidrome?.sync,
|
||||
},
|
||||
params: {
|
||||
id: query.id,
|
||||
@@ -340,7 +343,6 @@ const deletePlaylist = async (args: DeletePlaylistArgs): Promise<DeletePlaylistR
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps).deletePlaylist({
|
||||
body: null,
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
@@ -360,7 +362,9 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise<PlaylistListResp
|
||||
query: {
|
||||
_end: query.startIndex + (query.limit || 0),
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined,
|
||||
_sort: query.sortBy
|
||||
? playlistListSortMap.navidrome[query.sortBy]
|
||||
: playlistListSortMap.navidrome.name,
|
||||
_start: query.startIndex,
|
||||
q: query.searchTerm,
|
||||
...query._custom?.navidrome,
|
||||
@@ -449,7 +453,6 @@ const removeFromPlaylist = async (
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
|
||||
body: null,
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
@@ -465,6 +468,41 @@ const removeFromPlaylist = async (
|
||||
return null;
|
||||
};
|
||||
|
||||
export const NavidromeController: ControllerEndpoint = {
|
||||
addToPlaylist,
|
||||
authenticate,
|
||||
clearPlaylist: undefined,
|
||||
createFavorite: SubsonicController.createFavorite,
|
||||
createPlaylist,
|
||||
deleteFavorite: SubsonicController.deleteFavorite,
|
||||
deletePlaylist,
|
||||
getAlbumArtistDetail,
|
||||
getAlbumArtistList,
|
||||
getAlbumDetail,
|
||||
getAlbumList,
|
||||
getArtistDetail: undefined,
|
||||
getArtistInfo: undefined,
|
||||
getFavoritesList: undefined,
|
||||
getFolderItemList: undefined,
|
||||
getFolderList: undefined,
|
||||
getFolderSongs: undefined,
|
||||
getGenreList,
|
||||
getMusicFolderList: SubsonicController.getMusicFolderList,
|
||||
getPlaylistDetail,
|
||||
getPlaylistList,
|
||||
getPlaylistSongList,
|
||||
getRandomSongList: SubsonicController.getRandomSongList,
|
||||
getSongDetail,
|
||||
getSongList,
|
||||
getTopSongs: SubsonicController.getTopSongs,
|
||||
getUserList,
|
||||
removeFromPlaylist,
|
||||
scrobble: SubsonicController.scrobble,
|
||||
search: SubsonicController.search,
|
||||
setRating: SubsonicController.setRating,
|
||||
updatePlaylist,
|
||||
};
|
||||
|
||||
export const ndController = {
|
||||
addToPlaylist,
|
||||
authenticate,
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
import md5 from 'md5';
|
||||
import { z } from 'zod';
|
||||
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 { subsonicApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { subsonicNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
|
||||
import { AlbumListSortType, SubsonicApi } from '/@/renderer/api/subsonic/subsonic-types';
|
||||
import {
|
||||
ArtistInfoArgs,
|
||||
AlbumListSort,
|
||||
AuthenticationResponse,
|
||||
FavoriteArgs,
|
||||
FavoriteResponse,
|
||||
ControllerEndpoint,
|
||||
LibraryItem,
|
||||
MusicFolderListArgs,
|
||||
MusicFolderListResponse,
|
||||
SetRatingArgs,
|
||||
RatingResponse,
|
||||
ScrobbleArgs,
|
||||
ScrobbleResponse,
|
||||
SongListResponse,
|
||||
TopSongListArgs,
|
||||
SearchArgs,
|
||||
SearchResponse,
|
||||
RandomSongListResponse,
|
||||
RandomSongListArgs,
|
||||
} from '/@/renderer/api/types';
|
||||
import { randomString } from '/@/renderer/utils';
|
||||
import { fsLog } from '/@/logger';
|
||||
|
||||
const authenticate = async (
|
||||
url: string,
|
||||
@@ -59,7 +46,7 @@ const authenticate = async (
|
||||
};
|
||||
}
|
||||
|
||||
await ssApiClient({ server: null, url: cleanServerUrl }).authenticate({
|
||||
await subsonicApiClient({ server: null, url: cleanServerUrl }).ping({
|
||||
query: {
|
||||
c: 'Feishin',
|
||||
f: 'json',
|
||||
@@ -75,246 +62,402 @@ const authenticate = async (
|
||||
};
|
||||
};
|
||||
|
||||
const getMusicFolderList = async (args: MusicFolderListArgs): Promise<MusicFolderListResponse> => {
|
||||
const { apiClientProps } = args;
|
||||
export const SubsonicController: ControllerEndpoint = {
|
||||
addToPlaylist: async (args) => {
|
||||
const { body, query, apiClientProps } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getMusicFolderList({});
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
// export const getAlbumArtistDetail = async (
|
||||
// args: AlbumArtistDetailArgs,
|
||||
// ): Promise<SSAlbumArtistDetail> => {
|
||||
// const { server, signal, query } = args;
|
||||
// const defaultParams = getDefaultParams(server);
|
||||
|
||||
// const searchParams: SSAlbumArtistDetailParams = {
|
||||
// id: query.id,
|
||||
// ...defaultParams,
|
||||
// };
|
||||
|
||||
// const data = await api
|
||||
// .get('/getArtist.view', {
|
||||
// prefixUrl: server?.url,
|
||||
// searchParams,
|
||||
// signal,
|
||||
// })
|
||||
// .json<SSAlbumArtistDetailResponse>();
|
||||
|
||||
// return data.artist;
|
||||
// };
|
||||
|
||||
// const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArtistList> => {
|
||||
// const { signal, server, query } = args;
|
||||
// const defaultParams = getDefaultParams(server);
|
||||
|
||||
// const searchParams: SSAlbumArtistListParams = {
|
||||
// musicFolderId: query.musicFolderId,
|
||||
// ...defaultParams,
|
||||
// };
|
||||
|
||||
// const data = await api
|
||||
// .get('rest/getArtists.view', {
|
||||
// prefixUrl: server?.url,
|
||||
// searchParams,
|
||||
// signal,
|
||||
// })
|
||||
// .json<SSAlbumArtistListResponse>();
|
||||
|
||||
// const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist);
|
||||
|
||||
// return {
|
||||
// items: artists,
|
||||
// startIndex: query.startIndex,
|
||||
// totalRecordCount: null,
|
||||
// };
|
||||
// };
|
||||
|
||||
// const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
|
||||
// const { server, signal } = args;
|
||||
// const defaultParams = getDefaultParams(server);
|
||||
|
||||
// const data = await api
|
||||
// .get('rest/getGenres.view', {
|
||||
// prefixUrl: server?.url,
|
||||
// searchParams: defaultParams,
|
||||
// signal,
|
||||
// })
|
||||
// .json<SSGenreListResponse>();
|
||||
|
||||
// return data.genres.genre;
|
||||
// };
|
||||
|
||||
// const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> => {
|
||||
// const { server, query, signal } = args;
|
||||
// const defaultParams = getDefaultParams(server);
|
||||
|
||||
// const searchParams = {
|
||||
// id: query.id,
|
||||
// ...defaultParams,
|
||||
// };
|
||||
|
||||
// const data = await api
|
||||
// .get('rest/getAlbum.view', {
|
||||
// prefixUrl: server?.url,
|
||||
// searchParams: parseSearchParams(searchParams),
|
||||
// signal,
|
||||
// })
|
||||
// .json<SSAlbumDetailResponse>();
|
||||
|
||||
// const { song: songs, ...dataWithoutSong } = data.album;
|
||||
// return { ...dataWithoutSong, songs };
|
||||
// };
|
||||
|
||||
// const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => {
|
||||
// const { server, query, signal } = args;
|
||||
// const defaultParams = getDefaultParams(server);
|
||||
|
||||
// const searchParams = {
|
||||
// ...defaultParams,
|
||||
// };
|
||||
// const data = await api
|
||||
// .get('rest/getAlbumList2.view', {
|
||||
// prefixUrl: server?.url,
|
||||
// searchParams: parseSearchParams(searchParams),
|
||||
// signal,
|
||||
// })
|
||||
// .json<SSAlbumListResponse>();
|
||||
|
||||
// return {
|
||||
// items: data.albumList2.album,
|
||||
// startIndex: query.startIndex,
|
||||
// totalRecordCount: null,
|
||||
// };
|
||||
// };
|
||||
|
||||
const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
||||
const { 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,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to create favorite');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const removeFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to delete favorite');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const setRating = async (args: SetRatingArgs): Promise<RatingResponse> => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const itemIds = query.item.map((item) => item.id);
|
||||
|
||||
for (const id of itemIds) {
|
||||
await ssApiClient(apiClientProps).setRating({
|
||||
const res = await subsonicApiClient(apiClientProps).updatePlaylist({
|
||||
query: {
|
||||
id,
|
||||
rating: query.rating,
|
||||
playlistId: query.id,
|
||||
songIdToAdd: body.songId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to add to playlist');
|
||||
throw new Error('Failed to add to playlist');
|
||||
}
|
||||
|
||||
const getTopSongList = async (args: TopSongListArgs): Promise<SongListResponse> => {
|
||||
const { query, apiClientProps } = args;
|
||||
return null;
|
||||
},
|
||||
authenticate: async (url, body) => {
|
||||
const res = await authenticate(url, body);
|
||||
return res;
|
||||
},
|
||||
createFavorite: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getTopSongsList({
|
||||
query: {
|
||||
artist: query.artist,
|
||||
count: query.limit,
|
||||
},
|
||||
});
|
||||
const res = await subsonicApiClient(apiClientProps).star({
|
||||
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 get top songs');
|
||||
}
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to create favorite');
|
||||
throw new Error('Failed to create favorite');
|
||||
}
|
||||
|
||||
return {
|
||||
items:
|
||||
res.body.topSongs?.song?.map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server, ''),
|
||||
) || [],
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||
};
|
||||
};
|
||||
return null;
|
||||
},
|
||||
createPlaylist: async (args) => {
|
||||
const { body, apiClientProps } = args;
|
||||
|
||||
const getArtistInfo = async (
|
||||
args: ArtistInfoArgs,
|
||||
): Promise<z.infer<typeof ssType._response.artistInfo>> => {
|
||||
const { query, apiClientProps } = args;
|
||||
const res = await subsonicApiClient(apiClientProps).createPlaylist({
|
||||
query: {
|
||||
name: body.name,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getArtistInfo({
|
||||
query: {
|
||||
count: query.limit,
|
||||
id: query.artistId,
|
||||
},
|
||||
});
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to create playlist');
|
||||
throw new Error('Failed to create playlist');
|
||||
}
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get artist info');
|
||||
}
|
||||
return {
|
||||
id: res.body['subsonic-response'].playlist.id,
|
||||
name: res.body['subsonic-response'].playlist.name,
|
||||
};
|
||||
},
|
||||
deleteFavorite: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
return res.body;
|
||||
};
|
||||
const res = await subsonicApiClient(apiClientProps).unstar({
|
||||
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 scrobble = async (args: ScrobbleArgs): Promise<ScrobbleResponse> => {
|
||||
const { query, apiClientProps } = args;
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to delete favorite');
|
||||
throw new Error('Failed to delete favorite');
|
||||
}
|
||||
|
||||
const res = await ssApiClient(apiClientProps).scrobble({
|
||||
query: {
|
||||
id: query.id,
|
||||
submission: query.submission,
|
||||
},
|
||||
});
|
||||
return null;
|
||||
},
|
||||
deletePlaylist: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to scrobble');
|
||||
}
|
||||
const res = await subsonicApiClient(apiClientProps).deletePlaylist({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to delete playlist');
|
||||
throw new Error('Failed to delete playlist');
|
||||
}
|
||||
|
||||
const search3 = async (args: SearchArgs): Promise<SearchResponse> => {
|
||||
const { query, apiClientProps } = args;
|
||||
return null;
|
||||
},
|
||||
getAlbumArtistDetail: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).search3({
|
||||
query: {
|
||||
const artistInfoRes = await subsonicApiClient(apiClientProps).getArtistInfo({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getArtist({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get album artist detail');
|
||||
throw new Error('Failed to get album artist detail');
|
||||
}
|
||||
|
||||
const artist = res.body['subsonic-response'].artist;
|
||||
|
||||
let artistInfo;
|
||||
if (artistInfoRes.status === 200) {
|
||||
artistInfo = artistInfoRes.body['subsonic-response'].artistInfo;
|
||||
fsLog.warn('Failed to get artist info');
|
||||
}
|
||||
|
||||
return {
|
||||
...subsonicNormalize.albumArtist(artist, apiClientProps.server),
|
||||
albums: artist.album.map((album) =>
|
||||
subsonicNormalize.album(album, apiClientProps.server),
|
||||
),
|
||||
artistInfo,
|
||||
};
|
||||
},
|
||||
getAlbumArtistList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getArtists({
|
||||
query: {
|
||||
musicFolderId: query.musicFolderId,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get album artist list');
|
||||
throw new Error('Failed to get album artist list');
|
||||
}
|
||||
|
||||
const artists = (res.body['subsonic-response'].artists?.index || []).flatMap(
|
||||
(index) => index.artist,
|
||||
);
|
||||
|
||||
return {
|
||||
items: artists.map((artist) =>
|
||||
subsonicNormalize.albumArtist(artist, apiClientProps.server),
|
||||
),
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: null,
|
||||
};
|
||||
},
|
||||
getAlbumDetail: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getAlbum({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get album detail', {
|
||||
context: { id: query.id },
|
||||
});
|
||||
throw new Error('Failed to get album detail');
|
||||
}
|
||||
|
||||
return subsonicNormalize.album(res.body['subsonic-response'].album, apiClientProps.server);
|
||||
},
|
||||
getAlbumList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const sortType: Record<AlbumListSort, AlbumListSortType | undefined> = {
|
||||
[AlbumListSort.RANDOM]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.RANDOM,
|
||||
[AlbumListSort.ALBUM_ARTIST]:
|
||||
SubsonicApi.getAlbumList2.enum.AlbumListSortType.ALPHABETICAL_BY_ARTIST,
|
||||
[AlbumListSort.PLAY_COUNT]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.FREQUENT,
|
||||
[AlbumListSort.RECENTLY_ADDED]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.NEWEST,
|
||||
[AlbumListSort.FAVORITED]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.STARRED,
|
||||
[AlbumListSort.YEAR]: SubsonicApi.getAlbumList2.enum.AlbumListSortType.RECENT,
|
||||
[AlbumListSort.NAME]:
|
||||
SubsonicApi.getAlbumList2.enum.AlbumListSortType.ALPHABETICAL_BY_NAME,
|
||||
[AlbumListSort.COMMUNITY_RATING]: undefined,
|
||||
[AlbumListSort.DURATION]: undefined,
|
||||
[AlbumListSort.CRITIC_RATING]: undefined,
|
||||
[AlbumListSort.RATING]: undefined,
|
||||
[AlbumListSort.ARTIST]: undefined,
|
||||
[AlbumListSort.RECENTLY_PLAYED]: undefined,
|
||||
[AlbumListSort.RELEASE_DATE]: undefined,
|
||||
[AlbumListSort.SONG_COUNT]: undefined,
|
||||
};
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getAlbumList2({
|
||||
query: {
|
||||
fromYear: query.minYear,
|
||||
genre: query.genre,
|
||||
musicFolderId: query.musicFolderId,
|
||||
offset: query.startIndex,
|
||||
size: query.limit,
|
||||
toYear: query.maxYear,
|
||||
type:
|
||||
sortType[query.sortBy] ??
|
||||
SubsonicApi.getAlbumList2.enum.AlbumListSortType.ALPHABETICAL_BY_NAME,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get album list');
|
||||
throw new Error('Failed to get album list');
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body['subsonic-response'].albumList2.album.map((album) =>
|
||||
subsonicNormalize.album(album, apiClientProps.server),
|
||||
),
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: null,
|
||||
};
|
||||
},
|
||||
getAlbumSongList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getAlbum({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get album song list');
|
||||
throw new Error('Failed to get album song list');
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body['subsonic-response'].album.song.map((song) =>
|
||||
subsonicNormalize.song(song, apiClientProps.server, ''),
|
||||
),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body['subsonic-response'].album.song.length,
|
||||
};
|
||||
},
|
||||
getArtistInfo: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getArtistInfo({
|
||||
query: {
|
||||
id: query.artistId,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get artist info', {
|
||||
context: { id: query.artistId },
|
||||
});
|
||||
throw new Error('Failed to get artist info');
|
||||
}
|
||||
|
||||
return res.body['subsonic-response'].artistInfo;
|
||||
},
|
||||
getGenreList: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getGenres({});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get genre list');
|
||||
throw new Error('Failed to get genre list');
|
||||
}
|
||||
|
||||
const genres = res.body['subsonic-response'].genres.genre.map(subsonicNormalize.genre);
|
||||
|
||||
return {
|
||||
items: genres,
|
||||
startIndex: 0,
|
||||
totalRecordCount: genres.length,
|
||||
};
|
||||
},
|
||||
getMusicFolderList: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getMusicFolders({});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get music folder list');
|
||||
throw new Error('Failed to get music folder list');
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body['subsonic-response'].musicFolders.musicFolder.map(
|
||||
subsonicNormalize.musicFolder,
|
||||
),
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body['subsonic-response'].musicFolders.musicFolder.length,
|
||||
};
|
||||
},
|
||||
getRandomSongList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getRandomSongs({
|
||||
query: {
|
||||
fromYear: query.minYear,
|
||||
genre: query.genre,
|
||||
musicFolderId: query.musicFolderId,
|
||||
size: query.limit,
|
||||
toYear: query.maxYear,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get random songs');
|
||||
throw new Error('Failed to get random songs');
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body['subsonic-response'].randomSongs?.song?.map((song) =>
|
||||
subsonicNormalize.song(song, apiClientProps.server, ''),
|
||||
),
|
||||
startIndex: 0,
|
||||
totalRecordCount: null,
|
||||
};
|
||||
},
|
||||
getSongDetail: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getSong({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get song detail');
|
||||
throw new Error('Failed to get song detail');
|
||||
}
|
||||
|
||||
return subsonicNormalize.song(
|
||||
res.body['subsonic-response'].song,
|
||||
apiClientProps.server,
|
||||
'',
|
||||
);
|
||||
},
|
||||
getTopSongs: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).getTopSongs({
|
||||
query: {
|
||||
artist: query.artist,
|
||||
count: query.limit,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to get top songs', {
|
||||
context: { artist: query.artist },
|
||||
});
|
||||
throw new Error('Failed to get top songs');
|
||||
}
|
||||
|
||||
return {
|
||||
items:
|
||||
res.body['subsonic-response'].topSongs?.song?.map((song) =>
|
||||
subsonicNormalize.song(song, apiClientProps.server, ''),
|
||||
) || [],
|
||||
startIndex: 0,
|
||||
totalRecordCount: null,
|
||||
};
|
||||
},
|
||||
scrobble: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).scrobble({
|
||||
query: {
|
||||
id: query.id,
|
||||
submission: query.submission,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to scrobble', {
|
||||
context: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
throw new Error('Failed to scrobble');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
search: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const searchQuery = {
|
||||
albumCount: query.albumLimit,
|
||||
albumOffset: query.albumStartIndex,
|
||||
artistCount: query.albumArtistLimit,
|
||||
@@ -322,61 +465,64 @@ const search3 = async (args: SearchArgs): Promise<SearchResponse> => {
|
||||
query: query.query,
|
||||
songCount: query.songLimit,
|
||||
songOffset: query.songStartIndex,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to search');
|
||||
}
|
||||
const res = await subsonicApiClient(apiClientProps).search3({
|
||||
query: searchQuery,
|
||||
});
|
||||
|
||||
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 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');
|
||||
}
|
||||
|
||||
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,
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to search', {
|
||||
context: searchQuery,
|
||||
});
|
||||
throw new Error('Failed to search');
|
||||
}
|
||||
|
||||
return {
|
||||
albumArtists: res.body['subsonic-response'].searchResult3?.artist?.map((artist) =>
|
||||
subsonicNormalize.albumArtist(artist, apiClientProps.server),
|
||||
),
|
||||
albums: res.body['subsonic-response'].searchResult3?.album?.map((album) =>
|
||||
subsonicNormalize.album(album, apiClientProps.server),
|
||||
),
|
||||
songs: res.body['subsonic-response'].searchResult3?.song?.map((song) =>
|
||||
subsonicNormalize.song(song, apiClientProps.server, ''),
|
||||
),
|
||||
};
|
||||
},
|
||||
setRating: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const itemIds = query.item.map((item) => item.id);
|
||||
|
||||
for (const id of itemIds) {
|
||||
await subsonicApiClient(apiClientProps).setRating({
|
||||
query: {
|
||||
id,
|
||||
rating: query.rating,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
updatePlaylist: async (args) => {
|
||||
const { body, query, apiClientProps } = args;
|
||||
|
||||
const res = await subsonicApiClient(apiClientProps).updatePlaylist({
|
||||
query: {
|
||||
comment: body.comment,
|
||||
name: body.name,
|
||||
playlistId: query.id,
|
||||
public: body.public,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
fsLog.error('Failed to update playlist');
|
||||
throw new Error('Failed to update playlist');
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { z } from 'zod';
|
||||
import { SubsonicApi } from '/@/renderer/api/subsonic/subsonic-types';
|
||||
import { QueueSong, LibraryItem, AlbumArtist, Album, Genre } from '/@/renderer/api/types';
|
||||
import {
|
||||
QueueSong,
|
||||
LibraryItem,
|
||||
AlbumArtist,
|
||||
Album,
|
||||
Genre,
|
||||
MusicFolder,
|
||||
} from '/@/renderer/api/types';
|
||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||
|
||||
const getCoverArtUrl = (args: {
|
||||
@@ -105,7 +112,9 @@ const normalizeSong = (
|
||||
};
|
||||
|
||||
const normalizeAlbumArtist = (
|
||||
item: z.infer<typeof SubsonicApi._baseTypes.artist>,
|
||||
item:
|
||||
| z.infer<typeof SubsonicApi._baseTypes.artist>
|
||||
| z.infer<typeof SubsonicApi._baseTypes.artistListEntry>,
|
||||
server: ServerListItem | null,
|
||||
): AlbumArtist => {
|
||||
const imageUrl =
|
||||
@@ -202,9 +211,19 @@ const normalizeGenre = (item: z.infer<typeof SubsonicApi._baseTypes.genre>): Gen
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeMusicFolder = (
|
||||
item: z.infer<typeof SubsonicApi._baseTypes.musicFolder>,
|
||||
): MusicFolder => {
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
};
|
||||
};
|
||||
|
||||
export const subsonicNormalize = {
|
||||
album: normalizeAlbum,
|
||||
albumArtist: normalizeAlbumArtist,
|
||||
genre: normalizeGenre,
|
||||
musicFolder: normalizeMusicFolder,
|
||||
song: normalizeSong,
|
||||
};
|
||||
|
||||
@@ -156,23 +156,21 @@ const playlistListEntry = playlist.omit({
|
||||
});
|
||||
|
||||
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(),
|
||||
}),
|
||||
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 albumInfo = z.object({
|
||||
|
||||
Reference in New Issue
Block a user