mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-07 21:03:27 +02:00
feat: adds option to view top rated songs in the Favorite Songs section (#2289)
* Adds Top Rating option songs to Artist's favorite songs section
This commit is contained in:
@@ -535,6 +535,18 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
getFavoriteSongs(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`${i18n.t('error.apiRouteError')}: getFavoriteSongs`);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'getFavoriteSongs',
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
getFolder(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
|
||||
@@ -679,6 +679,43 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return `${apiClientProps.server?.url}/items/${query.id}/download?apiKey=${apiClientProps.server?.credential}`;
|
||||
},
|
||||
getFavoriteSongs: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
if (!apiClientProps.server?.userId) {
|
||||
throw new Error('No userId found');
|
||||
}
|
||||
|
||||
// Gets songs sorted by play count and filters favorited songs
|
||||
const res = await jfApiClient(apiClientProps).getTopSongsList({
|
||||
params: {
|
||||
userId: apiClientProps.server?.userId,
|
||||
},
|
||||
query: {
|
||||
ArtistIds: query.artistId,
|
||||
Fields: JF_FIELDS.SONG,
|
||||
IncludeItemTypes: 'Audio',
|
||||
IsFavorite: true,
|
||||
Limit: query.limit,
|
||||
Recursive: true,
|
||||
SortBy: JFSongListSort.PLAY_COUNT,
|
||||
SortOrder: 'Descending',
|
||||
UserId: apiClientProps.server?.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get top song list');
|
||||
}
|
||||
|
||||
const items = res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server));
|
||||
|
||||
return {
|
||||
items,
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.body.TotalRecordCount,
|
||||
};
|
||||
},
|
||||
getFolder: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
const userId = apiClientProps.server?.userId;
|
||||
|
||||
@@ -557,6 +557,57 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
);
|
||||
},
|
||||
getDownloadUrl: SubsonicController.getDownloadUrl,
|
||||
getFavoriteSongs: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
// if user selects 'rating'
|
||||
if (query.type === 'rating') {
|
||||
const res = await NavidromeController.getSongList({
|
||||
apiClientProps,
|
||||
query: {
|
||||
artistIds: [query.artistId],
|
||||
sortBy: SongListSort.RATING,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const songsWithHighRating = orderBy(
|
||||
res.items.filter((song) => song.userRating !== null && song.userRating > 2),
|
||||
['userRating', 'userFavorite', 'playCount', 'albumId', 'trackNumber'],
|
||||
['desc', 'desc', 'desc', 'asc', 'asc'],
|
||||
);
|
||||
|
||||
return {
|
||||
items: songsWithHighRating,
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.totalRecordCount,
|
||||
};
|
||||
}
|
||||
|
||||
// else if user selects 'favorite'
|
||||
const res = await NavidromeController.getSongList({
|
||||
apiClientProps,
|
||||
query: {
|
||||
artistIds: [query.artistId],
|
||||
sortBy: SongListSort.FAVORITED,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const songsWithFavorite = orderBy(
|
||||
res.items.filter((song) => song.userFavorite),
|
||||
['userFavorite', 'userRating', 'playCount', 'albumId', 'trackNumber'],
|
||||
['desc', 'desc', 'desc', 'asc', 'asc'],
|
||||
);
|
||||
|
||||
return {
|
||||
items: songsWithFavorite,
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.totalRecordCount,
|
||||
};
|
||||
},
|
||||
getFolder: SubsonicController.getFolder,
|
||||
getGenreList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
AlbumRadioQuery,
|
||||
ArtistListQuery,
|
||||
ArtistRadioQuery,
|
||||
FavoriteSongListQuery,
|
||||
FolderQuery,
|
||||
GenreListQuery,
|
||||
LyricSearchQuery,
|
||||
@@ -75,9 +76,9 @@ export const queryKeys: Record<
|
||||
|
||||
return [serverId, 'albumArtists', 'detail'] as const;
|
||||
},
|
||||
favoriteSongs: (serverId: string, artistId?: string) => {
|
||||
if (artistId) {
|
||||
return [serverId, 'albumArtists', 'favoriteSongs', artistId] as const;
|
||||
favoriteSongs: (serverId: string, query?: FavoriteSongListQuery) => {
|
||||
if (query) {
|
||||
return [serverId, 'albumArtists', 'favoriteSongs', query] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'albumArtists', 'favoriteSongs'] as const;
|
||||
|
||||
@@ -976,6 +976,56 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
'&c=Feishin'
|
||||
);
|
||||
},
|
||||
getFavoriteSongs: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
// if user selects 'rating'
|
||||
if (query.type === 'rating') {
|
||||
const res = await SubsonicController.getSongList({
|
||||
apiClientProps,
|
||||
query: {
|
||||
artistIds: [query.artistId],
|
||||
sortBy: SongListSort.RATING,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const songsWithHighRating = orderBy(
|
||||
res.items.filter((song) => song.userRating !== null && song.userRating > 2),
|
||||
['userRating', 'userFavorite', 'playCount', 'albumId', 'trackNumber'],
|
||||
['desc', 'desc', 'desc', 'asc', 'asc'],
|
||||
);
|
||||
|
||||
return {
|
||||
items: songsWithHighRating,
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.totalRecordCount,
|
||||
};
|
||||
}
|
||||
|
||||
// else if user selects 'favorites'
|
||||
const res = await SubsonicController.getSongList({
|
||||
apiClientProps,
|
||||
query: {
|
||||
artistIds: [query.artistId],
|
||||
sortBy: SongListSort.FAVORITED,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
});
|
||||
const songsWithFavorite = orderBy(
|
||||
res.items.filter((song) => song.userFavorite),
|
||||
['userFavorite', 'userRating', 'playCount', 'albumId', 'trackNumber'],
|
||||
['desc', 'desc', 'desc', 'asc', 'asc'],
|
||||
);
|
||||
|
||||
return {
|
||||
items: songsWithFavorite,
|
||||
startIndex: 0,
|
||||
totalRecordCount: res.totalRecordCount,
|
||||
};
|
||||
},
|
||||
getFolder: async ({ apiClientProps, query }) => {
|
||||
const sortOrder = (query.sortOrder?.toLowerCase() ?? 'asc') as 'asc' | 'desc';
|
||||
|
||||
|
||||
@@ -10,9 +10,8 @@ import {
|
||||
AlbumArtistInfoQuery,
|
||||
AlbumArtistListQuery,
|
||||
ArtistListQuery,
|
||||
FavoriteSongListQuery,
|
||||
ListCountQuery,
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
TopSongListQuery,
|
||||
} from '/@/shared/types/domain-types';
|
||||
|
||||
@@ -137,22 +136,16 @@ export const artistsQueries = {
|
||||
...args.options,
|
||||
});
|
||||
},
|
||||
favoriteSongs: (args: QueryHookArgs<{ artistId: string }>) => {
|
||||
favoriteSongs: (args: QueryHookArgs<FavoriteSongListQuery>) => {
|
||||
return queryOptions({
|
||||
queryFn: ({ signal }) => {
|
||||
return api.controller.getSongList({
|
||||
return api.controller.getFavoriteSongs({
|
||||
apiClientProps: { serverId: args.serverId, signal },
|
||||
query: {
|
||||
artistIds: [args.query.artistId],
|
||||
favorite: true,
|
||||
limit: -1,
|
||||
sortBy: SongListSort.RELEASE_DATE,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
query: args.query,
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.favoriteSongs(args.serverId, args.query.artistId),
|
||||
queryKey: queryKeys.albumArtists.favoriteSongs(args.serverId, args.query),
|
||||
...args.options,
|
||||
});
|
||||
},
|
||||
topSongs: (args: QueryHookArgs<TopSongListQuery>) => {
|
||||
|
||||
@@ -52,6 +52,8 @@ import {
|
||||
useCurrentServer,
|
||||
useCurrentServerId,
|
||||
usePlayerSong,
|
||||
useShowFavorites,
|
||||
useShowRatings,
|
||||
} from '/@/renderer/store';
|
||||
import {
|
||||
useArtistItems,
|
||||
@@ -605,6 +607,12 @@ const AlbumArtistMetadataFavoriteSongs = ({
|
||||
const { t } = useTranslation();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
|
||||
const [favoriteSongsQueryType, setFavoriteSongsQueryType] = useLocalStorage<
|
||||
'favorite' | 'rating'
|
||||
>({
|
||||
defaultValue: 'favorite',
|
||||
key: 'album-artist-favorite-songs-query-type',
|
||||
});
|
||||
const albumArtistDetailFavoriteSongsSort = useAppStore(
|
||||
(state) => state.albumArtistDetailFavoriteSongsSort,
|
||||
);
|
||||
@@ -617,11 +625,24 @@ const AlbumArtistMetadataFavoriteSongs = ({
|
||||
const currentSong = usePlayerSong();
|
||||
const player = usePlayer();
|
||||
const serverId = useCurrentServerId();
|
||||
const server = useCurrentServer();
|
||||
const showRatings = useShowRatings();
|
||||
const showFavorites = useShowFavorites();
|
||||
const showFavoriteAndRatingSegmentControl =
|
||||
server?.type !== ServerType.JELLYFIN && showFavorites && showRatings;
|
||||
|
||||
let favoriteSongsQueryTypeFilter = favoriteSongsQueryType;
|
||||
if (showRatings && !showFavorites) {
|
||||
favoriteSongsQueryTypeFilter = 'rating';
|
||||
} else if (!showRatings && showFavorites) {
|
||||
favoriteSongsQueryTypeFilter = 'favorite';
|
||||
}
|
||||
|
||||
const favoriteSongsQuery = useQuery({
|
||||
...artistsQueries.favoriteSongs({
|
||||
query: {
|
||||
artistId: routeId,
|
||||
type: favoriteSongsQueryTypeFilter,
|
||||
},
|
||||
serverId: serverId,
|
||||
}),
|
||||
@@ -795,6 +816,27 @@ const AlbumArtistMetadataFavoriteSongs = ({
|
||||
}}
|
||||
value={searchTerm}
|
||||
/>
|
||||
{showFavoriteAndRatingSegmentControl && (
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{
|
||||
label: t('common.favorite'),
|
||||
value: 'favorite',
|
||||
},
|
||||
{
|
||||
label: t('common.rating'),
|
||||
value: 'rating',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setFavoriteSongsQueryType(
|
||||
value as 'favorite' | 'rating',
|
||||
)
|
||||
}
|
||||
size="xs"
|
||||
value={favoriteSongsQueryType}
|
||||
/>
|
||||
)}
|
||||
<ListSortByDropdownControlled
|
||||
filters={CLIENT_SIDE_SONG_FILTERS}
|
||||
itemType={LibraryItem.SONG}
|
||||
|
||||
+22
-12
@@ -1,4 +1,4 @@
|
||||
import { useSuspenseQueries } from '@tanstack/react-query';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useAppStore } from '/@/renderer/store/app.store';
|
||||
import { useCurrentServer } from '/@/renderer/store/auth.store';
|
||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||
import { sortSongList } from '/@/shared/api/utils';
|
||||
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
@@ -36,19 +37,28 @@ const AlbumArtistDetailFavoriteSongsListRoute = () => {
|
||||
const server = useCurrentServer();
|
||||
const pageKey = LibraryItem.SONG;
|
||||
|
||||
const [detailQuery, favoriteSongsQuery] = useSuspenseQueries({
|
||||
queries: [
|
||||
artistsQueries.albumArtistDetail({
|
||||
query: { id: routeId },
|
||||
serverId: server?.id,
|
||||
}),
|
||||
artistsQueries.favoriteSongs({
|
||||
query: { artistId: routeId },
|
||||
serverId: server?.id,
|
||||
}),
|
||||
],
|
||||
const [favoriteSongsQueryType] = useLocalStorage<'favorite' | 'rating'>({
|
||||
defaultValue: 'favorite',
|
||||
key: 'album-artist-favorite-songs-query-type',
|
||||
});
|
||||
|
||||
const detailQuery = useSuspenseQuery(
|
||||
artistsQueries.albumArtistDetail({
|
||||
query: { id: routeId },
|
||||
serverId: server?.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const favoriteSongsQuery = useSuspenseQuery(
|
||||
artistsQueries.favoriteSongs({
|
||||
query: {
|
||||
artistId: routeId,
|
||||
type: favoriteSongsQueryType,
|
||||
},
|
||||
serverId: server?.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const songs = useMemo(
|
||||
() => favoriteSongsQuery?.data?.items || [],
|
||||
[favoriteSongsQuery?.data?.items],
|
||||
|
||||
@@ -1329,6 +1329,17 @@ export type ArtistInfoQuery = {
|
||||
musicFolderId?: string | string[];
|
||||
};
|
||||
|
||||
export type FavoriteSongListArgs = BaseEndpointArgs & { query: FavoriteSongListQuery };
|
||||
|
||||
export type FavoriteSongListQuery = {
|
||||
artistId: string;
|
||||
limit?: number;
|
||||
type?: 'favorite' | 'rating';
|
||||
};
|
||||
|
||||
// Favorite Songs List
|
||||
export type FavoriteSongListResponse = BasePaginatedResponse<Song[]>;
|
||||
|
||||
export type FullLyricsMetadata = Omit<InternetProviderLyricResponse, 'id' | 'lyrics' | 'source'> & {
|
||||
lyrics: LyricsResponse;
|
||||
offsetMs?: number;
|
||||
@@ -1566,6 +1577,7 @@ export type ControllerEndpoint = {
|
||||
getArtistListCount: (args: ArtistListCountArgs) => Promise<number>;
|
||||
getArtistRadio: (args: ArtistRadioArgs) => Promise<Song[]>;
|
||||
getDownloadUrl: (args: DownloadArgs) => string;
|
||||
getFavoriteSongs: (args: FavoriteSongListArgs) => Promise<FavoriteSongListResponse>;
|
||||
getFolder: (args: FolderArgs) => Promise<FolderResponse>;
|
||||
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
||||
getImageRequest: (args: ImageArgs) => ImageRequest | null;
|
||||
@@ -1719,6 +1731,9 @@ export type InternalControllerEndpoint = {
|
||||
getArtistListCount: (args: ReplaceApiClientProps<ArtistListCountArgs>) => Promise<number>;
|
||||
getArtistRadio: (args: ReplaceApiClientProps<ArtistRadioArgs>) => Promise<Song[]>;
|
||||
getDownloadUrl: (args: ReplaceApiClientProps<DownloadArgs>) => string;
|
||||
getFavoriteSongs: (
|
||||
args: ReplaceApiClientProps<FavoriteSongListArgs>,
|
||||
) => Promise<FavoriteSongListResponse>;
|
||||
getFolder: (args: ReplaceApiClientProps<FolderArgs>) => Promise<FolderResponse>;
|
||||
getGenreList: (args: ReplaceApiClientProps<GenreListArgs>) => Promise<GenreListResponse>;
|
||||
getImageRequest: (args: ReplaceApiClientProps<ImageArgs>) => ImageRequest | null;
|
||||
|
||||
Reference in New Issue
Block a user