mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-15 16:04:19 +02:00
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
This commit is contained in:
@@ -143,6 +143,7 @@
|
|||||||
"resetToDefault": "Reset to default",
|
"resetToDefault": "Reset to default",
|
||||||
"restartRequired": "Restart required",
|
"restartRequired": "Restart required",
|
||||||
"right": "Right",
|
"right": "Right",
|
||||||
|
"role": "Role",
|
||||||
"sampleRate": "Sample rate",
|
"sampleRate": "Sample rate",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"saveAndReplace": "Save and replace",
|
"saveAndReplace": "Save and replace",
|
||||||
|
|||||||
@@ -428,16 +428,18 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
? query.artistIds
|
? query.artistIds
|
||||||
: query.artistIds?.[0];
|
: query.artistIds?.[0];
|
||||||
|
|
||||||
|
const key = query.role ? `role_${query.role}_id` : 'artist_id';
|
||||||
|
|
||||||
const res = await ndApiClient(apiClientProps).getAlbumList({
|
const res = await ndApiClient(apiClientProps).getAlbumList({
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
_end: query.startIndex + (query.limit || 0),
|
||||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||||
_sort: albumListSortMap.navidrome[query.sortBy],
|
_sort: albumListSortMap.navidrome[query.sortBy],
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
artist_id: artistIds,
|
|
||||||
compilation: query.compilation,
|
compilation: query.compilation,
|
||||||
genre_id: genres,
|
genre_id: genres,
|
||||||
has_rating: query.hasRating,
|
has_rating: query.hasRating,
|
||||||
|
[key]: artistIds,
|
||||||
library_id: getLibraryId(query.musicFolderId),
|
library_id: getLibraryId(query.musicFolderId),
|
||||||
name: query.searchTerm,
|
name: query.searchTerm,
|
||||||
recently_played: query.isRecentlyPlayed,
|
recently_played: query.isRecentlyPlayed,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
|
UseQueryResult,
|
||||||
useSuspenseQuery,
|
useSuspenseQuery,
|
||||||
UseSuspenseQueryResult,
|
UseSuspenseQueryResult,
|
||||||
} from '@tanstack/react-query';
|
} from '@tanstack/react-query';
|
||||||
@@ -1061,6 +1062,7 @@ const AlbumArtistMetadataSimilarArtists = ({
|
|||||||
mbz: null,
|
mbz: null,
|
||||||
name: relatedArtist.name,
|
name: relatedArtist.name,
|
||||||
playCount: null,
|
playCount: null,
|
||||||
|
roles: null,
|
||||||
similarArtists: null,
|
similarArtists: null,
|
||||||
songCount: null,
|
songCount: null,
|
||||||
userFavorite: relatedArtist.userFavorite,
|
userFavorite: relatedArtist.userFavorite,
|
||||||
@@ -1101,13 +1103,17 @@ const AlbumArtistMetadataSimilarArtists = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface AlbumArtistDetailContentProps {
|
interface AlbumArtistDetailContentProps {
|
||||||
albumsQuery: UseSuspenseQueryResult<AlbumListResponse, Error>;
|
albumsQuery: UseQueryResult<AlbumListResponse, Error>;
|
||||||
detailQuery: UseSuspenseQueryResult<AlbumArtistDetailResponse, Error>;
|
detailQuery: UseSuspenseQueryResult<AlbumArtistDetailResponse, Error>;
|
||||||
|
role: null | string;
|
||||||
|
setRole: (role: null | string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumArtistDetailContent = ({
|
export const AlbumArtistDetailContent = ({
|
||||||
albumsQuery,
|
albumsQuery,
|
||||||
detailQuery,
|
detailQuery,
|
||||||
|
role,
|
||||||
|
setRole,
|
||||||
}: AlbumArtistDetailContentProps) => {
|
}: AlbumArtistDetailContentProps) => {
|
||||||
const artistItems = useArtistItems();
|
const artistItems = useArtistItems();
|
||||||
const artistRadioCount = useArtistRadioCount();
|
const artistRadioCount = useArtistRadioCount();
|
||||||
@@ -1220,7 +1226,13 @@ export const AlbumArtistDetailContent = ({
|
|||||||
routeId={routeId}
|
routeId={routeId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ArtistAlbums albumsQuery={albumsQuery} order={itemOrder.recentAlbums} />
|
<ArtistAlbums
|
||||||
|
albumsQuery={albumsQuery}
|
||||||
|
order={itemOrder.recentAlbums}
|
||||||
|
role={role}
|
||||||
|
roles={detailQuery.data?.roles}
|
||||||
|
setRole={setRole}
|
||||||
|
/>
|
||||||
{enabledItem.similarArtists && (
|
{enabledItem.similarArtists && (
|
||||||
<AlbumArtistMetadataSimilarArtists
|
<AlbumArtistMetadataSimilarArtists
|
||||||
order={itemOrder.similarArtists}
|
order={itemOrder.similarArtists}
|
||||||
@@ -1420,13 +1432,17 @@ const AlbumSection = memo(function AlbumSection({
|
|||||||
});
|
});
|
||||||
|
|
||||||
import { useArtistAlbumsGrouped } from '/@/renderer/features/artists/hooks/use-artist-albums-grouped';
|
import { useArtistAlbumsGrouped } from '/@/renderer/features/artists/hooks/use-artist-albums-grouped';
|
||||||
|
import { Select } from '/@/shared/components/select/select';
|
||||||
|
|
||||||
interface ArtistAlbumsProps {
|
interface ArtistAlbumsProps {
|
||||||
albumsQuery: UseSuspenseQueryResult<AlbumListResponse, Error>;
|
albumsQuery: UseQueryResult<AlbumListResponse, Error>;
|
||||||
order?: number;
|
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 { t } = useTranslation();
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
|
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
|
||||||
@@ -1521,6 +1537,17 @@ const ArtistAlbums = ({ albumsQuery, order }: ArtistAlbumsProps) => {
|
|||||||
}}
|
}}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
/>
|
/>
|
||||||
|
{roles?.length && (
|
||||||
|
<Select
|
||||||
|
aria-label="role"
|
||||||
|
clearable
|
||||||
|
data={roles}
|
||||||
|
onChange={setRole}
|
||||||
|
placeholder={t('common.role')}
|
||||||
|
value={role}
|
||||||
|
w={200}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ListSortByDropdownControlled
|
<ListSortByDropdownControlled
|
||||||
filters={CLIENT_SIDE_ALBUM_FILTERS}
|
filters={CLIENT_SIDE_ALBUM_FILTERS}
|
||||||
itemType={LibraryItem.ALBUM}
|
itemType={LibraryItem.ALBUM}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query';
|
import { UseQueryResult, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { forwardRef, Fragment, useCallback } from 'react';
|
import { forwardRef, Fragment, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
@@ -38,7 +38,7 @@ import { ServerFeature } from '/@/shared/types/features-types';
|
|||||||
import { Play } from '/@/shared/types/types';
|
import { Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface AlbumArtistDetailHeaderProps {
|
interface AlbumArtistDetailHeaderProps {
|
||||||
albumsQuery: UseSuspenseQueryResult<AlbumListResponse, Error>;
|
albumsQuery: UseQueryResult<AlbumListResponse, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArtistImageUploadOverlay({
|
function ArtistImageUploadOverlay({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSuspenseQueries } from '@tanstack/react-query';
|
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { Suspense, useRef } from 'react';
|
import { Suspense, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||||
@@ -34,22 +34,24 @@ const AlbumArtistDetailRouteContent = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const routeId = (artistId || albumArtistId) as string;
|
const routeId = (artistId || albumArtistId) as string;
|
||||||
|
const [role, setRole] = useState<null | string>(null);
|
||||||
|
|
||||||
const [detailQuery, albumsQuery] = useSuspenseQueries({
|
const detailQuery = useSuspenseQuery(
|
||||||
queries: [
|
artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }),
|
||||||
artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }),
|
);
|
||||||
albumQueries.list({
|
const albumsQuery = useQuery(
|
||||||
query: {
|
albumQueries.list({
|
||||||
artistIds: [routeId],
|
query: {
|
||||||
limit: -1,
|
artistIds: [routeId],
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
limit: -1,
|
||||||
sortOrder: SortOrder.DESC,
|
role: role || undefined,
|
||||||
startIndex: 0,
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
},
|
sortOrder: SortOrder.DESC,
|
||||||
serverId,
|
startIndex: 0,
|
||||||
}),
|
},
|
||||||
],
|
serverId,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const imageUrl = useItemImageUrl({
|
const imageUrl = useItemImageUrl({
|
||||||
id: detailQuery.data?.imageId || undefined,
|
id: detailQuery.data?.imageId || undefined,
|
||||||
@@ -117,7 +119,12 @@ const AlbumArtistDetailRouteContent = () => {
|
|||||||
albumsQuery={albumsQuery}
|
albumsQuery={albumsQuery}
|
||||||
ref={headerRef as React.Ref<HTMLDivElement>}
|
ref={headerRef as React.Ref<HTMLDivElement>}
|
||||||
/>
|
/>
|
||||||
<AlbumArtistDetailContent albumsQuery={albumsQuery} detailQuery={detailQuery} />
|
<AlbumArtistDetailContent
|
||||||
|
albumsQuery={albumsQuery}
|
||||||
|
detailQuery={detailQuery}
|
||||||
|
role={role}
|
||||||
|
setRole={setRole}
|
||||||
|
/>
|
||||||
</LibraryContainer>
|
</LibraryContainer>
|
||||||
</NativeScrollArea>
|
</NativeScrollArea>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
|
|||||||
@@ -395,6 +395,7 @@ const normalizeAlbumArtist = (
|
|||||||
mbz: item.ProviderIds?.MusicBrainzArtist || null,
|
mbz: item.ProviderIds?.MusicBrainzArtist || null,
|
||||||
name: item.Name,
|
name: item.Name,
|
||||||
playCount: item.UserData?.PlayCount || 0,
|
playCount: item.UserData?.PlayCount || 0,
|
||||||
|
roles: null,
|
||||||
similarArtists,
|
similarArtists,
|
||||||
songCount: item.SongCount ?? null,
|
songCount: item.SongCount ?? null,
|
||||||
uploadedImage: item.ImageTags?.Primary ?? undefined,
|
uploadedImage: item.ImageTags?.Primary ?? undefined,
|
||||||
|
|||||||
@@ -410,10 +410,12 @@ const normalizeAlbumArtist = (
|
|||||||
|
|
||||||
if (item.stats) {
|
if (item.stats) {
|
||||||
albumCount = Math.max(
|
albumCount = Math.max(
|
||||||
|
item.stats.maincredit?.albumCount ?? 0,
|
||||||
item.stats.albumartist?.albumCount ?? 0,
|
item.stats.albumartist?.albumCount ?? 0,
|
||||||
item.stats.artist?.albumCount ?? 0,
|
item.stats.artist?.albumCount ?? 0,
|
||||||
);
|
);
|
||||||
songCount = Math.max(
|
songCount = Math.max(
|
||||||
|
item.stats.maincredit?.songCount ?? 0,
|
||||||
item.stats.albumartist?.songCount ?? 0,
|
item.stats.albumartist?.songCount ?? 0,
|
||||||
item.stats.artist?.songCount ?? 0,
|
item.stats.artist?.songCount ?? 0,
|
||||||
);
|
);
|
||||||
@@ -453,6 +455,8 @@ const normalizeAlbumArtist = (
|
|||||||
mbz: item.mbzArtistId || null,
|
mbz: item.mbzArtistId || null,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
playCount: item.playCount || 0,
|
playCount: item.playCount || 0,
|
||||||
|
// filter out specifically maincredit. This is not filterable properly
|
||||||
|
roles: item.stats ? Object.keys(item.stats).filter((key) => key !== 'maincredit') : null,
|
||||||
similarArtists:
|
similarArtists:
|
||||||
item.similarArtists?.map((artist) => ({
|
item.similarArtists?.map((artist) => ({
|
||||||
id: artist.id,
|
id: artist.id,
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ const normalizeAlbumArtist = (
|
|||||||
mbz: null,
|
mbz: null,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
playCount: null,
|
playCount: null,
|
||||||
|
roles: null,
|
||||||
similarArtists:
|
similarArtists:
|
||||||
item.similarArtists?.map((artist) => ({
|
item.similarArtists?.map((artist) => ({
|
||||||
id: artist.id,
|
id: artist.id,
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ export type AlbumArtist = {
|
|||||||
mbz: null | string;
|
mbz: null | string;
|
||||||
name: string;
|
name: string;
|
||||||
playCount: null | number;
|
playCount: null | number;
|
||||||
|
roles: null | string[];
|
||||||
similarArtists: null | RelatedArtist[];
|
similarArtists: null | RelatedArtist[];
|
||||||
songCount: null | number;
|
songCount: null | number;
|
||||||
uploadedImage?: string;
|
uploadedImage?: string;
|
||||||
@@ -500,6 +501,7 @@ export interface AlbumListQuery extends AlbumListNavidromeQuery, BaseQuery<Album
|
|||||||
maxYear?: number;
|
maxYear?: number;
|
||||||
minYear?: number;
|
minYear?: number;
|
||||||
musicFolderId?: string | string[];
|
musicFolderId?: string | string[];
|
||||||
|
role?: string;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user