From 5c06624f8c287d306e5f7467499cf50742a710a7 Mon Sep 17 00:00:00 2001
From: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
Date: Sun, 18 Jan 2026 21:53:34 +0000
Subject: [PATCH] Initial work: support showing studios for jellyfin, allow
pill to be clickable (#1566)
---
src/renderer/api/jellyfin/jellyfin-api.ts | 9 ++
.../api/jellyfin/jellyfin-controller.ts | 39 +++++-
.../api/navidrome/navidrome-controller.ts | 5 +-
.../components/album-detail-content.tsx | 34 +++--
.../components/jellyfin-album-filters.tsx | 35 +----
.../components/navidrome-album-filters.tsx | 121 +----------------
.../features/shared/components/tag-filter.tsx | 124 ++++++++++++++++++
.../components/jellyfin-song-filters.tsx | 36 +----
.../components/navidrome-song-filters.tsx | 124 ++----------------
src/shared/api/jellyfin/jellyfin-types.ts | 12 ++
src/shared/types/domain-types.ts | 4 +-
11 files changed, 228 insertions(+), 315 deletions(-)
create mode 100644 src/renderer/features/shared/components/tag-filter.tsx
diff --git a/src/renderer/api/jellyfin/jellyfin-api.ts b/src/renderer/api/jellyfin/jellyfin-api.ts
index 6de03ccd6..449a2d61e 100644
--- a/src/renderer/api/jellyfin/jellyfin-api.ts
+++ b/src/renderer/api/jellyfin/jellyfin-api.ts
@@ -248,6 +248,15 @@ export const contract = c.router({
404: jfType._response.error,
},
},
+ getStudioList: {
+ method: 'GET',
+ path: 'studios',
+ query: jfType._parameters.studioList,
+ responses: {
+ 200: jfType._response.studioList,
+ 400: jfType._response.error,
+ },
+ },
getTopSongsList: {
method: 'GET',
path: 'users/:userId/items',
diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts
index cc43cbe20..f4066bea9 100644
--- a/src/renderer/api/jellyfin/jellyfin-controller.ts
+++ b/src/renderer/api/jellyfin/jellyfin-controller.ts
@@ -25,6 +25,7 @@ import {
songListSortMap,
SortOrder,
sortOrderMap,
+ Tag,
} from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
@@ -1233,12 +1234,38 @@ export const JellyfinController: InternalControllerEndpoint = {
throw new Error('failed to get tags');
}
- return {
- boolTags: res.body.Tags?.sort((a, b) =>
- a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()),
- ),
- excluded: { album: [], song: [] },
- };
+ const studioRes = await jfApiClient(apiClientProps).getStudioList({
+ query: {
+ EnableTotalRecordCount: true,
+ IncludeItemTypes: query.type === LibraryItem.SONG ? 'Audio' : 'MusicAlbum',
+ ParentId: query.folder,
+ },
+ });
+
+ if (studioRes.status !== 200) {
+ throw new Error('failed to get studios');
+ }
+
+ const tags: Tag[] = [];
+ if (res.body.Tags?.length) {
+ tags.push({
+ name: 'Tags',
+ options: res.body.Tags.sort((a, b) =>
+ a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()),
+ ).map((tag) => ({ id: tag, name: tag })),
+ });
+ }
+
+ if (studioRes.body.Items.length) {
+ tags.push({
+ name: 'Studios',
+ options: studioRes.body.Items.sort((a, b) =>
+ a.Name.toLocaleLowerCase().localeCompare(b.Name.toLocaleLowerCase()),
+ ).map((option) => ({ id: option.Name, name: option.Name })),
+ });
+ }
+
+ return { excluded: { album: [], song: [] }, tags };
},
getTopSongs: async (args) => {
const { apiClientProps, query } = args;
diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts
index 0fb63c70c..b51da1596 100644
--- a/src/renderer/api/navidrome/navidrome-controller.ts
+++ b/src/renderer/api/navidrome/navidrome-controller.ts
@@ -778,7 +778,7 @@ export const NavidromeController: InternalControllerEndpoint = {
}
}
- const enumTags = Array.from(tagsToValues)
+ const tags = Array.from(tagsToValues)
.map((data) => ({
name: data[0],
options: data[1]
@@ -793,12 +793,11 @@ export const NavidromeController: InternalControllerEndpoint = {
const excludedSongTags = Array.from(EXCLUDED_SONG_TAGS.values());
return {
- boolTags: undefined,
- enumTags,
excluded: {
album: excludedAlbumTags,
song: excludedSongTags,
},
+ tags,
};
},
getTopSongs: SubsonicController.getTopSongs,
diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx
index 2fad55b76..ae101d8eb 100644
--- a/src/renderer/features/albums/components/album-detail-content.tsx
+++ b/src/renderer/features/albums/components/album-detail-content.tsx
@@ -49,6 +49,7 @@ import {
AlbumListSort,
ExplicitStatus,
LibraryItem,
+ ServerType,
Song,
SongListSort,
SortOrder,
@@ -152,8 +153,15 @@ const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
if (!album?.recordLabels || album.recordLabels.length === 0) return [];
return album.recordLabels.map((label) => {
+ if (album._serverType === ServerType.SUBSONIC) {
+ return { id: label, label: label, url: null };
+ }
+
const searchParams = new URLSearchParams();
- const customFilters = { recordlabel: [label] };
+ const customFilters =
+ album._serverType === ServerType.JELLYFIN
+ ? { Studios: [label] }
+ : { recordlabel: [label] };
const paramsWithCustom = setJsonSearchParam(
searchParams,
FILTER_KEYS.ALBUM._CUSTOM,
@@ -183,15 +191,21 @@ const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
- {recordLabels.map((recordLabel) => (
-
- {recordLabel.label}
-
- ))}
+ {recordLabels.map((recordLabel) =>
+ recordLabel.url ? (
+
+ {recordLabel.label}
+
+ ) : (
+
+ {recordLabel.label}
+
+ ),
+ )}
diff --git a/src/renderer/features/albums/components/jellyfin-album-filters.tsx b/src/renderer/features/albums/components/jellyfin-album-filters.tsx
index 5597d81b4..f21ae6b4e 100644
--- a/src/renderer/features/albums/components/jellyfin-album-filters.tsx
+++ b/src/renderer/features/albums/components/jellyfin-album-filters.tsx
@@ -3,16 +3,15 @@ import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { getItemImageUrl } from '/@/renderer/components/item-image/item-image';
-import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { useListContext } from '/@/renderer/context/list-context';
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
-import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import {
ArtistMultiSelectRow,
GenreMultiSelectRow,
} from '/@/renderer/features/shared/components/multi-select-rows';
+import { TagFilters } from '/@/renderer/features/shared/components/tag-filter';
import { useCurrentServerId } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { Button } from '/@/shared/components/button/button';
@@ -78,19 +77,6 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
}));
}, [genreListQuery.data]);
- const tagsQuery = useQuery(
- sharedQueries.tagList({
- options: {
- gcTime: 1000 * 60 * 2,
- staleTime: 1000 * 60 * 1,
- },
- query: {
- type: LibraryItem.ALBUM,
- },
- serverId,
- }),
- );
-
const yesNoFilter = useMemo(() => {
const filters = [
{
@@ -204,13 +190,6 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
[setAlbumArtist],
);
- const handleTagFilter = useCallback(
- (e: null | string[]) => {
- setCustom({ Tags: e && e.length > 0 ? e.join('|') : null });
- },
- [setCustom],
- );
-
const debouncedHandleMinYearFilter = useDebouncedCallback(handleMinYearFilter, 300);
const debouncedHandleMaxYearFilter = useDebouncedCallback(handleMaxYearFilter, 300);
@@ -358,17 +337,7 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
value={query.maxYear ?? undefined}
/>
- {tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
-
- )}
+