diff --git a/src/renderer/components/item-card/item-card.tsx b/src/renderer/components/item-card/item-card.tsx index 8b00e7a55..1c4cb65db 100644 --- a/src/renderer/components/item-card/item-card.tsx +++ b/src/renderer/components/item-card/item-card.tsx @@ -27,6 +27,7 @@ import { formatDurationString, formatRating, } from '/@/renderer/utils/format'; +import { SEPARATOR_STRING } from '/@/shared/api/utils'; import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { Separator } from '/@/shared/components/separator/separator'; @@ -1054,7 +1055,17 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[] { format: (data) => { if ('releaseYear' in data && data.releaseYear !== null) { - return String(data.releaseYear); + const releaseYear = data.releaseYear; + const originalYear = + 'originalYear' in data && data.originalYear !== null + ? data.originalYear + : null; + + if (originalYear !== null && originalYear !== releaseYear) { + return `♫ ${originalYear}${SEPARATOR_STRING}${releaseYear}`; + } + + return String(releaseYear); } return ''; }, @@ -1063,7 +1074,15 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[] { format: (data) => { if ('releaseDate' in data && data.releaseDate) { - return formatDateAbsoluteUTC(data.releaseDate); + if ( + 'originalDate' in data && + data.originalDate && + data.originalDate !== data.releaseDate + ) { + return `♫ ${formatDateAbsoluteUTC(data.originalDate)}${SEPARATOR_STRING}${formatDateAbsoluteUTC(data.releaseDate)}`; + } + + return `${formatDateAbsoluteUTC(data.releaseDate)}`; } return ''; }, diff --git a/src/renderer/components/item-list/item-table-list/columns/date-column.tsx b/src/renderer/components/item-list/item-table-list/columns/date-column.tsx index 09bb7ce74..e0d662229 100644 --- a/src/renderer/components/item-list/item-table-list/columns/date-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/date-column.tsx @@ -10,9 +10,11 @@ import { formatDateRelative, formatHrDateTime, } from '/@/renderer/utils/format'; +import { SEPARATOR_STRING } from '/@/shared/api/utils'; import { Stack } from '/@/shared/components/stack/stack'; import { Text } from '/@/shared/components/text/text'; import { Tooltip } from '/@/shared/components/tooltip/tooltip'; +import { TableColumn } from '/@/shared/types/types'; const getDateTooltipLabel = (utcString: string) => { return ( @@ -54,6 +56,47 @@ export const AbsoluteDateColumn = (props: ItemTableListInnerColumn) => { props.columns[props.columnIndex].id ]; + if (props.type === TableColumn.RELEASE_DATE) { + const item = (props.data as (any | undefined)[])[props.rowIndex]; + if (item && 'releaseDate' in item && item.releaseDate) { + const releaseDate = item.releaseDate; + const originalDate = + 'originalDate' in item && item.originalDate && item.originalDate !== releaseDate + ? item.originalDate + : null; + + if (originalDate) { + const formattedOriginalDate = formatDateAbsoluteUTC(originalDate); + const formattedReleaseDate = formatDateAbsoluteUTC(releaseDate); + const displayText = `♫ ${formattedOriginalDate}${SEPARATOR_STRING}${formattedReleaseDate}`; + + return ( + + + {displayText} + + + ); + } + + if (typeof releaseDate === 'string' && releaseDate) { + return ( + + + {formatDateAbsoluteUTC(releaseDate)} + + + ); + } + } + + if (row === null) { + return ; + } + + return ; + } + if (typeof row === 'string' && row) { return ( diff --git a/src/renderer/components/item-list/item-table-list/columns/year-column.tsx b/src/renderer/components/item-list/item-table-list/columns/year-column.tsx new file mode 100644 index 000000000..69acf6c5b --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/columns/year-column.tsx @@ -0,0 +1,41 @@ +import { + ColumnNullFallback, + ColumnSkeletonFixed, + ItemTableListInnerColumn, + TableColumnTextContainer, +} from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; +import { SEPARATOR_STRING } from '/@/shared/api/utils'; + +export const YearColumn = (props: ItemTableListInnerColumn) => { + const item = (props.data as (any | undefined)[])[props.rowIndex]; + + if (item && 'releaseYear' in item && item.releaseYear !== null) { + const releaseYear = item.releaseYear; + const originalYear = + 'originalYear' in item && item.originalYear !== null ? item.originalYear : null; + + if (originalYear !== null && originalYear !== releaseYear) { + return ( + + ♫ {originalYear} + {SEPARATOR_STRING} + {releaseYear} + + ); + } + + if (typeof releaseYear === 'number') { + return {releaseYear}; + } + } + + const row: number | undefined = (props.data as (any | undefined)[])[props.rowIndex]?.[ + props.columns[props.columnIndex].id + ]; + + if (row === null) { + return ; + } + + return ; +}; diff --git a/src/renderer/components/item-list/item-table-list/default-columns.ts b/src/renderer/components/item-list/item-table-list/default-columns.ts index e5c058719..b2233f5d4 100644 --- a/src/renderer/components/item-list/item-table-list/default-columns.ts +++ b/src/renderer/components/item-list/item-table-list/default-columns.ts @@ -110,7 +110,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.YEAR, - width: 100, + width: 200, }, { align: 'center', @@ -119,7 +119,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.RELEASE_DATE, - width: 120, + width: 240, }, { align: 'center', @@ -376,7 +376,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.YEAR, - width: 100, + width: 200, }, { align: 'center', @@ -385,7 +385,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.RELEASE_DATE, - width: 120, + width: 240, }, { align: 'center', diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx index b3832773a..f09b67974 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx @@ -47,6 +47,7 @@ import { SizeColumn } from '/@/renderer/components/item-list/item-table-list/col import { TextColumn } from '/@/renderer/components/item-list/item-table-list/columns/text-column'; import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-column'; import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column'; +import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column'; import { TableItemProps } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { ItemControls, ItemListItem } from '/@/renderer/components/item-list/types'; import { eventEmitter } from '/@/renderer/events/event-emitter'; @@ -487,9 +488,11 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => { case TableColumn.DISC_NUMBER: case TableColumn.SAMPLE_RATE: case TableColumn.TRACK_NUMBER: - case TableColumn.YEAR: return ; + case TableColumn.YEAR: + return ; + case TableColumn.DATE_ADDED: return ; diff --git a/src/shared/api/jellyfin/jellyfin-normalize.ts b/src/shared/api/jellyfin/jellyfin-normalize.ts index 5411d95e7..eb441e553 100644 --- a/src/shared/api/jellyfin/jellyfin-normalize.ts +++ b/src/shared/api/jellyfin/jellyfin-normalize.ts @@ -238,7 +238,7 @@ const normalizeSong = ( peak: null, playCount: (item.UserData && item.UserData.PlayCount) || 0, playlistItemId: item.PlaylistItemId, - releaseDate: item.PremiereDate ? item.PremiereDate : null, + releaseDate: item.PremiereDate || null, releaseYear: item.ProductionYear || null, sampleRate, size, @@ -302,7 +302,8 @@ const normalizeAlbum = ( lastPlayedAt: null, mbzId: item.ProviderIds?.MusicBrainzAlbum || null, name: item.Name, - originalDate: null, + originalDate: item.PremiereDate || null, + originalYear: item.ProductionYear || null, participants: getPeople(item), playCount: item.UserData?.PlayCount || 0, recordLabels: item.Studios?.map((entry) => entry.Name) || [], diff --git a/src/shared/api/navidrome/navidrome-normalize.ts b/src/shared/api/navidrome/navidrome-normalize.ts index e970117e9..c52891a85 100644 --- a/src/shared/api/navidrome/navidrome-normalize.ts +++ b/src/shared/api/navidrome/navidrome-normalize.ts @@ -49,6 +49,26 @@ const normalizeReleaseDate = (item: { date?: string; releaseDate?: string }) => return null; }; +const normalizeOriginalDate = (item: { + date?: string; + originalDate?: string; + releaseDate?: string; +}) => { + if (item.originalDate && matchesFullDate(item.originalDate)) { + return item.originalDate; + } + + if (item.releaseDate && matchesFullDate(item.releaseDate)) { + return item.releaseDate; + } + + if (item.date && matchesFullDate(item.date)) { + return item.date; + } + + return null; +}; + const getArtists = ( item: | z.infer @@ -282,6 +302,11 @@ const normalizeAlbum = ( pathReplace?: string, pathReplaceWith?: string, ): Album => { + const releaseDate = normalizeReleaseDate(item); + const releaseYear = releaseDate ? parseInt(releaseDate.split('-')[0]) : null; + const originalDate = normalizeOriginalDate(item); + const originalYear = originalDate ? parseInt(originalDate.split('-')[0]) : null; + return { ...parseAlbumTags(item), ...getArtists(item, false), @@ -316,11 +341,12 @@ const normalizeAlbum = ( lastPlayedAt: normalizePlayDate(item), mbzId: item.mbzAlbumId || null, name: item.name, - originalDate: item.originalDate || null, + originalDate, + originalYear, playCount: item.playCount || 0, - releaseDate: normalizeReleaseDate(item), + releaseDate, releaseType: item.mbzAlbumType || null, - releaseYear: item.maxYear || null, + releaseYear, size: item.size, songCount: item.songCount, songs: item.songs diff --git a/src/shared/api/navidrome/navidrome-types.ts b/src/shared/api/navidrome/navidrome-types.ts index 404fd989e..c2a935877 100644 --- a/src/shared/api/navidrome/navidrome-types.ts +++ b/src/shared/api/navidrome/navidrome-types.ts @@ -458,17 +458,18 @@ const album = z.object({ libraryId: z.number(), libraryName: z.string(), libraryPath: z.string(), + maxOriginalYear: z.number().optional(), maxYear: z.number(), mbzAlbumArtistId: z.string().optional(), mbzAlbumId: z.string().optional(), mbzAlbumType: z.string().optional(), mbzReleaseGroupId: z.string().optional(), + minOriginalYear: z.number().optional(), minYear: z.number(), name: z.string(), orderAlbumArtistName: z.string(), orderAlbumName: z.string(), originalDate: z.string().optional(), - originalYear: z.number().optional(), participants: z.optional(participants), playCount: z.number().optional(), playDate: z.string().optional(), diff --git a/src/shared/api/subsonic/subsonic-normalize.ts b/src/shared/api/subsonic/subsonic-normalize.ts index 9e9f2964b..8c4308e9f 100644 --- a/src/shared/api/subsonic/subsonic-normalize.ts +++ b/src/shared/api/subsonic/subsonic-normalize.ts @@ -265,6 +265,14 @@ const normalizeAlbum = ( pathReplace?: string, pathReplaceWith?: string, ): Album => { + const releaseDate = + item.releaseDate && + typeof item.releaseDate.year === 'number' && + typeof item.releaseDate.month === 'number' && + typeof item.releaseDate.day === 'number' + ? `${item.releaseDate.year}-${item.releaseDate.month}-${item.releaseDate.day}` + : null; + return { _itemType: LibraryItem.ALBUM, _serverId: server?.id || 'unknown', @@ -289,17 +297,12 @@ const normalizeAlbum = ( lastPlayedAt: null, mbzId: null, name: item.name, - originalDate: null, + originalDate: releaseDate, + originalYear: item.year || null, participants: getParticipants(item), playCount: null, recordLabels: item.recordLabels?.map((item) => item.name) || [], - releaseDate: - item.releaseDate && - typeof item.releaseDate.year === 'number' && - typeof item.releaseDate.month === 'number' && - typeof item.releaseDate.day === 'number' - ? `${item.releaseDate.year}-${item.releaseDate.month}-${item.releaseDate.day}` - : null, + releaseDate, releaseType: getReleaseType(item), releaseTypes: item.releaseTypes || [], releaseYear: item.year || null, diff --git a/src/shared/api/utils.ts b/src/shared/api/utils.ts index f1335f8b9..cfb3f04d7 100644 --- a/src/shared/api/utils.ts +++ b/src/shared/api/utils.ts @@ -139,7 +139,7 @@ export const getClientType = (): string => { } }; -export const SEPARATOR_STRING = ' · '; +export const SEPARATOR_STRING = ' • '; export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: SortOrder) => { let results: Song[] = songs; @@ -419,13 +419,13 @@ export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: results, [ (v) => { - if (v.releaseDate) { - return new Date(v.releaseDate).getTime(); + if (v.originalDate) { + return new Date(v.originalDate).getTime(); } // Fallback to the first day of the release year - if (v.releaseYear) { - return new Date(v.releaseYear, 0, 1).getTime(); + if (v.originalYear) { + return new Date(v.originalYear, 0, 1).getTime(); } return 0; }, diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 564449795..2bee07f35 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -182,6 +182,7 @@ export type Album = { mbzId: null | string; name: string; originalDate: null | string; + originalYear: null | number; participants: null | Record; playCount: null | number; recordLabels: string[];