From 62ab4b7a00b997f9d2e11b53167a1a981fd5ea36 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Mon, 29 Dec 2025 21:16:39 -0800 Subject: [PATCH] add releaseType to album header --- .../albums/components/album-detail-header.tsx | 4 +- .../shared/components/library-header.tsx | 84 ++++++++++++++++++- src/shared/api/jellyfin/jellyfin-normalize.ts | 1 + .../api/navidrome/navidrome-normalize.ts | 1 + src/shared/api/navidrome/navidrome-types.ts | 9 ++ src/shared/api/subsonic/subsonic-normalize.ts | 14 ++++ src/shared/types/domain-types.ts | 1 + 7 files changed, 111 insertions(+), 3 deletions(-) diff --git a/src/renderer/features/albums/components/album-detail-header.tsx b/src/renderer/features/albums/components/album-detail-header.tsx index 0365f60a6..c19a28601 100644 --- a/src/renderer/features/albums/components/album-detail-header.tsx +++ b/src/renderer/features/albums/components/album-detail-header.tsx @@ -91,11 +91,13 @@ export const AlbumDetailHeader = forwardRef((_props, ref) => { type: 'header', }); + const releaseType = detailQuery?.data?.releaseType || undefined; + return ( diff --git a/src/renderer/features/shared/components/library-header.tsx b/src/renderer/features/shared/components/library-header.tsx index 52f786ccb..6a85c0615 100644 --- a/src/renderer/features/shared/components/library-header.tsx +++ b/src/renderer/features/shared/components/library-header.tsx @@ -17,6 +17,7 @@ import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-b import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation'; +import { titleCase } from '/@/renderer/utils'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Button } from '/@/shared/components/button/button'; import { Center } from '/@/shared/components/center/center'; @@ -34,7 +35,7 @@ interface LibraryHeaderProps { containerClassName?: string; imagePlaceholderUrl?: null | string; imageUrl?: null | string; - item: { route: string; type: LibraryItem }; + item: { releaseType?: string; route: string; type?: LibraryItem }; loading?: boolean; title: string; } @@ -51,7 +52,86 @@ export const LibraryHeader = forwardRef( setIsImageError(true); }; - const itemTypeString = () => { + const itemTypeString = (): string => { + if (item.releaseType) { + switch (item.releaseType) { + case 'album': + return t('releaseType.primary.album', { + postProcess: 'sentenceCase', + }); + case 'appears-on': + return t('page.albumArtistDetail.appearsOn', { + postProcess: 'sentenceCase', + }); + case 'audiobook': + return t('releaseType.secondary.audiobook', { + postProcess: 'sentenceCase', + }); + case 'audio drama': + return t('releaseType.secondary.audioDrama', { + postProcess: 'sentenceCase', + }); + case 'broadcast': + return t('releaseType.primary.broadcast', { + postProcess: 'sentenceCase', + }); + case 'compilation': + return t('releaseType.secondary.compilation', { + postProcess: 'sentenceCase', + }); + case 'demo': + return t('releaseType.secondary.demo', { + postProcess: 'sentenceCase', + }); + case 'dj-mix': + return t('releaseType.secondary.djMix', { + postProcess: 'sentenceCase', + }); + case 'ep': + return t('releaseType.primary.ep', { + postProcess: 'sentenceCase', + }); + case 'field recording': + return t('releaseType.secondary.fieldRecording', { + postProcess: 'sentenceCase', + }); + case 'interview': + return t('releaseType.secondary.interview', { + postProcess: 'sentenceCase', + }); + case 'live': + return t('releaseType.secondary.live', { + postProcess: 'sentenceCase', + }); + case 'mixtape/street': + return t('releaseType.secondary.mixtape', { + postProcess: 'sentenceCase', + }); + case 'other': + return t('releaseType.primary.other', { + postProcess: 'sentenceCase', + }); + case 'remix': + return t('releaseType.secondary.remix', { + postProcess: 'sentenceCase', + }); + case 'single': + return t('releaseType.primary.single', { + postProcess: 'sentenceCase', + }); + case 'soundtrack': + return t('releaseType.secondary.soundtrack', { + postProcess: 'sentenceCase', + }); + case 'spokenword': + return t('releaseType.secondary.spokenWord', { + postProcess: 'sentenceCase', + }); + default: + return titleCase(item.releaseType); + } + } + switch (item.type) { case LibraryItem.ALBUM: return t('entity.album', { count: 1 }); diff --git a/src/shared/api/jellyfin/jellyfin-normalize.ts b/src/shared/api/jellyfin/jellyfin-normalize.ts index eb3b8ff85..6e2f558ea 100644 --- a/src/shared/api/jellyfin/jellyfin-normalize.ts +++ b/src/shared/api/jellyfin/jellyfin-normalize.ts @@ -279,6 +279,7 @@ const normalizeAlbum = ( playCount: item.UserData?.PlayCount || 0, recordLabels: item.Studios?.map((entry) => entry.Name) || [], releaseDate: item.PremiereDate || null, + releaseType: null, releaseTypes: [], releaseYear: item.ProductionYear || null, size: null, diff --git a/src/shared/api/navidrome/navidrome-normalize.ts b/src/shared/api/navidrome/navidrome-normalize.ts index 8e9f8c5f6..5052168de 100644 --- a/src/shared/api/navidrome/navidrome-normalize.ts +++ b/src/shared/api/navidrome/navidrome-normalize.ts @@ -304,6 +304,7 @@ const normalizeAlbum = ( originalDate: item.originalDate || null, playCount: item.playCount || 0, releaseDate: normalizeReleaseDate(item), + releaseType: item.mbzAlbumType || null, releaseYear: item.maxYear || null, size: item.size, songCount: item.songCount, diff --git a/src/shared/api/navidrome/navidrome-types.ts b/src/shared/api/navidrome/navidrome-types.ts index 01735e24a..404fd989e 100644 --- a/src/shared/api/navidrome/navidrome-types.ts +++ b/src/shared/api/navidrome/navidrome-types.ts @@ -440,6 +440,7 @@ const album = z.object({ allArtistIds: z.string(), artist: z.string(), artistId: z.string(), + catalogNum: z.string().optional(), comment: z.string().optional(), compilation: z.boolean(), coverArtId: z.string().optional(), // Removed after v0.48.0 @@ -447,13 +448,21 @@ const album = z.object({ createdAt: z.string(), duration: z.number().optional(), explicitStatus: z.string().optional(), + externalInfoUpdatedAt: z.string().optional(), + externalUrl: z.string().optional(), fullText: z.string(), genre: z.string(), genres: z.array(genre).nullable(), id: z.string(), + importedAt: z.string().optional(), + libraryId: z.number(), + libraryName: z.string(), + libraryPath: z.string(), maxYear: z.number(), mbzAlbumArtistId: z.string().optional(), mbzAlbumId: z.string().optional(), + mbzAlbumType: z.string().optional(), + mbzReleaseGroupId: z.string().optional(), minYear: z.number(), name: z.string(), orderAlbumArtistName: z.string(), diff --git a/src/shared/api/subsonic/subsonic-normalize.ts b/src/shared/api/subsonic/subsonic-normalize.ts index c2fdf0104..3bb3ae277 100644 --- a/src/shared/api/subsonic/subsonic-normalize.ts +++ b/src/shared/api/subsonic/subsonic-normalize.ts @@ -226,6 +226,19 @@ const normalizeAlbumArtist = ( }; }; +const PRIMARY_RELEASE_TYPES = ['album', 'ep', 'single', 'broadcast', 'other']; + +const getReleaseType = ( + item: z.infer | z.infer, +) => { + if (!item.releaseTypes) { + return null; + } + + // Return the first primary release type + return item.releaseTypes.find((type) => PRIMARY_RELEASE_TYPES.includes(type)) || null; +}; + const normalizeAlbum = ( item: z.infer | z.infer, server?: null | ServerListItemWithCredential, @@ -269,6 +282,7 @@ const normalizeAlbum = ( item.releaseDate.day, ).toISOString() : null, + releaseType: getReleaseType(item), releaseTypes: item.releaseTypes || [], releaseYear: item.year || null, size: null, diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index cd90a12a6..c3b9b4c8e 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -191,6 +191,7 @@ export type Album = { playCount: null | number; recordLabels: string[]; releaseDate: null | string; + releaseType: null | string; releaseTypes: string[]; releaseYear: null | number; size: null | number;