Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/renderer/api/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
37 changes: 37 additions & 0 deletions src/renderer/api/jellyfin/jellyfin-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
50 changes: 50 additions & 0 deletions src/renderer/api/navidrome/navidrome-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,56 @@ export const NavidromeController: InternalControllerEndpoint = {
);
},
getDownloadUrl: SubsonicController.getDownloadUrl,
getFavoriteSongs: async (args) => {
const { apiClientProps, query } = args;

const type = query.type === 'favorite' ? 'favorite' : 'rating';

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nitpick, but the type const is redundant as you can just perform the comparison with query.type.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Updated to just use query.type here, and in the same spot in subsonic-controller.ts


if (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,
};
}

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;
Expand Down
7 changes: 4 additions & 3 deletions src/renderer/api/query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
AlbumRadioQuery,
ArtistListQuery,
ArtistRadioQuery,
FavoriteSongListQuery,
FolderQuery,
GenreListQuery,
LyricSearchQuery,
Expand Down Expand Up @@ -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;
Expand Down
50 changes: 50 additions & 0 deletions src/renderer/api/subsonic/subsonic-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,56 @@ export const SubsonicController: InternalControllerEndpoint = {
'&c=Feishin'
);
},
getFavoriteSongs: async (args) => {
const { apiClientProps, query } = args;

const type = query.type === 'favorite' ? 'favorite' : 'rating';

if (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,
};
}

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';

Expand Down
19 changes: 6 additions & 13 deletions src/renderer/features/artists/api/artists-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ import {
AlbumArtistInfoQuery,
AlbumArtistListQuery,
ArtistListQuery,
FavoriteSongListQuery,
ListCountQuery,
SongListSort,
SortOrder,
TopSongListQuery,
} from '/@/shared/types/domain-types';

Expand Down Expand Up @@ -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>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import {
useCurrentServer,
useCurrentServerId,
usePlayerSong,
useShowFavorites,
useShowRatings,
} from '/@/renderer/store';
import {
useArtistItems,
Expand Down Expand Up @@ -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,
);
Expand All @@ -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,
}),
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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';

Expand All @@ -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],
Expand Down
Loading
Loading