mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-14 04:20:07 +02:00
feat: add artist radio and track radio (in context menu) (#1437)
* Add API support for artist radio and track radio features * Add translation strings and settings UI for artist radio count --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
@@ -320,6 +320,20 @@ export const controller: GeneralController = {
|
||||
query: mergeMusicFolderId(args.query, server),
|
||||
});
|
||||
},
|
||||
getArtistRadio(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: getArtistRadio`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'getArtistRadio',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
getDownloadUrl(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
|
||||
@@ -426,6 +426,27 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getArtistRadio: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
// For Jellyfin, use instant mix for artist radio
|
||||
const res = await jfApiClient(apiClientProps).getInstantMix({
|
||||
params: {
|
||||
itemId: query.artistId,
|
||||
},
|
||||
query: {
|
||||
Fields: 'Genres, DateCreated, MediaSources, ParentId',
|
||||
Limit: query.count,
|
||||
UserId: apiClientProps.server?.userId || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get artist radio songs');
|
||||
}
|
||||
|
||||
return res.body.Items.map((song) => jfNormalize.song(song, apiClientProps.server));
|
||||
},
|
||||
getDownloadUrl: (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
|
||||
@@ -401,6 +401,32 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getArtistRadio: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
// Use getSimilarSongs2 API for artist radio
|
||||
const res = await ssApiClient({
|
||||
...apiClientProps,
|
||||
silent: true,
|
||||
}).getSimilarSongs2({
|
||||
query: {
|
||||
count: query.count,
|
||||
id: query.artistId,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get artist radio songs');
|
||||
}
|
||||
|
||||
if (!res.body.similarSongs2?.song) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return res.body.similarSongs2.song.map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
);
|
||||
},
|
||||
getDownloadUrl: SubsonicController.getDownloadUrl,
|
||||
getFolder: SubsonicController.getFolder,
|
||||
getGenreList: async (args) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
AlbumDetailQuery,
|
||||
AlbumListQuery,
|
||||
ArtistListQuery,
|
||||
ArtistRadioQuery,
|
||||
FolderQuery,
|
||||
GenreListQuery,
|
||||
LyricSearchQuery,
|
||||
@@ -340,6 +341,10 @@ export const queryKeys: Record<
|
||||
root: (serverId: string) => [serverId] as const,
|
||||
},
|
||||
songs: {
|
||||
artistRadio: (serverId: string, query?: ArtistRadioQuery) => {
|
||||
if (query) return [serverId, 'songs', 'artistRadio', query] as const;
|
||||
return [serverId, 'songs', 'artistRadio'] as const;
|
||||
},
|
||||
count: (serverId: string, query?: SongListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
|
||||
@@ -201,6 +201,14 @@ export const contract = c.router({
|
||||
200: ssType._response.similarSongs,
|
||||
},
|
||||
},
|
||||
getSimilarSongs2: {
|
||||
method: 'GET',
|
||||
path: 'getSimilarSongs2',
|
||||
query: ssType._parameters.similarSongs2,
|
||||
responses: {
|
||||
200: ssType._response.similarSongs2,
|
||||
},
|
||||
},
|
||||
getSong: {
|
||||
method: 'GET',
|
||||
path: 'getSong.view',
|
||||
|
||||
@@ -682,6 +682,28 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
...args,
|
||||
query: { ...args.query, startIndex: 0 },
|
||||
}).then((res) => res!.totalRecordCount!),
|
||||
getArtistRadio: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getSimilarSongs2({
|
||||
query: {
|
||||
count: query.count,
|
||||
id: query.artistId,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get artist radio songs');
|
||||
}
|
||||
|
||||
if (!res.body.similarSongs2?.song) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return res.body.similarSongs2.song.map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
);
|
||||
},
|
||||
getDownloadUrl: (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -880,6 +902,7 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
totalRecordCount: res.body.musicFolders.musicFolder.length,
|
||||
};
|
||||
},
|
||||
|
||||
getPlaylistDetail: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -895,7 +918,6 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
|
||||
},
|
||||
|
||||
getPlaylistList: async ({ apiClientProps, query }) => {
|
||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user