mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 18:13:31 +00:00
Add album artist list route
This commit is contained in:
parent
185175aa89
commit
24af17b8fe
19 changed files with 1269 additions and 32 deletions
|
|
@ -191,9 +191,19 @@ const getGenreList = async (args: GenreListArgs) => {
|
|||
return (apiController('getGenreList') as ControllerEndpoint['getGenreList'])?.(args);
|
||||
};
|
||||
|
||||
const getAlbumArtistList = async (args: AlbumArtistListArgs) => {
|
||||
return (apiController('getAlbumArtistList') as ControllerEndpoint['getAlbumArtistList'])?.(args);
|
||||
};
|
||||
|
||||
const getArtistList = async (args: ArtistListArgs) => {
|
||||
return (apiController('getArtistList') as ControllerEndpoint['getArtistList'])?.(args);
|
||||
};
|
||||
|
||||
export const controller = {
|
||||
getAlbumArtistList,
|
||||
getAlbumDetail,
|
||||
getAlbumList,
|
||||
getArtistList,
|
||||
getGenreList,
|
||||
getMusicFolderList,
|
||||
getSongList,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import ky from 'ky';
|
|||
import { nanoid } from 'nanoid/non-secure';
|
||||
import type {
|
||||
JFAlbum,
|
||||
JFAlbumArtist,
|
||||
JFAlbumArtistDetail,
|
||||
JFAlbumArtistDetailResponse,
|
||||
JFAlbumArtistList,
|
||||
|
|
@ -33,6 +34,7 @@ import type {
|
|||
import { JFCollectionType } from '/@/renderer/api/jellyfin.types';
|
||||
import type {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
AlbumArtistDetailArgs,
|
||||
AlbumArtistListArgs,
|
||||
AlbumDetailArgs,
|
||||
|
|
@ -138,7 +140,7 @@ const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs): Promise<JFAlbu
|
|||
};
|
||||
|
||||
const data = await api
|
||||
.get(`/users/${server?.userId}/items/${query.id}`, {
|
||||
.get(`users/${server?.userId}/items/${query.id}`, {
|
||||
headers: { 'X-MediaBrowser-Token': server?.credential },
|
||||
prefixUrl: server?.url,
|
||||
searchParams: parseSearchParams(searchParams),
|
||||
|
|
@ -170,12 +172,15 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<JFAlbumArt
|
|||
const { query, server, signal } = args;
|
||||
|
||||
const searchParams: JFAlbumArtistListParams = {
|
||||
fields: 'Genres, DateCreated, ExternalUrls, Overview',
|
||||
imageTypeLimit: 1,
|
||||
limit: query.limit,
|
||||
parentId: query.musicFolderId,
|
||||
recursive: true,
|
||||
sortBy: albumArtistListSortMap.jellyfin[query.sortBy],
|
||||
sortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||
startIndex: query.startIndex,
|
||||
userId: server?.userId || undefined,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
|
|
@ -187,7 +192,11 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<JFAlbumArt
|
|||
})
|
||||
.json<JFAlbumArtistListResponse>();
|
||||
|
||||
return data;
|
||||
return {
|
||||
items: data.Items,
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: data.TotalRecordCount,
|
||||
};
|
||||
};
|
||||
|
||||
const getArtistList = async (args: ArtistListArgs): Promise<JFArtistList> => {
|
||||
|
|
@ -303,9 +312,11 @@ const getSongList = async (args: SongListArgs): Promise<JFSongList> => {
|
|||
|
||||
const yearsFilter = yearsGroup.length ? getCommaDelimitedString(yearsGroup) : undefined;
|
||||
const albumIdsFilter = query.albumIds ? getCommaDelimitedString(query.albumIds) : undefined;
|
||||
const artistIdsFilter = query.artistIds ? getCommaDelimitedString(query.artistIds) : undefined;
|
||||
|
||||
const searchParams: JFSongListParams & { maxYear?: number; minYear?: number } = {
|
||||
albumIds: albumIdsFilter,
|
||||
artistIds: artistIdsFilter,
|
||||
fields: 'Genres, DateCreated, MediaSources, ParentId',
|
||||
includeItemTypes: 'Audio',
|
||||
limit: query.limit,
|
||||
|
|
@ -496,6 +507,26 @@ const getStreamUrl = (args: {
|
|||
);
|
||||
};
|
||||
|
||||
const getAlbumArtistCoverArtUrl = (args: {
|
||||
baseUrl: string;
|
||||
item: JFAlbumArtist;
|
||||
size: number;
|
||||
}) => {
|
||||
const size = args.size ? args.size : 300;
|
||||
|
||||
if (!args.item.ImageTags?.Primary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
`${args.baseUrl}/Items` +
|
||||
`/${args.item.Id}` +
|
||||
'/Images/Primary' +
|
||||
`?width=${size}&height=${size}` +
|
||||
'&quality=96'
|
||||
);
|
||||
};
|
||||
|
||||
const getAlbumCoverArtUrl = (args: { baseUrl: string; item: JFAlbum; size: number }) => {
|
||||
const size = args.size ? args.size : 300;
|
||||
|
||||
|
|
@ -628,6 +659,32 @@ const normalizeAlbum = (item: JFAlbum, server: ServerListItem, imageSize?: numbe
|
|||
};
|
||||
};
|
||||
|
||||
const normalizeAlbumArtist = (
|
||||
item: JFAlbumArtist,
|
||||
server: ServerListItem,
|
||||
imageSize?: number,
|
||||
): AlbumArtist => {
|
||||
return {
|
||||
albumCount: null,
|
||||
backgroundImageUrl: null,
|
||||
biography: item.Overview || null,
|
||||
duration: item.RunTimeTicks / 10000000,
|
||||
genres: item.GenreItems?.map((entry) => ({ id: entry.Id, name: entry.Name })),
|
||||
id: item.Id,
|
||||
imageUrl: getAlbumArtistCoverArtUrl({
|
||||
baseUrl: server.url,
|
||||
item,
|
||||
size: imageSize || 300,
|
||||
}),
|
||||
isFavorite: item.UserData.IsFavorite || false,
|
||||
lastPlayedAt: null,
|
||||
name: item.Name,
|
||||
playCount: item.UserData.PlayCount,
|
||||
rating: null,
|
||||
songCount: null,
|
||||
};
|
||||
};
|
||||
|
||||
// const normalizeArtist = (item: any) => {
|
||||
// return {
|
||||
// album: (item.album || []).map((entry: any) => normalizeAlbum(entry)),
|
||||
|
|
@ -717,5 +774,6 @@ export const jellyfinApi = {
|
|||
|
||||
export const jfNormalize = {
|
||||
album: normalizeAlbum,
|
||||
albumArtist: normalizeAlbumArtist,
|
||||
song: normalizeSong,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,7 +23,11 @@ export interface JFAlbumArtistListResponse extends JFBasePaginatedResponse {
|
|||
Items: JFAlbumArtist[];
|
||||
}
|
||||
|
||||
export type JFAlbumArtistList = JFAlbumArtistListResponse;
|
||||
export type JFAlbumArtistList = {
|
||||
items: JFAlbumArtist[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export interface JFArtistListResponse extends JFBasePaginatedResponse {
|
||||
Items: JFAlbumArtist[];
|
||||
|
|
@ -149,6 +153,13 @@ export type JFAlbumArtist = {
|
|||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
UserData: {
|
||||
IsFavorite: boolean;
|
||||
Key: string;
|
||||
PlayCount: number;
|
||||
PlaybackPositionTicks: number;
|
||||
Played: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type JFArtist = {
|
||||
|
|
@ -474,6 +485,7 @@ type JFBaseParams = {
|
|||
imageTypeLimit?: number;
|
||||
parentId?: string;
|
||||
recursive?: boolean;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
type JFPaginationParams = {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import type {
|
|||
NDPlaylistDetailResponse,
|
||||
NDSongList,
|
||||
NDSongListResponse,
|
||||
NDAlbumArtist,
|
||||
} from '/@/renderer/api/navidrome.types';
|
||||
import { NDPlaylistListSort, NDSongListSort, NDSortOrder } from '/@/renderer/api/navidrome.types';
|
||||
import type {
|
||||
|
|
@ -49,6 +50,7 @@ import type {
|
|||
PlaylistDetailArgs,
|
||||
CreatePlaylistResponse,
|
||||
PlaylistSongListArgs,
|
||||
AlbumArtist,
|
||||
} from '/@/renderer/api/types';
|
||||
import {
|
||||
playlistListSortMap,
|
||||
|
|
@ -160,15 +162,21 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<NDAlbumArt
|
|||
...query.ndParams,
|
||||
};
|
||||
|
||||
const data = await api
|
||||
.get('api/artist', {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||
searchParams,
|
||||
signal,
|
||||
})
|
||||
.json<NDArtistListResponse>();
|
||||
const res = await api.get('api/artist', {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||
prefixUrl: server?.url,
|
||||
searchParams: parseSearchParams(searchParams),
|
||||
signal,
|
||||
});
|
||||
|
||||
return data;
|
||||
const data = await res.json<NDArtistListResponse>();
|
||||
const itemCount = res.headers.get('x-total-count');
|
||||
|
||||
return {
|
||||
items: data,
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: Number(itemCount),
|
||||
};
|
||||
};
|
||||
|
||||
const getAlbumDetail = async (args: AlbumDetailArgs): Promise<NDAlbumDetail> => {
|
||||
|
|
@ -238,6 +246,7 @@ const getSongList = async (args: SongListArgs): Promise<NDSongList> => {
|
|||
_sort: songListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
album_id: query.albumIds,
|
||||
artist_id: query.artistIds,
|
||||
title: query.searchTerm,
|
||||
...query.ndParams,
|
||||
};
|
||||
|
|
@ -487,6 +496,24 @@ const normalizeAlbum = (item: NDAlbum, server: ServerListItem, imageSize?: numbe
|
|||
};
|
||||
};
|
||||
|
||||
const normalizeAlbumArtist = (item: NDAlbumArtist): AlbumArtist => {
|
||||
return {
|
||||
albumCount: item.albumCount,
|
||||
backgroundImageUrl: null,
|
||||
biography: item.biography,
|
||||
duration: null,
|
||||
genres: item.genres,
|
||||
id: item.id,
|
||||
imageUrl: item.largeImageUrl,
|
||||
isFavorite: item.starred,
|
||||
lastPlayedAt: item.playDate ? item.playDate.split('T')[0] : null,
|
||||
name: item.name,
|
||||
playCount: item.playCount,
|
||||
rating: item.rating,
|
||||
songCount: item.songCount,
|
||||
};
|
||||
};
|
||||
|
||||
export const navidromeApi = {
|
||||
authenticate,
|
||||
createPlaylist,
|
||||
|
|
@ -505,5 +532,6 @@ export const navidromeApi = {
|
|||
|
||||
export const ndNormalize = {
|
||||
album: normalizeAlbum,
|
||||
albumArtist: normalizeAlbumArtist,
|
||||
song: normalizeSong,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -118,7 +118,11 @@ export type NDAlbumArtist = {
|
|||
|
||||
export type NDAuthenticationResponse = NDAuthenticate;
|
||||
|
||||
export type NDAlbumArtistList = NDAlbumArtist[];
|
||||
export type NDAlbumArtistList = {
|
||||
items: NDAlbumArtist[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number;
|
||||
};
|
||||
|
||||
export type NDAlbumArtistDetail = NDAlbumArtist;
|
||||
|
||||
|
|
@ -230,6 +234,7 @@ export enum NDSongListSort {
|
|||
export type NDSongListParams = {
|
||||
_sort?: NDSongListSort;
|
||||
album_id?: string[];
|
||||
artist_id?: string[];
|
||||
genre_id?: string;
|
||||
starred?: boolean;
|
||||
} & NDPagination &
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { jfNormalize } from '/@/renderer/api/jellyfin.api';
|
||||
import type {
|
||||
JFAlbum,
|
||||
JFAlbumArtist,
|
||||
JFGenreList,
|
||||
JFMusicFolderList,
|
||||
JFSong,
|
||||
} from '/@/renderer/api/jellyfin.types';
|
||||
import { ndNormalize } from '/@/renderer/api/navidrome.api';
|
||||
import type { NDAlbum, NDGenreList, NDSong } from '/@/renderer/api/navidrome.types';
|
||||
import type { NDAlbum, NDAlbumArtist, NDGenreList, NDSong } from '/@/renderer/api/navidrome.types';
|
||||
import { SSGenreList, SSMusicFolderList } from '/@/renderer/api/subsonic.types';
|
||||
import type {
|
||||
Album,
|
||||
RawAlbumArtistListResponse,
|
||||
RawAlbumDetailResponse,
|
||||
RawAlbumListResponse,
|
||||
RawGenreListResponse,
|
||||
|
|
@ -136,7 +138,33 @@ const genreList = (data: RawGenreListResponse | undefined, server: ServerListIte
|
|||
return genres;
|
||||
};
|
||||
|
||||
const albumArtistList = (
|
||||
data: RawAlbumArtistListResponse | undefined,
|
||||
server: ServerListItem | null,
|
||||
) => {
|
||||
let albumArtists;
|
||||
switch (server?.type) {
|
||||
case 'jellyfin':
|
||||
albumArtists = data?.items.map((item) =>
|
||||
jfNormalize.albumArtist(item as JFAlbumArtist, server),
|
||||
);
|
||||
break;
|
||||
case 'navidrome':
|
||||
albumArtists = data?.items.map((item) => ndNormalize.albumArtist(item as NDAlbumArtist));
|
||||
break;
|
||||
case 'subsonic':
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
items: albumArtists,
|
||||
startIndex: data?.startIndex,
|
||||
totalRecordCount: data?.totalRecordCount,
|
||||
};
|
||||
};
|
||||
|
||||
export const normalize = {
|
||||
albumArtistList,
|
||||
albumDetail,
|
||||
albumList,
|
||||
genreList,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,32 @@
|
|||
import type { AlbumListQuery, SongListQuery, AlbumDetailQuery } from './types';
|
||||
import type {
|
||||
AlbumListQuery,
|
||||
SongListQuery,
|
||||
AlbumDetailQuery,
|
||||
AlbumArtistListQuery,
|
||||
ArtistListQuery,
|
||||
} from './types';
|
||||
|
||||
export const queryKeys = {
|
||||
albumArtists: {
|
||||
list: (serverId: string, query?: AlbumArtistListQuery) =>
|
||||
[serverId, 'albumArtists', 'list', query] as const,
|
||||
root: (serverId: string) => [serverId, 'albumArtists'] as const,
|
||||
},
|
||||
albums: {
|
||||
detail: (serverId: string, query: AlbumDetailQuery) =>
|
||||
detail: (serverId: string, query?: AlbumDetailQuery) =>
|
||||
[serverId, 'albums', 'detail', query] as const,
|
||||
list: (serverId: string, query: AlbumListQuery) => [serverId, 'albums', 'list', query] as const,
|
||||
root: ['albums'],
|
||||
list: (serverId: string, query?: AlbumListQuery) =>
|
||||
[serverId, 'albums', 'list', query] as const,
|
||||
root: (serverId: string) => [serverId, 'albums'],
|
||||
serverRoot: (serverId: string) => [serverId, 'albums'],
|
||||
songs: (serverId: string, query: SongListQuery) =>
|
||||
[serverId, 'albums', 'songs', query] as const,
|
||||
},
|
||||
artists: {
|
||||
list: (serverId: string, query?: ArtistListQuery) =>
|
||||
[serverId, 'artists', 'list', query] as const,
|
||||
root: (serverId: string) => [serverId, 'artists'] as const,
|
||||
},
|
||||
genres: {
|
||||
list: (serverId: string) => [serverId, 'genres', 'list'] as const,
|
||||
root: (serverId: string) => [serverId, 'genres'] as const,
|
||||
|
|
@ -21,6 +38,7 @@ export const queryKeys = {
|
|||
root: (serverId: string) => [serverId] as const,
|
||||
},
|
||||
songs: {
|
||||
list: (serverId: string, query: SongListQuery) => [serverId, 'songs', 'list', query] as const,
|
||||
list: (serverId: string, query?: SongListQuery) => [serverId, 'songs', 'list', query] as const,
|
||||
root: (serverId: string) => [serverId, 'songs'] as const,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -177,7 +177,11 @@ const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArt
|
|||
|
||||
const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist);
|
||||
|
||||
return artists;
|
||||
return {
|
||||
items: artists,
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: null,
|
||||
};
|
||||
};
|
||||
|
||||
const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,11 @@ export type SSAlbumArtistDetailResponse = {
|
|||
};
|
||||
};
|
||||
|
||||
export type SSAlbumArtistList = SSAlbumArtistListEntry[];
|
||||
export type SSAlbumArtistList = {
|
||||
items: SSAlbumArtistListEntry[];
|
||||
startIndex: number;
|
||||
totalRecordCount: number | null;
|
||||
};
|
||||
|
||||
export type SSAlbumArtistListResponse = {
|
||||
artists: {
|
||||
|
|
|
|||
|
|
@ -202,13 +202,19 @@ export type Song = {
|
|||
};
|
||||
|
||||
export type AlbumArtist = {
|
||||
albumCount: number | null;
|
||||
backgroundImageUrl: string | null;
|
||||
biography: string | null;
|
||||
createdAt: string;
|
||||
duration: number | null;
|
||||
genres: Genre[];
|
||||
id: string;
|
||||
imageUrl: string | null;
|
||||
isFavorite: boolean;
|
||||
lastPlayedAt: string | null;
|
||||
name: string;
|
||||
remoteCreatedAt: string | null;
|
||||
serverFolderId: string;
|
||||
updatedAt: string;
|
||||
playCount: number | null;
|
||||
rating: number | null;
|
||||
songCount: number | null;
|
||||
};
|
||||
|
||||
export type RelatedAlbumArtist = {
|
||||
|
|
@ -418,6 +424,7 @@ export enum SongListSort {
|
|||
|
||||
export type SongListQuery = {
|
||||
albumIds?: string[];
|
||||
artistIds?: string[];
|
||||
jfParams?: {
|
||||
filters?: string;
|
||||
genreIds?: string;
|
||||
|
|
@ -432,7 +439,8 @@ export type SongListQuery = {
|
|||
limit?: number;
|
||||
musicFolderId?: string;
|
||||
ndParams?: {
|
||||
artist_id?: string;
|
||||
album_id?: string[];
|
||||
artist_id?: string[];
|
||||
compilation?: boolean;
|
||||
genre_id?: string;
|
||||
has_rating?: boolean;
|
||||
|
|
@ -554,6 +562,7 @@ export type AlbumArtistListQuery = {
|
|||
name?: string;
|
||||
starred?: boolean;
|
||||
};
|
||||
searchTerm?: string;
|
||||
sortBy: AlbumArtistListSort;
|
||||
sortOrder: SortOrder;
|
||||
startIndex: number;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue