mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 02:13:33 +00:00
Add files
This commit is contained in:
commit
e87c814068
266 changed files with 63938 additions and 0 deletions
190
src/renderer/api/controller.ts
Normal file
190
src/renderer/api/controller.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { navidromeApi } from '/@/renderer/api/navidrome.api';
|
||||
import { toast } from '/@/renderer/components';
|
||||
import type {
|
||||
AlbumDetailArgs,
|
||||
RawAlbumDetailResponse,
|
||||
RawAlbumListResponse,
|
||||
AlbumListArgs,
|
||||
SongListArgs,
|
||||
RawSongListResponse,
|
||||
SongDetailArgs,
|
||||
RawSongDetailResponse,
|
||||
AlbumArtistDetailArgs,
|
||||
RawAlbumArtistDetailResponse,
|
||||
AlbumArtistListArgs,
|
||||
RawAlbumArtistListResponse,
|
||||
RatingArgs,
|
||||
RawRatingResponse,
|
||||
FavoriteArgs,
|
||||
RawFavoriteResponse,
|
||||
GenreListArgs,
|
||||
RawGenreListResponse,
|
||||
CreatePlaylistArgs,
|
||||
RawCreatePlaylistResponse,
|
||||
DeletePlaylistArgs,
|
||||
RawDeletePlaylistResponse,
|
||||
PlaylistDetailArgs,
|
||||
RawPlaylistDetailResponse,
|
||||
PlaylistListArgs,
|
||||
RawPlaylistListResponse,
|
||||
MusicFolderListArgs,
|
||||
RawMusicFolderListResponse,
|
||||
PlaylistSongListArgs,
|
||||
ArtistListArgs,
|
||||
RawArtistListResponse,
|
||||
} from '/@/renderer/api/types';
|
||||
import { subsonicApi } from '/@/renderer/api/subsonic.api';
|
||||
import { jellyfinApi } from '/@/renderer/api/jellyfin.api';
|
||||
|
||||
export type ControllerEndpoint = Partial<{
|
||||
clearPlaylist: () => void;
|
||||
createFavorite: (args: FavoriteArgs) => Promise<RawFavoriteResponse>;
|
||||
createPlaylist: (args: CreatePlaylistArgs) => Promise<RawCreatePlaylistResponse>;
|
||||
deleteFavorite: (args: FavoriteArgs) => Promise<RawFavoriteResponse>;
|
||||
deletePlaylist: (args: DeletePlaylistArgs) => Promise<RawDeletePlaylistResponse>;
|
||||
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<RawAlbumArtistDetailResponse>;
|
||||
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<RawAlbumArtistListResponse>;
|
||||
getAlbumDetail: (args: AlbumDetailArgs) => Promise<RawAlbumDetailResponse>;
|
||||
getAlbumList: (args: AlbumListArgs) => Promise<RawAlbumListResponse>;
|
||||
getArtistDetail: () => void;
|
||||
getArtistList: (args: ArtistListArgs) => Promise<RawArtistListResponse>;
|
||||
getFavoritesList: () => void;
|
||||
getFolderItemList: () => void;
|
||||
getFolderList: () => void;
|
||||
getFolderSongs: () => void;
|
||||
getGenreList: (args: GenreListArgs) => Promise<RawGenreListResponse>;
|
||||
getMusicFolderList: (args: MusicFolderListArgs) => Promise<RawMusicFolderListResponse>;
|
||||
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<RawPlaylistDetailResponse>;
|
||||
getPlaylistList: (args: PlaylistListArgs) => Promise<RawPlaylistListResponse>;
|
||||
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<RawSongListResponse>;
|
||||
getSongDetail: (args: SongDetailArgs) => Promise<RawSongDetailResponse>;
|
||||
getSongList: (args: SongListArgs) => Promise<RawSongListResponse>;
|
||||
updatePlaylist: () => void;
|
||||
updateRating: (args: RatingArgs) => Promise<RawRatingResponse>;
|
||||
}>;
|
||||
|
||||
type ApiController = {
|
||||
jellyfin: ControllerEndpoint;
|
||||
navidrome: ControllerEndpoint;
|
||||
subsonic: ControllerEndpoint;
|
||||
};
|
||||
|
||||
const endpoints: ApiController = {
|
||||
jellyfin: {
|
||||
clearPlaylist: undefined,
|
||||
createFavorite: jellyfinApi.createFavorite,
|
||||
createPlaylist: jellyfinApi.createPlaylist,
|
||||
deleteFavorite: jellyfinApi.deleteFavorite,
|
||||
deletePlaylist: jellyfinApi.deletePlaylist,
|
||||
getAlbumArtistDetail: jellyfinApi.getAlbumArtistDetail,
|
||||
getAlbumArtistList: jellyfinApi.getAlbumArtistList,
|
||||
getAlbumDetail: jellyfinApi.getAlbumDetail,
|
||||
getAlbumList: jellyfinApi.getAlbumList,
|
||||
getArtistDetail: undefined,
|
||||
getArtistList: jellyfinApi.getArtistList,
|
||||
getFavoritesList: undefined,
|
||||
getFolderItemList: undefined,
|
||||
getFolderList: undefined,
|
||||
getFolderSongs: undefined,
|
||||
getGenreList: jellyfinApi.getGenreList,
|
||||
getMusicFolderList: jellyfinApi.getMusicFolderList,
|
||||
getPlaylistDetail: jellyfinApi.getPlaylistDetail,
|
||||
getPlaylistList: jellyfinApi.getPlaylistList,
|
||||
getPlaylistSongList: jellyfinApi.getPlaylistSongList,
|
||||
getSongDetail: undefined,
|
||||
getSongList: jellyfinApi.getSongList,
|
||||
updatePlaylist: undefined,
|
||||
updateRating: undefined,
|
||||
},
|
||||
navidrome: {
|
||||
clearPlaylist: undefined,
|
||||
createFavorite: subsonicApi.createFavorite,
|
||||
createPlaylist: navidromeApi.createPlaylist,
|
||||
deleteFavorite: subsonicApi.deleteFavorite,
|
||||
deletePlaylist: navidromeApi.deletePlaylist,
|
||||
getAlbumArtistDetail: navidromeApi.getAlbumArtistDetail,
|
||||
getAlbumArtistList: navidromeApi.getAlbumArtistList,
|
||||
getAlbumDetail: navidromeApi.getAlbumDetail,
|
||||
getAlbumList: navidromeApi.getAlbumList,
|
||||
getArtistDetail: undefined,
|
||||
getArtistList: undefined,
|
||||
getFavoritesList: undefined,
|
||||
getFolderItemList: undefined,
|
||||
getFolderList: undefined,
|
||||
getFolderSongs: undefined,
|
||||
getGenreList: navidromeApi.getGenreList,
|
||||
getMusicFolderList: undefined,
|
||||
getPlaylistDetail: navidromeApi.getPlaylistDetail,
|
||||
getPlaylistList: navidromeApi.getPlaylistList,
|
||||
getPlaylistSongList: navidromeApi.getPlaylistSongList,
|
||||
getSongDetail: navidromeApi.getSongDetail,
|
||||
getSongList: navidromeApi.getSongList,
|
||||
updatePlaylist: undefined,
|
||||
updateRating: subsonicApi.updateRating,
|
||||
},
|
||||
subsonic: {
|
||||
clearPlaylist: undefined,
|
||||
createFavorite: subsonicApi.createFavorite,
|
||||
createPlaylist: undefined,
|
||||
deleteFavorite: subsonicApi.deleteFavorite,
|
||||
deletePlaylist: undefined,
|
||||
getAlbumArtistDetail: subsonicApi.getAlbumArtistDetail,
|
||||
getAlbumArtistList: subsonicApi.getAlbumArtistList,
|
||||
getAlbumDetail: subsonicApi.getAlbumDetail,
|
||||
getAlbumList: subsonicApi.getAlbumList,
|
||||
getArtistDetail: undefined,
|
||||
getArtistList: undefined,
|
||||
getFavoritesList: undefined,
|
||||
getFolderItemList: undefined,
|
||||
getFolderList: undefined,
|
||||
getFolderSongs: undefined,
|
||||
getGenreList: undefined,
|
||||
getMusicFolderList: undefined,
|
||||
getPlaylistDetail: undefined,
|
||||
getPlaylistList: undefined,
|
||||
getSongDetail: undefined,
|
||||
getSongList: undefined,
|
||||
updatePlaylist: undefined,
|
||||
updateRating: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
const apiController = (endpoint: keyof ControllerEndpoint) => {
|
||||
const serverType = useAuthStore.getState().currentServer?.type;
|
||||
|
||||
if (!serverType) {
|
||||
toast.error({ message: 'No server selected', title: 'Unable to route request' });
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const controllerFn = endpoints[serverType][endpoint];
|
||||
|
||||
if (typeof controllerFn !== 'function') {
|
||||
toast.error({
|
||||
message: `Endpoint ${endpoint} is not implemented for ${serverType}`,
|
||||
title: 'Unable to route request',
|
||||
});
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
return endpoints[serverType][endpoint];
|
||||
};
|
||||
|
||||
const getAlbumList = async (args: AlbumListArgs) => {
|
||||
return (apiController('getAlbumList') as ControllerEndpoint['getAlbumList'])?.(args);
|
||||
};
|
||||
|
||||
const getAlbumDetail = async (args: AlbumDetailArgs) => {
|
||||
return (apiController('getAlbumDetail') as ControllerEndpoint['getAlbumDetail'])?.(args);
|
||||
};
|
||||
|
||||
const getSongList = async (args: SongListArgs) => {
|
||||
return (apiController('getSongList') as ControllerEndpoint['getSongList'])?.(args);
|
||||
};
|
||||
|
||||
export const controller = {
|
||||
getAlbumDetail,
|
||||
getAlbumList,
|
||||
getSongList,
|
||||
};
|
||||
7
src/renderer/api/index.ts
Normal file
7
src/renderer/api/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { controller } from '/@/renderer/api/controller';
|
||||
import { normalize } from '/@/renderer/api/normalize';
|
||||
|
||||
export const api = {
|
||||
controller,
|
||||
normalize,
|
||||
};
|
||||
683
src/renderer/api/jellyfin.api.ts
Normal file
683
src/renderer/api/jellyfin.api.ts
Normal file
|
|
@ -0,0 +1,683 @@
|
|||
import ky from 'ky';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import type {
|
||||
JFAlbum,
|
||||
JFAlbumArtistDetail,
|
||||
JFAlbumArtistDetailResponse,
|
||||
JFAlbumArtistList,
|
||||
JFAlbumArtistListParams,
|
||||
JFAlbumArtistListResponse,
|
||||
JFAlbumDetail,
|
||||
JFAlbumDetailResponse,
|
||||
JFAlbumList,
|
||||
JFAlbumListParams,
|
||||
JFAlbumListResponse,
|
||||
JFArtistList,
|
||||
JFArtistListParams,
|
||||
JFArtistListResponse,
|
||||
JFAuthenticate,
|
||||
JFCreatePlaylistResponse,
|
||||
JFGenreList,
|
||||
JFGenreListResponse,
|
||||
JFMusicFolderList,
|
||||
JFMusicFolderListResponse,
|
||||
JFPlaylistDetail,
|
||||
JFPlaylistDetailResponse,
|
||||
JFPlaylistList,
|
||||
JFPlaylistListResponse,
|
||||
JFSong,
|
||||
JFSongList,
|
||||
JFSongListParams,
|
||||
JFSongListResponse,
|
||||
} from '/@/renderer/api/jellyfin.types';
|
||||
import { JFCollectionType } from '/@/renderer/api/jellyfin.types';
|
||||
import type {
|
||||
Album,
|
||||
AlbumArtistDetailArgs,
|
||||
AlbumArtistListArgs,
|
||||
AlbumDetailArgs,
|
||||
AlbumListArgs,
|
||||
ArtistListArgs,
|
||||
AuthenticationResponse,
|
||||
CreatePlaylistArgs,
|
||||
CreatePlaylistResponse,
|
||||
DeletePlaylistArgs,
|
||||
FavoriteArgs,
|
||||
FavoriteResponse,
|
||||
GenreListArgs,
|
||||
MusicFolderListArgs,
|
||||
PlaylistDetailArgs,
|
||||
PlaylistListArgs,
|
||||
PlaylistSongListArgs,
|
||||
Song,
|
||||
SongListArgs,
|
||||
} from '/@/renderer/api/types';
|
||||
import {
|
||||
songListSortMap,
|
||||
albumListSortMap,
|
||||
artistListSortMap,
|
||||
sortOrderMap,
|
||||
albumArtistListSortMap,
|
||||
} from '/@/renderer/api/types';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||
import { parseSearchParams } from '/@/renderer/utils';
|
||||
|
||||
const api = ky.create({});
|
||||
|
||||
const authenticate = async (
|
||||
url: string,
|
||||
body: {
|
||||
password: string;
|
||||
username: string;
|
||||
},
|
||||
): Promise<AuthenticationResponse> => {
|
||||
const cleanServerUrl = url.replace(/\/$/, '');
|
||||
|
||||
const data = await ky
|
||||
.post(`${cleanServerUrl}/users/authenticatebyname`, {
|
||||
headers: {
|
||||
'X-Emby-Authorization':
|
||||
'MediaBrowser Client="Feishin", Device="PC", DeviceId="Feishin", Version="0.0.1-alpha1"',
|
||||
},
|
||||
json: {
|
||||
pw: body.password,
|
||||
username: body.username,
|
||||
},
|
||||
})
|
||||
.json<JFAuthenticate>();
|
||||
|
||||
return {
|
||||
credential: data.AccessToken,
|
||||
userId: data.User.Id,
|
||||
username: data.User.Name,
|
||||
};
|
||||
};
|
||||
|
||||
const getMusicFolderList = async (args: MusicFolderListArgs): Promise<JFMusicFolderList> => {
|
||||
const { signal } = args;
|
||||
const userId = useAuthStore.getState().currentServer?.userId;
|
||||
|
||||
const data = await api
|
||||
.get(`users/${userId}/items`, {
|
||||
signal,
|
||||
})
|
||||
.json<JFMusicFolderListResponse>();
|
||||
|
||||
const musicFolders = data.Items.filter(
|
||||
(folder) => folder.CollectionType === JFCollectionType.MUSIC,
|
||||
);
|
||||
|
||||
return {
|
||||
items: musicFolders,
|
||||
startIndex: data.StartIndex,
|
||||
totalRecordCount: data.TotalRecordCount,
|
||||
};
|
||||
};
|
||||
|
||||
const getGenreList = async (args: GenreListArgs): Promise<JFGenreList> => {
|
||||
const { signal, server } = args;
|
||||
|
||||
const data = await api
|
||||
.get('genres', {
|
||||
headers: { 'X-MediaBrowser-Token': server?.credential },
|
||||
prefixUrl: server?.url,
|
||||
signal,
|
||||
})
|
||||
.json<JFGenreListResponse>();
|
||||
return data;
|
||||
};
|
||||
|
||||
const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs): Promise<JFAlbumArtistDetail> => {
|
||||
const { query, server, signal } = args;
|
||||
|
||||
const searchParams = {
|
||||
fields: 'Genres',
|
||||
};
|
||||
|
||||
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<JFAlbumArtistDetailResponse>();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// 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<JFAlbumArtistList> => {
|
||||
const { query, server, signal } = args;
|
||||
|
||||
const searchParams: JFAlbumArtistListParams = {
|
||||
limit: query.limit,
|
||||
parentId: query.musicFolderId,
|
||||
recursive: true,
|
||||
sortBy: albumArtistListSortMap.jellyfin[query.sortBy],
|
||||
sortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||
startIndex: query.startIndex,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.get('artists/albumArtists', {
|
||||
headers: { 'X-MediaBrowser-Token': server?.credential },
|
||||
prefixUrl: server?.url,
|
||||
searchParams: parseSearchParams(searchParams),
|
||||
signal,
|
||||
})
|
||||
.json<JFAlbumArtistListResponse>();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getArtistList = async (args: ArtistListArgs): Promise<JFArtistList> => {
|
||||
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<JFArtistListResponse>();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<JFAlbumDetail> => {
|
||||
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<JFAlbumDetailResponse>();
|
||||
|
||||
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<JFSongListResponse>();
|
||||
|
||||
return { ...data, songs: songsData.Items };
|
||||
};
|
||||
|
||||
const getAlbumList = async (args: AlbumListArgs): Promise<JFAlbumList> => {
|
||||
const { query, server, signal } = args;
|
||||
|
||||
const searchParams: JFAlbumListParams = {
|
||||
includeItemTypes: 'MusicAlbum',
|
||||
limit: query.limit,
|
||||
parentId: query.musicFolderId,
|
||||
recursive: true,
|
||||
sortBy: albumListSortMap.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<JFAlbumListResponse>();
|
||||
|
||||
return {
|
||||
items: data.Items,
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: data.TotalRecordCount,
|
||||
};
|
||||
};
|
||||
|
||||
const getSongList = async (args: SongListArgs): Promise<JFSongList> => {
|
||||
const { query, server, signal } = args;
|
||||
|
||||
const searchParams: JFSongListParams = {
|
||||
fields: 'Genres, DateCreated, MediaSources, ParentId',
|
||||
includeItemTypes: 'Audio',
|
||||
limit: query.limit,
|
||||
parentId: query.musicFolderId,
|
||||
recursive: true,
|
||||
sortBy: songListSortMap.jellyfin[query.sortBy],
|
||||
sortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||
startIndex: query.startIndex,
|
||||
...query.jfParams,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.get(`users/${server?.userId}/items`, {
|
||||
headers: { 'X-MediaBrowser-Token': server?.credential },
|
||||
prefixUrl: server?.url,
|
||||
searchParams: parseSearchParams(searchParams),
|
||||
signal,
|
||||
})
|
||||
.json<JFSongListResponse>();
|
||||
|
||||
return {
|
||||
items: data.Items,
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: data.TotalRecordCount,
|
||||
};
|
||||
};
|
||||
|
||||
const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise<JFPlaylistDetail> => {
|
||||
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<JFPlaylistDetailResponse>();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise<JFSongList> => {
|
||||
const { query, server, signal } = args;
|
||||
|
||||
const searchParams: JFSongListParams = {
|
||||
fields: 'Genres, DateCreated, MediaSources, UserData, ParentId',
|
||||
includeItemTypes: 'Audio',
|
||||
sortOrder: query.sortOrder ? sortOrderMap.jellyfin[query.sortOrder] : undefined,
|
||||
startIndex: 0,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.get(`playlists/${query.id}/items`, {
|
||||
headers: { 'X-MediaBrowser-Token': server?.credential },
|
||||
prefixUrl: server?.url,
|
||||
searchParams: parseSearchParams(searchParams),
|
||||
signal,
|
||||
})
|
||||
.json<JFSongListResponse>();
|
||||
|
||||
return {
|
||||
items: data.Items,
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: data.TotalRecordCount,
|
||||
};
|
||||
};
|
||||
|
||||
const getPlaylistList = async (args: PlaylistListArgs): Promise<JFPlaylistList> => {
|
||||
const { server, signal } = args;
|
||||
|
||||
const searchParams = {
|
||||
fields: 'ChildCount, Genres, DateCreated, ParentId, Overview',
|
||||
includeItemTypes: 'Playlist',
|
||||
recursive: true,
|
||||
sortBy: 'SortName',
|
||||
sortOrder: 'Ascending',
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.get(`/users/${server?.userId}/items`, {
|
||||
headers: { 'X-MediaBrowser-Token': server?.credential },
|
||||
prefixUrl: server?.url,
|
||||
searchParams: parseSearchParams(searchParams),
|
||||
signal,
|
||||
})
|
||||
.json<JFPlaylistListResponse>();
|
||||
|
||||
const playlistData = data.Items.filter((item) => item.MediaType === 'Audio');
|
||||
|
||||
return {
|
||||
Items: playlistData,
|
||||
StartIndex: 0,
|
||||
TotalRecordCount: playlistData.length,
|
||||
};
|
||||
};
|
||||
|
||||
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
||||
const { query, server } = args;
|
||||
|
||||
const body = {
|
||||
MediaType: 'Audio',
|
||||
Name: query.name,
|
||||
UserId: server?.userId,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.post('playlists', {
|
||||
headers: { 'X-MediaBrowser-Token': server?.credential },
|
||||
json: body,
|
||||
prefixUrl: server?.url,
|
||||
})
|
||||
.json<JFCreatePlaylistResponse>();
|
||||
|
||||
return {
|
||||
id: data.Id,
|
||||
name: query.name,
|
||||
};
|
||||
};
|
||||
|
||||
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<null> => {
|
||||
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<FavoriteResponse> => {
|
||||
const { query, server } = args;
|
||||
|
||||
await api.post(`users/${server?.userId}/favoriteitems/${query.id}`, {
|
||||
headers: { 'X-MediaBrowser-Token': server?.credential },
|
||||
prefixUrl: server?.url,
|
||||
});
|
||||
|
||||
return {
|
||||
id: query.id,
|
||||
};
|
||||
};
|
||||
|
||||
const deleteFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
||||
const { query, server } = args;
|
||||
|
||||
await api.delete(`users/${server?.userId}/favoriteitems/${query.id}`, {
|
||||
headers: { 'X-MediaBrowser-Token': server?.credential },
|
||||
prefixUrl: server?.url,
|
||||
});
|
||||
|
||||
return {
|
||||
id: query.id,
|
||||
};
|
||||
};
|
||||
|
||||
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 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 : 300;
|
||||
|
||||
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 normalizeAlbum = (item: JFAlbum, server: ServerListItem, imageSize?: number): Album => {
|
||||
return {
|
||||
albumArtists:
|
||||
item.AlbumArtists?.map((entry) => ({
|
||||
id: entry.Id,
|
||||
name: entry.Name,
|
||||
})) || [],
|
||||
artists: item.ArtistItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
|
||||
backdropImageUrl: null,
|
||||
createdAt: item.DateCreated,
|
||||
duration: item.RunTimeTicks / 10000000,
|
||||
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,
|
||||
isFavorite: item.UserData?.IsFavorite || false,
|
||||
name: item.Name,
|
||||
playCount: item.UserData?.PlayCount || 0,
|
||||
rating: null,
|
||||
releaseDate: item.PremiereDate || null,
|
||||
releaseYear: item.ProductionYear,
|
||||
serverType: ServerType.JELLYFIN,
|
||||
size: null,
|
||||
songCount: item?.ChildCount || null,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeSong = (
|
||||
item: JFSong,
|
||||
server: ServerListItem,
|
||||
deviceId: string,
|
||||
imageSize?: number,
|
||||
): Song => {
|
||||
return {
|
||||
album: item.Album,
|
||||
albumArtists: item.AlbumArtists?.map((entry) => ({ id: entry.Id, name: entry.Name })),
|
||||
albumId: item.AlbumId,
|
||||
artistName: item.ArtistItems[0]?.Name,
|
||||
artists: item.ArtistItems.map((entry) => ({ id: entry.Id, name: entry.Name })),
|
||||
bitRate: item.MediaSources && Number(Math.trunc(item.MediaSources[0]?.Bitrate / 1000)),
|
||||
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,
|
||||
imageUrl: getSongCoverArtUrl({ baseUrl: server.url, item, size: imageSize || 300 }),
|
||||
isFavorite: (item.UserData && item.UserData.IsFavorite) || false,
|
||||
name: item.Name,
|
||||
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
|
||||
playCount: (item.UserData && item.UserData.PlayCount) || 0,
|
||||
releaseDate: (item.ProductionYear && new Date(item.ProductionYear, 0, 1).toISOString()) || null,
|
||||
releaseYear: (item.ProductionYear && String(item.ProductionYear)) || null,
|
||||
serverId: server.id,
|
||||
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,
|
||||
type: ServerType.JELLYFIN,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.DateCreated,
|
||||
};
|
||||
};
|
||||
|
||||
// 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 normalizePlaylist = (item: any) => {
|
||||
// return {
|
||||
// changed: item.DateLastMediaAdded,
|
||||
// comment: item.Overview,
|
||||
// created: item.DateCreated,
|
||||
// duration: item.RunTimeTicks / 10000000,
|
||||
// genre: item.GenreItems && item.GenreItems.map((entry: any) => normalizeItem(entry)),
|
||||
// id: item.Id,
|
||||
// image: getCoverArtUrl(item, 350),
|
||||
// owner: undefined,
|
||||
// public: undefined,
|
||||
// song: [],
|
||||
// songCount: item.ChildCount,
|
||||
// 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 = {
|
||||
authenticate,
|
||||
createFavorite,
|
||||
createPlaylist,
|
||||
deleteFavorite,
|
||||
deletePlaylist,
|
||||
getAlbumArtistDetail,
|
||||
getAlbumArtistList,
|
||||
getAlbumDetail,
|
||||
getAlbumList,
|
||||
getArtistList,
|
||||
getGenreList,
|
||||
getMusicFolderList,
|
||||
getPlaylistDetail,
|
||||
getPlaylistList,
|
||||
getPlaylistSongList,
|
||||
getSongList,
|
||||
};
|
||||
|
||||
export const jfNormalize = {
|
||||
album: normalizeAlbum,
|
||||
song: normalizeSong,
|
||||
};
|
||||
574
src/renderer/api/jellyfin.types.ts
Normal file
574
src/renderer/api/jellyfin.types.ts
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
export type JFBasePaginatedResponse = {
|
||||
StartIndex: number;
|
||||
TotalRecordCount: number;
|
||||
};
|
||||
|
||||
export interface JFMusicFolderListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFMusicFolder[];
|
||||
}
|
||||
|
||||
export type JFMusicFolderList = {
|
||||
items: JFMusicFolder[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export interface JFGenreListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFGenre[];
|
||||
}
|
||||
|
||||
export type JFGenreList = JFGenreListResponse;
|
||||
|
||||
export type JFAlbumArtistDetailResponse = JFAlbumArtist;
|
||||
|
||||
export type JFAlbumArtistDetail = JFAlbumArtistDetailResponse;
|
||||
|
||||
export interface JFAlbumArtistListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFAlbumArtist[];
|
||||
}
|
||||
|
||||
export type JFAlbumArtistList = JFAlbumArtistListResponse;
|
||||
|
||||
export interface JFArtistListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFAlbumArtist[];
|
||||
}
|
||||
|
||||
export type JFArtistList = JFArtistListResponse;
|
||||
|
||||
export interface JFAlbumListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFAlbum[];
|
||||
}
|
||||
|
||||
export type JFAlbumList = {
|
||||
items: JFAlbum[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type JFAlbumDetailResponse = JFAlbum;
|
||||
|
||||
export type JFAlbumDetail = JFAlbum & { songs?: JFSong[] };
|
||||
|
||||
export interface JFSongListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFSong[];
|
||||
}
|
||||
|
||||
export type JFSongList = {
|
||||
items: JFSong[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export interface JFPlaylistListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFPlaylist[];
|
||||
}
|
||||
|
||||
export type JFPlaylistList = JFPlaylistListResponse;
|
||||
|
||||
export type JFPlaylistDetailResponse = JFPlaylist;
|
||||
|
||||
export type JFPlaylistDetail = JFPlaylist & { songs?: JFSong[] };
|
||||
|
||||
export type JFPlaylist = {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
ChildCount?: number;
|
||||
DateCreated: string;
|
||||
GenreItems: GenreItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
MediaType: string;
|
||||
Name: string;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
UserData: UserData;
|
||||
};
|
||||
|
||||
export type JFRequestParams = {
|
||||
albumArtistIds?: string;
|
||||
artistIds?: string;
|
||||
enableImageTypes?: string;
|
||||
enableTotalRecordCount?: boolean;
|
||||
enableUserData?: boolean;
|
||||
excludeItemTypes?: string;
|
||||
fields?: string;
|
||||
imageTypeLimit?: number;
|
||||
includeItemTypes?: string;
|
||||
isFavorite?: boolean;
|
||||
limit?: number;
|
||||
parentId?: string;
|
||||
recursive?: boolean;
|
||||
searchTerm?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'Ascending' | 'Descending';
|
||||
startIndex?: number;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
export type JFMusicFolder = {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
CollectionType: string;
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
UserData: UserData;
|
||||
};
|
||||
|
||||
export type JFGenre = {
|
||||
BackdropImageTags: any[];
|
||||
ChannelId: null;
|
||||
Id: string;
|
||||
ImageBlurHashes: any;
|
||||
ImageTags: ImageTags;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
};
|
||||
|
||||
export type JFAlbumArtist = {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
DateCreated: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: GenreItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: any;
|
||||
ImageTags: ImageTags;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
Overview?: string;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
};
|
||||
|
||||
export type JFArtist = {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
DateCreated: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: GenreItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: any;
|
||||
ImageTags: string[];
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
Overview?: string;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
};
|
||||
|
||||
export type JFAlbum = {
|
||||
AlbumArtist: string;
|
||||
AlbumArtists: JFGenericItem[];
|
||||
AlbumPrimaryImageTag: string;
|
||||
ArtistItems: JFGenericItem[];
|
||||
Artists: string[];
|
||||
ChannelId: null;
|
||||
ChildCount?: number;
|
||||
DateCreated: string;
|
||||
DateLastMediaAdded?: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: JFGenericItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
ParentLogoImageTag: string;
|
||||
ParentLogoItemId: string;
|
||||
PremiereDate?: string;
|
||||
ProductionYear: number;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
UserData?: UserData;
|
||||
} & {
|
||||
songs?: JFSong[];
|
||||
};
|
||||
|
||||
export type JFSong = {
|
||||
Album: string;
|
||||
AlbumArtist: string;
|
||||
AlbumArtists: JFGenericItem[];
|
||||
AlbumId: string;
|
||||
AlbumPrimaryImageTag: string;
|
||||
ArtistItems: JFGenericItem[];
|
||||
Artists: string[];
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
DateCreated: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: JFGenericItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IndexNumber: number;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
MediaSources: MediaSources[];
|
||||
MediaType: string;
|
||||
Name: string;
|
||||
ParentIndexNumber: number;
|
||||
PremiereDate?: string;
|
||||
ProductionYear: number;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
SortName: string;
|
||||
Type: string;
|
||||
UserData?: UserData;
|
||||
};
|
||||
|
||||
type ImageBlurHashes = {
|
||||
Backdrop?: any;
|
||||
Logo?: any;
|
||||
Primary?: any;
|
||||
};
|
||||
|
||||
type ImageTags = {
|
||||
Logo?: string;
|
||||
Primary?: string;
|
||||
};
|
||||
|
||||
type UserData = {
|
||||
IsFavorite: boolean;
|
||||
Key: string;
|
||||
PlayCount: number;
|
||||
PlaybackPositionTicks: number;
|
||||
Played: boolean;
|
||||
};
|
||||
|
||||
type ExternalURL = {
|
||||
Name: string;
|
||||
Url: string;
|
||||
};
|
||||
|
||||
type GenreItem = {
|
||||
Id: string;
|
||||
Name: string;
|
||||
};
|
||||
|
||||
export type JFGenericItem = {
|
||||
Id: string;
|
||||
Name: string;
|
||||
};
|
||||
|
||||
type MediaSources = {
|
||||
Bitrate: number;
|
||||
Container: string;
|
||||
DefaultAudioStreamIndex: number;
|
||||
ETag: string;
|
||||
Formats: any[];
|
||||
GenPtsInput: boolean;
|
||||
Id: string;
|
||||
IgnoreDts: boolean;
|
||||
IgnoreIndex: boolean;
|
||||
IsInfiniteStream: boolean;
|
||||
IsRemote: boolean;
|
||||
MediaAttachments: any[];
|
||||
MediaStreams: MediaStream[];
|
||||
Name: string;
|
||||
Path: string;
|
||||
Protocol: string;
|
||||
ReadAtNativeFramerate: boolean;
|
||||
RequiredHttpHeaders: any;
|
||||
RequiresClosing: boolean;
|
||||
RequiresLooping: boolean;
|
||||
RequiresOpening: boolean;
|
||||
RunTimeTicks: number;
|
||||
Size: number;
|
||||
SupportsDirectPlay: boolean;
|
||||
SupportsDirectStream: boolean;
|
||||
SupportsProbing: boolean;
|
||||
SupportsTranscoding: boolean;
|
||||
Type: string;
|
||||
};
|
||||
|
||||
type MediaStream = {
|
||||
AspectRatio?: string;
|
||||
BitDepth?: number;
|
||||
BitRate?: number;
|
||||
ChannelLayout?: string;
|
||||
Channels?: number;
|
||||
Codec: string;
|
||||
CodecTimeBase: string;
|
||||
ColorSpace?: string;
|
||||
Comment?: string;
|
||||
DisplayTitle?: string;
|
||||
Height?: number;
|
||||
Index: number;
|
||||
IsDefault: boolean;
|
||||
IsExternal: boolean;
|
||||
IsForced: boolean;
|
||||
IsInterlaced: boolean;
|
||||
IsTextSubtitleStream: boolean;
|
||||
Level: number;
|
||||
PixelFormat?: string;
|
||||
Profile?: string;
|
||||
RealFrameRate?: number;
|
||||
RefFrames?: number;
|
||||
SampleRate?: number;
|
||||
SupportsExternalStream: boolean;
|
||||
TimeBase: string;
|
||||
Type: string;
|
||||
Width?: number;
|
||||
};
|
||||
|
||||
export enum JFExternalType {
|
||||
MUSICBRAINZ = 'MusicBrainz',
|
||||
THEAUDIODB = 'TheAudioDb',
|
||||
}
|
||||
|
||||
export enum JFImageType {
|
||||
LOGO = 'Logo',
|
||||
PRIMARY = 'Primary',
|
||||
}
|
||||
|
||||
export enum JFItemType {
|
||||
AUDIO = 'Audio',
|
||||
MUSICALBUM = 'MusicAlbum',
|
||||
}
|
||||
|
||||
export enum JFCollectionType {
|
||||
MUSIC = 'music',
|
||||
PLAYLISTS = 'playlists',
|
||||
}
|
||||
|
||||
export interface JFAuthenticate {
|
||||
AccessToken: string;
|
||||
ServerId: string;
|
||||
SessionInfo: SessionInfo;
|
||||
User: User;
|
||||
}
|
||||
|
||||
type SessionInfo = {
|
||||
AdditionalUsers: any[];
|
||||
ApplicationVersion: string;
|
||||
Capabilities: Capabilities;
|
||||
Client: string;
|
||||
DeviceId: string;
|
||||
DeviceName: string;
|
||||
HasCustomDeviceName: boolean;
|
||||
Id: string;
|
||||
IsActive: boolean;
|
||||
LastActivityDate: string;
|
||||
LastPlaybackCheckIn: string;
|
||||
NowPlayingQueue: any[];
|
||||
NowPlayingQueueFullItems: any[];
|
||||
PlayState: PlayState;
|
||||
PlayableMediaTypes: any[];
|
||||
RemoteEndPoint: string;
|
||||
ServerId: string;
|
||||
SupportedCommands: any[];
|
||||
SupportsMediaControl: boolean;
|
||||
SupportsRemoteControl: boolean;
|
||||
UserId: string;
|
||||
UserName: string;
|
||||
};
|
||||
|
||||
type Capabilities = {
|
||||
PlayableMediaTypes: any[];
|
||||
SupportedCommands: any[];
|
||||
SupportsContentUploading: boolean;
|
||||
SupportsMediaControl: boolean;
|
||||
SupportsPersistentIdentifier: boolean;
|
||||
SupportsSync: boolean;
|
||||
};
|
||||
|
||||
type PlayState = {
|
||||
CanSeek: boolean;
|
||||
IsMuted: boolean;
|
||||
IsPaused: boolean;
|
||||
RepeatMode: string;
|
||||
};
|
||||
|
||||
type User = {
|
||||
Configuration: Configuration;
|
||||
EnableAutoLogin: boolean;
|
||||
HasConfiguredEasyPassword: boolean;
|
||||
HasConfiguredPassword: boolean;
|
||||
HasPassword: boolean;
|
||||
Id: string;
|
||||
LastActivityDate: string;
|
||||
LastLoginDate: string;
|
||||
Name: string;
|
||||
Policy: Policy;
|
||||
ServerId: string;
|
||||
};
|
||||
|
||||
type Configuration = {
|
||||
DisplayCollectionsView: boolean;
|
||||
DisplayMissingEpisodes: boolean;
|
||||
EnableLocalPassword: boolean;
|
||||
EnableNextEpisodeAutoPlay: boolean;
|
||||
GroupedFolders: any[];
|
||||
HidePlayedInLatest: boolean;
|
||||
LatestItemsExcludes: any[];
|
||||
MyMediaExcludes: any[];
|
||||
OrderedViews: any[];
|
||||
PlayDefaultAudioTrack: boolean;
|
||||
RememberAudioSelections: boolean;
|
||||
RememberSubtitleSelections: boolean;
|
||||
SubtitleLanguagePreference: string;
|
||||
SubtitleMode: string;
|
||||
};
|
||||
|
||||
type Policy = {
|
||||
AccessSchedules: any[];
|
||||
AuthenticationProviderId: string;
|
||||
BlockUnratedItems: any[];
|
||||
BlockedChannels: any[];
|
||||
BlockedMediaFolders: any[];
|
||||
BlockedTags: any[];
|
||||
EnableAllChannels: boolean;
|
||||
EnableAllDevices: boolean;
|
||||
EnableAllFolders: boolean;
|
||||
EnableAudioPlaybackTranscoding: boolean;
|
||||
EnableContentDeletion: boolean;
|
||||
EnableContentDeletionFromFolders: any[];
|
||||
EnableContentDownloading: boolean;
|
||||
EnableLiveTvAccess: boolean;
|
||||
EnableLiveTvManagement: boolean;
|
||||
EnableMediaConversion: boolean;
|
||||
EnableMediaPlayback: boolean;
|
||||
EnablePlaybackRemuxing: boolean;
|
||||
EnablePublicSharing: boolean;
|
||||
EnableRemoteAccess: boolean;
|
||||
EnableRemoteControlOfOtherUsers: boolean;
|
||||
EnableSharedDeviceControl: boolean;
|
||||
EnableSyncTranscoding: boolean;
|
||||
EnableUserPreferenceAccess: boolean;
|
||||
EnableVideoPlaybackTranscoding: boolean;
|
||||
EnabledChannels: any[];
|
||||
EnabledDevices: any[];
|
||||
EnabledFolders: any[];
|
||||
ForceRemoteSourceTranscoding: boolean;
|
||||
InvalidLoginAttemptCount: number;
|
||||
IsAdministrator: boolean;
|
||||
IsDisabled: boolean;
|
||||
IsHidden: boolean;
|
||||
LoginAttemptsBeforeLockout: number;
|
||||
MaxActiveSessions: number;
|
||||
PasswordResetProviderId: string;
|
||||
RemoteClientBitrateLimit: number;
|
||||
SyncPlayAccess: string;
|
||||
};
|
||||
|
||||
type JFBaseParams = {
|
||||
enableImageTypes?: JFImageType[];
|
||||
fields?: string;
|
||||
imageTypeLimit?: number;
|
||||
parentId?: string;
|
||||
recursive?: boolean;
|
||||
};
|
||||
|
||||
type JFPaginationParams = {
|
||||
limit?: number;
|
||||
nameStartsWith?: string;
|
||||
sortOrder?: JFSortOrder;
|
||||
startIndex?: number;
|
||||
};
|
||||
|
||||
export enum JFSortOrder {
|
||||
ASC = 'Ascending',
|
||||
DESC = 'Descending',
|
||||
}
|
||||
|
||||
export enum JFAlbumListSort {
|
||||
ALBUM_ARTIST = 'AlbumArtist,SortName',
|
||||
COMMUNITY_RATING = 'CommunityRating,SortName',
|
||||
CRITIC_RATING = 'CriticRating,SortName',
|
||||
NAME = 'SortName',
|
||||
RANDOM = 'Random,SortName',
|
||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||
RELEASE_DATE = 'ProductionYear,PremiereDate,SortName',
|
||||
}
|
||||
|
||||
export type JFAlbumListParams = {
|
||||
filters?: string;
|
||||
genres?: string;
|
||||
includeItemTypes: 'MusicAlbum';
|
||||
sortBy?: JFAlbumListSort;
|
||||
years?: string;
|
||||
} & JFBaseParams &
|
||||
JFPaginationParams;
|
||||
|
||||
export enum JFSongListSort {
|
||||
ALBUM = 'Album,SortName',
|
||||
ALBUM_ARTIST = 'AlbumArtist,Album,SortName',
|
||||
ARTIST = 'Artist,Album,SortName',
|
||||
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
||||
NAME = 'Name,SortName',
|
||||
PLAY_COUNT = 'PlayCount,SortName',
|
||||
RANDOM = 'Random,SortName',
|
||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||
RECENTLY_PLAYED = 'DatePlayed,SortName',
|
||||
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
||||
}
|
||||
|
||||
export type JFSongListParams = {
|
||||
filters?: string;
|
||||
genres?: string;
|
||||
includeItemTypes: 'Audio';
|
||||
sortBy?: JFSongListSort;
|
||||
years?: string;
|
||||
} & JFBaseParams &
|
||||
JFPaginationParams;
|
||||
|
||||
export enum JFAlbumArtistListSort {
|
||||
ALBUM = 'Album,SortName',
|
||||
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
||||
NAME = 'Name,SortName',
|
||||
RANDOM = 'Random,SortName',
|
||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
||||
}
|
||||
|
||||
export type JFAlbumArtistListParams = {
|
||||
filters?: string;
|
||||
genres?: string;
|
||||
sortBy?: JFAlbumArtistListSort;
|
||||
years?: string;
|
||||
} & JFBaseParams &
|
||||
JFPaginationParams;
|
||||
|
||||
export enum JFArtistListSort {
|
||||
ALBUM = 'Album,SortName',
|
||||
DURATION = 'Runtime,AlbumArtist,Album,SortName',
|
||||
NAME = 'Name,SortName',
|
||||
RANDOM = 'Random,SortName',
|
||||
RECENTLY_ADDED = 'DateCreated,SortName',
|
||||
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
|
||||
}
|
||||
|
||||
export type JFArtistListParams = {
|
||||
filters?: string;
|
||||
genres?: string;
|
||||
sortBy?: JFArtistListSort;
|
||||
years?: string;
|
||||
} & JFBaseParams &
|
||||
JFPaginationParams;
|
||||
|
||||
export type JFCreatePlaylistResponse = {
|
||||
Id: string;
|
||||
};
|
||||
|
||||
export type JFCreatePlaylist = JFCreatePlaylistResponse;
|
||||
499
src/renderer/api/navidrome.api.ts
Normal file
499
src/renderer/api/navidrome.api.ts
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
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,
|
||||
} from '/@/renderer/api/navidrome.types';
|
||||
import { NDPlaylistListSort, NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types';
|
||||
import type {
|
||||
Album,
|
||||
Song,
|
||||
AuthenticationResponse,
|
||||
AlbumDetailArgs,
|
||||
GenreListArgs,
|
||||
AlbumListArgs,
|
||||
AlbumArtistListArgs,
|
||||
AlbumArtistDetailArgs,
|
||||
SongListArgs,
|
||||
SongDetailArgs,
|
||||
CreatePlaylistArgs,
|
||||
DeletePlaylistArgs,
|
||||
PlaylistListArgs,
|
||||
PlaylistDetailArgs,
|
||||
CreatePlaylistResponse,
|
||||
PlaylistSongListArgs,
|
||||
} from '/@/renderer/api/types';
|
||||
import {
|
||||
playlistListSortMap,
|
||||
albumArtistListSortMap,
|
||||
songListSortMap,
|
||||
albumListSortMap,
|
||||
sortOrderMap,
|
||||
} from '/@/renderer/api/types';
|
||||
import { toast } from '/@/renderer/components';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||
import { parseSearchParams } from '/@/renderer/utils';
|
||||
|
||||
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;
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const authenticate = async (
|
||||
url: string,
|
||||
body: { password: string; username: string },
|
||||
): Promise<AuthenticationResponse> => {
|
||||
const cleanServerUrl = url.replace(/\/$/, '');
|
||||
|
||||
const data = await ky
|
||||
.post(`${cleanServerUrl}/auth/login`, {
|
||||
json: {
|
||||
password: body.password,
|
||||
username: body.username,
|
||||
},
|
||||
})
|
||||
.json<NDAuthenticationResponse>();
|
||||
|
||||
return {
|
||||
credential: `u=${body.username}&s=${data.subsonicSalt}&t=${data.subsonicToken}`,
|
||||
ndCredential: data.token,
|
||||
userId: data.id,
|
||||
username: data.username,
|
||||
};
|
||||
};
|
||||
|
||||
const getGenreList = async (args: GenreListArgs): Promise<NDGenreList> => {
|
||||
const { server, signal } = args;
|
||||
|
||||
const data = await api
|
||||
.get('api/genre', {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||
prefixUrl: server?.url,
|
||||
signal,
|
||||
})
|
||||
.json<NDGenreListResponse>();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs): Promise<NDAlbumArtistDetail> => {
|
||||
const { query, server, signal } = args;
|
||||
|
||||
const data = await api
|
||||
.get(`api/artist/${query.id}`, {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||
prefixUrl: server?.url,
|
||||
signal,
|
||||
})
|
||||
.json<NDAlbumArtistDetailResponse>();
|
||||
|
||||
return { ...data };
|
||||
};
|
||||
|
||||
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<NDAlbumArtistList> => {
|
||||
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,
|
||||
...query.ndParams,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.get('api/artist', {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||
searchParams,
|
||||
signal,
|
||||
})
|
||||
.json<NDArtistListResponse>();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<NDAlbumDetail> => {
|
||||
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<NDAlbumDetailResponse>();
|
||||
|
||||
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<NDSongListResponse>();
|
||||
|
||||
return { ...data, songs: songsData };
|
||||
};
|
||||
|
||||
const getAlbumList = async (args: AlbumListArgs): Promise<NDAlbumList> => {
|
||||
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,
|
||||
...query.ndParams,
|
||||
};
|
||||
|
||||
const res = await api.get('api/album', {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||
prefixUrl: server?.url,
|
||||
searchParams,
|
||||
signal,
|
||||
});
|
||||
|
||||
const data = await res.json<NDAlbumListResponse>();
|
||||
const itemCount = res.headers.get('x-total-count');
|
||||
|
||||
return {
|
||||
items: data,
|
||||
startIndex: query?.startIndex || 0,
|
||||
totalRecordCount: Number(itemCount),
|
||||
};
|
||||
};
|
||||
|
||||
const getSongList = async (args: SongListArgs): Promise<NDSongList> => {
|
||||
const { query, server, signal } = args;
|
||||
|
||||
const searchParams: NDSongListParams = {
|
||||
_end: query.startIndex + (query.limit || 0),
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: songListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
...query.ndParams,
|
||||
};
|
||||
|
||||
const res = await api.get('api/song', {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||
prefixUrl: server?.url,
|
||||
searchParams,
|
||||
signal,
|
||||
});
|
||||
|
||||
const data = await res.json<NDSongListResponse>();
|
||||
const itemCount = res.headers.get('x-total-count');
|
||||
|
||||
return {
|
||||
items: data,
|
||||
startIndex: query?.startIndex || 0,
|
||||
totalRecordCount: Number(itemCount),
|
||||
};
|
||||
};
|
||||
|
||||
const getSongDetail = async (args: SongDetailArgs): Promise<NDSongDetail> => {
|
||||
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<NDSongDetailResponse>();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
|
||||
const { query, server, signal } = args;
|
||||
|
||||
const json: NDCreatePlaylistParams = {
|
||||
comment: query.comment,
|
||||
name: query.name,
|
||||
public: query.public || false,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.post('api/playlist', {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||
json,
|
||||
prefixUrl: server?.url,
|
||||
signal,
|
||||
})
|
||||
.json<NDCreatePlaylistResponse>();
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
name: query.name,
|
||||
};
|
||||
};
|
||||
|
||||
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<NDDeletePlaylist> => {
|
||||
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<NDDeletePlaylistResponse>();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getPlaylistList = async (args: PlaylistListArgs): Promise<NDPlaylistList> => {
|
||||
const { query, server, signal } = args;
|
||||
|
||||
const searchParams: NDPlaylistListParams = {
|
||||
_end: query.startIndex + (query.limit || 0),
|
||||
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : NDSortOrder.ASC,
|
||||
_sort: query.sortBy ? playlistListSortMap.navidrome[query.sortBy] : NDPlaylistListSort.NAME,
|
||||
_start: query.startIndex,
|
||||
};
|
||||
|
||||
const res = await api.get('api/playlist', {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||
prefixUrl: server?.url,
|
||||
searchParams,
|
||||
signal,
|
||||
});
|
||||
|
||||
const data = await res.json<NDPlaylistListResponse>();
|
||||
const itemCount = res.headers.get('x-total-count');
|
||||
|
||||
return {
|
||||
items: data,
|
||||
startIndex: query?.startIndex || 0,
|
||||
totalRecordCount: Number(itemCount),
|
||||
};
|
||||
};
|
||||
|
||||
const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise<NDPlaylistDetail> => {
|
||||
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<NDPlaylistDetailResponse>();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise<NDSongList> => {
|
||||
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 data = await api
|
||||
.get(`api/playlist/${query.id}/tracks`, {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||
prefixUrl: server?.url,
|
||||
searchParams: parseSearchParams(searchParams),
|
||||
signal,
|
||||
})
|
||||
.json<NDSongListResponse>();
|
||||
|
||||
return {
|
||||
items: data,
|
||||
startIndex: query?.startIndex || 0,
|
||||
totalRecordCount: data.length,
|
||||
};
|
||||
};
|
||||
|
||||
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 normalizeAlbum = (item: NDAlbum, server: ServerListItem, imageSize?: number): Album => {
|
||||
const imageUrl = getCoverArtUrl({
|
||||
baseUrl: server.url,
|
||||
coverArtId: item.coverArtId,
|
||||
credential: server.credential,
|
||||
size: imageSize || 300,
|
||||
});
|
||||
|
||||
const imagePlaceholderUrl = imageUrl?.replace(/size=\d+/, 'size=50') || null;
|
||||
|
||||
return {
|
||||
albumArtists: [{ id: item.albumArtistId, name: item.albumArtist }],
|
||||
artists: [{ id: item.artistId, name: item.artist }],
|
||||
backdropImageUrl: null,
|
||||
createdAt: item.createdAt,
|
||||
duration: null,
|
||||
genres: item.genres,
|
||||
id: item.id,
|
||||
imagePlaceholderUrl,
|
||||
imageUrl,
|
||||
isCompilation: item.compilation,
|
||||
isFavorite: item.starred,
|
||||
name: item.name,
|
||||
playCount: item.playCount,
|
||||
rating: item.rating,
|
||||
releaseDate: new Date(item.minYear, 0, 1).toISOString(),
|
||||
releaseYear: item.minYear,
|
||||
serverType: ServerType.NAVIDROME,
|
||||
size: item.size,
|
||||
songCount: item.songCount,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.updatedAt,
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeSong = (
|
||||
item: NDSong,
|
||||
server: ServerListItem,
|
||||
deviceId: string,
|
||||
imageSize?: number,
|
||||
): Song => {
|
||||
const imageUrl = getCoverArtUrl({
|
||||
baseUrl: server.url,
|
||||
coverArtId: item.albumId,
|
||||
credential: server.credential,
|
||||
size: imageSize || 300,
|
||||
});
|
||||
|
||||
return {
|
||||
album: item.album,
|
||||
albumArtists: [{ id: item.artistId, name: item.artist }],
|
||||
albumId: item.albumId,
|
||||
artistName: item.artist,
|
||||
artists: [{ id: item.artistId, name: item.artist }],
|
||||
bitRate: item.bitRate,
|
||||
compilation: item.compilation,
|
||||
container: item.suffix,
|
||||
createdAt: item.createdAt,
|
||||
discNumber: item.discNumber,
|
||||
duration: item.duration,
|
||||
genres: item.genres,
|
||||
id: item.id,
|
||||
imageUrl,
|
||||
isFavorite: item.starred,
|
||||
name: item.title,
|
||||
path: item.path,
|
||||
playCount: item.playCount,
|
||||
releaseDate: new Date(item.year, 0, 1).toISOString(),
|
||||
releaseYear: String(item.year),
|
||||
serverId: server.id,
|
||||
size: item.size,
|
||||
streamUrl: `${server.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server.credential}`,
|
||||
trackNumber: item.trackNumber,
|
||||
type: ServerType.NAVIDROME,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.updatedAt,
|
||||
};
|
||||
};
|
||||
|
||||
export const navidromeApi = {
|
||||
authenticate,
|
||||
createPlaylist,
|
||||
deletePlaylist,
|
||||
getAlbumArtistDetail,
|
||||
getAlbumArtistList,
|
||||
getAlbumDetail,
|
||||
getAlbumList,
|
||||
getGenreList,
|
||||
getPlaylistDetail,
|
||||
getPlaylistList,
|
||||
getPlaylistSongList,
|
||||
getSongDetail,
|
||||
getSongList,
|
||||
};
|
||||
|
||||
export const ndNormalize = {
|
||||
album: normalizeAlbum,
|
||||
song: normalizeSong,
|
||||
};
|
||||
313
src/renderer/api/navidrome.types.ts
Normal file
313
src/renderer/api/navidrome.types.ts
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
export type NDAuthenticate = {
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
name: string;
|
||||
subsonicSalt: string;
|
||||
subsonicToken: string;
|
||||
token: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type NDGenre = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type NDAlbum = {
|
||||
albumArtist: string;
|
||||
albumArtistId: string;
|
||||
allArtistIds: string;
|
||||
artist: string;
|
||||
artistId: string;
|
||||
compilation: boolean;
|
||||
coverArtId: string;
|
||||
coverArtPath: string;
|
||||
createdAt: string;
|
||||
duration: number;
|
||||
fullText: string;
|
||||
genre: string;
|
||||
genres: NDGenre[];
|
||||
id: string;
|
||||
maxYear: number;
|
||||
mbzAlbumArtistId: string;
|
||||
mbzAlbumId: string;
|
||||
minYear: number;
|
||||
name: string;
|
||||
orderAlbumArtistName: string;
|
||||
orderAlbumName: string;
|
||||
playCount: number;
|
||||
playDate: string;
|
||||
rating: number;
|
||||
size: number;
|
||||
songCount: number;
|
||||
sortAlbumArtistName: string;
|
||||
sortArtistName: string;
|
||||
starred: boolean;
|
||||
starredAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type NDSong = {
|
||||
album: string;
|
||||
albumArtist: string;
|
||||
albumArtistId: string;
|
||||
albumId: string;
|
||||
artist: string;
|
||||
artistId: string;
|
||||
bitRate: number;
|
||||
bookmarkPosition: number;
|
||||
channels: number;
|
||||
compilation: boolean;
|
||||
createdAt: string;
|
||||
discNumber: number;
|
||||
duration: number;
|
||||
fullText: string;
|
||||
genre: string;
|
||||
genres: NDGenre[];
|
||||
hasCoverArt: boolean;
|
||||
id: string;
|
||||
mbzAlbumArtistId: string;
|
||||
mbzAlbumId: string;
|
||||
mbzArtistId: string;
|
||||
mbzTrackId: string;
|
||||
orderAlbumArtistName: string;
|
||||
orderAlbumName: string;
|
||||
orderArtistName: string;
|
||||
orderTitle: string;
|
||||
path: string;
|
||||
playCount: number;
|
||||
playDate: string;
|
||||
rating: number;
|
||||
size: number;
|
||||
sortAlbumArtistName: string;
|
||||
sortArtistName: string;
|
||||
starred: boolean;
|
||||
starredAt: string;
|
||||
suffix: string;
|
||||
title: string;
|
||||
trackNumber: number;
|
||||
updatedAt: string;
|
||||
year: number;
|
||||
};
|
||||
|
||||
export type NDAlbumArtist = {
|
||||
albumCount: number;
|
||||
biography: string;
|
||||
externalInfoUpdatedAt: string;
|
||||
externalUrl: string;
|
||||
fullText: string;
|
||||
genres: NDGenre[];
|
||||
id: string;
|
||||
largeImageUrl: string;
|
||||
mbzArtistId: string;
|
||||
mediumImageUrl: string;
|
||||
name: string;
|
||||
orderArtistName: string;
|
||||
playCount: number;
|
||||
playDate: string;
|
||||
rating: number;
|
||||
size: number;
|
||||
smallImageUrl: string;
|
||||
songCount: number;
|
||||
starred: boolean;
|
||||
starredAt: string;
|
||||
};
|
||||
|
||||
export type NDAuthenticationResponse = NDAuthenticate;
|
||||
|
||||
export type NDAlbumArtistList = NDAlbumArtist[];
|
||||
|
||||
export type NDAlbumArtistDetail = NDAlbumArtist;
|
||||
|
||||
export type NDAlbumArtistDetailResponse = NDAlbumArtist;
|
||||
|
||||
export type NDGenreList = NDGenre[];
|
||||
|
||||
export type NDGenreListResponse = NDGenre[];
|
||||
|
||||
export type NDAlbumDetailResponse = NDAlbum;
|
||||
|
||||
export type NDAlbumDetail = NDAlbum & { songs?: NDSongListResponse };
|
||||
|
||||
export type NDAlbumListResponse = NDAlbum[];
|
||||
|
||||
export type NDAlbumList = {
|
||||
items: NDAlbum[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type NDSongDetail = NDSong;
|
||||
|
||||
export type NDSongDetailResponse = NDSong;
|
||||
|
||||
export type NDSongListResponse = NDSong[];
|
||||
|
||||
export type NDSongList = {
|
||||
items: NDSong[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type NDArtistListResponse = NDAlbumArtist[];
|
||||
|
||||
export type NDPagination = {
|
||||
_end?: number;
|
||||
_start?: number;
|
||||
};
|
||||
|
||||
export enum NDSortOrder {
|
||||
ASC = 'ASC',
|
||||
DESC = 'DESC',
|
||||
}
|
||||
|
||||
export type NDOrder = {
|
||||
_order?: NDSortOrder;
|
||||
};
|
||||
|
||||
export enum NDGenreListSort {
|
||||
NAME = 'name',
|
||||
}
|
||||
|
||||
export type NDGenreListParams = {
|
||||
_sort?: NDGenreListSort;
|
||||
id?: string;
|
||||
} & NDPagination &
|
||||
NDOrder;
|
||||
|
||||
export enum NDAlbumListSort {
|
||||
ALBUM_ARTIST = 'albumArtist',
|
||||
ARTIST = 'artist',
|
||||
DURATION = 'duration',
|
||||
NAME = 'name',
|
||||
PLAY_COUNT = 'playCount',
|
||||
PLAY_DATE = 'play_date',
|
||||
RANDOM = 'random',
|
||||
RATING = 'rating',
|
||||
RECENTLY_ADDED = 'recently_added',
|
||||
SONG_COUNT = 'songCount',
|
||||
STARRED = 'starred',
|
||||
YEAR = 'max_year',
|
||||
}
|
||||
|
||||
export type NDAlbumListParams = {
|
||||
_sort?: NDAlbumListSort;
|
||||
album_id?: string;
|
||||
artist_id?: string;
|
||||
compilation?: boolean;
|
||||
genre_id?: string;
|
||||
has_rating?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
recently_played?: boolean;
|
||||
starred?: boolean;
|
||||
year?: number;
|
||||
} & NDPagination &
|
||||
NDOrder;
|
||||
|
||||
export enum NDSongListSort {
|
||||
ALBUM = 'album',
|
||||
ALBUM_ARTIST = 'albumArtist',
|
||||
ARTIST = 'artist',
|
||||
BPM = 'bpm',
|
||||
CHANNELS = 'channels',
|
||||
COMMENT = 'comment',
|
||||
DURATION = 'duration',
|
||||
FAVORITED = 'starred ASC, starredAt ASC',
|
||||
GENRE = 'genre',
|
||||
ID = 'id',
|
||||
PLAY_COUNT = 'playCount',
|
||||
PLAY_DATE = 'playDate',
|
||||
RATING = 'rating',
|
||||
TITLE = 'title',
|
||||
TRACK = 'track',
|
||||
YEAR = 'year',
|
||||
}
|
||||
|
||||
export type NDSongListParams = {
|
||||
_sort?: NDSongListSort;
|
||||
genre_id?: string;
|
||||
starred?: boolean;
|
||||
} & NDPagination &
|
||||
NDOrder;
|
||||
|
||||
export enum NDAlbumArtistListSort {
|
||||
ALBUM_COUNT = 'albumCount',
|
||||
FAVORITED = 'starred ASC, starredAt ASC',
|
||||
NAME = 'name',
|
||||
PLAY_COUNT = 'playCount',
|
||||
RATING = 'rating',
|
||||
SONG_COUNT = 'songCount',
|
||||
}
|
||||
|
||||
export type NDAlbumArtistListParams = {
|
||||
_sort?: NDAlbumArtistListSort;
|
||||
genre_id?: string;
|
||||
starred?: boolean;
|
||||
} & NDPagination &
|
||||
NDOrder;
|
||||
|
||||
export type NDCreatePlaylistParams = {
|
||||
comment?: string;
|
||||
name: string;
|
||||
public: boolean;
|
||||
};
|
||||
|
||||
export type NDCreatePlaylistResponse = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type NDCreatePlaylist = NDCreatePlaylistResponse;
|
||||
|
||||
export type NDDeletePlaylistParams = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type NDDeletePlaylistResponse = null;
|
||||
|
||||
export type NDDeletePlaylist = NDDeletePlaylistResponse;
|
||||
|
||||
export type NDPlaylist = {
|
||||
comment: string;
|
||||
createdAt: string;
|
||||
duration: number;
|
||||
evaluatedAt: string;
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
ownerName: string;
|
||||
path: string;
|
||||
public: boolean;
|
||||
rules: null;
|
||||
size: number;
|
||||
songCount: number;
|
||||
sync: boolean;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type NDPlaylistDetail = NDPlaylist;
|
||||
|
||||
export type NDPlaylistDetailResponse = NDPlaylist;
|
||||
|
||||
export type NDPlaylistList = {
|
||||
items: NDPlaylist[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type NDPlaylistListResponse = NDPlaylist[];
|
||||
|
||||
export enum NDPlaylistListSort {
|
||||
DURATION = 'duration',
|
||||
NAME = 'name',
|
||||
OWNER = 'owner',
|
||||
PUBLIC = 'public',
|
||||
SONG_COUNT = 'songCount',
|
||||
UPDATED_AT = 'updatedAt',
|
||||
}
|
||||
|
||||
export type NDPlaylistListParams = {
|
||||
_sort?: NDPlaylistListSort;
|
||||
owner_id?: string;
|
||||
} & NDPagination &
|
||||
NDOrder;
|
||||
51
src/renderer/api/normalize.ts
Normal file
51
src/renderer/api/normalize.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { jfNormalize } from '/@/renderer/api/jellyfin.api';
|
||||
import type { JFAlbum, JFSong } from '/@/renderer/api/jellyfin.types';
|
||||
import { ndNormalize } from '/@/renderer/api/navidrome.api';
|
||||
import type { NDAlbum, NDSong } from '/@/renderer/api/navidrome.types';
|
||||
import type { RawAlbumListResponse, RawSongListResponse } 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 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,
|
||||
};
|
||||
};
|
||||
|
||||
export const normalize = {
|
||||
albumList,
|
||||
songList,
|
||||
};
|
||||
24
src/renderer/api/query-keys.ts
Normal file
24
src/renderer/api/query-keys.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type { AlbumListQuery, SongListQuery } from './types';
|
||||
import type { AlbumDetailQuery } from './types';
|
||||
|
||||
export const queryKeys = {
|
||||
albums: {
|
||||
detail: (serverId: string, query: AlbumDetailQuery) =>
|
||||
[serverId, 'albums', 'detail', query] as const,
|
||||
list: (serverId: string, query: AlbumListQuery) => [serverId, 'albums', 'list', query] as const,
|
||||
root: ['albums'],
|
||||
serverRoot: (serverId: string) => [serverId, 'albums'],
|
||||
songs: (serverId: string, query: SongListQuery) =>
|
||||
[serverId, 'albums', 'songs', query] as const,
|
||||
},
|
||||
genres: {
|
||||
list: (serverId: string) => [serverId, 'genres', 'list'] as const,
|
||||
root: (serverId: string) => [serverId, 'genres'] as const,
|
||||
},
|
||||
server: {
|
||||
root: (serverId: string) => [serverId] as const,
|
||||
},
|
||||
songs: {
|
||||
list: (serverId: string, query: SongListQuery) => [serverId, 'songs', 'list', query] as const,
|
||||
},
|
||||
};
|
||||
297
src/renderer/api/subsonic.api.ts
Normal file
297
src/renderer/api/subsonic.api.ts
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
import ky from 'ky';
|
||||
import md5 from 'md5';
|
||||
import { randomString } from '/@/renderer/utils';
|
||||
import type {
|
||||
SSAlbumListResponse,
|
||||
SSAlbumDetailResponse,
|
||||
SSArtistIndex,
|
||||
SSAlbumArtistList,
|
||||
SSAlbumArtistListResponse,
|
||||
SSGenreListResponse,
|
||||
SSMusicFolderList,
|
||||
SSMusicFolderListResponse,
|
||||
SSGenreList,
|
||||
SSAlbumDetail,
|
||||
SSAlbumList,
|
||||
SSAlbumArtistDetail,
|
||||
SSAlbumArtistDetailResponse,
|
||||
SSFavoriteParams,
|
||||
SSFavoriteResponse,
|
||||
SSRatingParams,
|
||||
SSRatingResponse,
|
||||
SSAlbumArtistDetailParams,
|
||||
SSAlbumArtistListParams,
|
||||
} from '/@/renderer/api/subsonic.types';
|
||||
import type {
|
||||
AlbumArtistDetailArgs,
|
||||
AlbumArtistListArgs,
|
||||
AlbumDetailArgs,
|
||||
AlbumListArgs,
|
||||
AuthenticationResponse,
|
||||
FavoriteArgs,
|
||||
FavoriteResponse,
|
||||
GenreListArgs,
|
||||
RatingArgs,
|
||||
} from '/@/renderer/api/types';
|
||||
import { useAuthStore } from '/@/renderer/store';
|
||||
import { toast } from '/@/renderer/components';
|
||||
|
||||
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}/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') {
|
||||
toast.warn({ message: 'Issue from Subsonic API' });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data['subsonic-response']), { status: 200 });
|
||||
},
|
||||
],
|
||||
beforeRequest: [
|
||||
(request) => {
|
||||
const server = useAuthStore.getState().currentServer;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (server) {
|
||||
const authParams = server.credential.split(/&?\w=/gm);
|
||||
|
||||
searchParams.set('u', server.username);
|
||||
searchParams.set('v', '1.13.0');
|
||||
searchParams.set('c', 'Feishin');
|
||||
searchParams.set('f', 'json');
|
||||
|
||||
if (authParams?.length === 4) {
|
||||
searchParams.set('s', authParams[2]);
|
||||
searchParams.set('t', authParams[3]);
|
||||
} else if (authParams?.length === 3) {
|
||||
searchParams.set('p', authParams[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return ky(request, { searchParams });
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const authenticate = async (
|
||||
url: string,
|
||||
body: {
|
||||
legacy?: boolean;
|
||||
password: string;
|
||||
username: string;
|
||||
},
|
||||
): Promise<AuthenticationResponse> => {
|
||||
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 (
|
||||
server: any,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SSMusicFolderList> => {
|
||||
const data = await api
|
||||
.get('rest/getMusicFolders.view', {
|
||||
prefixUrl: server.url,
|
||||
signal,
|
||||
})
|
||||
.json<SSMusicFolderListResponse>();
|
||||
|
||||
return data.musicFolders.musicFolder;
|
||||
};
|
||||
|
||||
export const getAlbumArtistDetail = async (
|
||||
args: AlbumArtistDetailArgs,
|
||||
): Promise<SSAlbumArtistDetail> => {
|
||||
const { signal, query } = args;
|
||||
|
||||
const searchParams: SSAlbumArtistDetailParams = {
|
||||
id: query.id,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.get('/getArtist.view', {
|
||||
searchParams,
|
||||
signal,
|
||||
})
|
||||
.json<SSAlbumArtistDetailResponse>();
|
||||
|
||||
return data.artist;
|
||||
};
|
||||
|
||||
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArtistList> => {
|
||||
const { signal, query } = args;
|
||||
|
||||
const searchParams: SSAlbumArtistListParams = {
|
||||
musicFolderId: query.musicFolderId,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.get('/rest/getArtists.view', {
|
||||
searchParams,
|
||||
signal,
|
||||
})
|
||||
.json<SSAlbumArtistListResponse>();
|
||||
|
||||
const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist);
|
||||
|
||||
return artists;
|
||||
};
|
||||
|
||||
const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
|
||||
const { signal } = args;
|
||||
|
||||
const data = await api
|
||||
.get('/rest/getGenres.view', {
|
||||
signal,
|
||||
})
|
||||
.json<SSGenreListResponse>();
|
||||
|
||||
return data.genres.genre;
|
||||
};
|
||||
|
||||
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> => {
|
||||
const { query, signal } = args;
|
||||
|
||||
const data = await api
|
||||
.get('/rest/getAlbum.view', {
|
||||
searchParams: { id: query.id },
|
||||
signal,
|
||||
})
|
||||
.json<SSAlbumDetailResponse>();
|
||||
|
||||
const { song: songs, ...dataWithoutSong } = data.album;
|
||||
return { ...dataWithoutSong, songs };
|
||||
};
|
||||
|
||||
const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => {
|
||||
const { query, signal } = args;
|
||||
|
||||
const normalizedParams = {};
|
||||
const data = await api
|
||||
.get('/rest/getAlbumList2.view', {
|
||||
searchParams: normalizedParams,
|
||||
signal,
|
||||
})
|
||||
.json<SSAlbumListResponse>();
|
||||
|
||||
return {
|
||||
items: data.albumList2.album,
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: null,
|
||||
};
|
||||
};
|
||||
|
||||
const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
||||
const { query, signal } = args;
|
||||
|
||||
const searchParams: SSFavoriteParams = {
|
||||
albumId: query.type === 'album' ? query.id : undefined,
|
||||
artistId: query.type === 'albumArtist' ? query.id : undefined,
|
||||
id: query.type === 'song' ? query.id : undefined,
|
||||
};
|
||||
|
||||
await api
|
||||
.get('/rest/star.view', {
|
||||
searchParams,
|
||||
signal,
|
||||
})
|
||||
.json<SSFavoriteResponse>();
|
||||
|
||||
return {
|
||||
id: query.id,
|
||||
};
|
||||
};
|
||||
|
||||
const deleteFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
|
||||
const { query, signal } = args;
|
||||
|
||||
const searchParams: SSFavoriteParams = {
|
||||
albumId: query.type === 'album' ? query.id : undefined,
|
||||
artistId: query.type === 'albumArtist' ? query.id : undefined,
|
||||
id: query.type === 'song' ? query.id : undefined,
|
||||
};
|
||||
|
||||
await api
|
||||
.get('/rest/unstar.view', {
|
||||
searchParams,
|
||||
signal,
|
||||
})
|
||||
.json<SSFavoriteResponse>();
|
||||
|
||||
return {
|
||||
id: query.id,
|
||||
};
|
||||
};
|
||||
|
||||
const updateRating = async (args: RatingArgs) => {
|
||||
const { query, signal } = args;
|
||||
|
||||
const searchParams: SSRatingParams = {
|
||||
id: query.id,
|
||||
rating: query.rating,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.get('/rest/setRating.view', {
|
||||
searchParams,
|
||||
signal,
|
||||
})
|
||||
.json<SSRatingResponse>();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const subsonicApi = {
|
||||
authenticate,
|
||||
createFavorite,
|
||||
deleteFavorite,
|
||||
getAlbumArtistDetail,
|
||||
getAlbumArtistList,
|
||||
getAlbumDetail,
|
||||
getAlbumList,
|
||||
getCoverArtUrl,
|
||||
getGenreList,
|
||||
getMusicFolderList,
|
||||
updateRating,
|
||||
};
|
||||
184
src/renderer/api/subsonic.types.ts
Normal file
184
src/renderer/api/subsonic.types.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
export type SSBaseResponse = {
|
||||
serverVersion?: 'string';
|
||||
status: 'string';
|
||||
type?: 'string';
|
||||
version: 'string';
|
||||
};
|
||||
|
||||
export type SSMusicFolderList = SSMusicFolder[];
|
||||
|
||||
export type SSMusicFolderListResponse = {
|
||||
musicFolders: {
|
||||
musicFolder: SSMusicFolder[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SSGenreList = SSGenre[];
|
||||
|
||||
export type SSGenreListResponse = {
|
||||
genres: {
|
||||
genre: SSGenre[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SSAlbumArtistDetailParams = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type SSAlbumArtistDetail = SSAlbumArtistListEntry & { album: SSAlbumListEntry[] };
|
||||
|
||||
export type SSAlbumArtistDetailResponse = {
|
||||
artist: SSAlbumArtistListEntry & {
|
||||
album: SSAlbumListEntry[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SSAlbumArtistList = SSAlbumArtistListEntry[];
|
||||
|
||||
export type SSAlbumArtistListResponse = {
|
||||
artists: {
|
||||
ignoredArticles: string;
|
||||
index: SSArtistIndex[];
|
||||
lastModified: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type SSAlbumList = {
|
||||
items: SSAlbumListEntry[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number | null;
|
||||
};
|
||||
|
||||
export type SSAlbumListResponse = {
|
||||
albumList2: {
|
||||
album: SSAlbumListEntry[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SSAlbumDetail = Omit<SSAlbum, 'song'> & { songs: SSSong[] };
|
||||
|
||||
export type SSAlbumDetailResponse = {
|
||||
album: SSAlbum;
|
||||
};
|
||||
|
||||
export type SSArtistInfoResponse = {
|
||||
artistInfo2: SSArtistInfo;
|
||||
};
|
||||
|
||||
export type SSArtistInfo = {
|
||||
biography: string;
|
||||
largeImageUrl?: string;
|
||||
lastFmUrl?: string;
|
||||
mediumImageUrl?: string;
|
||||
musicBrainzId?: string;
|
||||
smallImageUrl?: string;
|
||||
};
|
||||
|
||||
export type SSMusicFolder = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type SSGenre = {
|
||||
albumCount?: number;
|
||||
songCount?: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type SSArtistIndex = {
|
||||
artist: SSAlbumArtistListEntry[];
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type SSAlbumArtistListEntry = {
|
||||
albumCount: string;
|
||||
artistImageUrl?: string;
|
||||
coverArt?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type SSAlbumListEntry = {
|
||||
album: string;
|
||||
artist: string;
|
||||
artistId: string;
|
||||
coverArt: string;
|
||||
created: string;
|
||||
duration: number;
|
||||
genre?: string;
|
||||
id: string;
|
||||
isDir: boolean;
|
||||
isVideo: boolean;
|
||||
name: string;
|
||||
parent: string;
|
||||
songCount: number;
|
||||
starred?: boolean;
|
||||
title: string;
|
||||
userRating?: number;
|
||||
year: number;
|
||||
};
|
||||
|
||||
export type SSAlbum = {
|
||||
song: SSSong[];
|
||||
} & SSAlbumListEntry;
|
||||
|
||||
export type SSSong = {
|
||||
album: string;
|
||||
albumId: string;
|
||||
artist: string;
|
||||
artistId?: string;
|
||||
bitRate: number;
|
||||
contentType: string;
|
||||
coverArt: string;
|
||||
created: string;
|
||||
discNumber?: number;
|
||||
duration: number;
|
||||
genre: string;
|
||||
id: string;
|
||||
isDir: boolean;
|
||||
isVideo: boolean;
|
||||
parent: string;
|
||||
path: string;
|
||||
playCount: number;
|
||||
size: number;
|
||||
starred?: boolean;
|
||||
suffix: string;
|
||||
title: string;
|
||||
track: number;
|
||||
type: string;
|
||||
userRating?: number;
|
||||
year: number;
|
||||
};
|
||||
|
||||
export type SSAlbumListParams = {
|
||||
fromYear?: number;
|
||||
genre?: string;
|
||||
musicFolderId?: string;
|
||||
offset?: number;
|
||||
size?: number;
|
||||
toYear?: number;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type SSAlbumArtistListParams = {
|
||||
musicFolderId?: string;
|
||||
};
|
||||
|
||||
export type SSFavoriteParams = {
|
||||
albumId?: string;
|
||||
artistId?: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type SSFavorite = null;
|
||||
|
||||
export type SSFavoriteResponse = null;
|
||||
|
||||
export type SSRatingParams = {
|
||||
id: string;
|
||||
rating: number;
|
||||
};
|
||||
|
||||
export type SSRating = null;
|
||||
|
||||
export type SSRatingResponse = null;
|
||||
795
src/renderer/api/types.ts
Normal file
795
src/renderer/api/types.ts
Normal file
|
|
@ -0,0 +1,795 @@
|
|||
import {
|
||||
JFSortOrder,
|
||||
JFGenreList,
|
||||
JFAlbumList,
|
||||
JFAlbumListSort,
|
||||
JFAlbumDetail,
|
||||
JFSongList,
|
||||
JFSongListSort,
|
||||
JFAlbumArtistList,
|
||||
JFAlbumArtistListSort,
|
||||
JFAlbumArtistDetail,
|
||||
JFArtistList,
|
||||
JFArtistListSort,
|
||||
JFPlaylistList,
|
||||
JFPlaylistDetail,
|
||||
JFMusicFolderList,
|
||||
} from '/@/renderer/api/jellyfin.types';
|
||||
import {
|
||||
NDSortOrder,
|
||||
NDOrder,
|
||||
NDGenreList,
|
||||
NDAlbumList,
|
||||
NDAlbumListSort,
|
||||
NDAlbumDetail,
|
||||
NDSongList,
|
||||
NDSongListSort,
|
||||
NDSongDetail,
|
||||
NDAlbumArtistList,
|
||||
NDAlbumArtistListSort,
|
||||
NDAlbumArtistDetail,
|
||||
NDDeletePlaylist,
|
||||
NDPlaylistList,
|
||||
NDPlaylistListSort,
|
||||
NDPlaylistDetail,
|
||||
} from '/@/renderer/api/navidrome.types';
|
||||
import {
|
||||
SSAlbumList,
|
||||
SSAlbumDetail,
|
||||
SSAlbumArtistList,
|
||||
SSAlbumArtistDetail,
|
||||
SSMusicFolderList,
|
||||
} from '/@/renderer/api/subsonic.types';
|
||||
import { ServerListItem, ServerType } from '/@/renderer/types';
|
||||
|
||||
export enum SortOrder {
|
||||
ASC = 'ASC',
|
||||
DESC = 'DESC',
|
||||
}
|
||||
|
||||
type SortOrderMap = {
|
||||
jellyfin: Record<SortOrder, JFSortOrder>;
|
||||
navidrome: Record<SortOrder, NDSortOrder>;
|
||||
subsonic: Record<SortOrder, undefined>;
|
||||
};
|
||||
|
||||
export const sortOrderMap: SortOrderMap = {
|
||||
jellyfin: {
|
||||
ASC: JFSortOrder.ASC,
|
||||
DESC: JFSortOrder.DESC,
|
||||
},
|
||||
navidrome: {
|
||||
ASC: NDSortOrder.ASC,
|
||||
DESC: NDSortOrder.DESC,
|
||||
},
|
||||
subsonic: {
|
||||
ASC: undefined,
|
||||
DESC: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export enum ExternalSource {
|
||||
LASTFM = 'LASTFM',
|
||||
MUSICBRAINZ = 'MUSICBRAINZ',
|
||||
SPOTIFY = 'SPOTIFY',
|
||||
THEAUDIODB = 'THEAUDIODB',
|
||||
}
|
||||
|
||||
export enum ExternalType {
|
||||
ID = 'ID',
|
||||
LINK = 'LINK',
|
||||
}
|
||||
|
||||
export enum ImageType {
|
||||
BACKDROP = 'BACKDROP',
|
||||
LOGO = 'LOGO',
|
||||
PRIMARY = 'PRIMARY',
|
||||
SCREENSHOT = 'SCREENSHOT',
|
||||
}
|
||||
|
||||
export type EndpointDetails = {
|
||||
server: ServerListItem;
|
||||
};
|
||||
|
||||
export interface BasePaginatedResponse<T> {
|
||||
error?: string | any;
|
||||
items: T;
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
}
|
||||
|
||||
export type ApiError = {
|
||||
error: {
|
||||
message: string;
|
||||
path: string;
|
||||
trace: string[];
|
||||
};
|
||||
response: string;
|
||||
statusCode: number;
|
||||
};
|
||||
|
||||
export type AuthenticationResponse = {
|
||||
credential: string;
|
||||
ndCredential?: string;
|
||||
userId: string | null;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type Genre = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Album = {
|
||||
albumArtists: RelatedArtist[];
|
||||
artists: RelatedArtist[];
|
||||
backdropImageUrl: string | null;
|
||||
createdAt: string;
|
||||
duration: number | null;
|
||||
genres: Genre[];
|
||||
id: string;
|
||||
imagePlaceholderUrl: string | null;
|
||||
imageUrl: string | null;
|
||||
isCompilation: boolean | null;
|
||||
isFavorite: boolean;
|
||||
name: string;
|
||||
playCount: number | null;
|
||||
rating: number | null;
|
||||
releaseDate: string | null;
|
||||
releaseYear: number | null;
|
||||
serverType: ServerType;
|
||||
size: number | null;
|
||||
songCount: number | null;
|
||||
songs?: Song[];
|
||||
uniqueId: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type Song = {
|
||||
album: string;
|
||||
albumArtists: RelatedArtist[];
|
||||
albumId: string;
|
||||
artistName: string;
|
||||
artists: RelatedArtist[];
|
||||
bitRate: number;
|
||||
compilation: boolean | null;
|
||||
container: string | null;
|
||||
createdAt: string;
|
||||
discNumber: number;
|
||||
duration: number;
|
||||
genres: Genre[];
|
||||
id: string;
|
||||
imageUrl: string | null;
|
||||
isFavorite: boolean;
|
||||
name: string;
|
||||
path: string | null;
|
||||
playCount: number;
|
||||
releaseDate: string | null;
|
||||
releaseYear: string | null;
|
||||
serverId: string;
|
||||
size: number;
|
||||
streamUrl: string;
|
||||
trackNumber: number;
|
||||
type: ServerType;
|
||||
uniqueId: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type AlbumArtist = {
|
||||
biography: string | null;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
name: string;
|
||||
remoteCreatedAt: string | null;
|
||||
serverFolderId: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type RelatedAlbumArtist = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Artist = {
|
||||
biography: string | null;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
name: string;
|
||||
remoteCreatedAt: string | null;
|
||||
serverFolderId: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type RelatedArtist = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type MusicFolder = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type Playlist = {
|
||||
duration?: number;
|
||||
id: string;
|
||||
name: string;
|
||||
public?: boolean;
|
||||
size?: number;
|
||||
songCount?: number;
|
||||
userId: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type GenresResponse = Genre[];
|
||||
|
||||
export type MusicFoldersResponse = MusicFolder[];
|
||||
|
||||
export type ListSortOrder = NDOrder | JFSortOrder;
|
||||
|
||||
type BaseEndpointArgs = {
|
||||
server: ServerListItem | null;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
// Genre List
|
||||
export type RawGenreListResponse = NDGenreList | JFGenreList | undefined;
|
||||
|
||||
export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined;
|
||||
|
||||
export type GenreListArgs = { query: GenreListQuery } & BaseEndpointArgs;
|
||||
|
||||
export type GenreListQuery = null;
|
||||
|
||||
// Album List
|
||||
export type RawAlbumListResponse = NDAlbumList | SSAlbumList | JFAlbumList | undefined;
|
||||
|
||||
export type AlbumListResponse = BasePaginatedResponse<Album[]> | null | undefined;
|
||||
|
||||
export enum AlbumListSort {
|
||||
ALBUM_ARTIST = 'albumArtist',
|
||||
ARTIST = 'artist',
|
||||
COMMUNITY_RATING = 'communityRating',
|
||||
CRITIC_RATING = 'criticRating',
|
||||
DURATION = 'duration',
|
||||
FAVORITED = 'favorited',
|
||||
NAME = 'name',
|
||||
PLAY_COUNT = 'playCount',
|
||||
RANDOM = 'random',
|
||||
RATING = 'rating',
|
||||
RECENTLY_ADDED = 'recentlyAdded',
|
||||
RECENTLY_PLAYED = 'recentlyPlayed',
|
||||
RELEASE_DATE = 'releaseDate',
|
||||
SONG_COUNT = 'songCount',
|
||||
YEAR = 'year',
|
||||
}
|
||||
|
||||
export type AlbumListQuery = {
|
||||
jfParams?: {
|
||||
filters?: string;
|
||||
genres?: string;
|
||||
years?: string;
|
||||
};
|
||||
limit?: number;
|
||||
musicFolderId?: string;
|
||||
ndParams?: {
|
||||
artist_id?: string;
|
||||
compilation?: boolean;
|
||||
genre_id?: string;
|
||||
has_rating?: boolean;
|
||||
name?: string;
|
||||
starred?: boolean;
|
||||
year?: number;
|
||||
};
|
||||
sortBy: AlbumListSort;
|
||||
sortOrder: SortOrder;
|
||||
startIndex: number;
|
||||
};
|
||||
|
||||
export type AlbumListArgs = { query: AlbumListQuery } & BaseEndpointArgs;
|
||||
|
||||
type AlbumListSortMap = {
|
||||
jellyfin: Record<AlbumListSort, JFAlbumListSort | undefined>;
|
||||
navidrome: Record<AlbumListSort, NDAlbumListSort | undefined>;
|
||||
subsonic: Record<AlbumListSort, undefined>;
|
||||
};
|
||||
|
||||
export const albumListSortMap: AlbumListSortMap = {
|
||||
jellyfin: {
|
||||
albumArtist: JFAlbumListSort.ALBUM_ARTIST,
|
||||
artist: undefined,
|
||||
communityRating: JFAlbumListSort.COMMUNITY_RATING,
|
||||
criticRating: JFAlbumListSort.CRITIC_RATING,
|
||||
duration: undefined,
|
||||
favorited: undefined,
|
||||
name: JFAlbumListSort.NAME,
|
||||
playCount: undefined,
|
||||
random: JFAlbumListSort.RANDOM,
|
||||
rating: undefined,
|
||||
recentlyAdded: JFAlbumListSort.RECENTLY_ADDED,
|
||||
recentlyPlayed: undefined,
|
||||
releaseDate: JFAlbumListSort.RELEASE_DATE,
|
||||
songCount: undefined,
|
||||
year: undefined,
|
||||
},
|
||||
navidrome: {
|
||||
albumArtist: NDAlbumListSort.ALBUM_ARTIST,
|
||||
artist: NDAlbumListSort.ARTIST,
|
||||
communityRating: undefined,
|
||||
criticRating: undefined,
|
||||
duration: NDAlbumListSort.DURATION,
|
||||
favorited: NDAlbumListSort.STARRED,
|
||||
name: NDAlbumListSort.NAME,
|
||||
playCount: NDAlbumListSort.PLAY_COUNT,
|
||||
random: NDAlbumListSort.RANDOM,
|
||||
rating: NDAlbumListSort.RATING,
|
||||
recentlyAdded: NDAlbumListSort.RECENTLY_ADDED,
|
||||
recentlyPlayed: NDAlbumListSort.PLAY_DATE,
|
||||
releaseDate: undefined,
|
||||
songCount: NDAlbumListSort.SONG_COUNT,
|
||||
year: NDAlbumListSort.YEAR,
|
||||
},
|
||||
subsonic: {
|
||||
albumArtist: undefined,
|
||||
artist: undefined,
|
||||
communityRating: undefined,
|
||||
criticRating: undefined,
|
||||
duration: undefined,
|
||||
favorited: undefined,
|
||||
name: undefined,
|
||||
playCount: undefined,
|
||||
random: undefined,
|
||||
rating: undefined,
|
||||
recentlyAdded: undefined,
|
||||
recentlyPlayed: undefined,
|
||||
releaseDate: undefined,
|
||||
songCount: undefined,
|
||||
year: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
// Album Detail
|
||||
export type RawAlbumDetailResponse = NDAlbumDetail | SSAlbumDetail | JFAlbumDetail | undefined;
|
||||
|
||||
export type AlbumDetailResponse = Album | null | undefined;
|
||||
|
||||
export type AlbumDetailQuery = { id: string };
|
||||
|
||||
export type AlbumDetailArgs = { query: AlbumDetailQuery } & BaseEndpointArgs;
|
||||
|
||||
// Song List
|
||||
export type RawSongListResponse = NDSongList | JFSongList | undefined;
|
||||
|
||||
export type SongListResponse = BasePaginatedResponse<Song[]>;
|
||||
|
||||
export enum SongListSort {
|
||||
ALBUM_ARTIST = 'albumArtist',
|
||||
ARTIST = 'artist',
|
||||
BPM = 'bpm',
|
||||
CHANNELS = 'channels',
|
||||
COMMENT = 'comment',
|
||||
DURATION = 'duration',
|
||||
FAVORITED = 'favorited',
|
||||
GENRE = 'genre',
|
||||
NAME = 'name',
|
||||
PLAY_COUNT = 'playCount',
|
||||
RANDOM = 'random',
|
||||
RATING = 'rating',
|
||||
RECENTLY_ADDED = 'recentlyAdded',
|
||||
RECENTLY_PLAYED = 'recentlyPlayed',
|
||||
RELEASE_DATE = 'releaseDate',
|
||||
YEAR = 'year',
|
||||
}
|
||||
|
||||
export type SongListQuery = {
|
||||
jfParams?: {
|
||||
filters?: string;
|
||||
genres?: string;
|
||||
includeItemTypes: 'Audio';
|
||||
sortBy?: JFSongListSort;
|
||||
years?: string;
|
||||
};
|
||||
limit?: number;
|
||||
musicFolderId?: string;
|
||||
ndParams?: {
|
||||
artist_id?: string;
|
||||
compilation?: boolean;
|
||||
genre_id?: string;
|
||||
has_rating?: boolean;
|
||||
starred?: boolean;
|
||||
title?: string;
|
||||
year?: number;
|
||||
};
|
||||
sortBy: SongListSort;
|
||||
sortOrder: SortOrder;
|
||||
startIndex: number;
|
||||
};
|
||||
|
||||
export type SongListArgs = { query: SongListQuery } & BaseEndpointArgs;
|
||||
|
||||
type SongListSortMap = {
|
||||
jellyfin: Record<SongListSort, JFSongListSort | undefined>;
|
||||
navidrome: Record<SongListSort, NDSongListSort | undefined>;
|
||||
subsonic: Record<SongListSort, undefined>;
|
||||
};
|
||||
|
||||
export const songListSortMap: SongListSortMap = {
|
||||
jellyfin: {
|
||||
albumArtist: JFSongListSort.ALBUM_ARTIST,
|
||||
artist: JFSongListSort.ARTIST,
|
||||
bpm: undefined,
|
||||
channels: undefined,
|
||||
comment: undefined,
|
||||
duration: JFSongListSort.DURATION,
|
||||
favorited: undefined,
|
||||
genre: undefined,
|
||||
name: JFSongListSort.NAME,
|
||||
playCount: JFSongListSort.PLAY_COUNT,
|
||||
random: JFSongListSort.RANDOM,
|
||||
rating: undefined,
|
||||
recentlyAdded: JFSongListSort.RECENTLY_ADDED,
|
||||
recentlyPlayed: JFSongListSort.RECENTLY_PLAYED,
|
||||
releaseDate: JFSongListSort.RELEASE_DATE,
|
||||
year: undefined,
|
||||
},
|
||||
navidrome: {
|
||||
albumArtist: NDSongListSort.ALBUM_ARTIST,
|
||||
artist: NDSongListSort.ARTIST,
|
||||
bpm: NDSongListSort.BPM,
|
||||
channels: NDSongListSort.CHANNELS,
|
||||
comment: NDSongListSort.COMMENT,
|
||||
duration: NDSongListSort.DURATION,
|
||||
favorited: NDSongListSort.FAVORITED,
|
||||
genre: NDSongListSort.GENRE,
|
||||
name: NDSongListSort.TITLE,
|
||||
playCount: NDSongListSort.PLAY_COUNT,
|
||||
random: undefined,
|
||||
rating: NDSongListSort.RATING,
|
||||
recentlyAdded: NDSongListSort.PLAY_DATE,
|
||||
recentlyPlayed: NDSongListSort.PLAY_DATE,
|
||||
releaseDate: undefined,
|
||||
year: NDSongListSort.YEAR,
|
||||
},
|
||||
subsonic: {
|
||||
albumArtist: undefined,
|
||||
artist: undefined,
|
||||
bpm: undefined,
|
||||
channels: undefined,
|
||||
comment: undefined,
|
||||
duration: undefined,
|
||||
favorited: undefined,
|
||||
genre: undefined,
|
||||
name: undefined,
|
||||
playCount: undefined,
|
||||
random: undefined,
|
||||
rating: undefined,
|
||||
recentlyAdded: undefined,
|
||||
recentlyPlayed: undefined,
|
||||
releaseDate: undefined,
|
||||
year: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
// Song Detail
|
||||
export type RawSongDetailResponse = NDSongDetail | undefined;
|
||||
|
||||
export type SongDetailResponse = Song | null | undefined;
|
||||
|
||||
export type SongDetailQuery = { id: string };
|
||||
|
||||
export type SongDetailArgs = { query: SongDetailQuery } & BaseEndpointArgs;
|
||||
|
||||
// Album Artist List
|
||||
export type RawAlbumArtistListResponse =
|
||||
| NDAlbumArtistList
|
||||
| SSAlbumArtistList
|
||||
| JFAlbumArtistList
|
||||
| undefined;
|
||||
|
||||
export type AlbumArtistListResponse = BasePaginatedResponse<AlbumArtist[]>;
|
||||
|
||||
export enum AlbumArtistListSort {
|
||||
ALBUM = 'album',
|
||||
ALBUM_COUNT = 'albumCount',
|
||||
DURATION = 'duration',
|
||||
FAVORITED = 'favorited',
|
||||
NAME = 'name',
|
||||
PLAY_COUNT = 'playCount',
|
||||
RANDOM = 'random',
|
||||
RATING = 'rating',
|
||||
RECENTLY_ADDED = 'recentlyAdded',
|
||||
RELEASE_DATE = 'releaseDate',
|
||||
SONG_COUNT = 'songCount',
|
||||
}
|
||||
|
||||
export type AlbumArtistListQuery = {
|
||||
limit?: number;
|
||||
musicFolderId?: string;
|
||||
ndParams?: {
|
||||
genre_id?: string;
|
||||
name?: string;
|
||||
starred?: boolean;
|
||||
};
|
||||
sortBy: AlbumArtistListSort;
|
||||
sortOrder: SortOrder;
|
||||
startIndex: number;
|
||||
};
|
||||
|
||||
export type AlbumArtistListArgs = { query: AlbumArtistListQuery } & BaseEndpointArgs;
|
||||
|
||||
type AlbumArtistListSortMap = {
|
||||
jellyfin: Record<AlbumArtistListSort, JFAlbumArtistListSort | undefined>;
|
||||
navidrome: Record<AlbumArtistListSort, NDAlbumArtistListSort | undefined>;
|
||||
subsonic: Record<AlbumArtistListSort, undefined>;
|
||||
};
|
||||
|
||||
export const albumArtistListSortMap: AlbumArtistListSortMap = {
|
||||
jellyfin: {
|
||||
album: JFAlbumArtistListSort.ALBUM,
|
||||
albumCount: undefined,
|
||||
duration: JFAlbumArtistListSort.DURATION,
|
||||
favorited: undefined,
|
||||
name: JFAlbumArtistListSort.NAME,
|
||||
playCount: undefined,
|
||||
random: JFAlbumArtistListSort.RANDOM,
|
||||
rating: undefined,
|
||||
recentlyAdded: JFAlbumArtistListSort.RECENTLY_ADDED,
|
||||
releaseDate: undefined,
|
||||
songCount: undefined,
|
||||
},
|
||||
navidrome: {
|
||||
album: undefined,
|
||||
albumCount: NDAlbumArtistListSort.ALBUM_COUNT,
|
||||
duration: undefined,
|
||||
favorited: NDAlbumArtistListSort.FAVORITED,
|
||||
name: NDAlbumArtistListSort.NAME,
|
||||
playCount: NDAlbumArtistListSort.PLAY_COUNT,
|
||||
random: undefined,
|
||||
rating: NDAlbumArtistListSort.RATING,
|
||||
recentlyAdded: undefined,
|
||||
releaseDate: undefined,
|
||||
songCount: NDAlbumArtistListSort.SONG_COUNT,
|
||||
},
|
||||
subsonic: {
|
||||
album: undefined,
|
||||
albumCount: undefined,
|
||||
duration: undefined,
|
||||
favorited: undefined,
|
||||
name: undefined,
|
||||
playCount: undefined,
|
||||
random: undefined,
|
||||
rating: undefined,
|
||||
recentlyAdded: undefined,
|
||||
releaseDate: undefined,
|
||||
songCount: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
// Album Artist Detail
|
||||
export type RawAlbumArtistDetailResponse =
|
||||
| NDAlbumArtistDetail
|
||||
| SSAlbumArtistDetail
|
||||
| JFAlbumArtistDetail
|
||||
| undefined;
|
||||
|
||||
export type AlbumArtistDetailResponse = BasePaginatedResponse<AlbumArtist[]>;
|
||||
|
||||
export type AlbumArtistDetailQuery = { id: string };
|
||||
|
||||
export type AlbumArtistDetailArgs = { query: AlbumArtistDetailQuery } & BaseEndpointArgs;
|
||||
|
||||
// Artist List
|
||||
export type RawArtistListResponse = JFArtistList | undefined;
|
||||
|
||||
export type ArtistListResponse = BasePaginatedResponse<Artist[]>;
|
||||
|
||||
export enum ArtistListSort {
|
||||
ALBUM = 'album',
|
||||
ALBUM_COUNT = 'albumCount',
|
||||
DURATION = 'duration',
|
||||
FAVORITED = 'favorited',
|
||||
NAME = 'name',
|
||||
PLAY_COUNT = 'playCount',
|
||||
RANDOM = 'random',
|
||||
RATING = 'rating',
|
||||
RECENTLY_ADDED = 'recentlyAdded',
|
||||
RELEASE_DATE = 'releaseDate',
|
||||
SONG_COUNT = 'songCount',
|
||||
}
|
||||
|
||||
export type ArtistListQuery = {
|
||||
limit?: number;
|
||||
musicFolderId?: string;
|
||||
ndParams?: {
|
||||
genre_id?: string;
|
||||
name?: string;
|
||||
starred?: boolean;
|
||||
};
|
||||
sortBy: ArtistListSort;
|
||||
sortOrder: SortOrder;
|
||||
startIndex: number;
|
||||
};
|
||||
|
||||
export type ArtistListArgs = { query: ArtistListQuery } & BaseEndpointArgs;
|
||||
|
||||
type ArtistListSortMap = {
|
||||
jellyfin: Record<ArtistListSort, JFArtistListSort | undefined>;
|
||||
navidrome: Record<ArtistListSort, undefined>;
|
||||
subsonic: Record<ArtistListSort, undefined>;
|
||||
};
|
||||
|
||||
export const artistListSortMap: ArtistListSortMap = {
|
||||
jellyfin: {
|
||||
album: JFArtistListSort.ALBUM,
|
||||
albumCount: undefined,
|
||||
duration: JFArtistListSort.DURATION,
|
||||
favorited: undefined,
|
||||
name: JFArtistListSort.NAME,
|
||||
playCount: undefined,
|
||||
random: JFArtistListSort.RANDOM,
|
||||
rating: undefined,
|
||||
recentlyAdded: JFArtistListSort.RECENTLY_ADDED,
|
||||
releaseDate: undefined,
|
||||
songCount: undefined,
|
||||
},
|
||||
navidrome: {
|
||||
album: undefined,
|
||||
albumCount: undefined,
|
||||
duration: undefined,
|
||||
favorited: undefined,
|
||||
name: undefined,
|
||||
playCount: undefined,
|
||||
random: undefined,
|
||||
rating: undefined,
|
||||
recentlyAdded: undefined,
|
||||
releaseDate: undefined,
|
||||
songCount: undefined,
|
||||
},
|
||||
subsonic: {
|
||||
album: undefined,
|
||||
albumCount: undefined,
|
||||
duration: undefined,
|
||||
favorited: undefined,
|
||||
name: undefined,
|
||||
playCount: undefined,
|
||||
random: undefined,
|
||||
rating: undefined,
|
||||
recentlyAdded: undefined,
|
||||
releaseDate: undefined,
|
||||
songCount: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
// Artist Detail
|
||||
|
||||
// Favorite
|
||||
export type RawFavoriteResponse = FavoriteResponse | undefined;
|
||||
|
||||
export type FavoriteResponse = { id: string };
|
||||
|
||||
export type FavoriteQuery = { id: string; type?: 'song' | 'album' | 'albumArtist' };
|
||||
|
||||
export type FavoriteArgs = { query: FavoriteQuery } & BaseEndpointArgs;
|
||||
|
||||
// Rating
|
||||
export type RawRatingResponse = null | undefined;
|
||||
|
||||
export type RatingResponse = null;
|
||||
|
||||
export type RatingQuery = { id: string; rating: number };
|
||||
|
||||
export type RatingArgs = { query: RatingQuery } & BaseEndpointArgs;
|
||||
|
||||
// Create Playlist
|
||||
export type RawCreatePlaylistResponse = CreatePlaylistResponse | undefined;
|
||||
|
||||
export type CreatePlaylistResponse = { id: string; name: string };
|
||||
|
||||
export type CreatePlaylistQuery = { comment?: string; name: string; public?: boolean };
|
||||
|
||||
export type CreatePlaylistArgs = { query: CreatePlaylistQuery } & BaseEndpointArgs;
|
||||
|
||||
// Delete Playlist
|
||||
export type RawDeletePlaylistResponse = NDDeletePlaylist | undefined;
|
||||
|
||||
export type DeletePlaylistResponse = null;
|
||||
|
||||
export type DeletePlaylistQuery = { id: string };
|
||||
|
||||
export type DeletePlaylistArgs = { query: DeletePlaylistQuery } & BaseEndpointArgs;
|
||||
|
||||
// Playlist List
|
||||
export type RawPlaylistListResponse = NDPlaylistList | JFPlaylistList | undefined;
|
||||
|
||||
export type PlaylistListResponse = BasePaginatedResponse<Playlist[]>;
|
||||
|
||||
export type PlaylistListSort = NDPlaylistListSort;
|
||||
|
||||
export type PlaylistListQuery = {
|
||||
limit?: number;
|
||||
musicFolderId?: string;
|
||||
sortBy: PlaylistListSort;
|
||||
sortOrder: SortOrder;
|
||||
startIndex: number;
|
||||
};
|
||||
|
||||
export type PlaylistListArgs = { query: PlaylistListQuery } & BaseEndpointArgs;
|
||||
|
||||
type PlaylistListSortMap = {
|
||||
jellyfin: Record<PlaylistListSort, undefined>;
|
||||
navidrome: Record<PlaylistListSort, NDPlaylistListSort | undefined>;
|
||||
subsonic: Record<PlaylistListSort, undefined>;
|
||||
};
|
||||
|
||||
export const playlistListSortMap: PlaylistListSortMap = {
|
||||
jellyfin: {
|
||||
duration: undefined,
|
||||
name: undefined,
|
||||
owner: undefined,
|
||||
public: undefined,
|
||||
songCount: undefined,
|
||||
updatedAt: undefined,
|
||||
},
|
||||
navidrome: {
|
||||
duration: NDPlaylistListSort.DURATION,
|
||||
name: NDPlaylistListSort.NAME,
|
||||
owner: NDPlaylistListSort.OWNER,
|
||||
public: NDPlaylistListSort.PUBLIC,
|
||||
songCount: NDPlaylistListSort.SONG_COUNT,
|
||||
updatedAt: NDPlaylistListSort.UPDATED_AT,
|
||||
},
|
||||
subsonic: {
|
||||
duration: undefined,
|
||||
name: undefined,
|
||||
owner: undefined,
|
||||
public: undefined,
|
||||
songCount: undefined,
|
||||
updatedAt: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
// Playlist Detail
|
||||
export type RawPlaylistDetailResponse = NDPlaylistDetail | JFPlaylistDetail | undefined;
|
||||
|
||||
export type PlaylistDetailResponse = BasePaginatedResponse<Playlist[]>;
|
||||
|
||||
export type PlaylistDetailQuery = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type PlaylistDetailArgs = { query: PlaylistDetailQuery } & BaseEndpointArgs;
|
||||
|
||||
// Playlist Songs
|
||||
export type RawPlaylistSongListResponse = JFSongList | undefined;
|
||||
|
||||
export type PlaylistSongListResponse = BasePaginatedResponse<Song[]>;
|
||||
|
||||
export type PlaylistSongListQuery = {
|
||||
id: string;
|
||||
limit?: number;
|
||||
sortBy?: SongListSort;
|
||||
sortOrder?: SortOrder;
|
||||
startIndex: number;
|
||||
};
|
||||
|
||||
export type PlaylistSongListArgs = { query: PlaylistSongListQuery } & BaseEndpointArgs;
|
||||
|
||||
// Music Folder List
|
||||
export type RawMusicFolderListResponse = SSMusicFolderList | JFMusicFolderList | undefined;
|
||||
|
||||
export type MusicFolderListResponse = BasePaginatedResponse<Playlist[]>;
|
||||
|
||||
export type MusicFolderListQuery = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type MusicFolderListArgs = { query: MusicFolderListQuery } & BaseEndpointArgs;
|
||||
|
||||
// Create Favorite
|
||||
export type RawCreateFavoriteResponse = CreateFavoriteResponse | undefined;
|
||||
|
||||
export type CreateFavoriteResponse = { id: string };
|
||||
|
||||
export type CreateFavoriteQuery = { comment?: string; name: string; public?: boolean };
|
||||
|
||||
export type CreateFavoriteArgs = { query: CreateFavoriteQuery } & BaseEndpointArgs;
|
||||
Loading…
Add table
Add a link
Reference in a new issue