diff --git a/package-lock.json b/package-lock.json index 89915eb9..6a5a6e96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,6 @@ "i18next": "^21.6.16", "immer": "^9.0.15", "is-electron": "^2.2.1", - "ky": "^0.33.0", "lodash": "^4.17.21", "md5": "^2.3.0", "memoize-one": "^6.0.0", @@ -13552,17 +13551,6 @@ "integrity": "sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==", "dev": true }, - "node_modules/ky": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.0.tgz", - "integrity": "sha512-peKzuOlN/q3Q3jOgi4t0cp6DOgif5rVnmiSIsjsmkiOcdnSjkrKSUqQmRWYCTqjUtR9b3xQQr8aj7KwSW1r49A==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/ky?sponsor=1" - } - }, "node_modules/language-subtag-registry": { "version": "0.3.21", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", @@ -33640,11 +33628,6 @@ "integrity": "sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==", "dev": true }, - "ky": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.0.tgz", - "integrity": "sha512-peKzuOlN/q3Q3jOgi4t0cp6DOgif5rVnmiSIsjsmkiOcdnSjkrKSUqQmRWYCTqjUtR9b3xQQr8aj7KwSW1r49A==" - }, "language-subtag-registry": { "version": "0.3.21", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", diff --git a/package.json b/package.json index 8487c2ef..8a744ffe 100644 --- a/package.json +++ b/package.json @@ -279,7 +279,6 @@ "i18next": "^21.6.16", "immer": "^9.0.15", "is-electron": "^2.2.1", - "ky": "^0.33.0", "lodash": "^4.17.21", "md5": "^2.3.0", "memoize-one": "^6.0.0", diff --git a/src/renderer/api/index.ts b/src/renderer/api/index.ts index d67312fd..c2d6bfde 100644 --- a/src/renderer/api/index.ts +++ b/src/renderer/api/index.ts @@ -1,7 +1,5 @@ import { controller } from '/@/renderer/api/controller'; -import { normalize } from '/@/renderer/api/normalize'; export const api = { controller, - normalize, }; diff --git a/src/renderer/api/jellyfin.api.ts b/src/renderer/api/jellyfin.api.ts deleted file mode 100644 index 5f506ff1..00000000 --- a/src/renderer/api/jellyfin.api.ts +++ /dev/null @@ -1,1054 +0,0 @@ -import ky from 'ky'; -import { nanoid } from 'nanoid/non-secure'; -import { - JFAddToPlaylist, - JFAddToPlaylistParams, - JFAlbum, - JFAlbumArtist, - JFAlbumArtistDetail, - JFAlbumArtistDetailResponse, - JFAlbumArtistList, - JFAlbumArtistListParams, - JFAlbumArtistListResponse, - JFAlbumDetail, - JFAlbumDetailResponse, - JFAlbumList, - JFAlbumListParams, - JFAlbumListResponse, - JFArtistList, - JFArtistListParams, - JFArtistListResponse, - JFAuthenticate, - JFCreatePlaylistResponse, - JFGenreList, - JFGenreListResponse, - JFMusicFolderList, - JFMusicFolderListResponse, - JFPlaylist, - JFPlaylistDetail, - JFPlaylistDetailResponse, - JFPlaylistList, - JFPlaylistListResponse, - JFRemoveFromPlaylist, - JFRemoveFromPlaylistParams, - JFSong, - JFSongList, - JFSongListParams, - JFSongListResponse, - JFSongListSort, - JFCollectionType, - JFSortOrder, -} from '/@/renderer/api/jellyfin.types'; -import { - Album, - AlbumArtist, - AlbumArtistDetailArgs, - AlbumArtistListArgs, - AlbumDetailArgs, - AlbumListArgs, - ArtistListArgs, - AuthenticationResponse, - CreatePlaylistArgs, - CreatePlaylistResponse, - DeletePlaylistArgs, - FavoriteArgs, - FavoriteResponse, - GenreListArgs, - MusicFolderListArgs, - Playlist, - PlaylistDetailArgs, - PlaylistListArgs, - playlistListSortMap, - PlaylistSongListArgs, - Song, - SongListArgs, - songListSortMap, - albumListSortMap, - artistListSortMap, - sortOrderMap, - albumArtistListSortMap, - UpdatePlaylistArgs, - UpdatePlaylistResponse, - LibraryItem, - RemoveFromPlaylistArgs, - AddToPlaylistArgs, - ScrobbleArgs, - RawScrobbleResponse, - TopSongListArgs, -} from '/@/renderer/api/types'; -import { useAuthStore } from '/@/renderer/store'; -import { ServerListItem, ServerType } from '/@/renderer/types'; -import { parseSearchParams } from '/@/renderer/utils'; -import packageJson from '../../../package.json'; - -const IGNORE_CORS = localStorage.getItem('IGNORE_CORS') === 'true'; - -const getCommaDelimitedString = (value: string[]) => { - return value.join(','); -}; - -const api = ky.create({ - mode: IGNORE_CORS ? 'cors' : undefined, -}); - -const authenticate = async ( - url: string, - body: { - password: string; - username: string; - }, -): Promise => { - const cleanServerUrl = url.replace(/\/$/, ''); - - const data = await ky - .post(`${cleanServerUrl}/users/authenticatebyname`, { - headers: { - 'X-Emby-Authorization': `MediaBrowser Client="Feishin", Device="PC", DeviceId="Feishin", Version="${packageJson.version}"`, - }, - json: { - pw: body.password, - username: body.username, - }, - }) - .json(); - - return { - credential: data.AccessToken, - userId: data.User.Id, - username: data.User.Name, - }; -}; - -const getMusicFolderList = async (args: MusicFolderListArgs): Promise => { - const { server, signal } = args; - const userId = useAuthStore.getState().currentServer?.userId; - - const data = await api - .get(`users/${userId}/items`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - signal, - }) - .json(); - - const musicFolders = data.Items.filter( - (folder) => folder.CollectionType === JFCollectionType.MUSIC, - ); - - return musicFolders; -}; - -const getGenreList = async (args: GenreListArgs): Promise => { - const { signal, server } = args; - - const data = await api - .get('genres', { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - signal, - }) - .json(); - return data; -}; - -const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs): Promise => { - const { query, server, signal } = args; - - const searchParams = { - fields: 'Genres, Overview', - }; - - const data = await api - .get(`users/${server?.userId}/items/${query.id}`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - const similarArtists = await api - .get(`artists/${query.id}/similar`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams({ limit: 10 }), - signal, - }) - .json(); - - return { ...data, similarArtists: { items: similarArtists.Items } }; -}; - -// const getAlbumArtistAlbums = () => { -// const { data: albumData } = await api.get(`/users/${auth.username}/items`, { -// params: { -// artistIds: options.id, -// fields: 'AudioInfo, ParentId, Genres, DateCreated, ChildCount, ParentId', -// includeItemTypes: 'MusicAlbum', -// parentId: options.musicFolderId, -// recursive: true, -// sortBy: 'SortName', -// }, -// }); - -// const { data: similarData } = await api.get(`/artists/${options.id}/similar`, { -// params: { limit: 15, parentId: options.musicFolderId, userId: auth.username }, -// }); -// }; - -const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: JFAlbumArtistListParams = { - fields: 'Genres, DateCreated, ExternalUrls, Overview', - imageTypeLimit: 1, - limit: query.limit, - parentId: query.musicFolderId, - recursive: true, - searchTerm: query.searchTerm, - sortBy: albumArtistListSortMap.jellyfin[query.sortBy], - sortOrder: sortOrderMap.jellyfin[query.sortOrder], - startIndex: query.startIndex, - userId: server?.userId || undefined, - }; - - const data = await api - .get('artists/albumArtists', { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - return { - items: data.Items, - startIndex: query.startIndex, - totalRecordCount: data.TotalRecordCount, - }; -}; - -const getArtistList = async (args: ArtistListArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: JFArtistListParams = { - limit: query.limit, - parentId: query.musicFolderId, - recursive: true, - sortBy: artistListSortMap.jellyfin[query.sortBy], - sortOrder: sortOrderMap.jellyfin[query.sortOrder], - startIndex: query.startIndex, - }; - - const data = await api - .get('artists', { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - return data; -}; - -const getAlbumDetail = async (args: AlbumDetailArgs): Promise => { - const { query, server, signal } = args; - - const searchParams = { - fields: 'Genres, DateCreated, ChildCount', - }; - - const data = await api - .get(`users/${server?.userId}/items/${query.id}`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams, - signal, - }) - .json(); - - const songsSearchParams = { - fields: 'Genres, DateCreated, MediaSources, ParentId', - parentId: query.id, - sortBy: 'SortName', - }; - - const songsData = await api - .get(`users/${server?.userId}/items`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: songsSearchParams, - signal, - }) - .json(); - - return { ...data, songs: songsData.Items }; -}; - -const getAlbumList = async (args: AlbumListArgs): Promise => { - const { query, server, signal } = args; - - const yearsGroup = []; - if (query.jfParams?.minYear && query.jfParams?.maxYear) { - for (let i = Number(query.jfParams.minYear); i <= Number(query.jfParams.maxYear); i += 1) { - yearsGroup.push(String(i)); - } - } - - const yearsFilter = yearsGroup.length ? yearsGroup.join(',') : undefined; - - const searchParams: JFAlbumListParams & { maxYear?: number; minYear?: number } = { - includeItemTypes: 'MusicAlbum', - limit: query.limit, - parentId: query.musicFolderId, - recursive: true, - searchTerm: query.searchTerm, - sortBy: albumListSortMap.jellyfin[query.sortBy], - sortOrder: sortOrderMap.jellyfin[query.sortOrder], - startIndex: query.startIndex, - ...query.jfParams, - maxYear: undefined, - minYear: undefined, - years: yearsFilter, - }; - - const data = await api - .get(`users/${server?.userId}/items`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - return { - items: data.Items, - startIndex: query.startIndex, - totalRecordCount: data.TotalRecordCount, - }; -}; - -const getTopSongList = async (args: TopSongListArgs): Promise => { - const { signal, server, query } = args; - - const searchParams: JFSongListParams = { - artistIds: query.artistId, - fields: 'Genres, DateCreated, MediaSources, ParentId', - includeItemTypes: 'Audio', - limit: query.limit, - recursive: true, - sortBy: JFSongListSort.COMMUNITY_RATING, - sortOrder: JFSortOrder.DESC, - userId: server?.userId || '', - }; - - const data = await api - .get(`users/${server?.userId}/items`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - return { - items: data.Items, - startIndex: 0, - totalRecordCount: data.TotalRecordCount, - }; -}; - -const getSongList = async (args: SongListArgs): Promise => { - const { query, server, signal } = args; - - const yearsGroup = []; - if (query.jfParams?.minYear && query.jfParams?.maxYear) { - for (let i = Number(query.jfParams.minYear); i <= Number(query.jfParams.maxYear); i += 1) { - yearsGroup.push(String(i)); - } - } - - const yearsFilter = yearsGroup.length ? getCommaDelimitedString(yearsGroup) : undefined; - const albumIdsFilter = query.albumIds ? getCommaDelimitedString(query.albumIds) : undefined; - const artistIdsFilter = query.artistIds ? getCommaDelimitedString(query.artistIds) : undefined; - - const searchParams: JFSongListParams & { maxYear?: number; minYear?: number } = { - albumIds: albumIdsFilter, - artistIds: artistIdsFilter, - fields: 'Genres, DateCreated, MediaSources, ParentId', - includeItemTypes: 'Audio', - limit: query.limit, - parentId: query.musicFolderId, - recursive: true, - searchTerm: query.searchTerm, - sortBy: songListSortMap.jellyfin[query.sortBy], - sortOrder: sortOrderMap.jellyfin[query.sortOrder], - startIndex: query.startIndex, - ...query.jfParams, - maxYear: undefined, - minYear: undefined, - years: yearsFilter, - }; - - const data = await api - .get(`users/${server?.userId}/items`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - return { - items: data.Items, - startIndex: query.startIndex, - totalRecordCount: data.TotalRecordCount, - }; -}; - -const addToPlaylist = async (args: AddToPlaylistArgs): Promise => { - const { query, body, server, signal } = args; - - const searchParams: JFAddToPlaylistParams = { - ids: body.songId, - userId: server?.userId || '', - }; - - await api - .post(`playlists/${query.id}/items`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - return null; -}; - -const removeFromPlaylist = async (args: RemoveFromPlaylistArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: JFRemoveFromPlaylistParams = { - entryIds: query.songId, - }; - - await api - .delete(`playlists/${query.id}/items`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - return null; -}; - -const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise => { - const { query, server, signal } = args; - - const searchParams = { - fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId', - ids: query.id, - }; - - const data = await api - .get(`users/${server?.userId}/items/${query.id}`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams, - signal, - }) - .json(); - - return data; -}; - -const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: JFSongListParams = { - fields: 'Genres, DateCreated, MediaSources, UserData, ParentId', - includeItemTypes: 'Audio', - limit: query.limit, - sortBy: query.sortBy ? songListSortMap.jellyfin[query.sortBy] : undefined, - sortOrder: query.sortOrder ? sortOrderMap.jellyfin[query.sortOrder] : undefined, - startIndex: 0, - userId: server?.userId || '', - }; - - const data = await api - .get(`playlists/${query.id}/items`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - return { - items: data.Items, - startIndex: query.startIndex, - totalRecordCount: data.TotalRecordCount, - }; -}; - -const getPlaylistList = async (args: PlaylistListArgs): Promise => { - const { query, server, signal } = args; - - const searchParams = { - fields: 'ChildCount, Genres, DateCreated, ParentId, Overview', - includeItemTypes: 'Playlist', - limit: query.limit, - recursive: true, - sortBy: playlistListSortMap.jellyfin[query.sortBy], - sortOrder: sortOrderMap.jellyfin[query.sortOrder], - startIndex: query.startIndex, - }; - - const data = await api - .get(`users/${server?.userId}/items`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - const playlistItems = data.Items.filter((item) => item.MediaType === 'Audio'); - - return { - items: playlistItems, - startIndex: 0, - totalRecordCount: playlistItems.length, - }; -}; - -const createPlaylist = async (args: CreatePlaylistArgs): Promise => { - const { body, server } = args; - - const json = { - MediaType: 'Audio', - Name: body.name, - Overview: body.comment || '', - UserId: server?.userId, - }; - - const data = await api - .post('playlists', { - headers: { 'X-MediaBrowser-Token': server?.credential }, - json, - prefixUrl: server?.url, - }) - .json(); - - return { - id: data.Id, - name: body.name, - }; -}; - -const updatePlaylist = async (args: UpdatePlaylistArgs): Promise => { - const { query, body, server } = args; - - const json = { - Genres: body.genres?.map((item) => ({ Id: item.id, Name: item.name })) || [], - MediaType: 'Audio', - Name: body.name, - Overview: body.comment || '', - PremiereDate: null, - ProviderIds: {}, - Tags: [], - UserId: server?.userId, // Required - }; - - await api - .post(`items/${query.id}`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - json, - prefixUrl: server?.url, - }) - .json(); - - return { - id: query.id, - }; -}; - -const deletePlaylist = async (args: DeletePlaylistArgs): Promise => { - const { query, server } = args; - - await api.delete(`items/${query.id}`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - }); - - return null; -}; - -const createFavorite = async (args: FavoriteArgs): Promise => { - const { query, server } = args; - - for (const id of query.id) { - await api.post(`users/${server?.userId}/favoriteitems/${id}`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - }); - } - - return { - id: query.id, - type: query.type, - }; -}; - -const deleteFavorite = async (args: FavoriteArgs): Promise => { - const { query, server } = args; - - for (const id of query.id) { - await api.delete(`users/${server?.userId}/favoriteitems/${id}`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - prefixUrl: server?.url, - }); - } - - return { - id: query.id, - type: query.type, - }; -}; - -const scrobble = async (args: ScrobbleArgs): Promise => { - const { query, server } = args; - - const position = query.position && Math.round(query.position); - - if (query.submission) { - // Checked by jellyfin-plugin-lastfm for whether or not to send the "finished" scrobble (uses PositionTicks) - api.post(`sessions/playing/stopped`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - json: { - IsPaused: true, - ItemId: query.id, - PositionTicks: position, - }, - prefixUrl: server?.url, - }); - - return null; - } - - if (query.event === 'start') { - await api.post(`sessions/playing`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - json: { - ItemId: query.id, - PositionTicks: position, - }, - prefixUrl: server?.url, - }); - - return null; - } - - if (query.event === 'pause') { - await api.post(`sessions/playing/progress`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - json: { - EventName: query.event, - IsPaused: true, - ItemId: query.id, - PositionTicks: position, - }, - prefixUrl: server?.url, - }); - - return null; - } - - if (query.event === 'unpause') { - await api.post(`sessions/playing/progress`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - json: { - EventName: query.event, - IsPaused: false, - ItemId: query.id, - PositionTicks: position, - }, - prefixUrl: server?.url, - }); - - return null; - } - - await api.post(`sessions/playing/progress`, { - headers: { 'X-MediaBrowser-Token': server?.credential }, - json: { - ItemId: query.id, - PositionTicks: position, - }, - prefixUrl: server?.url, - }); - - return null; -}; - -const getStreamUrl = (args: { - container?: string; - deviceId: string; - eTag?: string; - id: string; - mediaSourceId?: string; - server: ServerListItem; -}) => { - const { id, server, deviceId } = args; - - return ( - `${server?.url}/audio` + - `/${id}/universal` + - `?userId=${server.userId}` + - `&deviceId=${deviceId}` + - '&audioCodec=aac' + - `&api_key=${server.credential}` + - `&playSessionId=${deviceId}` + - '&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' + - '&transcodingContainer=ts' + - '&transcodingProtocol=hls' - ); -}; - -const getAlbumArtistCoverArtUrl = (args: { - baseUrl: string; - item: JFAlbumArtist; - size: number; -}) => { - const size = args.size ? args.size : 300; - - if (!args.item.ImageTags?.Primary) { - return null; - } - - return ( - `${args.baseUrl}/Items` + - `/${args.item.Id}` + - '/Images/Primary' + - `?width=${size}&height=${size}` + - '&quality=96' - ); -}; - -const getAlbumCoverArtUrl = (args: { baseUrl: string; item: JFAlbum; size: number }) => { - const size = args.size ? args.size : 300; - - if (!args.item.ImageTags?.Primary && !args.item?.AlbumPrimaryImageTag) { - return null; - } - - return ( - `${args.baseUrl}/Items` + - `/${args.item.Id}` + - '/Images/Primary' + - `?width=${size}&height=${size}` + - '&quality=96' - ); -}; - -const getSongCoverArtUrl = (args: { baseUrl: string; item: JFSong; size: number }) => { - const size = args.size ? args.size : 100; - - if (!args.item.ImageTags?.Primary) { - return null; - } - - if (args.item.ImageTags.Primary) { - return ( - `${args.baseUrl}/Items` + - `/${args.item.Id}` + - '/Images/Primary' + - `?width=${size}&height=${size}` + - '&quality=96' - ); - } - - if (!args.item?.AlbumPrimaryImageTag) { - return null; - } - - // Fall back to album art if no image embedded - return ( - `${args.baseUrl}/Items` + - `/${args.item?.AlbumId}` + - '/Images/Primary' + - `?width=${size}&height=${size}` + - '&quality=96' - ); -}; - -const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size: number }) => { - const size = args.size ? args.size : 300; - - if (!args.item.ImageTags?.Primary) { - return null; - } - - return ( - `${args.baseUrl}/Items` + - `/${args.item.Id}` + - '/Images/Primary' + - `?width=${size}&height=${size}` + - '&quality=96' - ); -}; - -const normalizeSong = ( - item: JFSong, - server: ServerListItem, - deviceId: string, - imageSize?: number, -): Song => { - return { - album: item.Album, - albumArtists: item.AlbumArtists?.map((entry) => ({ - id: entry.Id, - imageUrl: null, - name: entry.Name, - })), - albumId: item.AlbumId, - artistName: item.ArtistItems[0]?.Name, - artists: item.ArtistItems.map((entry) => ({ id: entry.Id, imageUrl: null, name: entry.Name })), - bitRate: item.MediaSources && Number(Math.trunc(item.MediaSources[0]?.Bitrate / 1000)), - bpm: null, - channels: null, - comment: null, - compilation: null, - container: (item.MediaSources && item.MediaSources[0]?.Container) || null, - createdAt: item.DateCreated, - discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1, - duration: item.RunTimeTicks / 10000000, - genres: item.GenreItems.map((entry: any) => ({ id: entry.Id, name: entry.Name })), - id: item.Id, - imagePlaceholderUrl: null, - imageUrl: getSongCoverArtUrl({ baseUrl: server.url, item, size: imageSize || 100 }), - itemType: LibraryItem.SONG, - lastPlayedAt: null, - name: item.Name, - path: (item.MediaSources && item.MediaSources[0]?.Path) || null, - playCount: (item.UserData && item.UserData.PlayCount) || 0, - playlistItemId: item.PlaylistItemId, - // releaseDate: (item.ProductionYear && new Date(item.ProductionYear, 0, 1).toISOString()) || null, - releaseDate: null, - releaseYear: item.ProductionYear ? String(item.ProductionYear) : null, - serverId: server.id, - serverType: ServerType.JELLYFIN, - size: item.MediaSources && item.MediaSources[0]?.Size, - streamUrl: getStreamUrl({ - container: item.MediaSources[0]?.Container, - deviceId, - eTag: item.MediaSources[0]?.ETag, - id: item.Id, - mediaSourceId: item.MediaSources[0]?.Id, - server, - }), - trackNumber: item.IndexNumber, - uniqueId: nanoid(), - updatedAt: item.DateCreated, - userFavorite: (item.UserData && item.UserData.IsFavorite) || false, - userRating: null, - }; -}; - -const normalizeAlbum = (item: JFAlbum, server: ServerListItem, imageSize?: number): Album => { - return { - albumArtists: - item.AlbumArtists.map((entry) => ({ - id: entry.Id, - imageUrl: null, - name: entry.Name, - })) || [], - artists: item.ArtistItems?.map((entry) => ({ id: entry.Id, imageUrl: null, name: entry.Name })), - backdropImageUrl: null, - createdAt: item.DateCreated, - duration: item.RunTimeTicks / 10000, - genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })), - id: item.Id, - imagePlaceholderUrl: null, - imageUrl: getAlbumCoverArtUrl({ - baseUrl: server.url, - item, - size: imageSize || 300, - }), - isCompilation: null, - itemType: LibraryItem.ALBUM, - lastPlayedAt: null, - name: item.Name, - playCount: item.UserData?.PlayCount || 0, - releaseDate: item.PremiereDate?.split('T')[0] || null, - releaseYear: item.ProductionYear || null, - serverId: server.id, - serverType: ServerType.JELLYFIN, - size: null, - songCount: item?.ChildCount || null, - songs: item.songs?.map((song) => normalizeSong(song, server, '', imageSize)), - uniqueId: nanoid(), - updatedAt: item?.DateLastMediaAdded || item.DateCreated, - userFavorite: item.UserData?.IsFavorite || false, - userRating: null, - }; -}; - -const normalizeAlbumArtist = ( - item: JFAlbumArtist, - server: ServerListItem, - imageSize?: number, -): AlbumArtist => { - return { - albumCount: null, - backgroundImageUrl: null, - biography: item.Overview || null, - duration: item.RunTimeTicks / 10000, - genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })), - id: item.Id, - imageUrl: getAlbumArtistCoverArtUrl({ - baseUrl: server.url, - item, - size: imageSize || 300, - }), - itemType: LibraryItem.ALBUM_ARTIST, - lastPlayedAt: null, - name: item.Name, - playCount: item.UserData.PlayCount, - serverId: server.id, - serverType: ServerType.JELLYFIN, - similarArtists: item.similarArtists?.items - ?.filter((entry) => entry.Name !== 'Various Artists') - .map((entry) => ({ - id: entry.Id, - imageUrl: getAlbumArtistCoverArtUrl({ - baseUrl: server.url, - item: entry, - size: imageSize || 300, - }), - name: entry.Name, - })), - songCount: null, - userFavorite: item.UserData.IsFavorite || false, - userRating: null, - }; -}; - -const normalizePlaylist = ( - item: JFPlaylist, - server: ServerListItem, - imageSize?: number, -): Playlist => { - const imageUrl = getPlaylistCoverArtUrl({ - baseUrl: server.url, - item, - size: imageSize || 300, - }); - - const imagePlaceholderUrl = null; - - return { - description: item.Overview || null, - duration: item.RunTimeTicks / 10000, - genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })), - id: item.Id, - imagePlaceholderUrl, - imageUrl: imageUrl || null, - itemType: LibraryItem.PLAYLIST, - name: item.Name, - owner: null, - ownerId: null, - public: null, - rules: null, - serverId: server.id, - serverType: ServerType.JELLYFIN, - size: null, - songCount: item?.ChildCount || null, - sync: null, - }; -}; - -// const normalizeArtist = (item: any) => { -// return { -// album: (item.album || []).map((entry: any) => normalizeAlbum(entry)), -// albumCount: item.AlbumCount, -// duration: item.RunTimeTicks / 10000000, -// genre: item.GenreItems && item.GenreItems.map((entry: any) => normalizeItem(entry)), -// id: item.Id, -// image: getCoverArtUrl(item), -// info: { -// biography: item.Overview, -// externalUrl: (item.ExternalUrls || []).map((entry: any) => normalizeItem(entry)), -// imageUrl: undefined, -// similarArtist: (item.similarArtist || []).map((entry: any) => normalizeArtist(entry)), -// }, -// starred: item.UserData && item.UserData?.IsFavorite ? 'true' : undefined, -// title: item.Name, -// uniqueId: nanoid(), -// }; -// }; - -// const normalizeGenre = (item: any) => { -// return { -// albumCount: undefined, -// id: item.Id, -// songCount: undefined, -// title: item.Name, -// type: Item.Genre, -// uniqueId: nanoid(), -// }; -// }; - -// const normalizeFolder = (item: any) => { -// return { -// created: item.DateCreated, -// id: item.Id, -// image: getCoverArtUrl(item, 150), -// isDir: true, -// title: item.Name, -// type: Item.Folder, -// uniqueId: nanoid(), -// }; -// }; - -// const normalizeScanStatus = () => { -// return { -// count: 'N/a', -// scanning: false, -// }; -// }; - -export const jellyfinApi = { - addToPlaylist, - authenticate, - createFavorite, - createPlaylist, - deleteFavorite, - deletePlaylist, - getAlbumArtistDetail, - getAlbumArtistList, - getAlbumDetail, - getAlbumList, - getArtistList, - getGenreList, - getMusicFolderList, - getPlaylistDetail, - getPlaylistList, - getPlaylistSongList, - getSongList, - getTopSongList, - removeFromPlaylist, - scrobble, - updatePlaylist, -}; - -export const jfNormalize = { - album: normalizeAlbum, - albumArtist: normalizeAlbumArtist, - playlist: normalizePlaylist, - song: normalizeSong, -}; diff --git a/src/renderer/api/navidrome.api.ts b/src/renderer/api/navidrome.api.ts deleted file mode 100644 index 6f67628b..00000000 --- a/src/renderer/api/navidrome.api.ts +++ /dev/null @@ -1,756 +0,0 @@ -import { nanoid } from 'nanoid/non-secure'; -import ky from 'ky'; -import type { - NDGenreListResponse, - NDArtistListResponse, - NDAlbumDetail, - NDAlbumListParams, - NDAlbumList, - NDSongDetailResponse, - NDAlbum, - NDSong, - NDAuthenticationResponse, - NDAlbumDetailResponse, - NDSongDetail, - NDGenreList, - NDAlbumArtistListParams, - NDAlbumArtistDetail, - NDAlbumListResponse, - NDAlbumArtistDetailResponse, - NDAlbumArtistList, - NDSongListParams, - NDCreatePlaylistParams, - NDCreatePlaylistResponse, - NDDeletePlaylist, - NDDeletePlaylistResponse, - NDPlaylistListParams, - NDPlaylistDetail, - NDPlaylistList, - NDPlaylistListResponse, - NDPlaylistDetailResponse, - NDSongList, - NDSongListResponse, - NDAlbumArtist, - NDPlaylist, - NDUpdatePlaylistParams, - NDUpdatePlaylistResponse, - NDPlaylistSongListResponse, - NDPlaylistSongList, - NDPlaylistSong, - NDUserList, - NDUserListResponse, - NDUserListParams, - NDUser, - NDAddToPlaylist, - NDAddToPlaylistBody, - NDAddToPlaylistResponse, - NDRemoveFromPlaylistParams, - NDRemoveFromPlaylistResponse, - NDRemoveFromPlaylist, -} from '/@/renderer/api/navidrome.types'; -import { NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types'; -import { - Album, - Song, - AuthenticationResponse, - AlbumDetailArgs, - GenreListArgs, - AlbumListArgs, - AlbumArtistListArgs, - AlbumArtistDetailArgs, - SongListArgs, - SongDetailArgs, - CreatePlaylistArgs, - DeletePlaylistArgs, - PlaylistListArgs, - PlaylistDetailArgs, - CreatePlaylistResponse, - PlaylistSongListArgs, - AlbumArtist, - Playlist, - UpdatePlaylistResponse, - UpdatePlaylistArgs, - UserListArgs, - userListSortMap, - playlistListSortMap, - albumArtistListSortMap, - songListSortMap, - albumListSortMap, - sortOrderMap, - User, - LibraryItem, - AddToPlaylistArgs, - RemoveFromPlaylistArgs, -} from '/@/renderer/api/types'; -import { toast } from '/@/renderer/components/toast'; -import { useAuthStore } from '/@/renderer/store'; -import { ServerListItem, ServerType } from '/@/renderer/types'; -import { parseSearchParams } from '/@/renderer/utils'; -import { subsonicApi } from '/@/renderer/api/subsonic.api'; - -const IGNORE_CORS = localStorage.getItem('IGNORE_CORS') === 'true'; - -const api = ky.create({ - hooks: { - afterResponse: [ - async (_request, _options, response) => { - const serverId = useAuthStore.getState().currentServer?.id; - - if (serverId) { - useAuthStore.getState().actions.updateServer(serverId, { - ndCredential: response.headers.get('x-nd-authorization') as string, - }); - } - - return response; - }, - ], - beforeError: [ - (error) => { - if (error.response && error.response.status === 401) { - toast.error({ - message: 'Your session has expired.', - }); - - const serverId = useAuthStore.getState().currentServer?.id; - - if (serverId) { - useAuthStore.getState().actions.setCurrentServer(null); - useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined }); - } - } - - return error; - }, - ], - }, - mode: IGNORE_CORS ? 'cors' : undefined, -}); - -const authenticate = async ( - url: string, - body: { password: string; username: string }, -): Promise => { - const cleanServerUrl = url.replace(/\/$/, ''); - - const data = await ky - .post(`${cleanServerUrl}/auth/login`, { - json: { - password: body.password, - username: body.username, - }, - }) - .json(); - - return { - credential: `u=${body.username}&s=${data.subsonicSalt}&t=${data.subsonicToken}`, - ndCredential: data.token, - userId: data.id, - username: data.username, - }; -}; - -const getUserList = async (args: UserListArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: NDUserListParams = { - _end: query.startIndex + (query.limit || 0), - _order: sortOrderMap.navidrome[query.sortOrder], - _sort: userListSortMap.navidrome[query.sortBy], - _start: query.startIndex, - ...query.ndParams, - }; - - const res = await api.get('api/user', { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }); - - const data = await res.json(); - const itemCount = res.headers.get('x-total-count'); - - return { - items: data, - startIndex: query?.startIndex || 0, - totalRecordCount: Number(itemCount), - }; -}; - -const getGenreList = async (args: GenreListArgs): Promise => { - const { server, signal } = args; - - const data = await api - .get('api/genre', { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - signal, - }) - .json(); - - return data; -}; - -const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs): Promise => { - const { query, server, signal } = args; - - const artistInfo = await subsonicApi.getArtistInfo({ - query: { - artistId: query.id, - limit: 15, - }, - server, - signal, - }); - - const data = await api - .get(`api/artist/${query.id}`, { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - signal, - }) - .json(); - - return { ...data, similarArtists: artistInfo.similarArtist }; -}; - -const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: NDAlbumArtistListParams = { - _end: query.startIndex + (query.limit || 0), - _order: sortOrderMap.navidrome[query.sortOrder], - _sort: albumArtistListSortMap.navidrome[query.sortBy], - _start: query.startIndex, - name: query.searchTerm, - ...query.ndParams, - }; - - const res = await api.get('api/artist', { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }); - - const data = await res.json(); - const itemCount = res.headers.get('x-total-count'); - - return { - items: data, - startIndex: query.startIndex, - totalRecordCount: Number(itemCount), - }; -}; - -const getAlbumDetail = async (args: AlbumDetailArgs): Promise => { - const { query, server, signal } = args; - - const data = await api - .get(`api/album/${query.id}`, { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - signal, - }) - .json(); - - const songsData = await api - .get('api/song', { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - searchParams: { - _end: 0, - _order: NDSortOrder.ASC, - _sort: 'album', - _start: 0, - album_id: query.id, - }, - signal, - }) - .json(); - - return { ...data, songs: songsData }; -}; - -const getAlbumList = async (args: AlbumListArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: NDAlbumListParams = { - _end: query.startIndex + (query.limit || 0), - _order: sortOrderMap.navidrome[query.sortOrder], - _sort: albumListSortMap.navidrome[query.sortBy], - _start: query.startIndex, - artist_id: query.artistIds?.[0], - name: query.searchTerm, - ...query.ndParams, - }; - - const res = await api.get('api/album', { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }); - - const data = await res.json(); - const itemCount = res.headers.get('x-total-count'); - - return { - items: data, - startIndex: query?.startIndex || 0, - totalRecordCount: Number(itemCount), - }; -}; - -const getSongList = async (args: SongListArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: NDSongListParams = { - _end: query.startIndex + (query.limit || -1), - _order: sortOrderMap.navidrome[query.sortOrder], - _sort: songListSortMap.navidrome[query.sortBy], - _start: query.startIndex, - album_id: query.albumIds, - artist_id: query.artistIds, - title: query.searchTerm, - ...query.ndParams, - }; - - const res = await api.get('api/song', { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }); - - const data = await res.json(); - const itemCount = res.headers.get('x-total-count'); - - return { - items: data, - startIndex: query?.startIndex || 0, - totalRecordCount: Number(itemCount), - }; -}; - -const getSongDetail = async (args: SongDetailArgs): Promise => { - const { query, server, signal } = args; - - const data = await api - .get(`api/song/${query.id}`, { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - signal, - }) - .json(); - - return data; -}; - -const createPlaylist = async (args: CreatePlaylistArgs): Promise => { - const { body, server } = args; - - const json: NDCreatePlaylistParams = { - comment: body.comment, - name: body.name, - ...body.ndParams, - public: body.ndParams?.public || false, - rules: body.ndParams?.rules ? body.ndParams.rules : undefined, - }; - - const data = await api - .post('api/playlist', { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - json, - prefixUrl: server?.url, - }) - .json(); - - return { - id: data.id, - name: body.name, - }; -}; - -const updatePlaylist = async (args: UpdatePlaylistArgs): Promise => { - const { query, body, server, signal } = args; - - const json: NDUpdatePlaylistParams = { - comment: body.comment || '', - name: body.name, - ownerId: body.ndParams?.ownerId || undefined, - ownerName: body.ndParams?.owner || undefined, - public: body.ndParams?.public || false, - rules: body.ndParams?.rules ? body.ndParams?.rules : undefined, - sync: body.ndParams?.sync || undefined, - }; - - const data = await api - .put(`api/playlist/${query.id}`, { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - json, - prefixUrl: server?.url, - signal, - }) - .json(); - - return { - id: data.id, - }; -}; - -const deletePlaylist = async (args: DeletePlaylistArgs): Promise => { - const { query, server, signal } = args; - - const data = await api - .delete(`api/playlist/${query.id}`, { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - signal, - }) - .json(); - - return data; -}; - -const getPlaylistList = async (args: PlaylistListArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: NDPlaylistListParams = { - _end: query.startIndex + (query.limit || 0), - _order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : undefined, - _sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : undefined, - _start: query.startIndex, - ...query.ndParams, - }; - - const res = await api.get('api/playlist', { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }); - - const data = await res.json(); - const itemCount = res.headers.get('x-total-count'); - - return { - items: data, - startIndex: query?.startIndex || 0, - totalRecordCount: Number(itemCount), - }; -}; - -const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise => { - const { query, server, signal } = args; - - const data = await api - .get(`api/playlist/${query.id}`, { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - signal, - }) - .json(); - - return data; -}; - -const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: NDSongListParams & { playlist_id: string } = { - _end: query.startIndex + (query.limit || 0), - _order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : NDSortOrder.ASC, - _sort: query.sortBy ? songListSortMap.navidrome[query.sortBy] : NDSongListSort.ID, - _start: query.startIndex, - playlist_id: query.id, - }; - - const res = await api.get(`api/playlist/${query.id}/tracks`, { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }); - - const data = await res.json(); - const itemCount = res.headers.get('x-total-count'); - - return { - items: data, - startIndex: query?.startIndex || 0, - totalRecordCount: Number(itemCount), - }; -}; - -const addToPlaylist = async (args: AddToPlaylistArgs): Promise => { - const { query, body, server, signal } = args; - - const json: NDAddToPlaylistBody = { - ids: body.songId, - }; - - await api - .post(`api/playlist/${query.id}/tracks`, { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - json, - prefixUrl: server?.url, - signal, - }) - .json(); - - return null; -}; - -const removeFromPlaylist = async (args: RemoveFromPlaylistArgs): Promise => { - const { query, server, signal } = args; - - const searchParams: NDRemoveFromPlaylistParams = { - id: query.songId, - }; - - await api - .delete(`api/playlist/${query.id}/tracks`, { - headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` }, - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - return null; -}; - -const getCoverArtUrl = (args: { - baseUrl: string; - coverArtId: string; - credential: string; - size: number; -}) => { - const size = args.size ? args.size : 250; - - if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) { - return null; - } - - return ( - `${args.baseUrl}/rest/getCoverArt.view` + - `?id=${args.coverArtId}` + - `&${args.credential}` + - '&v=1.13.0' + - '&c=feishin' + - `&size=${size}` - ); -}; - -const normalizeSong = ( - item: NDSong | NDPlaylistSong, - server: ServerListItem, - deviceId: string, - imageSize?: number, -): Song => { - let id; - let playlistItemId; - - // Dynamically determine the id field based on whether or not the item is a playlist song - if ('mediaFileId' in item) { - id = item.mediaFileId; - playlistItemId = item.id; - } else { - id = item.id; - } - - const imageUrl = getCoverArtUrl({ - baseUrl: server.url, - coverArtId: id, - credential: server.credential, - size: imageSize || 100, - }); - - const imagePlaceholderUrl = null; - - return { - album: item.album, - albumArtists: [{ id: item.artistId, imageUrl: null, name: item.artist }], - albumId: item.albumId, - artistName: item.artist, - artists: [{ id: item.artistId, imageUrl: null, name: item.artist }], - bitRate: item.bitRate, - bpm: item.bpm ? item.bpm : null, - channels: item.channels ? item.channels : null, - comment: item.comment ? item.comment : null, - compilation: item.compilation, - container: item.suffix, - createdAt: item.createdAt.split('T')[0], - discNumber: item.discNumber, - duration: item.duration, - genres: item.genres, - id, - imagePlaceholderUrl, - imageUrl, - itemType: LibraryItem.SONG, - lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate, - name: item.title, - path: item.path, - playCount: item.playCount, - playlistItemId, - releaseDate: new Date(item.year, 0, 1).toISOString(), - releaseYear: String(item.year), - serverId: server.id, - serverType: ServerType.NAVIDROME, - size: item.size, - streamUrl: `${server.url}/rest/stream.view?id=${id}&v=1.13.0&c=feishin_${deviceId}&${server.credential}`, - trackNumber: item.trackNumber, - uniqueId: nanoid(), - updatedAt: item.updatedAt, - userFavorite: item.starred || false, - userRating: item.rating || null, - }; -}; - -const normalizeAlbum = (item: NDAlbum, server: ServerListItem, imageSize?: number): Album => { - const imageUrl = getCoverArtUrl({ - baseUrl: server.url, - coverArtId: item.coverArtId || item.id, - credential: server.credential, - size: imageSize || 300, - }); - - const imagePlaceholderUrl = null; - - const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || null; - - return { - albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }], - artists: [{ id: item.artistId, imageUrl: null, name: item.artist }], - backdropImageUrl: imageBackdropUrl, - createdAt: item.createdAt.split('T')[0], - duration: item.duration * 1000 || null, - genres: item.genres, - id: item.id, - imagePlaceholderUrl, - imageUrl, - isCompilation: item.compilation, - itemType: LibraryItem.ALBUM, - lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate, - name: item.name, - playCount: item.playCount, - releaseDate: new Date(item.minYear, 0, 1).toISOString(), - releaseYear: item.minYear, - serverId: server.id, - serverType: ServerType.NAVIDROME, - size: item.size, - songCount: item.songCount, - songs: item.songs ? item.songs.map((song) => normalizeSong(song, server, '')) : undefined, - uniqueId: nanoid(), - updatedAt: item.updatedAt, - userFavorite: item.starred, - userRating: item.rating, - }; -}; - -const normalizeAlbumArtist = (item: NDAlbumArtist, server: ServerListItem): AlbumArtist => { - const imageUrl = - item.largeImageUrl === '/app/artist-placeholder.webp' ? null : item.largeImageUrl; - - return { - albumCount: item.albumCount, - backgroundImageUrl: null, - biography: item.biography || null, - duration: null, - genres: item.genres, - id: item.id, - imageUrl: imageUrl || null, - itemType: LibraryItem.ALBUM_ARTIST, - lastPlayedAt: item.playDate.includes('0001-') ? null : item.playDate, - name: item.name, - playCount: item.playCount, - serverId: server.id, - serverType: ServerType.NAVIDROME, - similarArtists: - item.similarArtists?.map((artist) => ({ - id: artist.id, - imageUrl: artist?.artistImageUrl || null, - name: artist.name, - })) || null, - songCount: item.songCount, - userFavorite: item.starred, - userRating: item.rating, - }; -}; - -const normalizePlaylist = ( - item: NDPlaylist, - server: ServerListItem, - imageSize?: number, -): Playlist => { - const imageUrl = getCoverArtUrl({ - baseUrl: server.url, - coverArtId: item.id, - credential: server.credential, - size: imageSize || 300, - }); - - const imagePlaceholderUrl = null; - - return { - description: item.comment, - duration: item.duration * 1000, - genres: [], - id: item.id, - imagePlaceholderUrl, - imageUrl, - itemType: LibraryItem.PLAYLIST, - name: item.name, - owner: item.ownerName, - ownerId: item.ownerId, - public: item.public, - rules: item?.rules || null, - serverId: server.id, - serverType: ServerType.NAVIDROME, - size: item.size, - songCount: item.songCount, - sync: item.sync, - }; -}; - -const normalizeUser = (item: NDUser): User => { - return { - createdAt: item.createdAt, - email: item.email, - id: item.id, - isAdmin: item.isAdmin, - lastLoginAt: item.lastLoginAt, - name: item.userName, - updatedAt: item.updatedAt, - }; -}; - -export const navidromeApi = { - addToPlaylist, - authenticate, - createPlaylist, - deletePlaylist, - getAlbumArtistDetail, - getAlbumArtistList, - getAlbumDetail, - getAlbumList, - getGenreList, - getPlaylistDetail, - getPlaylistList, - getPlaylistSongList, - getSongDetail, - getSongList, - getUserList, - removeFromPlaylist, - updatePlaylist, -}; - -export const ndNormalize = { - album: normalizeAlbum, - albumArtist: normalizeAlbumArtist, - playlist: normalizePlaylist, - song: normalizeSong, - user: normalizeUser, -}; diff --git a/src/renderer/api/normalize.ts b/src/renderer/api/normalize.ts deleted file mode 100644 index 2ce5a0ef..00000000 --- a/src/renderer/api/normalize.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { jfNormalize } from '/@/renderer/api/jellyfin.api'; -import type { - JFAlbum, - JFAlbumArtist, - JFGenreList, - JFMusicFolderList, - JFPlaylist, - JFSong, -} from '/@/renderer/api/jellyfin.types'; -import { ndNormalize } from '/@/renderer/api/navidrome.api'; -import type { - NDAlbum, - NDAlbumArtist, - NDGenreList, - NDPlaylist, - NDSong, - NDUser, -} from '/@/renderer/api/navidrome.types'; -import { ssNormalize } from '/@/renderer/api/subsonic.api'; -import { SSGenreList, SSMusicFolderList, SSSong } from '/@/renderer/api/subsonic.types'; -import type { - Album, - AlbumArtist, - RawAlbumArtistDetailResponse, - RawAlbumArtistListResponse, - RawAlbumDetailResponse, - RawAlbumListResponse, - RawGenreListResponse, - RawMusicFolderListResponse, - RawPlaylistDetailResponse, - RawPlaylistListResponse, - RawSongListResponse, - RawTopSongListResponse, - RawUserListResponse, -} from '/@/renderer/api/types'; -import { ServerListItem } from '/@/renderer/types'; - -const albumList = (data: RawAlbumListResponse | undefined, server: ServerListItem | null) => { - let albums; - switch (server?.type) { - case 'jellyfin': - albums = data?.items.map((item) => jfNormalize.album(item as JFAlbum, server)); - break; - case 'navidrome': - albums = data?.items.map((item) => ndNormalize.album(item as NDAlbum, server)); - break; - case 'subsonic': - break; - } - - return { - items: albums, - startIndex: data?.startIndex, - totalRecordCount: data?.totalRecordCount, - }; -}; - -const albumDetail = ( - data: RawAlbumDetailResponse | undefined, - server: ServerListItem | null, -): Album | undefined => { - let album: Album | undefined; - switch (server?.type) { - case 'jellyfin': - album = jfNormalize.album(data as JFAlbum, server); - break; - case 'navidrome': - album = ndNormalize.album(data as NDAlbum, server); - break; - case 'subsonic': - break; - } - - return album; -}; - -const songList = (data: RawSongListResponse | undefined, server: ServerListItem | null) => { - let songs; - switch (server?.type) { - case 'jellyfin': - songs = data?.items.map((item) => jfNormalize.song(item as JFSong, server, '')); - break; - case 'navidrome': - songs = data?.items.map((item) => ndNormalize.song(item as NDSong, server, '')); - break; - case 'subsonic': - break; - } - - return { - items: songs, - startIndex: data?.startIndex, - totalRecordCount: data?.totalRecordCount, - }; -}; - -const topSongList = (data: RawTopSongListResponse | undefined, server: ServerListItem | null) => { - let songs; - - switch (server?.type) { - case 'jellyfin': - songs = data?.items.map((item) => jfNormalize.song(item as JFSong, server, '')); - break; - case 'navidrome': - songs = data?.items?.map((item) => ssNormalize.song(item as SSSong, server, '')); - break; - case 'subsonic': - songs = data?.items?.map((item) => ssNormalize.song(item as SSSong, server, '')); - break; - } - - return { - items: songs, - }; -}; - -const musicFolderList = ( - data: RawMusicFolderListResponse | undefined, - server: ServerListItem | null, -) => { - let musicFolders; - switch (server?.type) { - case 'jellyfin': - musicFolders = (data as JFMusicFolderList)?.map((item) => ({ - id: String(item.Id), - name: item.Name, - })); - break; - case 'navidrome': - musicFolders = (data as SSMusicFolderList)?.map((item) => ({ - id: String(item.id), - name: item.name, - })); - break; - case 'subsonic': - musicFolders = (data as SSMusicFolderList)?.map((item) => ({ - id: String(item.id), - name: item.name, - })); - break; - } - - return musicFolders; -}; - -const genreList = (data: RawGenreListResponse | undefined, server: ServerListItem | null) => { - let genres; - switch (server?.type) { - case 'jellyfin': - genres = (data as JFGenreList)?.Items.map((item) => ({ - id: String(item.Id), - name: item.Name, - })).sort((a, b) => a.name.localeCompare(b.name)); - break; - case 'navidrome': - genres = (data as NDGenreList) - ?.map((item) => ({ - id: String(item.id), - name: item.name, - })) - .sort((a, b) => a.name.localeCompare(b.name)); - break; - case 'subsonic': - genres = (data as SSGenreList) - ?.map((item) => ({ - id: item.value, - name: item.value, - })) - .sort((a, b) => a.name.localeCompare(b.name)); - break; - } - - return genres; -}; - -const albumArtistDetail = ( - data: RawAlbumArtistDetailResponse | undefined, - server: ServerListItem | null, -): AlbumArtist | undefined => { - let albumArtist: AlbumArtist | undefined; - switch (server?.type) { - case 'jellyfin': - albumArtist = jfNormalize.albumArtist(data as JFAlbumArtist, server); - break; - case 'navidrome': - albumArtist = ndNormalize.albumArtist(data as NDAlbumArtist, server); - break; - case 'subsonic': - break; - } - - return albumArtist; -}; - -const albumArtistList = ( - data: RawAlbumArtistListResponse | undefined, - server: ServerListItem | null, -) => { - let albumArtists; - switch (server?.type) { - case 'jellyfin': - albumArtists = data?.items.map((item) => - jfNormalize.albumArtist(item as JFAlbumArtist, server), - ); - break; - case 'navidrome': - albumArtists = data?.items.map((item) => - ndNormalize.albumArtist(item as NDAlbumArtist, server), - ); - break; - case 'subsonic': - break; - } - - return { - items: albumArtists, - startIndex: data?.startIndex, - totalRecordCount: data?.totalRecordCount, - }; -}; - -const playlistList = (data: RawPlaylistListResponse | undefined, server: ServerListItem | null) => { - let playlists; - switch (server?.type) { - case 'jellyfin': - playlists = data?.items.map((item) => jfNormalize.playlist(item as JFPlaylist, server)); - break; - case 'navidrome': - playlists = data?.items.map((item) => ndNormalize.playlist(item as NDPlaylist, server)); - break; - case 'subsonic': - break; - } - - return { - items: playlists, - startIndex: data?.startIndex, - totalRecordCount: data?.totalRecordCount, - }; -}; - -const playlistDetail = ( - data: RawPlaylistDetailResponse | undefined, - server: ServerListItem | null, -) => { - let playlist; - switch (server?.type) { - case 'jellyfin': - playlist = jfNormalize.playlist(data as JFPlaylist, server); - break; - case 'navidrome': - playlist = ndNormalize.playlist(data as NDPlaylist, server); - break; - case 'subsonic': - break; - } - - return playlist; -}; - -const userList = (data: RawUserListResponse | undefined, server: ServerListItem | null) => { - let users; - switch (server?.type) { - case 'jellyfin': - break; - case 'navidrome': - users = data?.items.map((item) => ndNormalize.user(item as NDUser)); - break; - case 'subsonic': - break; - } - - return { - items: users, - startIndex: data?.startIndex, - totalRecordCount: data?.totalRecordCount, - }; -}; - -export const normalize = { - albumArtistDetail, - albumArtistList, - albumDetail, - albumList, - genreList, - musicFolderList, - playlistDetail, - playlistList, - songList, - topSongList, - userList, -}; diff --git a/src/renderer/api/subsonic.api.ts b/src/renderer/api/subsonic.api.ts deleted file mode 100644 index df3b4fde..00000000 --- a/src/renderer/api/subsonic.api.ts +++ /dev/null @@ -1,497 +0,0 @@ -import ky from 'ky'; -import md5 from 'md5'; -import { parseSearchParams, randomString } from '/@/renderer/utils'; -import type { - SSAlbumListResponse, - SSAlbumDetailResponse, - SSArtistIndex, - SSAlbumArtistList, - SSAlbumArtistListResponse, - SSGenreListResponse, - SSMusicFolderList, - SSMusicFolderListResponse, - SSGenreList, - SSAlbumDetail, - SSAlbumList, - SSAlbumArtistDetail, - SSAlbumArtistDetailResponse, - SSFavoriteParams, - SSRatingParams, - SSAlbumArtistDetailParams, - SSAlbumArtistListParams, - SSTopSongListParams, - SSTopSongListResponse, - SSArtistInfoParams, - SSArtistInfoResponse, - SSArtistInfo, - SSSong, - SSTopSongList, - SSScrobbleParams, -} from '/@/renderer/api/subsonic.types'; -import { - AlbumArtistDetailArgs, - AlbumArtistListArgs, - AlbumDetailArgs, - AlbumListArgs, - ArtistInfoArgs, - AuthenticationResponse, - FavoriteArgs, - FavoriteResponse, - GenreListArgs, - LibraryItem, - MusicFolderListArgs, - QueueSong, - RatingArgs, - RatingResponse, - RawScrobbleResponse, - ScrobbleArgs, - ServerListItem, - ServerType, - TopSongListArgs, -} from '/@/renderer/api/types'; -import { toast } from '/@/renderer/components/toast'; -import { nanoid } from 'nanoid/non-secure'; - -const IGNORE_CORS = localStorage.getItem('IGNORE_CORS') === 'true'; - -const getCoverArtUrl = (args: { - baseUrl: string; - coverArtId: string; - credential: string; - size: number; -}) => { - const size = args.size ? args.size : 150; - - if (!args.coverArtId || args.coverArtId.match('2a96cbd8b46e442fc41c2b86b821562f')) { - return null; - } - - return ( - `${args.baseUrl}/rest/getCoverArt.view` + - `?id=${args.coverArtId}` + - `&${args.credential}` + - '&v=1.13.0' + - '&c=feishin' + - `&size=${size}` - ); -}; - -const api = ky.create({ - hooks: { - afterResponse: [ - async (_request, _options, response) => { - const data = await response.json(); - if (data['subsonic-response'].status !== 'ok') { - // Suppress code related to non-linked lastfm or spotify from Navidrome - if (data['subsonic-response'].error.code !== 0) { - toast.error({ - message: data['subsonic-response'].error.message, - title: 'Issue from Subsonic API', - }); - } - } - - return new Response(JSON.stringify(data['subsonic-response']), { status: 200 }); - }, - ], - }, - mode: IGNORE_CORS ? 'cors' : undefined, -}); - -const getDefaultParams = (server: ServerListItem | null) => { - if (!server) return {}; - - const authParams = server.credential.split(/&?\w=/gm); - - const params: Record = { - c: 'Feishin', - f: 'json', - u: server.username, - v: '1.13.0', - }; - - if (authParams?.length === 4) { - params.s = authParams[2]; - params.t = authParams[3]; - } else if (authParams?.length === 3) { - params.p = authParams[2]; - } - - return params; -}; - -const authenticate = async ( - url: string, - body: { - legacy?: boolean; - password: string; - username: string; - }, -): Promise => { - let credential; - const cleanServerUrl = url.replace(/\/$/, ''); - - if (body.legacy) { - credential = `u=${body.username}&p=${body.password}`; - } else { - const salt = randomString(12); - const hash = md5(body.password + salt); - credential = `u=${body.username}&s=${salt}&t=${hash}`; - } - - await ky.get(`${cleanServerUrl}/rest/ping.view?v=1.13.0&c=Feishin&f=json&${credential}`); - - return { - credential, - userId: null, - username: body.username, - }; -}; - -const getMusicFolderList = async (args: MusicFolderListArgs): Promise => { - const { signal, server } = args; - const defaultParams = getDefaultParams(server); - - const data = await api - .get('rest/getMusicFolders.view', { - prefixUrl: server?.url, - searchParams: defaultParams, - signal, - }) - .json(); - - return data.musicFolders.musicFolder; -}; - -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 { server, query, signal } = args; - const defaultParams = getDefaultParams(server); - - for (const id of query.id) { - const searchParams: SSFavoriteParams = { - albumId: query.type === LibraryItem.ALBUM ? id : undefined, - artistId: query.type === LibraryItem.ALBUM_ARTIST ? id : undefined, - id: query.type === LibraryItem.SONG ? id : undefined, - ...defaultParams, - }; - - await api.get('rest/star.view', { - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }); - // .json(); - } - - return { - id: query.id, - type: query.type, - }; -}; - -const deleteFavorite = async (args: FavoriteArgs): Promise => { - const { server, query, signal } = args; - const defaultParams = getDefaultParams(server); - - for (const id of query.id) { - const searchParams: SSFavoriteParams = { - albumId: query.type === LibraryItem.ALBUM ? id : undefined, - artistId: query.type === LibraryItem.ALBUM_ARTIST ? id : undefined, - id: query.type === LibraryItem.SONG ? id : undefined, - ...defaultParams, - }; - - await api.get('rest/unstar.view', { - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }); - // .json(); - } - - return { - id: query.id, - type: query.type, - }; -}; - -const updateRating = async (args: RatingArgs): Promise => { - const { server, query, signal } = args; - const defaultParams = getDefaultParams(server); - - const itemIds = query.item.map((item) => item.id); - - for (const id of itemIds) { - const searchParams: SSRatingParams = { - id, - rating: query.rating, - ...defaultParams, - }; - - await api.get('rest/setRating.view', { - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }); - } - - return null; -}; - -const getTopSongList = async (args: TopSongListArgs): Promise => { - const { signal, server, query } = args; - const defaultParams = getDefaultParams(server); - - const searchParams: SSTopSongListParams = { - artist: query.artist, - count: query.limit, - ...defaultParams, - }; - - const data = await api - .get('rest/getTopSongs.view', { - prefixUrl: server?.url, - searchParams: parseSearchParams(searchParams), - signal, - }) - .json(); - - return { - items: data?.topSongs?.song, - startIndex: 0, - totalRecordCount: data?.topSongs?.song?.length || 0, - }; -}; - -const getArtistInfo = async (args: ArtistInfoArgs): Promise => { - const { signal, server, query } = args; - const defaultParams = getDefaultParams(server); - - const searchParams: SSArtistInfoParams = { - count: query.limit, - id: query.artistId, - ...defaultParams, - }; - - const data = await api - .get('rest/getArtistInfo2.view', { - prefixUrl: server?.url, - searchParams, - signal, - }) - .json(); - - return data.artistInfo2; -}; - -const scrobble = async (args: ScrobbleArgs): Promise => { - const { signal, server, query } = args; - const defaultParams = getDefaultParams(server); - - const searchParams: SSScrobbleParams = { - id: query.id, - submission: query.submission, - ...defaultParams, - }; - - await api.get('rest/scrobble.view', { - prefixUrl: server?.url, - searchParams, - signal, - }); - - return null; -}; - -const normalizeSong = (item: SSSong, server: ServerListItem, deviceId: string): QueueSong => { - const imageUrl = - getCoverArtUrl({ - baseUrl: server.url, - coverArtId: item.coverArt, - credential: server.credential, - size: 300, - }) || null; - - const streamUrl = `${server.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server.credential}`; - - return { - album: item.album, - albumArtists: [ - { - id: item.artistId || '', - imageUrl: null, - name: item.artist, - }, - ], - albumId: item.albumId, - artistName: item.artist, - artists: [ - { - id: item.artistId || '', - imageUrl: null, - name: item.artist, - }, - ], - bitRate: item.bitRate, - bpm: null, - channels: null, - comment: null, - compilation: null, - container: item.contentType, - createdAt: item.created, - discNumber: item.discNumber || 1, - duration: item.duration, - genres: [ - { - id: item.genre, - name: item.genre, - }, - ], - id: item.id, - imagePlaceholderUrl: null, - imageUrl, - itemType: LibraryItem.SONG, - lastPlayedAt: null, - name: item.title, - path: item.path, - playCount: item?.playCount || 0, - releaseDate: null, - releaseYear: item.year ? String(item.year) : null, - serverId: server.id, - serverType: ServerType.SUBSONIC, - size: item.size, - streamUrl, - trackNumber: item.track, - uniqueId: nanoid(), - updatedAt: '', - userFavorite: item.starred || false, - userRating: item.userRating || null, - }; -}; - -export const subsonicApi = { - authenticate, - createFavorite, - deleteFavorite, - getAlbumArtistDetail, - getAlbumArtistList, - getAlbumDetail, - getAlbumList, - getArtistInfo, - getCoverArtUrl, - getGenreList, - getMusicFolderList, - getTopSongList, - scrobble, - updateRating, -}; - -export const ssNormalize = { - song: normalizeSong, -};