From 8fcf5291c4ad9358db4df93d5dbec4ffe973fc6b Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 3 Dec 2023 22:15:45 -0800 Subject: [PATCH] Add first iteration of new subsonic controller --- src/renderer/api/controller.ts | 178 +---- .../api/navidrome/navidrome-controller.ts | 58 +- .../api/subsonic/subsonic-controller.ts | 738 +++++++++++------- .../api/subsonic/subsonic-normalize.ts | 23 +- src/renderer/api/subsonic/subsonic-types.ts | 32 +- 5 files changed, 551 insertions(+), 478 deletions(-) diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index 196a848c1..fc3cd1d14 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -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; - authenticate: ( - url: string, - body: { password: string; username: string }, - ) => Promise; - clearPlaylist: () => void; - createFavorite: (args: FavoriteArgs) => Promise; - createPlaylist: (args: CreatePlaylistArgs) => Promise; - deleteFavorite: (args: FavoriteArgs) => Promise; - deletePlaylist: (args: DeletePlaylistArgs) => Promise; - getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise; - getAlbumArtistList: (args: AlbumArtistListArgs) => Promise; - getAlbumDetail: (args: AlbumDetailArgs) => Promise; - getAlbumList: (args: AlbumListArgs) => Promise; - getArtistDetail: () => void; - getArtistInfo: (args: any) => void; - getArtistList: (args: ArtistListArgs) => Promise; - getFavoritesList: () => void; - getFolderItemList: () => void; - getFolderList: () => void; - getFolderSongs: () => void; - getGenreList: (args: GenreListArgs) => Promise; - getLyrics: (args: LyricsArgs) => Promise; - getMusicFolderList: (args: MusicFolderListArgs) => Promise; - getPlaylistDetail: (args: PlaylistDetailArgs) => Promise; - getPlaylistList: (args: PlaylistListArgs) => Promise; - getPlaylistSongList: (args: PlaylistSongListArgs) => Promise; - getRandomSongList: (args: RandomSongListArgs) => Promise; - getSongDetail: (args: SongDetailArgs) => Promise; - getSongList: (args: SongListArgs) => Promise; - getTopSongs: (args: TopSongListArgs) => Promise; - getUserList: (args: UserListArgs) => Promise; - removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise; - scrobble: (args: ScrobbleArgs) => Promise; - search: (args: SearchArgs) => Promise; - setRating: (args: SetRatingArgs) => Promise; - updatePlaylist: (args: UpdatePlaylistArgs) => Promise; -}>; 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) => { diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 486cb1bc9..abb23bb99 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -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 => { - 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 => { -// 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(); - -// return data.artist; -// }; - -// const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise => { -// 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(); - -// const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist); - -// return { -// items: artists, -// startIndex: query.startIndex, -// totalRecordCount: null, -// }; -// }; - -// const getGenreList = async (args: GenreListArgs): Promise => { -// const { server, signal } = args; -// const defaultParams = getDefaultParams(server); - -// const data = await api -// .get('rest/getGenres.view', { -// prefixUrl: server?.url, -// searchParams: defaultParams, -// signal, -// }) -// .json(); - -// return data.genres.genre; -// }; - -// const getAlbumDetail = async (args: AlbumDetailArgs): Promise => { -// 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(); - -// const { song: songs, ...dataWithoutSong } = data.album; -// return { ...dataWithoutSong, songs }; -// }; - -// const getAlbumList = async (args: AlbumListArgs): Promise => { -// 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(); - -// return { -// items: data.albumList2.album, -// startIndex: query.startIndex, -// totalRecordCount: null, -// }; -// }; - -const createFavorite = async (args: FavoriteArgs): Promise => { - 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 => { - 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 => { - 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 => { - 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> => { - 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 => { - 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 => { - 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.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 => { 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 => { - 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; + }, }; diff --git a/src/renderer/api/subsonic/subsonic-normalize.ts b/src/renderer/api/subsonic/subsonic-normalize.ts index 2328515a5..9c3b8e381 100644 --- a/src/renderer/api/subsonic/subsonic-normalize.ts +++ b/src/renderer/api/subsonic/subsonic-normalize.ts @@ -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, + item: + | z.infer + | z.infer, server: ServerListItem | null, ): AlbumArtist => { const imageUrl = @@ -202,9 +211,19 @@ const normalizeGenre = (item: z.infer): Gen }; }; +const normalizeMusicFolder = ( + item: z.infer, +): MusicFolder => { + return { + id: item.id, + name: item.name, + }; +}; + export const subsonicNormalize = { album: normalizeAlbum, albumArtist: normalizeAlbumArtist, genre: normalizeGenre, + musicFolder: normalizeMusicFolder, song: normalizeSong, }; diff --git a/src/renderer/api/subsonic/subsonic-types.ts b/src/renderer/api/subsonic/subsonic-types.ts index 118622be4..737477509 100644 --- a/src/renderer/api/subsonic/subsonic-types.ts +++ b/src/renderer/api/subsonic/subsonic-types.ts @@ -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({