Add additional controller endpoints

This commit is contained in:
jeffvli
2022-12-12 20:44:23 -08:00
parent 2839b68e5f
commit c57c53972a
8 changed files with 1411 additions and 194 deletions
+28 -16
View File
@@ -28,8 +28,14 @@ import type {
RawPlaylistDetailResponse,
PlaylistListArgs,
RawPlaylistListResponse,
MusicFolderListArgs,
RawMusicFolderListResponse,
PlaylistSongListArgs,
ArtistListArgs,
RawArtistListResponse,
} from '/@/api/types';
import { subsonicApi } from '/@/api/subsonic.api';
import { jellyfinApi } from '/@/api/jellyfin.api';
export type ControllerEndpoint = Partial<{
clearPlaylist: () => void;
@@ -42,15 +48,16 @@ export type ControllerEndpoint = Partial<{
getAlbumDetail: (args: AlbumDetailArgs) => Promise<RawAlbumDetailResponse>;
getAlbumList: (args: AlbumListArgs) => Promise<RawAlbumListResponse>;
getArtistDetail: () => void;
getArtistList: () => void;
getArtistList: (args: ArtistListArgs) => Promise<RawArtistListResponse>;
getFavoritesList: () => void;
getFolderItemList: () => void;
getFolderList: () => void;
getFolderSongs: () => void;
getGenreList: (args: GenreListArgs) => Promise<RawGenreListResponse>;
getMusicFolderList: () => void;
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;
@@ -66,25 +73,27 @@ type ApiController = {
const endpoints: ApiController = {
jellyfin: {
clearPlaylist: undefined,
createFavorite: undefined,
createPlaylist: undefined,
deleteFavorite: undefined,
deletePlaylist: undefined,
getAlbumArtistDetail: undefined,
getAlbumArtistList: undefined,
getAlbumDetail: undefined,
getAlbumList: 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: undefined,
getArtistList: jellyfinApi.getArtistList,
getFavoritesList: undefined,
getFolderItemList: undefined,
getFolderList: undefined,
getFolderSongs: undefined,
getGenreList: undefined,
getMusicFolderList: undefined,
getPlaylistDetail: undefined,
getPlaylistList: undefined,
getSongList: 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,
},
@@ -108,6 +117,8 @@ const endpoints: ApiController = {
getMusicFolderList: undefined,
getPlaylistDetail: navidromeApi.getPlaylistDetail,
getPlaylistList: navidromeApi.getPlaylistList,
getPlaylistSongList: navidromeApi.getPlaylistSongList,
getSongDetail: navidromeApi.getSongDetail,
getSongList: navidromeApi.getSongList,
updatePlaylist: undefined,
updateRating: subsonicApi.updateRating,
@@ -132,6 +143,7 @@ const endpoints: ApiController = {
getMusicFolderList: undefined,
getPlaylistDetail: undefined,
getPlaylistList: undefined,
getSongDetail: undefined,
getSongList: undefined,
updatePlaylist: undefined,
updateRating: undefined,
+675
View File
@@ -0,0 +1,675 @@
import ky from 'ky';
import _ from 'lodash';
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 '/@/api/jellyfin.types';
import { JFCollectionType } from '/@/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 '/@/api/types';
import { songListSortMap } from '/@/api/types';
import { albumListSortMap } from '/@/api/types';
import { artistListSortMap } from '/@/api/types';
import { sortOrderMap } from '/@/api/types';
import { albumArtistListSortMap } from '/@/api/types';
import type { ServerListItem } from '/@/store';
import { useAuthStore } from '/@/store';
import { ServerType } from '/@/types';
import { parseSearchParams } from '/@/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 data;
};
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 data;
};
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,
};
+153 -16
View File
@@ -1,32 +1,90 @@
export type JFBaseResponse = {
export type JFBasePaginatedResponse = {
StartIndex: number;
TotalRecordCount: number;
};
export interface JFMusicFoldersResponse extends JFBaseResponse {
export interface JFMusicFolderListResponse extends JFBasePaginatedResponse {
Items: JFMusicFolder[];
}
export interface JFGenreResponse extends JFBaseResponse {
export type JFMusicFolderList = {
items: JFMusicFolder[];
startIndex: number;
totalRecordCount: number;
};
export interface JFGenreListResponse extends JFBasePaginatedResponse {
Items: JFGenre[];
}
export interface JFAlbumArtistsResponse extends JFBaseResponse {
export type JFGenreList = JFGenreListResponse;
export type JFAlbumArtistDetailResponse = JFAlbumArtist;
export type JFAlbumArtistDetail = JFAlbumArtistDetailResponse;
export interface JFAlbumArtistListResponse extends JFBasePaginatedResponse {
Items: JFAlbumArtist[];
}
export interface JFArtistsResponse extends JFBaseResponse {
export type JFAlbumArtistList = JFAlbumArtistListResponse;
export interface JFArtistListResponse extends JFBasePaginatedResponse {
Items: JFAlbumArtist[];
}
export interface JFAlbumsResponse extends JFBaseResponse {
export type JFArtistList = JFArtistListResponse;
export interface JFAlbumListResponse extends JFBasePaginatedResponse {
Items: JFAlbum[];
}
export interface JFSongsResponse extends JFBaseResponse {
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 = JFSongListResponse;
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;
@@ -114,10 +172,13 @@ export type JFArtist = {
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[];
@@ -134,6 +195,7 @@ export type JFAlbum = {
RunTimeTicks: number;
ServerId: string;
Type: string;
UserData?: UserData;
} & {
songs?: JFSong[];
};
@@ -168,6 +230,7 @@ export type JFSong = {
ServerId: string;
SortName: string;
Type: string;
UserData?: UserData;
};
type ImageBlurHashes = {
@@ -405,6 +468,21 @@ type Policy = {
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',
@@ -412,22 +490,81 @@ export enum JFSortOrder {
export enum JFAlbumListSort {
ALBUM_ARTIST = 'AlbumArtist,SortName',
COMMUNITY_RATING = 'CommunityRating,SortName',
CRITIC_RATING = 'CriticRating,SortName',
NAME = 'SortName',
RANDOM = 'Random,SortName',
RATING = 'CommunityRating,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RELEASE_DATE = 'ProductionYear,PremiereDate,SortName',
}
export type JFAlbumListParams = {
enableImageTypes: JFImageType[];
imageTypeLimit: number;
filters?: string;
genres?: string;
includeItemTypes: 'MusicAlbum';
limit?: number;
parentId: string;
recursive: boolean;
sortBy: JFAlbumListSort;
sortOrder: JFSortOrder;
startIndex: number;
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;
+67 -91
View File
@@ -4,7 +4,6 @@ import type {
NDGenreListResponse,
NDArtistListResponse,
NDAlbumDetail,
NDSongListResponse,
NDAlbumListParams,
NDAlbumList,
NDSongDetailResponse,
@@ -12,18 +11,14 @@ import type {
NDSong,
NDAuthenticationResponse,
NDAlbumDetailResponse,
NDSongList,
NDSongDetail,
NDGenreList,
NDAlbumArtistListParams,
NDAlbumArtistListSort,
NDAlbumArtistDetail,
NDAlbumListResponse,
NDAlbumArtistDetailResponse,
NDAlbumArtistList,
NDSongListParams,
NDSongListSort,
NDCreatePlaylist,
NDCreatePlaylistParams,
NDCreatePlaylistResponse,
NDDeletePlaylist,
@@ -31,11 +26,13 @@ import type {
NDPlaylistListParams,
NDPlaylistDetail,
NDPlaylistList,
NDPlaylistListSort,
NDPlaylistListResponse,
NDPlaylistDetailResponse,
NDSongList,
NDSongListResponse,
} from '/@/api/navidrome.types';
import { NDAlbumListSort } from '/@/api/navidrome.types';
import { NDPlaylistListSort } from '/@/api/navidrome.types';
import { NDSongListSort } from '/@/api/navidrome.types';
import { NDSortOrder } from '/@/api/navidrome.types';
import type {
Album,
@@ -52,12 +49,18 @@ import type {
DeletePlaylistArgs,
PlaylistListArgs,
PlaylistDetailArgs,
CreatePlaylistResponse,
PlaylistSongListArgs,
} from '/@/api/types';
import { SortOrder } from '/@/api/types';
import { playlistListSortMap } from '/@/api/types';
import { albumArtistListSortMap } from '/@/api/types';
import { songListSortMap } from '/@/api/types';
import { albumListSortMap, sortOrderMap } from '/@/api/types';
import { toast } from '/@/components';
import type { ServerListItem } from '/@/store';
import { useAuthStore } from '/@/store';
import { ServerType } from '/@/types';
import { parseSearchParams } from '/@/utils';
const api = ky.create({
hooks: {
@@ -95,56 +98,6 @@ const api = ky.create({
},
});
// api.interceptors.request.use(
// (config) => {
// const server = useAuthStore.getState().currentServer;
// config.baseURL = server?.url;
// config.headers = {
// 'Content-Type': 'application/json',
// 'x-nd-authorization': `Bearer ${server?.ndCredential}`,
// };
// return config;
// },
// (err) => {
// return Promise.reject(err);
// },
// );
// api.interceptors.response.use(
// (res) => {
// const serverId = useAuthStore.getState().currentServer?.id;
// if (serverId) {
// useAuthStore.getState().actions.updateServer(serverId, {
// ndCredential: res.headers['x-nd-authorization'] as string,
// });
// }
// return res;
// },
// async (err) => {
// if (!err.response) {
// return Promise.reject(err);
// }
// if (err.response && err.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 Promise.reject(err);
// },
// );
const authenticate = async (
url: string,
body: { password: string; username: string },
@@ -193,22 +146,7 @@ const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs): Promise<NDAlbu
})
.json<NDAlbumArtistDetailResponse>();
const albumsData = await api
.get('api/album', {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
prefixUrl: server?.url,
searchParams: {
_end: 0,
_order: NDSortOrder.ASC,
_sort: NDAlbumListSort.YEAR,
_start: 0,
artist_id: query.id,
} as NDAlbumListParams,
signal,
})
.json<NDAlbumListResponse>();
return { ...data, albums: albumsData };
return { ...data };
};
const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<NDAlbumArtistList> => {
@@ -216,8 +154,8 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<NDAlbumArt
const searchParams: NDAlbumArtistListParams = {
_end: query.startIndex + (query.limit || 0),
_order: query.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC,
_sort: query.sortBy as NDAlbumArtistListSort,
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumArtistListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
...query.ndParams,
};
@@ -267,8 +205,8 @@ const getAlbumList = async (args: AlbumListArgs): Promise<NDAlbumList> => {
const searchParams: NDAlbumListParams = {
_end: query.startIndex + (query.limit || 0),
_order: query.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC,
_sort: query.sortBy as NDAlbumListSort,
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: albumListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
...query.ndParams,
};
@@ -295,8 +233,8 @@ const getSongList = async (args: SongListArgs): Promise<NDSongList> => {
const searchParams: NDSongListParams = {
_end: query.startIndex + (query.limit || 0),
_order: query.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC,
_sort: query.sortBy as NDSongListSort,
_order: sortOrderMap.navidrome[query.sortOrder],
_sort: songListSortMap.navidrome[query.sortBy],
_start: query.startIndex,
...query.ndParams,
};
@@ -324,6 +262,7 @@ const getSongDetail = async (args: SongDetailArgs): Promise<NDSongDetail> => {
const data = await api
.get(`api/song/${query.id}`, {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
prefixUrl: server?.url,
signal,
})
.json<NDSongDetailResponse>();
@@ -331,7 +270,7 @@ const getSongDetail = async (args: SongDetailArgs): Promise<NDSongDetail> => {
return data;
};
const createPlaylist = async (args: CreatePlaylistArgs): Promise<NDCreatePlaylist> => {
const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistResponse> => {
const { query, server, signal } = args;
const json: NDCreatePlaylistParams = {
@@ -344,11 +283,15 @@ const createPlaylist = async (args: CreatePlaylistArgs): Promise<NDCreatePlaylis
.post('api/playlist', {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
json,
prefixUrl: server?.url,
signal,
})
.json<NDCreatePlaylistResponse>();
return data;
return {
id: data.id,
name: query.name,
};
};
const deletePlaylist = async (args: DeletePlaylistArgs): Promise<NDDeletePlaylist> => {
@@ -357,6 +300,7 @@ const deletePlaylist = async (args: DeletePlaylistArgs): Promise<NDDeletePlaylis
const data = await api
.delete(`api/playlist/${query.id}`, {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
prefixUrl: server?.url,
signal,
})
.json<NDDeletePlaylistResponse>();
@@ -369,12 +313,12 @@ const getPlaylistList = async (args: PlaylistListArgs): Promise<NDPlaylistList>
const searchParams: NDPlaylistListParams = {
_end: query.startIndex + (query.limit || 0),
_order: query.sortOrder === SortOrder.ASC ? NDSortOrder.ASC : NDSortOrder.DESC,
_sort: query.sortBy as NDPlaylistListSort,
_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/song', {
const res = await api.get('api/playlist', {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
prefixUrl: server?.url,
searchParams,
@@ -395,8 +339,9 @@ const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise<NDPlaylistDe
const { query, server, signal } = args;
const data = await api
.get(`api/song/${query.id}`, {
.get(`api/playlist/${query.id}`, {
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
prefixUrl: server?.url,
signal,
})
.json<NDPlaylistDetailResponse>();
@@ -404,6 +349,33 @@ const getPlaylistDetail = async (args: PlaylistDetailArgs): Promise<NDPlaylistDe
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;
@@ -426,7 +398,7 @@ const getCoverArtUrl = (args: {
);
};
const normalizeAlbum = (item: NDAlbum, server: ServerListItem, imageSize?: number) => {
const normalizeAlbum = (item: NDAlbum, server: ServerListItem, imageSize?: number): Album => {
const imageUrl = getCoverArtUrl({
baseUrl: server.url,
coverArtId: item.coverArtId,
@@ -434,13 +406,14 @@ const normalizeAlbum = (item: NDAlbum, server: ServerListItem, imageSize?: numbe
size: imageSize || 300,
});
const imagePlaceholderUrl = imageUrl?.replace(/size=\d+/, 'size=50');
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,
@@ -457,7 +430,7 @@ const normalizeAlbum = (item: NDAlbum, server: ServerListItem, imageSize?: numbe
songCount: item.songCount,
uniqueId: nanoid(),
updatedAt: item.updatedAt,
} as Album;
};
};
const normalizeSong = (
@@ -465,7 +438,7 @@ const normalizeSong = (
server: ServerListItem,
deviceId: string,
imageSize?: number,
) => {
): Song => {
const imageUrl = getCoverArtUrl({
baseUrl: server.url,
coverArtId: item.albumId,
@@ -490,6 +463,8 @@ const normalizeSong = (
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,
@@ -499,7 +474,7 @@ const normalizeSong = (
type: ServerType.NAVIDROME,
uniqueId: nanoid(),
updatedAt: item.updatedAt,
} as Song;
};
};
export const navidromeApi = {
@@ -513,6 +488,7 @@ export const navidromeApi = {
getGenreList,
getPlaylistDetail,
getPlaylistList,
getPlaylistSongList,
getSongDetail,
getSongList,
};
+7 -5
View File
@@ -45,8 +45,6 @@ export type NDAlbum = {
starred: boolean;
starredAt: string;
updatedAt: string;
} & {
songs?: NDSong[];
};
export type NDSong = {
@@ -119,7 +117,7 @@ export type NDAuthenticationResponse = NDAuthenticate;
export type NDAlbumArtistList = NDAlbumArtist[];
export type NDAlbumArtistDetail = NDAlbumArtist & { albums: NDAlbumListResponse };
export type NDAlbumArtistDetail = NDAlbumArtist;
export type NDAlbumArtistDetailResponse = NDAlbumArtist;
@@ -129,7 +127,7 @@ export type NDGenreListResponse = NDGenre[];
export type NDAlbumDetailResponse = NDAlbum;
export type NDAlbumDetail = NDAlbum & { songs: NDSongListResponse };
export type NDAlbumDetail = NDAlbum & { songs?: NDSongListResponse };
export type NDAlbumListResponse = NDAlbum[];
@@ -183,6 +181,7 @@ export enum NDAlbumListSort {
DURATION = 'duration',
NAME = 'name',
PLAY_COUNT = 'playCount',
PLAY_DATE = 'play_date',
RANDOM = 'random',
RATING = 'rating',
RECENTLY_ADDED = 'recently_added',
@@ -193,6 +192,7 @@ export enum NDAlbumListSort {
export type NDAlbumListParams = {
_sort?: NDAlbumListSort;
album_id?: string;
artist_id?: string;
compilation?: boolean;
genre_id?: string;
@@ -213,7 +213,9 @@ export enum NDSongListSort {
CHANNELS = 'channels',
COMMENT = 'comment',
DURATION = 'duration',
FAVORITED = 'starred ASC, starredAt ASC',
GENRE = 'genre',
ID = 'id',
NAME = 'name',
PLAY_COUNT = 'playCount',
PLAY_DATE = 'playDate',
@@ -231,11 +233,11 @@ export type NDSongListParams = {
export enum NDAlbumArtistListSort {
ALBUM_COUNT = 'albumCount',
FAVORITED = 'starred ASC, starredAt ASC',
NAME = 'name',
PLAY_COUNT = 'playCount',
RATING = 'rating',
SONG_COUNT = 'songCount',
STARRED = 'starred ASC, starredAt ASC',
}
export type NDAlbumArtistListParams = {
+8 -19
View File
@@ -1,33 +1,22 @@
import type { AlbumListQuery } from './types';
import type { AlbumListQuery, SongListQuery } from './types';
import type { AlbumDetailQuery } from './types';
export const queryKeys = {
albums: {
detail: (serverId: string, query: AlbumDetailQuery) => ['albums', serverId, query] as const,
list: (serverId: string, params: AlbumListQuery) =>
[serverId, 'albums', 'list', serverId, params] as const,
detail: (serverId: string, query: AlbumDetailQuery) =>
[serverId, 'albums', 'detail', query] as const,
list: (serverId: string, query: AlbumListQuery) =>
[serverId, 'albums', 'list', serverId, 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,
},
ping: (url: string) => ['ping', url] as const,
server: {
root: (serverId: string) => [serverId] as const,
},
servers: {
list: (params?: any) => ['servers', 'list', params] as const,
map: () => ['servers', 'map'] as const,
root: ['servers'],
},
tasks: {
list: () => ['tasks', 'list'] as const,
root: ['tasks'],
},
users: {
detail: (userId: string) => ['users', userId] as const,
list: (params?: any) => ['users', 'list', params] as const,
root: ['users'],
},
};
+11 -7
View File
@@ -16,7 +16,6 @@ import type {
SSAlbumArtistDetail,
SSAlbumArtistDetailResponse,
SSFavoriteParams,
SSFavorite,
SSFavoriteResponse,
SSRatingParams,
SSRatingResponse,
@@ -30,6 +29,7 @@ import type {
AlbumListArgs,
AuthenticationResponse,
FavoriteArgs,
FavoriteResponse,
GenreListArgs,
RatingArgs,
} from '/@/api/types';
@@ -222,7 +222,7 @@ const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => {
};
};
const createFavorite = async (args: FavoriteArgs): Promise<SSFavorite> => {
const createFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
const { query, signal } = args;
const searchParams: SSFavoriteParams = {
@@ -231,17 +231,19 @@ const createFavorite = async (args: FavoriteArgs): Promise<SSFavorite> => {
id: query.type === 'song' ? query.id : undefined,
};
const data = await api
await api
.get('/rest/star.view', {
searchParams,
signal,
})
.json<SSFavoriteResponse>();
return data;
return {
id: query.id,
};
};
const deleteFavorite = async (args: FavoriteArgs) => {
const deleteFavorite = async (args: FavoriteArgs): Promise<FavoriteResponse> => {
const { query, signal } = args;
const searchParams: SSFavoriteParams = {
@@ -250,14 +252,16 @@ const deleteFavorite = async (args: FavoriteArgs) => {
id: query.type === 'song' ? query.id : undefined,
};
const data = await api
await api
.get('/rest/unstar.view', {
searchParams,
signal,
})
.json<SSFavoriteResponse>();
return data;
return {
id: query.id,
};
};
const updateRating = async (args: RatingArgs) => {
+462 -40
View File
@@ -1,29 +1,43 @@
import type { ServerListItem } from '/@/store';
import type { ServerType } from '/@//types';
import type { JFAlbumListSort, JFSortOrder } from '/@/api/jellyfin.types';
import type {
JFAlbumArtistDetail,
JFAlbumArtistList,
JFAlbumDetail,
JFAlbumList,
JFArtistList,
JFGenreList,
JFMusicFolderList,
JFPlaylistDetail,
JFPlaylistList,
JFSongList,
} from '/@/api/jellyfin.types';
import { JFAlbumArtistListSort, JFArtistListSort } from '/@/api/jellyfin.types';
import { JFAlbumListSort, JFSongListSort } from '/@/api/jellyfin.types';
import { JFSortOrder } from '/@/api/jellyfin.types';
import type {
NDAlbumArtistDetail,
NDAlbumArtistList,
NDAlbumArtistListSort,
NDAlbumDetail,
NDAlbumList,
NDAlbumListSort,
NDCreatePlaylist,
NDDeletePlaylist,
NDGenreList,
NDOrder,
NDPlaylistDetail,
NDPlaylistList,
NDPlaylistListSort,
NDSongDetail,
NDSongList,
NDSongListSort,
} from '/@/api/navidrome.types';
import { NDPlaylistListSort } from '/@/api/navidrome.types';
import { NDAlbumArtistListSort } from '/@/api/navidrome.types';
import { NDSongListSort } from '/@/api/navidrome.types';
import { NDAlbumListSort, NDSortOrder } from '/@/api/navidrome.types';
import type {
SSAlbumArtistDetail,
SSAlbumArtistList,
SSAlbumDetail,
SSAlbumList,
SSMusicFolderList,
} from '/@/api/subsonic.types';
export enum SortOrder {
@@ -31,6 +45,27 @@ export enum SortOrder {
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',
@@ -88,6 +123,7 @@ export type Album = {
artists: RelatedArtist[];
backdropImageUrl: string | null;
createdAt: string;
duration: number | null;
genres: Genre[];
id: string;
imagePlaceholderUrl: string | null;
@@ -115,17 +151,19 @@ export type Song = {
artists: RelatedArtist[];
bitRate: number;
compilation: boolean | null;
container: string;
container: string | null;
createdAt: string;
discNumber: number;
duration: number;
genres: Genre[];
id: string;
imageUrl: string;
imageUrl: string | null;
isFavorite: boolean;
name: string;
releaseDate: string;
releaseYear: string;
path: string | null;
playCount: number;
releaseDate: string | null;
releaseYear: string | null;
serverId: string;
size: number;
streamUrl: string;
@@ -192,8 +230,8 @@ type BaseEndpointArgs = {
signal?: AbortSignal;
};
// Genre List ---------------------------------------------------------------------------
export type RawGenreListResponse = NDGenreList | undefined;
// Genre List
export type RawGenreListResponse = NDGenreList | JFGenreList | undefined;
export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined;
@@ -201,14 +239,35 @@ export type GenreListArgs = { query: GenreListQuery } & BaseEndpointArgs;
export type GenreListQuery = null;
// Album List ---------------------------------------------------------------------------
export type RawAlbumListResponse = NDAlbumList | SSAlbumList | undefined;
// Album List
export type RawAlbumListResponse = NDAlbumList | SSAlbumList | JFAlbumList | undefined;
export type AlbumListResponse = BasePaginatedResponse<Album[]> | null | undefined;
export type AlbumListSort = NDAlbumListSort | JFAlbumListSort;
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?: {
@@ -227,8 +286,68 @@ export type AlbumListQuery = {
export type AlbumListArgs = { query: AlbumListQuery } & BaseEndpointArgs;
// Album Detail -------------------------------------------------------------------------
export type RawAlbumDetailResponse = NDAlbumDetail | SSAlbumDetail | undefined;
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;
@@ -236,14 +355,38 @@ export type AlbumDetailQuery = { id: string };
export type AlbumDetailArgs = { query: AlbumDetailQuery } & BaseEndpointArgs;
// Song List ----------------------------------------------------------------------------
export type RawSongListResponse = NDSongList | undefined;
// Song List
export type RawSongListResponse = NDSongList | JFSongList | undefined;
export type SongListResponse = BasePaginatedResponse<Song[]>;
export type SongListSort = NDSongListSort;
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?: {
@@ -262,7 +405,70 @@ export type SongListQuery = {
export type SongListArgs = { query: SongListQuery } & BaseEndpointArgs;
// Song Detail -------------------------------------------------------------------------
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.NAME,
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;
@@ -271,12 +477,28 @@ export type SongDetailQuery = { id: string };
export type SongDetailArgs = { query: SongDetailQuery } & BaseEndpointArgs;
// Album Artist List -------------------------------------------------------------------
export type RawAlbumArtistListResponse = NDAlbumArtistList | SSAlbumArtistList | undefined;
// Album Artist List
export type RawAlbumArtistListResponse =
| NDAlbumArtistList
| SSAlbumArtistList
| JFAlbumArtistList
| undefined;
export type AlbumArtistListResponse = BasePaginatedResponse<AlbumArtist[]>;
export type AlbumArtistListSort = NDAlbumArtistListSort;
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;
@@ -293,8 +515,60 @@ export type AlbumArtistListQuery = {
export type AlbumArtistListArgs = { query: AlbumArtistListQuery } & BaseEndpointArgs;
// Album Artist Detail -----------------------------------------------------------------
export type RawAlbumArtistDetailResponse = NDAlbumArtistDetail | SSAlbumArtistDetail | undefined;
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[]>;
@@ -302,20 +576,100 @@ export type AlbumArtistDetailQuery = { id: string };
export type AlbumArtistDetailArgs = { query: AlbumArtistDetailQuery } & BaseEndpointArgs;
// Artist List -------------------------------------------------------------------------
// Artist List
export type RawArtistListResponse = JFArtistList | undefined;
// Artist Detail -----------------------------------------------------------------------
export type ArtistListResponse = BasePaginatedResponse<Artist[]>;
// Favorite ----------------------------------------------------------------------------
export type RawFavoriteResponse = null | undefined;
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 FavoriteResponse = null;
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 -------------------------------------------------------------------------------
// Rating
export type RawRatingResponse = null | undefined;
export type RatingResponse = null;
@@ -324,16 +678,16 @@ export type RatingQuery = { id: string; rating: number };
export type RatingArgs = { query: RatingQuery } & BaseEndpointArgs;
// Create Playlist -----------------------------------------------------------------------
export type RawCreatePlaylistResponse = NDCreatePlaylist | undefined;
// Create Playlist
export type RawCreatePlaylistResponse = CreatePlaylistResponse | undefined;
export type CreatePlaylistResponse = null;
export type CreatePlaylistResponse = { id: string; name: string };
export type CreatePlaylistQuery = { comment?: string; name: string; public?: boolean };
export type CreatePlaylistArgs = { query: CreatePlaylistQuery } & BaseEndpointArgs;
// Delete Playlist -----------------------------------------------------------------------
// Delete Playlist
export type RawDeletePlaylistResponse = NDDeletePlaylist | undefined;
export type DeletePlaylistResponse = null;
@@ -342,8 +696,8 @@ export type DeletePlaylistQuery = { id: string };
export type DeletePlaylistArgs = { query: DeletePlaylistQuery } & BaseEndpointArgs;
// Playlist List -------------------------------------------------------------------------
export type RawPlaylistListResponse = NDPlaylistList | undefined;
// Playlist List
export type RawPlaylistListResponse = NDPlaylistList | JFPlaylistList | undefined;
export type PlaylistListResponse = BasePaginatedResponse<Playlist[]>;
@@ -359,8 +713,41 @@ export type PlaylistListQuery = {
export type PlaylistListArgs = { query: PlaylistListQuery } & BaseEndpointArgs;
// Playlist Detail -----------------------------------------------------------------------
export type RawPlaylistDetailResponse = NDPlaylistDetail | undefined;
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[]>;
@@ -369,3 +756,38 @@ export type PlaylistDetailQuery = {
};
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;