From e7830afc86e088bf7cab40e16aa8b4a4fd564d67 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:02:06 -0700 Subject: [PATCH] feat: Support role filters for (album) artist role for Navidrome - Adds an optional (Navidrome only, currently) field `roles` to type `AlbumArtist` - `roles` is populated using artist.stats (excludign maincredit) - `getAlbumList` supports a role filter; if specified, only filter artists with that role (ND only) - Album list on artist page can filter by role if specified (only one) - Album query is no longer suspend, as it can change multiple times --- src/i18n/locales/en.json | 1 + .../api/navidrome/navidrome-controller.ts | 4 +- .../album-artist-detail-content.tsx | 35 +++++++++++++-- .../components/album-artist-detail-header.tsx | 4 +- .../routes/album-artist-detail-route.tsx | 43 +++++++++++-------- src/shared/api/jellyfin/jellyfin-normalize.ts | 1 + .../api/navidrome/navidrome-normalize.ts | 4 ++ src/shared/api/subsonic/subsonic-normalize.ts | 1 + src/shared/types/domain-types.ts | 2 + 9 files changed, 70 insertions(+), 25 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 4e96df9ac..abee3ac3b 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -143,6 +143,7 @@ "resetToDefault": "Reset to default", "restartRequired": "Restart required", "right": "Right", + "role": "Role", "sampleRate": "Sample rate", "save": "Save", "saveAndReplace": "Save and replace", diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 0c8416f75..ae0319d76 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -428,16 +428,18 @@ export const NavidromeController: InternalControllerEndpoint = { ? query.artistIds : query.artistIds?.[0]; + const key = query.role ? `role_${query.role}_id` : 'artist_id'; + const res = await ndApiClient(apiClientProps).getAlbumList({ query: { _end: query.startIndex + (query.limit || 0), _order: sortOrderMap.navidrome[query.sortOrder], _sort: albumListSortMap.navidrome[query.sortBy], _start: query.startIndex, - artist_id: artistIds, compilation: query.compilation, genre_id: genres, has_rating: query.hasRating, + [key]: artistIds, library_id: getLibraryId(query.musicFolderId), name: query.searchTerm, recently_played: query.isRecentlyPlayed, diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index 383085104..53636628f 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -1,6 +1,7 @@ import { useQuery, useQueryClient, + UseQueryResult, useSuspenseQuery, UseSuspenseQueryResult, } from '@tanstack/react-query'; @@ -1061,6 +1062,7 @@ const AlbumArtistMetadataSimilarArtists = ({ mbz: null, name: relatedArtist.name, playCount: null, + roles: null, similarArtists: null, songCount: null, userFavorite: relatedArtist.userFavorite, @@ -1101,13 +1103,17 @@ const AlbumArtistMetadataSimilarArtists = ({ }; interface AlbumArtistDetailContentProps { - albumsQuery: UseSuspenseQueryResult; + albumsQuery: UseQueryResult; detailQuery: UseSuspenseQueryResult; + role: null | string; + setRole: (role: null | string) => void; } export const AlbumArtistDetailContent = ({ albumsQuery, detailQuery, + role, + setRole, }: AlbumArtistDetailContentProps) => { const artistItems = useArtistItems(); const artistRadioCount = useArtistRadioCount(); @@ -1220,7 +1226,13 @@ export const AlbumArtistDetailContent = ({ routeId={routeId} /> )} - + {enabledItem.similarArtists && ( ; + albumsQuery: UseQueryResult; order?: number; + role: null | string; + roles?: null | string[]; + setRole: (role: null | string) => void; } -const ArtistAlbums = ({ albumsQuery, order }: ArtistAlbumsProps) => { +const ArtistAlbums = ({ albumsQuery, order, role, roles, setRole }: ArtistAlbumsProps) => { const { t } = useTranslation(); const [searchTerm, setSearchTerm] = useState(''); const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300); @@ -1521,6 +1537,17 @@ const ArtistAlbums = ({ albumsQuery, order }: ArtistAlbumsProps) => { }} value={searchTerm} /> + {roles?.length && ( +