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
+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;
},
};