Expanded date fields (#2276)

* added missing date/year fields for other song references

* added year table column & sorting

* added navidrome track date view for grid, detail & table.

* update table column config

* Displays date & release date using formatPartialIsoDateUTC, and displays respective year field if no date present
added releaseYear player filter
Made fullscreen player show formatted date
fixed minor year/release year value mismatches
added subsonic date/year field to enable the fields in folder view for navidrome using subsonic year field

---------

Co-authored-by: Jeff <42182408+jeffvli@users.noreply.github.com>
This commit is contained in:
ermmmwafle
2026-08-03 19:54:16 -07:00
committed by GitHub
parent fd9380a149
commit 64dc3c1bc2
30 changed files with 418 additions and 81 deletions
+8 -2
View File
@@ -268,6 +268,7 @@
"comment": "Comment",
"communityRating": "Community rating",
"criticRating": "Critic rating",
"date": "Date",
"dateAdded": "Date added",
"disc": "Disc",
"duration": "Duration",
@@ -300,6 +301,7 @@
"title": "Title",
"toYear": "To year",
"trackNumber": "Track",
"year": "Year",
"explicitStatus": "$t(common.explicitStatus)"
},
"datetime": {
@@ -1293,6 +1295,7 @@
"comment": "Comment",
"dateAdded": "Date Added",
"discNumber": "Disc",
"date": "Date",
"favorite": "Favorite",
"genre": "Genre",
"lastPlayed": "Last played",
@@ -1300,13 +1303,14 @@
"playCount": "Plays",
"rating": "Rating",
"releaseDate": "Release Date",
"releaseYear": "Year",
"releaseYear": "Release year",
"sampleRate": "Sample Rate",
"size": "Size",
"songCount": "Tracks",
"title": "Title",
"trackNumber": "Track",
"owner": "Owner"
"owner": "Owner",
"year": "Year"
},
"config": {
"general": {
@@ -1360,6 +1364,7 @@
"channels": "$t(common.channel, {\"count\": 2})",
"codec": "$t(common.codec)",
"composer": "Composer",
"date": "Date",
"dateAdded": "Date Added",
"discNumber": "Disc Number",
"duration": "$t(common.duration)",
@@ -1374,6 +1379,7 @@
"playCount": "Play Count",
"rating": "$t(common.rating)",
"releaseDate": "Release Date",
"releaseYear": "Release Year",
"rowIndex": "Row Index",
"sampleRate": "$t(common.sampleRate)",
"size": "$t(common.size)",
@@ -1249,6 +1249,24 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[]
},
id: 'releaseDate',
},
{
format: (data) => {
if ('year' in data && data.year) {
return String(data.year);
}
return '';
},
id: 'year',
},
{
format: (data) => {
if ('date' in data && data.date) {
return formatPartialIsoDateUTC(data.date);
}
return '';
},
id: 'date',
},
{
format: (data) => {
if ('createdAt' in data && data.createdAt) {
@@ -59,6 +59,7 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => {
[TableColumn.CODEC]: null,
[TableColumn.COMMENT]: null,
[TableColumn.COMPOSER]: null,
[TableColumn.DATE]: 'date',
[TableColumn.DATE_ADDED]: 'createdAt',
[TableColumn.DISC_NUMBER]: null,
[TableColumn.DURATION]: 'duration',
@@ -73,6 +74,7 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => {
[TableColumn.PLAY_COUNT]: 'playCount',
[TableColumn.PLAYLIST_REORDER]: null,
[TableColumn.RELEASE_DATE]: 'releaseDate',
[TableColumn.RELEASE_YEAR]: 'releaseYear',
[TableColumn.ROW_INDEX]: null,
[TableColumn.SAMPLE_RATE]: 'sampleRate',
[TableColumn.SIZE]: null,
@@ -84,7 +86,7 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => {
[TableColumn.TRACK_NUMBER]: null,
[TableColumn.USER_FAVORITE]: 'userFavorite',
[TableColumn.USER_RATING]: 'rating',
[TableColumn.YEAR]: 'releaseYear',
[TableColumn.YEAR]: 'year',
};
return columnToRowIdMap[tableColumn] || null;
};
@@ -0,0 +1,6 @@
import { ItemDetailListCellProps } from './types';
import { formatPartialIsoDateUTC } from '/@/renderer/utils/format';
export const DateColumn = ({ song }: ItemDetailListCellProps) =>
song.date ? formatPartialIsoDateUTC(song.date) : <>&nbsp;</>;
@@ -14,6 +14,7 @@ import { CodecColumn } from './codec-column';
import { CommentColumn } from './comment-column';
import { ComposerColumn } from './composer-column';
import { DateAddedColumn } from './date-added-column';
import { DateColumn } from './date-column';
import { DefaultColumn } from './default-column';
import { DiscNumberColumn } from './disc-number-column';
import { DurationColumn } from './duration-column';
@@ -26,6 +27,7 @@ import { PathColumn } from './path-column';
import { PlayCountColumn } from './play-count-column';
import { RatingColumn } from './rating-column';
import { ReleaseDateColumn } from './release-date-column';
import { ReleaseYearColumn } from './release-year-column';
import { RowIndexColumn } from './row-index-column';
import { ItemDetailRowPlayControlCell } from './row-play-control-cell';
import { SampleRateColumn } from './sample-rate-column';
@@ -52,6 +54,7 @@ const COLUMN_MAP: Partial<Record<TableColumn, CellComponent>> = {
[TableColumn.CODEC]: CodecColumn,
[TableColumn.COMMENT]: CommentColumn,
[TableColumn.COMPOSER]: ComposerColumn,
[TableColumn.DATE]: DateColumn,
[TableColumn.DATE_ADDED]: DateAddedColumn,
[TableColumn.DISC_NUMBER]: DiscNumberColumn,
[TableColumn.DURATION]: DurationColumn,
@@ -62,6 +65,7 @@ const COLUMN_MAP: Partial<Record<TableColumn, CellComponent>> = {
[TableColumn.PATH]: PathColumn,
[TableColumn.PLAY_COUNT]: PlayCountColumn,
[TableColumn.RELEASE_DATE]: ReleaseDateColumn,
[TableColumn.RELEASE_YEAR]: ReleaseYearColumn,
[TableColumn.ROW_INDEX]: RowIndexColumn,
[TableColumn.SAMPLE_RATE]: SampleRateColumn,
[TableColumn.SIZE]: SizeColumn,
@@ -105,6 +109,7 @@ export {
CommentColumn,
ComposerColumn,
DateAddedColumn,
DateColumn,
DefaultColumn,
DiscNumberColumn,
DurationColumn,
@@ -118,6 +123,7 @@ export {
PlayCountColumn,
RatingColumn,
ReleaseDateColumn,
ReleaseYearColumn,
RowIndexColumn,
SampleRateColumn,
SizeColumn,
@@ -0,0 +1,4 @@
import { ItemDetailListCellProps } from './types';
export const ReleaseYearColumn = ({ song }: ItemDetailListCellProps) =>
song.releaseYear ? String(song.releaseYear) : <>&nbsp;</>;
@@ -1,4 +1,4 @@
import { ItemDetailListCellProps } from './types';
export const YearColumn = ({ song }: ItemDetailListCellProps) =>
song.releaseYear ? String(song.releaseYear) : <>&nbsp;</>;
song.year ? String(song.year) : <>&nbsp;</>;
@@ -7,10 +7,12 @@ const FIXED_TRACK_COLUMN_WIDTHS: Partial<Record<TableColumn, number>> = {
[TableColumn.BPM]: 56,
[TableColumn.CHANNELS]: 80,
[TableColumn.CODEC]: 80,
[TableColumn.DATE]: 128,
[TableColumn.DATE_ADDED]: 128,
[TableColumn.DISC_NUMBER]: 36,
[TableColumn.DURATION]: 72,
[TableColumn.RELEASE_DATE]: 128,
[TableColumn.RELEASE_YEAR]: 128,
[TableColumn.SAMPLE_RATE]: 112,
[TableColumn.TRACK_NUMBER]: 64,
[TableColumn.USER_FAVORITE]: 32,
@@ -0,0 +1,45 @@
import { useMemo } from 'react';
import {
ColumnNullFallback,
ColumnSkeletonFixed,
ItemTableListInnerColumn,
TableColumnTextContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
const ReleaseYearColumnBase = (props: ItemTableListInnerColumn) => {
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
const item = rowItem as any;
const yearDisplay = useMemo(() => {
if (item && 'releaseYear' in item && item.releaseYear != null) {
const releaseYear = item.releaseYear;
const originalYear =
'originalYear' in item && item.originalYear > 0 ? item.originalYear : null;
if (originalYear !== null && originalYear !== releaseYear) {
return `${originalYear}${SEPARATOR_STRING}${releaseYear}`;
}
if (typeof releaseYear === 'number') {
return releaseYear;
}
}
return null;
}, [item]);
if (yearDisplay !== null) {
return <TableColumnTextContainer {...props}>{yearDisplay}</TableColumnTextContainer>;
}
const row: number | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id];
if (row === null) {
return <ColumnNullFallback {...props} />;
}
return <ColumnSkeletonFixed {...props} />;
};
export const ReleaseYearColumn = ReleaseYearColumnBase;
@@ -0,0 +1,31 @@
import { useMemo } from 'react';
import {
ColumnNullFallback,
ColumnSkeletonFixed,
ItemTableListInnerColumn,
TableColumnTextContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { formatPartialIsoDateUTC } from '/@/renderer/utils/format';
const TrackDateColumnBase = (props: ItemTableListInnerColumn) => {
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
const row: string | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id];
const dateDisplay = useMemo(
() => (typeof row === 'string' && row ? formatPartialIsoDateUTC(row) : null),
[row],
);
if (dateDisplay !== null) {
return <TableColumnTextContainer {...props}>{dateDisplay}</TableColumnTextContainer>;
}
if (row === null) {
return <ColumnNullFallback {...props} />;
}
return <ColumnSkeletonFixed {...props} />;
};
export const TrackDateColumn = TrackDateColumnBase;
@@ -6,24 +6,16 @@ import {
ItemTableListInnerColumn,
TableColumnTextContainer,
} from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
import { SEPARATOR_STRING } from '/@/shared/api/utils';
const YearColumnBase = (props: ItemTableListInnerColumn) => {
const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex];
const item = rowItem as any;
const yearDisplay = useMemo(() => {
if (item && 'releaseYear' in item && item.releaseYear != null) {
const releaseYear = item.releaseYear;
const originalYear =
'originalYear' in item && item.originalYear > 0 ? item.originalYear : null;
if (originalYear !== null && originalYear !== releaseYear) {
return `${originalYear}${SEPARATOR_STRING}${releaseYear}`;
}
if (typeof releaseYear === 'number') {
return releaseYear;
if (item && 'year' in item && item.year != null) {
const year = item.year;
if (typeof year === 'number') {
return year;
}
}
return null;
@@ -112,6 +112,15 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
value: TableColumn.COMPOSER,
width: 300,
},
{
align: 'center',
autoSize: false,
isEnabled: false,
label: i18n.t('table.config.label.date'),
pinned: null,
value: TableColumn.DATE,
width: 240,
},
{
align: 'start',
autoSize: false,
@@ -139,6 +148,15 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [
value: TableColumn.YEAR,
width: 200,
},
{
align: 'center',
autoSize: false,
isEnabled: false,
label: i18n.t('table.config.label.releaseYear'),
pinned: null,
value: TableColumn.RELEASE_YEAR,
width: 200,
},
{
align: 'center',
autoSize: false,
@@ -414,15 +432,6 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
value: TableColumn.GENRE_BADGE,
width: 300,
},
{
align: 'center',
autoSize: false,
isEnabled: true,
label: i18n.t('table.config.label.year'),
pinned: null,
value: TableColumn.YEAR,
width: 200,
},
{
align: 'center',
autoSize: false,
@@ -432,6 +441,15 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [
value: TableColumn.RELEASE_DATE,
width: 240,
},
{
align: 'center',
autoSize: false,
isEnabled: true,
label: i18n.t('table.config.label.releaseYear'),
pinned: null,
value: TableColumn.RELEASE_YEAR,
width: 200,
},
{
align: 'center',
autoSize: false,
@@ -48,12 +48,14 @@ import { NumericColumn } from '/@/renderer/components/item-list/item-table-list/
import { PathColumn } from '/@/renderer/components/item-list/item-table-list/columns/path-column';
import { PlaylistReorderColumn } from '/@/renderer/components/item-list/item-table-list/columns/playlist-reorder-column';
import { RatingColumn } from '/@/renderer/components/item-list/item-table-list/columns/rating-column';
import { ReleaseYearColumn } from '/@/renderer/components/item-list/item-table-list/columns/release-year-column';
import { RowIndexColumn } from '/@/renderer/components/item-list/item-table-list/columns/row-index-column';
import { SizeColumn } from '/@/renderer/components/item-list/item-table-list/columns/size-column';
import { TextColumn } from '/@/renderer/components/item-list/item-table-list/columns/text-column';
import { TitleArtistColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-artist-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 { TrackDateColumn } from '/@/renderer/components/item-list/item-table-list/columns/track-date-column';
import { TrackNumberColumn } from '/@/renderer/components/item-list/item-table-list/columns/track-number-column';
import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column';
import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state';
@@ -248,6 +250,11 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => {
case TableColumn.COMPOSER:
return <ComposerColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.DATE:
return (
<TrackDateColumn {...props} {...dragProps} controls={controls} type={type} />
);
case TableColumn.DATE_ADDED:
return <DateColumn {...props} {...dragProps} controls={controls} type={type} />;
@@ -281,6 +288,11 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => {
<AbsoluteDateColumn {...props} {...dragProps} controls={controls} type={type} />
);
case TableColumn.RELEASE_YEAR:
return (
<ReleaseYearColumn {...props} {...dragProps} controls={controls} type={type} />
);
case TableColumn.ROW_INDEX:
return <RowIndexColumn {...props} {...dragProps} controls={controls} type={type} />;
@@ -1268,6 +1280,9 @@ export const columnLabelMap: Record<TableColumn, ReactNode | string> = {
[TableColumn.COMPOSER]: i18n.t('table.config.label.composer', {
postProcess: 'upperCase',
}) as string,
[TableColumn.DATE]: i18n.t('table.column.date', {
postProcess: 'upperCase',
}) as string,
[TableColumn.DATE_ADDED]: i18n.t('table.column.dateAdded', {
postProcess: 'upperCase',
}) as string,
@@ -1304,6 +1319,9 @@ export const columnLabelMap: Record<TableColumn, ReactNode | string> = {
[TableColumn.RELEASE_DATE]: i18n.t('table.column.releaseDate', {
postProcess: 'upperCase',
}) as string,
[TableColumn.RELEASE_YEAR]: i18n.t('table.column.releaseYear', {
postProcess: 'upperCase',
}) as string,
[TableColumn.ROW_INDEX]: (
<Flex className={styles.headerIconWrapper}>
<Icon icon="hash" />
@@ -1339,7 +1357,7 @@ export const columnLabelMap: Record<TableColumn, ReactNode | string> = {
<Icon icon="star" />
</Flex>
),
[TableColumn.YEAR]: i18n.t('table.column.releaseYear', { postProcess: 'upperCase' }) as string,
[TableColumn.YEAR]: i18n.t('table.column.year', { postProcess: 'upperCase' }) as string,
};
export const ColumnNullFallback = (props: ItemTableListInnerColumn) => {
@@ -132,7 +132,7 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
if (!album) return [];
const originalDifferentFromRelease =
album?.originalDate && album?.originalDate !== album?.releaseDate;
album.originalDate && album.originalDate !== album.releaseDate;
const originalYearDifferentFromRelease =
album.originalYear > 0 &&
@@ -141,54 +141,39 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
const playCount = album?.playCount;
const releasePrefix = originalDifferentFromRelease ? t('page.albumDetail.released') : '♫';
const trackYearRangeDifferent = album.trackYearRange?.max !== album.trackYearRange?.min;
const releaseYearPrefix = originalYearDifferentFromRelease
? t('page.albumDetail.released')
: '';
if (album.trackYearRange && trackYearRangeDifferent) {
items.push({
id: 'trackYearRange',
value: `${album.trackYearRange.min}-${album.trackYearRange.max}`,
});
}
if (album.originalDate) {
if (originalDifferentFromRelease) {
items.push({
id: 'originalDate',
value: `${formatPartialIsoDateUTC(album.originalDate)}`,
});
}
if (album.originalDate && originalDifferentFromRelease) {
items.push({
id: 'originalDate',
value: `${formatPartialIsoDateUTC(album.originalDate)}`,
});
} else if (album.originalYear > 0 && originalYearDifferentFromRelease) {
items.push({
id: 'originalYear',
value: `${album.originalYear}`,
});
}
if (releaseDate) {
items.push({
id: 'releaseDate',
value: `${releasePrefix} ${formatPartialIsoDateUTC(releaseDate)}`,
});
}
} else if (album.originalYear > 0) {
if (originalYearDifferentFromRelease) {
items.push({
id: 'originalYear',
value: `${album.originalYear}`,
});
}
if (releaseDate) {
items.push({
id: 'releaseDate',
value: `${releaseYearPrefix} ${formatPartialIsoDateUTC(releaseDate)}`,
});
} else if (releaseYear != null && releaseYear > 0) {
items.push({
id: 'releaseYear',
value: `${releaseYearPrefix} ${releaseYear}`,
});
}
} else if (releaseDate) {
const prefixNecessary = items.length > 0;
const releasePrefix = prefixNecessary ? `${t('page.albumDetail.released')} ` : '';
const releaseYearPrefix = prefixNecessary ? `${t('page.albumDetail.released')} ` : '';
if (releaseDate) {
items.push({
id: 'releaseDate',
value: `${formatPartialIsoDateUTC(releaseDate)}`,
value: `${releasePrefix}${formatPartialIsoDateUTC(releaseDate)}`,
});
} else if (releaseYear != null && releaseYear > 0) {
items.push({
id: 'releaseYear',
value: `${releaseYear}`,
value: `${releaseYearPrefix}${releaseYear}`,
});
}
@@ -278,6 +263,7 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
<Group className={styles.metadataGroup} gap="xs">
{metadataItems.map((item, index) => (
<Fragment key={item.id}>
{index === 0 && <Text isNoSelect> </Text>}
{index > 0 && (
<Text isMuted isNoSelect>
<Separator />
@@ -6,7 +6,11 @@ import { generatePath, Link } from 'react-router';
import { SongPath } from '/@/renderer/features/item-details/components/song-path';
import { AppRoute } from '/@/renderer/router/routes';
import { formatDurationString, formatSizeString } from '/@/renderer/utils';
import { formatDateRelative, formatRating } from '/@/renderer/utils/format';
import {
formatDateRelative,
formatPartialIsoDateUTC,
formatRating,
} from '/@/renderer/utils/format';
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types';
import { sanitize } from '/@/renderer/utils/sanitize';
@@ -304,7 +308,24 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [
},
{ key: 'discNumber', label: 'common.disc' },
{ key: 'trackNumber', label: 'common.trackNumber' },
{ key: 'releaseYear', label: 'filter.releaseYear' },
{
key: 'date',
label: 'filter.date',
render: (song) => (song.date ? formatPartialIsoDateUTC(song.date) : null),
},
{
label: 'filter.year',
render: (song) => (!song.date ? song.year : null),
},
{
key: 'releaseDate',
label: 'filter.releaseDate',
render: (song) => (song.releaseDate ? formatPartialIsoDateUTC(song.releaseDate) : null),
},
{
label: 'filter.releaseYear',
render: (song) => (!song.releaseDate ? song.releaseYear : null),
},
{
label: 'filter.explicitStatus',
render: (song, t) =>
@@ -18,6 +18,7 @@ import {
usePlayerData,
usePlayerSong,
} from '/@/renderer/store';
import { formatPartialIsoDateUTC } from '/@/renderer/utils';
import { Badge } from '/@/shared/components/badge/badge';
import { Center } from '/@/shared/components/center/center';
import { Flex } from '/@/shared/components/flex/flex';
@@ -181,6 +182,7 @@ export const FullScreenPlayerImage = () => {
</Badge>
),
codec: currentSong?.container && <Badge>{currentSong?.container}</Badge>,
date: currentSong?.date && <Badge>{formatPartialIsoDateUTC(currentSong?.date)}</Badge>,
disc_number: currentSong?.discNumber && (
<Badge>
{t('common.disc')} {currentSong?.discNumber}
@@ -191,7 +193,9 @@ export const FullScreenPlayerImage = () => {
currentSong?.genres
.slice(0, 2)
.map((genre) => <Badge key={genre.id}>{genre.name}</Badge>),
release_date: currentSong?.releaseDate && <Badge>{currentSong?.releaseDate}</Badge>,
release_date: currentSong?.releaseDate && (
<Badge>{formatPartialIsoDateUTC(currentSong?.releaseDate)}</Badge>
),
release_type: currentSong?.tags?.releasetype && (
<Badge>{currentSong?.tags?.releasetype[0]}</Badge>
),
@@ -202,6 +206,7 @@ export const FullScreenPlayerImage = () => {
{t('common.trackNumber')} {currentSong?.trackNumber}
</Badge>
),
year: currentSong?.year && <Badge>{currentSong?.year}</Badge>,
};
return (
@@ -80,6 +80,7 @@ export const useMPRIS = () => {
compilation: null,
container: null,
createdAt: '',
date: null,
discNumber: 0,
discSubtitle: null,
duration: 0,
@@ -109,6 +110,7 @@ export const useMPRIS = () => {
updatedAt: new Date().toISOString(),
userFavorite: false,
userRating: null,
year: null,
};
}, [isRadioActive, isRadioPlaying, radioMetadata, stationName]);
+3 -1
View File
@@ -356,8 +356,10 @@ const getSongFieldValue = (song: Song, field: string): boolean | null | number |
return song.playCount;
case 'rating':
return song.userRating || 0;
case 'year':
case 'releaseYear':
return song.releaseYear || 0;
case 'year':
return song.year || 0;
default:
return null;
}
+1
View File
@@ -48,6 +48,7 @@ export function playlistSongsToAlbums(songs: Song[]): PlaylistAlbumRow[] {
songCount: null,
sortName: song.album ?? '',
tags: song.tags,
trackYearRange: null,
updatedAt: song.updatedAt,
userFavorite: false,
userRating: null,
@@ -13,6 +13,7 @@ const PLAYER_ITEMS: Array<[PlayerItem, string]> = [
[PlayerItem.BIT_RATE, 'common.bitrate'],
[PlayerItem.BPM, 'common.bpm'],
[PlayerItem.CODEC, 'common.codec'],
[PlayerItem.DATE, 'filter.date'],
[PlayerItem.DISC_NUMBER, 'table.config.label.discNumber'],
[PlayerItem.GENRES, 'entity.genre_other'],
[PlayerItem.RELEASE_DATE, 'filter.releaseDate'],
@@ -20,6 +21,7 @@ const PLAYER_ITEMS: Array<[PlayerItem, string]> = [
[PlayerItem.RELEASE_YEAR, 'filter.releaseYear'],
[PlayerItem.SAMPLE_RATE, 'common.sampleRate'],
[PlayerItem.TRACK_NUMBER, 'table.config.label.trackNumber'],
[PlayerItem.YEAR, 'filter.year'],
];
export const FullscreenPlayerSettings = memo(() => {
@@ -66,6 +66,11 @@ const getFilterFields = (t: (key: string, options?: any) => string): FilterField
type: 'number',
value: 'year',
},
{
label: t('table.config.label.releaseYear'),
type: 'number',
value: 'releaseYear',
},
{
label: t('table.config.label.note'),
type: 'string',
@@ -214,6 +214,11 @@ export const CLIENT_SIDE_SONG_FILTERS = [
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear'),
value: SongListSort.RELEASE_YEAR,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.year'),
value: SongListSort.YEAR,
},
];
@@ -591,6 +596,11 @@ const SONG_LIST_FILTERS: Partial<
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear'),
value: SongListSort.RELEASE_YEAR,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.year'),
value: SongListSort.YEAR,
},
],
+119 -8
View File
@@ -95,10 +95,12 @@ const PlayerItemSchema = z.enum([
'sample_rate',
'track_number',
'codec',
'date',
'release_year',
'release_type',
'release_date',
'genres',
'year',
]);
const ArtistItemSchema = z.enum([
@@ -650,6 +652,7 @@ const PlayerFilterFieldSchema = z.enum([
'duration',
'genre',
'year',
'releaseYear',
'note',
'path',
'playCount',
@@ -967,6 +970,7 @@ export enum PlayerItem {
BIT_RATE = 'bit_rate',
BPM = 'bpm',
CODEC = 'codec',
DATE = 'date',
DISC_NUMBER = 'disc_number',
GENRES = 'genres',
RELEASE_DATE = 'release_date',
@@ -974,6 +978,7 @@ export enum PlayerItem {
RELEASE_YEAR = 'release_year',
SAMPLE_RATE = 'sample_rate',
TRACK_NUMBER = 'track_number',
YEAR = 'year',
}
export enum PlaylistTarget {
@@ -1080,6 +1085,10 @@ export const playerItems: SortableItem<PlayerItem>[] = [
disabled: false,
id: PlayerItem.CODEC,
},
{
disabled: true,
id: PlayerItem.DATE,
},
{
disabled: true,
id: PlayerItem.DISC_NUMBER,
@@ -1108,6 +1117,10 @@ export const playerItems: SortableItem<PlayerItem>[] = [
disabled: true,
id: PlayerItem.TRACK_NUMBER,
},
{
disabled: false,
id: PlayerItem.YEAR,
},
];
export const sidebarItems: SidebarItemType[] = [
@@ -1522,10 +1535,14 @@ const initialState: SettingsState = {
alignLeftColumns: [
TableColumn.TITLE,
TableColumn.ALBUM_ARTIST,
TableColumn.YEAR,
TableColumn.RELEASE_YEAR,
],
columns: ALBUM_TABLE_COLUMNS,
enabledColumns: [TableColumn.TITLE, TableColumn.ALBUM_ARTIST, TableColumn.YEAR],
enabledColumns: [
TableColumn.TITLE,
TableColumn.ALBUM_ARTIST,
TableColumn.RELEASE_YEAR,
],
pickColumns: [
TableColumn.TITLE,
TableColumn.DURATION,
@@ -1537,8 +1554,8 @@ const initialState: SettingsState = {
TableColumn.PLAY_COUNT,
TableColumn.SONG_COUNT,
TableColumn.RELEASE_DATE,
TableColumn.RELEASE_YEAR,
TableColumn.LAST_PLAYED,
TableColumn.YEAR,
],
}),
size: 'default',
@@ -1599,10 +1616,14 @@ const initialState: SettingsState = {
alignLeftColumns: [
TableColumn.TITLE,
TableColumn.ALBUM_ARTIST,
TableColumn.YEAR,
TableColumn.RELEASE_YEAR,
],
columns: ALBUM_TABLE_COLUMNS,
enabledColumns: [TableColumn.TITLE, TableColumn.ALBUM_ARTIST, TableColumn.YEAR],
enabledColumns: [
TableColumn.TITLE,
TableColumn.ALBUM_ARTIST,
TableColumn.RELEASE_YEAR,
],
pickColumns: [
TableColumn.TITLE,
TableColumn.DURATION,
@@ -1614,8 +1635,8 @@ const initialState: SettingsState = {
TableColumn.PLAY_COUNT,
TableColumn.SONG_COUNT,
TableColumn.RELEASE_DATE,
TableColumn.RELEASE_YEAR,
TableColumn.LAST_PLAYED,
TableColumn.YEAR,
],
}),
size: 'default',
@@ -1823,15 +1844,17 @@ const initialState: SettingsState = {
TableColumn.TITLE,
TableColumn.ARTIST,
TableColumn.DURATION,
TableColumn.YEAR,
TableColumn.BIT_RATE,
TableColumn.BPM,
TableColumn.CODEC,
TableColumn.DATE,
TableColumn.DATE_ADDED,
TableColumn.GENRE,
TableColumn.LAST_PLAYED,
TableColumn.RELEASE_DATE,
TableColumn.RELEASE_YEAR,
TableColumn.TRACK_NUMBER,
TableColumn.YEAR,
],
}),
size: 'default',
@@ -1899,15 +1922,17 @@ const initialState: SettingsState = {
TableColumn.TITLE,
TableColumn.ARTIST,
TableColumn.DURATION,
TableColumn.YEAR,
TableColumn.BIT_RATE,
TableColumn.BPM,
TableColumn.CODEC,
TableColumn.DATE,
TableColumn.DATE_ADDED,
TableColumn.GENRE,
TableColumn.LAST_PLAYED,
TableColumn.RELEASE_DATE,
TableColumn.RELEASE_YEAR,
TableColumn.TRACK_NUMBER,
TableColumn.YEAR,
],
}),
size: 'default',
@@ -2714,6 +2739,92 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
);
}
if (version < 29) {
const dateColumn: ItemTableListColumnConfig = {
align: 'center',
autoSize: false,
id: TableColumn.DATE,
isEnabled: false,
pinned: null,
width: 240,
};
const yearColumn: ItemTableListColumnConfig = {
align: 'center',
autoSize: false,
id: TableColumn.YEAR,
isEnabled: false,
pinned: null,
width: 200,
};
const listKeysToUpdate: ItemListKey[] = [
ItemListKey.SONG,
ItemListKey.ALBUM_DETAIL,
ItemListKey.FOLDER,
ItemListKey.PLAYLIST_SONG,
ItemListKey.ALBUM_ARTIST_SONG,
ItemListKey.GENRE_SONG,
ItemListKey.QUEUE_SONG,
ItemListKey.FULL_SCREEN,
ItemListKey.SIDE_QUEUE,
];
listKeysToUpdate.forEach((listKey) => {
const listConfig = state.lists[listKey];
if (listConfig?.table?.columns) {
const columns = listConfig.table.columns;
const hasYear = columns.some((col) => col.id === TableColumn.YEAR);
if (!hasYear) {
const releaseYearIndex = columns.findIndex(
(col) => col.id === TableColumn.RELEASE_YEAR,
);
if (releaseYearIndex >= 0) {
columns.splice(releaseYearIndex, 0, yearColumn);
} else {
columns.push(yearColumn);
}
}
const hasDate = columns.some((col) => col.id === TableColumn.DATE);
if (!hasDate) {
const releaseDateIndex = columns.findIndex(
(col) => col.id === TableColumn.RELEASE_DATE,
);
if (releaseDateIndex >= 0) {
columns.splice(releaseDateIndex, 0, dateColumn);
} else {
columns.push(dateColumn);
}
}
}
});
const listConfig = state.lists[ItemListKey.ALBUM];
if (listConfig?.detail?.columns) {
const columns = listConfig.detail.columns;
const hasYear = columns.some((col) => col.id === TableColumn.YEAR);
if (!hasYear) {
const releaseYearIndex = columns.findIndex(
(col) => col.id === TableColumn.RELEASE_YEAR,
);
if (releaseYearIndex >= 0) {
columns.splice(releaseYearIndex, 0, yearColumn);
} else {
columns.push(yearColumn);
}
}
const hasDate = columns.some((col) => col.id === TableColumn.DATE);
if (!hasDate) {
const releaseDateIndex = columns.findIndex(
(col) => col.id === TableColumn.RELEASE_DATE,
);
if (releaseDateIndex >= 0) {
columns.splice(releaseDateIndex, 0, dateColumn);
} else {
columns.push(dateColumn);
}
}
}
}
if (version < 30) {
for (const [key, displaySettings] of Object.entries(state.lyricsDisplay)) {
const legacySettings = displaySettings as typeof displaySettings & {
@@ -155,6 +155,7 @@ const normalizeSong = (
compilation: null,
container,
createdAt: item.DateCreated,
date: releaseDate || String(releaseYear),
discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1,
discSubtitle: null,
duration: item.RunTimeTicks / TICKS_PER_MS,
@@ -209,6 +210,7 @@ const normalizeSong = (
updatedAt: item.DateCreated,
userFavorite: (item.UserData && item.UserData.IsFavorite) || false,
userRating: null,
year: releaseYear,
};
};
@@ -280,6 +282,7 @@ const normalizeAlbum = (
songs: item.Songs?.map((song) => normalizeSong(song, server)),
sortName: item.SortName || item.Name,
tags: getTags(item),
trackYearRange: null,
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
userFavorite: item.UserData?.IsFavorite || false,
userRating: null,
@@ -212,9 +212,13 @@ const normalizeSong = (
const fromSongRelease = parsePartialIsoDate(item.releaseDate);
const songApiYear = coerceYear(item.year);
const fromSongDate = parsePartialIsoDate(item.date);
const releaseYear: null | number =
fromSongRelease.year > 0 ? fromSongRelease.year : songApiYear > 0 ? songApiYear : null;
const releaseDate = fromSongRelease.date ?? (songApiYear > 0 ? String(songApiYear) : null);
const releaseDate =
fromSongRelease.date ?? fromSongDate.date ?? (songApiYear > 0 ? String(songApiYear) : null);
const date = fromSongDate.date ?? (songApiYear > 0 ? String(songApiYear) : null);
const year = fromSongDate.year > 0 ? fromSongDate.year : releaseYear;
return {
album: item.album,
@@ -233,6 +237,7 @@ const normalizeSong = (
compilation: item.compilation,
container: item.suffix,
createdAt: item.createdAt,
date,
discNumber: item.discNumber,
discSubtitle: item.discSubtitle ? item.discSubtitle : null,
duration: item.duration * 1000,
@@ -285,6 +290,7 @@ const normalizeSong = (
updatedAt: item.updatedAt,
userFavorite: item.starred || false,
userRating: item.rating || null,
year,
};
};
@@ -337,6 +343,7 @@ const normalizeAlbum = (
): Album => {
const releaseDate = normalizeNavidromeReleaseDate(item);
const originalDate = normalizeNavidromeOriginalDate(item);
const trackYearRange = { max: item.maxYear, min: item.minYear };
return {
...parseAlbumTags(item),
@@ -384,6 +391,7 @@ const normalizeAlbum = (
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server)) : undefined,
sortName: item.orderAlbumName,
tags: item.tags || null,
trackYearRange,
updatedAt: item.updatedAt,
userFavorite: item.starred || false,
userRating: item.rating || null,
@@ -59,6 +59,7 @@ export enum NDSongListSort {
RANDOM = 'random',
RATING = 'rating',
RECENTLY_ADDED = 'createdAt',
RELEASE_YEAR = 'releaseYear',
TITLE = 'title',
TRACK = 'track',
YEAR = 'year',
@@ -555,6 +556,7 @@ const song = z.object({
comment: z.string().optional(),
compilation: z.boolean(),
createdAt: z.string(),
date: z.string().optional(),
discNumber: z.number(),
discSubtitle: z.string().optional(),
duration: z.number(),
@@ -158,6 +158,16 @@ const subsonicReleaseFields = (item: {
return { releaseDate: null, releaseYear: null };
};
const subsonicTrackYearField = (item: {
year?: number;
}): { date: null | string; year: null | number } => {
const y = coerceYear(item.year);
if (y > 0) {
return { date: String(y), year: y };
}
return { date: null, year: null };
};
const normalizeSong = (
item: z.infer<typeof ssType._response.song>,
@@ -173,6 +183,7 @@ const normalizeSong = (
: item.artist || '';
const { releaseDate, releaseYear } = subsonicReleaseFields(item);
const { date, year } = subsonicTrackYearField(item);
return {
_itemType: LibraryItem.SONG,
@@ -192,6 +203,7 @@ const normalizeSong = (
compilation: null,
container: item.contentType.startsWith('audio/') ? item.contentType.split('/')[1] : null,
createdAt: item.created,
date,
discNumber: item.discNumber || 1,
discSubtitle: discTitleMap?.get(item.discNumber ?? 1) ?? null,
duration: item.duration ? item.duration * 1000 : 0,
@@ -239,6 +251,7 @@ const normalizeSong = (
updatedAt: '',
userFavorite: Boolean(item.starred) || false,
userRating: item.userRating || null,
year,
};
};
@@ -353,6 +366,7 @@ const normalizeAlbum = (
) || [],
sortName: item.title,
tags: null,
trackYearRange: null,
updatedAt: item.created,
userFavorite: Boolean(item.starred) || false,
userRating: item.userRating || null,
+9 -1
View File
@@ -311,6 +311,14 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
);
break;
case SongListSort.RELEASE_YEAR:
results = orderBy(
results,
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, order, order],
);
break;
case SongListSort.SORT_NAME:
results = orderBy(results, [(v) => v.sortName ?? v.name], [order]);
break;
@@ -318,7 +326,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
case SongListSort.YEAR:
results = orderBy(
results,
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
['year', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, order, order],
);
break;
+7
View File
@@ -207,6 +207,7 @@ export type Album = {
songs?: Song[];
sortName: string;
tags: null | Record<string, string[]>;
trackYearRange: null | { max: number; min: number };
updatedAt: string;
userFavorite: boolean;
userRating: null | number;
@@ -387,6 +388,7 @@ export type Song = {
compilation: boolean | null;
container: null | string;
createdAt: string;
date: null | PartialIsoDateString;
discNumber: number;
discSubtitle: null | string;
duration: number;
@@ -417,6 +419,7 @@ export type Song = {
updatedAt: string;
userFavorite: boolean;
userRating: null | number;
year: null | number;
};
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
@@ -620,6 +623,7 @@ export enum SongListSort {
RECENTLY_ADDED = 'recentlyAdded',
RECENTLY_PLAYED = 'recentlyPlayed',
RELEASE_DATE = 'releaseDate',
RELEASE_YEAR = 'releaseYear',
SORT_NAME = 'sortName',
YEAR = 'year',
}
@@ -688,6 +692,7 @@ export const songListSortMap: SongListSortMap = {
recentlyAdded: JFSongListSort.RECENTLY_ADDED,
recentlyPlayed: JFSongListSort.RECENTLY_PLAYED,
releaseDate: JFSongListSort.RELEASE_DATE,
releaseYear: undefined,
sortName: JFSongListSort.NAME,
year: undefined,
},
@@ -710,6 +715,7 @@ export const songListSortMap: SongListSortMap = {
recentlyAdded: NDSongListSort.RECENTLY_ADDED,
recentlyPlayed: NDSongListSort.PLAY_DATE,
releaseDate: undefined,
releaseYear: NDSongListSort.RELEASE_YEAR,
sortName: NDSongListSort.TITLE,
year: NDSongListSort.YEAR,
},
@@ -732,6 +738,7 @@ export const songListSortMap: SongListSortMap = {
recentlyAdded: undefined,
recentlyPlayed: undefined,
releaseDate: undefined,
releaseYear: undefined,
sortName: undefined,
year: undefined,
},
+3 -1
View File
@@ -178,6 +178,7 @@ export enum TableColumn {
CODEC = 'container',
COMMENT = 'comment',
COMPOSER = 'composer',
DATE = 'date',
DATE_ADDED = 'createdAt',
DISC_NUMBER = 'discNumber',
DURATION = 'duration',
@@ -192,6 +193,7 @@ export enum TableColumn {
PLAY_COUNT = 'playCount',
PLAYLIST_REORDER = 'playlistReorder',
RELEASE_DATE = 'releaseDate',
RELEASE_YEAR = 'releaseYear',
ROW_INDEX = 'rowIndex',
SAMPLE_RATE = 'sampleRate',
SIZE = 'size',
@@ -203,7 +205,7 @@ export enum TableColumn {
TRACK_NUMBER = 'trackNumber',
USER_FAVORITE = 'userFavorite',
USER_RATING = 'userRating',
YEAR = 'releaseYear',
YEAR = 'year',
}
export type DiscoveredServerItem = {