add has_rating filter for Navidrome song list

This commit is contained in:
jeffvli
2026-03-06 18:03:10 -08:00
parent d4c2b1e914
commit 9e08157517
6 changed files with 72 additions and 3 deletions
@@ -30,6 +30,7 @@ import { ServerFeature } from '/@/shared/types/features-types';
const VERSION_INFO: VersionInfo = [ const VERSION_INFO: VersionInfo = [
// Why 2? Subsonic controller will return 1 for its own implementation // Why 2? Subsonic controller will return 1 for its own implementation
// Use 2 to denote that Navidrome's own API has a different endpoint // Use 2 to denote that Navidrome's own API has a different endpoint
['0.60.4', { [ServerFeature.TRACK_YES_NO_RATING_FILTER]: [1] }],
['0.57.0', { [ServerFeature.SERVER_PLAY_QUEUE]: [2] }], ['0.57.0', { [ServerFeature.SERVER_PLAY_QUEUE]: [2] }],
['0.56.0', { [ServerFeature.TRACK_ALBUM_ARTIST_SEARCH]: [1] }], ['0.56.0', { [ServerFeature.TRACK_ALBUM_ARTIST_SEARCH]: [1] }],
['0.55.0', { [ServerFeature.BFR]: [1], [ServerFeature.TAGS]: [1] }], ['0.55.0', { [ServerFeature.BFR]: [1], [ServerFeature.TAGS]: [1] }],
@@ -669,6 +670,7 @@ export const NavidromeController: InternalControllerEndpoint = {
...subsonicArgs.features, ...subsonicArgs.features,
...navidromeFeatures, ...navidromeFeatures,
publicPlaylist: [1], publicPlaylist: [1],
[ServerFeature.ALBUM_YES_NO_RATING_FILTER]: [1],
[ServerFeature.MUSIC_FOLDER_MULTISELECT]: [1], [ServerFeature.MUSIC_FOLDER_MULTISELECT]: [1],
}; };
@@ -741,6 +743,10 @@ export const NavidromeController: InternalControllerEndpoint = {
album_id: query.albumIds, album_id: query.albumIds,
genre_id: query.genreIds, genre_id: query.genreIds,
[getArtistSongKey(apiClientProps.server)]: query.artistIds ?? query.albumArtistIds, [getArtistSongKey(apiClientProps.server)]: query.artistIds ?? query.albumArtistIds,
...(hasFeature(apiClientProps.server, ServerFeature.TRACK_YES_NO_RATING_FILTER) &&
query.hasRating !== undefined
? { has_rating: query.hasRating }
: {}),
library_id: getLibraryId(query.musicFolderId), library_id: getLibraryId(query.musicFolderId),
starred: query.favorite, starred: query.favorite,
title: query.searchTerm, title: query.searchTerm,
@@ -13,6 +13,7 @@ import { TagFilters } from '/@/renderer/features/shared/components/tag-filter';
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters'; import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store'; import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { hasFeature } from '/@/shared/api/utils';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select'; import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select';
@@ -27,6 +28,7 @@ import {
LibraryItem, LibraryItem,
SortOrder, SortOrder,
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
interface NavidromeSongFiltersProps { interface NavidromeSongFiltersProps {
disableArtistFilter?: boolean; disableArtistFilter?: boolean;
@@ -40,8 +42,18 @@ export const NavidromeSongFilters = ({
const { t } = useTranslation(); const { t } = useTranslation();
const server = useCurrentServer(); const server = useCurrentServer();
const serverId = server.id; const serverId = server.id;
const { query, setArtistIds, setCustom, setFavorite, setGenreId, setMaxYear, setMinYear } = const {
useSongListFilters(); query,
setArtistIds,
setCustom,
setFavorite,
setGenreId,
setHasRating,
setMaxYear,
setMinYear,
} = useSongListFilters();
const showRatingFilter = hasFeature(server, ServerFeature.TRACK_YES_NO_RATING_FILTER);
const genreListQuery = useQuery( const genreListQuery = useQuery(
genresQueries.list({ genresQueries.list({
@@ -278,6 +290,25 @@ export const NavidromeSongFilters = ({
w="100%" w="100%"
/> />
</Stack> </Stack>
{showRatingFilter && (
<>
<Divider my="md" />
<Stack gap="xs">
<Text size="sm" weight={500}>
{t('filter.isRated', { postProcess: 'sentenceCase' })}
</Text>
<SegmentedControl
data={segmentedControlData}
onChange={(value) => {
setHasRating(segmentValueToBoolean(value));
}}
size="sm"
value={booleanToSegmentValue(query.hasRating)}
w="100%"
/>
</Stack>
</>
)}
{!disableArtistFilter && ( {!disableArtistFilter && (
<> <>
<Divider my="md" /> <Divider my="md" />
@@ -53,6 +53,11 @@ export const useSongListFilters = (listKey?: ItemListKey) => {
[searchParams], [searchParams],
); );
const hasRating = useMemo(
() => parseBooleanParam(searchParams, FILTER_KEYS.SONG.HAS_RATING),
[searchParams],
);
const custom = useMemo( const custom = useMemo(
() => parseCustomFiltersParam(searchParams, FILTER_KEYS.SONG._CUSTOM), () => parseCustomFiltersParam(searchParams, FILTER_KEYS.SONG._CUSTOM),
[searchParams], [searchParams],
@@ -103,6 +108,15 @@ export const useSongListFilters = (listKey?: ItemListKey) => {
[setSearchParams], [setSearchParams],
); );
const setHasRating = useCallback(
(value: boolean | null) => {
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.SONG.HAS_RATING, value), {
replace: true,
});
},
[setSearchParams],
);
const setCustom = useCallback( const setCustom = useCallback(
( (
value: value:
@@ -142,6 +156,7 @@ export const useSongListFilters = (listKey?: ItemListKey) => {
[FILTER_KEYS.SONG.ARTIST_IDS]: null, [FILTER_KEYS.SONG.ARTIST_IDS]: null,
[FILTER_KEYS.SONG.FAVORITE]: null, [FILTER_KEYS.SONG.FAVORITE]: null,
[FILTER_KEYS.SONG.GENRE_ID]: null, [FILTER_KEYS.SONG.GENRE_ID]: null,
[FILTER_KEYS.SONG.HAS_RATING]: null,
[FILTER_KEYS.SONG.MAX_YEAR]: null, [FILTER_KEYS.SONG.MAX_YEAR]: null,
[FILTER_KEYS.SONG.MIN_YEAR]: null, [FILTER_KEYS.SONG.MIN_YEAR]: null,
}, },
@@ -160,10 +175,22 @@ export const useSongListFilters = (listKey?: ItemListKey) => {
[FILTER_KEYS.SONG.ARTIST_IDS]: artistIds ?? undefined, [FILTER_KEYS.SONG.ARTIST_IDS]: artistIds ?? undefined,
[FILTER_KEYS.SONG.FAVORITE]: favorite ?? undefined, [FILTER_KEYS.SONG.FAVORITE]: favorite ?? undefined,
[FILTER_KEYS.SONG.GENRE_ID]: genreId ?? undefined, [FILTER_KEYS.SONG.GENRE_ID]: genreId ?? undefined,
[FILTER_KEYS.SONG.HAS_RATING]: hasRating ?? undefined,
[FILTER_KEYS.SONG.MAX_YEAR]: maxYear ?? undefined, [FILTER_KEYS.SONG.MAX_YEAR]: maxYear ?? undefined,
[FILTER_KEYS.SONG.MIN_YEAR]: minYear ?? undefined, [FILTER_KEYS.SONG.MIN_YEAR]: minYear ?? undefined,
}), }),
[searchTerm, sortBy, sortOrder, custom, artistIds, favorite, genreId, maxYear, minYear], [
searchTerm,
sortBy,
sortOrder,
custom,
artistIds,
favorite,
genreId,
hasRating,
maxYear,
minYear,
],
); );
return { return {
@@ -173,6 +200,7 @@ export const useSongListFilters = (listKey?: ItemListKey) => {
setCustom, setCustom,
setFavorite, setFavorite,
setGenreId, setGenreId,
setHasRating,
setMaxYear, setMaxYear,
setMinYear, setMinYear,
setSearchTerm, setSearchTerm,
@@ -580,6 +580,7 @@ const songListParameters = paginationParameters.extend({
artist_id: z.array(z.string()).optional(), artist_id: z.array(z.string()).optional(),
artists_id: z.array(z.string()).optional(), artists_id: z.array(z.string()).optional(),
genre_id: z.array(z.string()).optional(), genre_id: z.array(z.string()).optional(),
has_rating: z.boolean().optional(),
library_id: z.array(z.string()).optional(), library_id: z.array(z.string()).optional(),
path: z.string().optional(), path: z.string().optional(),
starred: z.boolean().optional(), starred: z.boolean().optional(),
+1
View File
@@ -623,6 +623,7 @@ export interface SongListQuery extends BaseQuery<SongListSort> {
artistIds?: string[]; artistIds?: string[];
favorite?: boolean; favorite?: boolean;
genreIds?: string[]; genreIds?: string[];
hasRating?: boolean;
imageSize?: number; imageSize?: number;
limit?: number; limit?: number;
maxYear?: number; maxYear?: number;
+2
View File
@@ -1,6 +1,7 @@
// Should follow a strict naming convention: "<FEATURE GROUP>_<FEATURE NAME>" // Should follow a strict naming convention: "<FEATURE GROUP>_<FEATURE NAME>"
// For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART" // For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART"
export enum ServerFeature { export enum ServerFeature {
ALBUM_YES_NO_RATING_FILTER = 'albumYesNoRatingFilter',
BFR = 'bfr', BFR = 'bfr',
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured', LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured', LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
@@ -13,6 +14,7 @@ export enum ServerFeature {
SIMILAR_SONGS_MUSIC_FOLDER = 'similarSongsMusicFolder', SIMILAR_SONGS_MUSIC_FOLDER = 'similarSongsMusicFolder',
TAGS = 'tags', TAGS = 'tags',
TRACK_ALBUM_ARTIST_SEARCH = 'trackAlbumArtistSearch', TRACK_ALBUM_ARTIST_SEARCH = 'trackAlbumArtistSearch',
TRACK_YES_NO_RATING_FILTER = 'trackYesNoRatingFilter',
} }
export type ServerFeatures = Partial<Record<ServerFeature, number[]>>; export type ServerFeatures = Partial<Record<ServerFeature, number[]>>;