From 6aa390592294d272ee3000d12731dd86f5af3337 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 30 Dec 2025 03:39:53 -0800 Subject: [PATCH] use exact album artist names (#1459) --- .../components/item-card/item-card.tsx | 24 +++--- .../columns/album-artists-column.tsx | 48 +++++------- .../columns/artists-column.tsx | 48 +++++++++++- .../columns/title-combined-column.tsx | 76 ++++--------------- .../components/album-detail-content.tsx | 2 +- .../albums/components/album-detail-header.tsx | 8 +- ...ed-album-artist.tsx => joined-artists.tsx} | 58 ++++++++------ src/shared/api/jellyfin/jellyfin-normalize.ts | 5 +- .../api/navidrome/navidrome-normalize.ts | 3 +- src/shared/api/subsonic/subsonic-normalize.ts | 3 +- src/shared/types/domain-types.ts | 3 +- 11 files changed, 138 insertions(+), 140 deletions(-) rename src/renderer/features/albums/components/{joined-album-artist.tsx => joined-artists.tsx} (69%) diff --git a/src/renderer/components/item-card/item-card.tsx b/src/renderer/components/item-card/item-card.tsx index 4bc1b308a..195c91897 100644 --- a/src/renderer/components/item-card/item-card.tsx +++ b/src/renderer/components/item-card/item-card.tsx @@ -16,6 +16,7 @@ import { useItemSelectionState, } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ItemControls } from '/@/renderer/components/item-list/types'; +import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { AppRoute } from '/@/renderer/router/routes'; import { useGeneralSettings } from '/@/renderer/store'; @@ -953,21 +954,14 @@ export const getDataRows = (): DataRow[] => { { format: (data) => { if ('albumArtists' in data && Array.isArray(data.albumArtists)) { - return (data as Album | Song).albumArtists.map((artist, index) => ( - - - {artist.name} - - {index < (data as Album | Song).albumArtists.length - 1 && ( - - )} - - )); + return ( + + ); } return ''; }, diff --git a/src/renderer/components/item-list/item-table-list/columns/album-artists-column.tsx b/src/renderer/components/item-list/item-table-list/columns/album-artists-column.tsx index 4a7edfad6..3fb0a4b27 100644 --- a/src/renderer/components/item-list/item-table-list/columns/album-artists-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/album-artists-column.tsx @@ -1,6 +1,5 @@ import clsx from 'clsx'; -import { Fragment, memo, useMemo } from 'react'; -import { generatePath, Link } from 'react-router'; +import { memo } from 'react'; import styles from './album-artists-column.module.css'; @@ -10,24 +9,16 @@ import { ItemTableListInnerColumn, TableColumnContainer, } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; -import { AppRoute } from '/@/renderer/router/routes'; -import { Text } from '/@/shared/components/text/text'; -import { RelatedAlbumArtist } from '/@/shared/types/domain-types'; +import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists'; +import { Album, RelatedAlbumArtist, Song } from '/@/shared/types/domain-types'; const AlbumArtistsColumn = (props: ItemTableListInnerColumn) => { const row: RelatedAlbumArtist[] | undefined = ( props.data as (RelatedAlbumArtist[] | undefined)[] )[props.rowIndex]?.[props.columns[props.columnIndex].id]; - const albumArtists = useMemo(() => { - if (!row) return []; - return row.map((albumArtist) => { - const path = generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { - albumArtistId: albumArtist.id, - }); - return { ...albumArtist, path }; - }); - }, [row]); + const item = props.data[props.rowIndex] as Album | Song | undefined; + const albumArtistString = item && 'albumArtistName' in item ? item.albumArtistName : ''; if (Array.isArray(row)) { return ( @@ -38,21 +29,20 @@ const AlbumArtistsColumn = (props: ItemTableListInnerColumn) => { [styles.large]: props.size === 'large', })} > - {albumArtists.map((albumArtist, index) => ( - - - {albumArtist.name} - - {index < albumArtists.length - 1 && ', '} - - ))} + ); diff --git a/src/renderer/components/item-list/item-table-list/columns/artists-column.tsx b/src/renderer/components/item-list/item-table-list/columns/artists-column.tsx index 60ae3efce..a5c3477bb 100644 --- a/src/renderer/components/item-list/item-table-list/columns/artists-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/artists-column.tsx @@ -10,11 +10,12 @@ import { ItemTableListInnerColumn, TableColumnContainer, } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; +import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists'; import { AppRoute } from '/@/renderer/router/routes'; import { Text } from '/@/shared/components/text/text'; -import { RelatedAlbumArtist } from '/@/shared/types/domain-types'; +import { LibraryItem, RelatedAlbumArtist, Song } from '/@/shared/types/domain-types'; -const ArtistsColumn = (props: ItemTableListInnerColumn) => { +const AlbumArtistsColumn = (props: ItemTableListInnerColumn) => { const row: RelatedAlbumArtist[] | undefined = ( props.data as (RelatedAlbumArtist[] | undefined)[] )[props.rowIndex]?.[props.columns[props.columnIndex].id]; @@ -65,6 +66,47 @@ const ArtistsColumn = (props: ItemTableListInnerColumn) => { return ; }; -export const ArtistsColumnMemo = memo(ArtistsColumn); +const SongArtistsColumn = (props: ItemTableListInnerColumn) => { + const row: Song | undefined = (props.data as (Song | undefined)[])[props.rowIndex]; + + if (row) { + return ( + +
+ +
+
+ ); + } + + if (row === null) { + return ; + } + + return ; +}; + +const BaseArtistsColumn = (props: ItemTableListInnerColumn) => { + const { itemType } = props; + + switch (itemType) { + case LibraryItem.ALBUM: + return ; + default: + return ; + } +}; + +const ArtistsColumnMemo = memo(BaseArtistsColumn); export { ArtistsColumnMemo as ArtistsColumn }; diff --git a/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx b/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx index 070ea6d65..4f5356011 100644 --- a/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; -import { CSSProperties, useMemo, useState } from 'react'; -import { generatePath, Link } from 'react-router'; +import { CSSProperties, useState } from 'react'; +import { Link } from 'react-router'; import styles from './title-combined-column.module.css'; @@ -12,16 +12,16 @@ import { ItemTableListInnerColumn, TableColumnContainer, } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; +import { JoinedArtists } from '/@/renderer/features/albums/components/joined-artists'; import { PlayButton } from '/@/renderer/features/shared/components/play-button'; import { LONG_PRESS_PLAY_BEHAVIOR, PlayTooltip, } from '/@/renderer/features/shared/components/play-button-group'; -import { AppRoute } from '/@/renderer/router/routes'; import { usePlayButtonBehavior } from '/@/renderer/store'; import { Icon } from '/@/shared/components/icon/icon'; import { Text } from '/@/shared/components/text/text'; -import { Folder, LibraryItem, QueueSong, RelatedAlbumArtist } from '/@/shared/types/domain-types'; +import { Folder, LibraryItem, QueueSong } from '/@/shared/types/domain-types'; import { Play } from '/@/shared/types/types'; export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => { @@ -73,18 +73,6 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => { }); }; - const artists = useMemo(() => { - if (row && 'artists' in item && Array.isArray(item.artists)) { - return (item.artists as RelatedAlbumArtist[]).map((artist) => { - const path = generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, { - artistId: artist.id, - }); - return { ...artist, path }; - }); - } - return []; - }, [item, row]); - if (item && 'name' in item && 'imageUrl' in item && 'artists' in item) { const rowHeight = props.getRowHeight(props.rowIndex, props); const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string); @@ -146,22 +134,12 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => { {item.name as string}
- {artists.map((artist, index) => ( - - - {artist.name} - - {index < artists.length - 1 && ', '} - - ))} +
@@ -229,18 +207,6 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) => }); }; - const artists = useMemo(() => { - if (row && 'artists' in row && Array.isArray(row.artists)) { - return (row.artists as RelatedAlbumArtist[]).map((artist) => { - const path = generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, { - artistId: artist.id, - }); - return { ...artist, path }; - }); - } - return []; - }, [row]); - if (row && 'name' in row && 'imageUrl' in row && 'artists' in row) { const rowHeight = props.getRowHeight(props.rowIndex, props); const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string); @@ -310,22 +276,12 @@ export const QueueSongTitleCombinedColumn = (props: ItemTableListInnerColumn) => {row.name as string}
- {artists.map((artist, index) => ( - - - {artist.name} - - {index < artists.length - 1 && ', '} - - ))} +
diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 55d028177..1f7442831 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -377,7 +377,7 @@ export const AlbumDetailContent = () => { ((_props, ref) => { ))} - >; + rootTextProps?: Partial>; } -export const JoinedAlbumArtist = ({ albumArtist, albumArtists }: JoinedAlbumArtistProps) => { +export const JoinedArtists = ({ + artistName, + artists, + linkProps, + rootTextProps, +}: JoinedArtistsProps) => { const parts: ( | string - | { artist: AlbumArtist | RelatedArtist; end: number; start: number; text: string } + | { + artist: AlbumArtist | RelatedAlbumArtist | RelatedArtist; + end: number; + start: number; + text: string; + } )[] = []; const matches: Array<{ - artist: AlbumArtist | RelatedArtist; + artist: AlbumArtist | RelatedAlbumArtist | RelatedArtist; end: number; name: string; start: number; }> = []; - for (const artist of albumArtists) { + for (const artist of artists) { const name = artist.name; const regex = new RegExp(escapeRegex(name), 'gi'); let match: null | RegExpExecArray = null; - while ((match = regex.exec(albumArtist)) !== null) { + while ((match = regex.exec(artistName)) !== null) { matches.push({ artist, end: match.index + match[0].length, @@ -63,7 +73,7 @@ export const JoinedAlbumArtist = ({ albumArtist, albumArtists }: JoinedAlbumArti let lastIndex = 0; for (const match of nonOverlappingMatches) { if (match.start > lastIndex) { - parts.push(albumArtist.substring(lastIndex, match.start)); + parts.push(artistName.substring(lastIndex, match.start)); } parts.push({ @@ -76,19 +86,19 @@ export const JoinedAlbumArtist = ({ albumArtist, albumArtists }: JoinedAlbumArti lastIndex = match.end; } - if (lastIndex < albumArtist.length) { - parts.push(albumArtist.substring(lastIndex)); + if (lastIndex < artistName?.length) { + parts.push(artistName.substring(lastIndex)); } const hasArtistMatches = parts.some((part) => typeof part !== 'string'); // If no matches found and there are album artists, return the album artists - if (!hasArtistMatches && albumArtists.length > 0) { + if (!hasArtistMatches && artists.length > 0) { return ( - - {albumArtists.map((artist, index) => ( + + {artists.map((artist, index) => ( - {index > 0 && } + {index > 0 && ', '} {artist.name} ))} - + ); } // If no matches found and no albumArtists, return the original string if (!hasArtistMatches) { return ( - - {albumArtist} + + {artistName} ); } return ( - + {parts.map((part, index) => { if (typeof part === 'string') { return {part}; @@ -131,6 +142,7 @@ export const JoinedAlbumArtist = ({ albumArtist, albumArtists }: JoinedAlbumArti to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId: artist.id, })} + {...linkProps} > {text} diff --git a/src/shared/api/jellyfin/jellyfin-normalize.ts b/src/shared/api/jellyfin/jellyfin-normalize.ts index 6e2f558ea..962d35c58 100644 --- a/src/shared/api/jellyfin/jellyfin-normalize.ts +++ b/src/shared/api/jellyfin/jellyfin-normalize.ts @@ -145,6 +145,7 @@ const normalizeSong = ( _serverId: server?.id || '', _serverType: ServerType.JELLYFIN, album: item.Album, + albumArtistName: item.AlbumArtist || '', albumArtists: item.AlbumArtists?.map((entry) => ({ id: entry.Id, imageId: entry.Id, @@ -154,7 +155,7 @@ const normalizeSong = ( userRating: null, })), albumId: item.AlbumId || `dummy/${item.Id}`, - artistName: item?.ArtistItems?.[0]?.Name || item?.AlbumArtists?.[0]?.Name, + artistName: item?.ArtistItems?.map((entry) => entry.Name).join(', ') || '', artists: (item?.ArtistItems?.length ? item.ArtistItems : item.AlbumArtists)?.map( (entry) => ({ id: entry.Id, @@ -231,7 +232,7 @@ const normalizeAlbum = ( _itemType: LibraryItem.ALBUM, _serverId: server?.id || '', _serverType: ServerType.JELLYFIN, - albumArtist: item.AlbumArtist, + albumArtistName: item.AlbumArtist || '', albumArtists: item.AlbumArtists.map((entry) => ({ id: entry.Id, diff --git a/src/shared/api/navidrome/navidrome-normalize.ts b/src/shared/api/navidrome/navidrome-normalize.ts index 5052168de..84f522fb6 100644 --- a/src/shared/api/navidrome/navidrome-normalize.ts +++ b/src/shared/api/navidrome/navidrome-normalize.ts @@ -158,6 +158,7 @@ const normalizeSong = ( _itemType: LibraryItem.SONG, _serverId: server?.id || 'unknown', _serverType: ServerType.NAVIDROME, + albumArtistName: item.albumArtist, artistName: item.artist, bitDepth: item.bitDepth || null, bitRate: item.bitRate, @@ -273,7 +274,7 @@ const normalizeAlbum = ( _itemType: LibraryItem.ALBUM, _serverId: server?.id || 'unknown', _serverType: ServerType.NAVIDROME, - albumArtist: item.albumArtist, + albumArtistName: item.albumArtist, comment: item.comment || null, createdAt: item.createdAt, duration: item.duration !== undefined ? item.duration * 1000 : null, diff --git a/src/shared/api/subsonic/subsonic-normalize.ts b/src/shared/api/subsonic/subsonic-normalize.ts index 3bb3ae277..0b5f2ac1d 100644 --- a/src/shared/api/subsonic/subsonic-normalize.ts +++ b/src/shared/api/subsonic/subsonic-normalize.ts @@ -123,6 +123,7 @@ const normalizeSong = ( _serverId: server?.id || 'unknown', _serverType: ServerType.SUBSONIC, album: item.album || '', + albumArtistName: item.artist || '', albumArtists: getArtistList(item.albumArtists, item.artistId, item.artist), albumId: item.albumId?.toString() || '', artistName: item.artist || '', @@ -247,7 +248,7 @@ const normalizeAlbum = ( _itemType: LibraryItem.ALBUM, _serverId: server?.id || 'unknown', _serverType: ServerType.SUBSONIC, - albumArtist: item.artist, + albumArtistName: item.artist, albumArtists: getArtistList(item.artists, item.artistId, item.artist), artists: [], comment: null, diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index c3b9b4c8e..6086011ab 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -171,7 +171,7 @@ export type Album = { _itemType: LibraryItem.ALBUM; _serverId: string; _serverType: ServerType; - albumArtist: string; + albumArtistName: string; albumArtists: RelatedArtist[]; artists: RelatedArtist[]; comment: null | string; @@ -366,6 +366,7 @@ export type Song = { _serverId: string; _serverType: ServerType; album: null | string; + albumArtistName: string; albumArtists: RelatedArtist[]; albumId: string; artistName: string;