Add first iteration of new subsonic controller

This commit is contained in:
jeffvli
2023-12-03 22:15:45 -08:00
parent 3b155cc6e8
commit 8fcf5291c4
5 changed files with 551 additions and 478 deletions
+25 -153
View File
@@ -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,
+442 -296
View File
@@ -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,
};
+15 -17
View File
@@ -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({