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