fix: restore old date display/sort behaviour (#3862)

* fix(server): bring back legacy date mappings

Signed-off-by: Deluan <deluan@navidrome.org>

* reuse the mapDates logic in the legacyReleaseDate function

Signed-off-by: Deluan <deluan@navidrome.org>

* fix mappings

Signed-off-by: Deluan <deluan@navidrome.org>

* show original and release dates in album grid

Signed-off-by: Deluan <deluan@navidrome.org>

* fix tests based on new year mapping

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(subsonic): prefer returning original_year over (recording) year
when sorting albums

Signed-off-by: Deluan <deluan@navidrome.org>

* fix case when we don't have originalYear

Signed-off-by: Deluan <deluan@navidrome.org>

* show all dates in album's info, and remove the recording date from the album page

Signed-off-by: Deluan <deluan@navidrome.org>

* better?

Signed-off-by: Deluan <deluan@navidrome.org>

* add snapshot tests for Album Details

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(subsonic): sort order for getAlbumList?type=byYear

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão 2025-03-30 17:06:58 -04:00 committed by GitHub
parent 88f87e6c4f
commit 2b84c574ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 929 additions and 155 deletions

View file

@ -0,0 +1,19 @@
import { useRecordContext } from 'react-admin'
import { formatRange } from '../common/index.js'
const originalYearSymbol = '♫'
const releaseYearSymbol = '○'
export const AlbumDatesField = ({ className, ...rest }) => {
const record = useRecordContext(rest)
const releaseDate = record.releaseDate
const releaseYear = releaseDate?.toString().substring(0, 4)
const yearRange =
formatRange(record, 'originalYear') || record['maxYear']?.toString()
let label = yearRange
if (releaseYear !== undefined && yearRange !== releaseYear) {
label = `${originalYearSymbol} ${yearRange} · ${releaseYearSymbol} ${releaseYear}`
}
return <span className={className}>{label}</span>
}

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react'
import { useCallback, useEffect, useState } from 'react'
import {
Card,
CardContent,
@ -10,25 +10,25 @@ import {
withWidth,
} from '@material-ui/core'
import {
useRecordContext,
useTranslate,
ArrayField,
SingleFieldList,
ChipField,
Link,
SingleFieldList,
useRecordContext,
useTranslate,
} from 'react-admin'
import Lightbox from 'react-image-lightbox'
import 'react-image-lightbox/style.css'
import subsonic from '../subsonic'
import {
ArtistLinkField,
CollapsibleComment,
DurationField,
formatRange,
SizeField,
LoveButton,
RatingField,
SizeField,
useAlbumsPerPage,
CollapsibleComment,
} from '../common'
import config from '../config'
import { formatFullDate, intersperse } from '../utils'
@ -140,69 +140,55 @@ const GenreList = () => {
)
}
const Details = (props) => {
export const Details = (props) => {
const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs'))
const translate = useTranslate()
const record = useRecordContext(props)
// Create an array of detail elements
let details = []
const addDetail = (obj) => {
const id = details.length
details.push(<span key={`detail-${record.id}-${id}`}>{obj}</span>)
}
const originalYearRange = formatRange(record, 'originalYear')
const originalDate = record.originalDate
? formatFullDate(record.originalDate)
: originalYearRange
// Calculate date related fields
const yearRange = formatRange(record, 'year')
const date = record.date ? formatFullDate(record.date) : yearRange
const releaseDate = record.releaseDate
? formatFullDate(record.releaseDate)
: date
const showReleaseDate = date !== releaseDate && releaseDate.length > 3
const showOriginalDate =
date !== originalDate &&
originalDate !== releaseDate &&
originalDate.length > 3
const originalDate = record.originalDate
? formatFullDate(record.originalDate)
: formatRange(record, 'originalYear')
const releaseDate = record?.releaseDate && formatFullDate(record.releaseDate)
showOriginalDate &&
!isXsmall &&
const dateToUse = originalDate || date
const isOriginalDate = originalDate && dateToUse !== date
const showDate = dateToUse && dateToUse !== releaseDate
// Get label for the main date display
const getDateLabel = () => {
if (isXsmall) return '♫'
if (isOriginalDate) return translate('resources.album.fields.originalDate')
return null
}
// Get label for release date display
const getReleaseDateLabel = () => {
if (!isXsmall) return translate('resources.album.fields.releaseDate')
if (showDate) return '○'
return null
}
// Display dates with appropriate labels
if (showDate) {
addDetail(<>{[getDateLabel(), dateToUse].filter(Boolean).join(' ')}</>)
}
if (releaseDate) {
addDetail(
<>
{[translate('resources.album.fields.originalDate'), originalDate].join(
' ',
)}
</>,
<>{[getReleaseDateLabel(), releaseDate].filter(Boolean).join(' ')}</>,
)
yearRange && addDetail(<>{['♫', !isXsmall ? date : yearRange].join(' ')}</>)
showReleaseDate &&
addDetail(
<>
{(!isXsmall
? [translate('resources.album.fields.releaseDate'), releaseDate]
: ['○', record.releaseDate.substring(0, 4)]
).join(' ')}
</>,
)
const showReleases = record.releases > 1
showReleases &&
addDetail(
<>
{!isXsmall
? [
record.releases,
translate('resources.album.fields.releases', {
smart_count: record.releases,
}),
].join(' ')
: ['(', record.releases, ')))'].join(' ')}
</>,
)
}
addDetail(
<>
{record.songCount +
@ -215,6 +201,7 @@ const Details = (props) => {
!isXsmall && addDetail(<DurationField source={'duration'} />)
!isXsmall && addDetail(<SizeField source="size" />)
// Return the details rendered with separators
return <>{intersperse(details, ' · ')}</>
}

View file

@ -0,0 +1,327 @@
// ui/src/album/__tests__/AlbumDetails.test.jsx
import { describe, test, expect, beforeEach, afterEach } from 'vitest'
import { render } from '@testing-library/react'
import { RecordContextProvider } from 'react-admin'
import { useMediaQuery } from '@material-ui/core'
import { Details } from './AlbumDetails'
// Mock useMediaQuery
vi.mock('@material-ui/core', async () => {
const actual = await import('@material-ui/core')
return {
...actual,
useMediaQuery: vi.fn(),
}
})
describe('Details component', () => {
describe('Desktop view', () => {
beforeEach(() => {
// Set desktop view (isXsmall = false)
vi.mocked(useMediaQuery).mockReturnValue(false)
})
test('renders correctly with just year range', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
year: 2020,
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with date', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
date: '2020-05-01',
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with originalDate', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
originalDate: '2018-03-15',
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with date and originalDate', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
date: '2020-05-01',
originalDate: '2018-03-15',
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with releaseDate', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
releaseDate: '2020-06-15',
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with all date fields', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
date: '2020-05-01',
originalDate: '2018-03-15',
releaseDate: '2020-06-15',
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
})
describe('Mobile view', () => {
beforeEach(() => {
// Set mobile view (isXsmall = true)
vi.mocked(useMediaQuery).mockReturnValue(true)
})
afterEach(() => {
vi.clearAllMocks()
})
test('renders correctly with just year range', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
year: 2020,
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with date', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
date: '2020-05-01',
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with originalDate', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
originalDate: '2018-03-15',
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with date and originalDate', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
date: '2020-05-01',
originalDate: '2018-03-15',
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with releaseDate', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
releaseDate: '2020-06-15',
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with all date fields', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
date: '2020-05-01',
originalDate: '2018-03-15',
releaseDate: '2020-06-15',
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with no date fields', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with year range (start and end years)', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
year: 2018,
yearEnd: 2020,
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
test('renders correctly with originalYear range', () => {
const record = {
id: '123',
name: 'Test Album',
songCount: 12,
duration: 3600,
size: 102400,
originalYear: 2015,
originalYearEnd: 2016,
}
const { container } = render(
<RecordContextProvider value={record}>
<Details />
</RecordContextProvider>,
)
expect(container).toMatchSnapshot()
})
})
})

View file

@ -13,14 +13,10 @@ import { linkToRecord, useListContext, Loading } from 'react-admin'
import { withContentRect } from 'react-measure'
import { useDrag } from 'react-dnd'
import subsonic from '../subsonic'
import {
AlbumContextMenu,
PlayButton,
ArtistLinkField,
RangeDoubleField,
} from '../common'
import { AlbumContextMenu, PlayButton, ArtistLinkField } from '../common'
import { DraggableTypes } from '../consts'
import clsx from 'clsx'
import { AlbumDatesField } from './AlbumDatesField.jsx'
const useStyles = makeStyles(
(theme) => ({
@ -187,16 +183,7 @@ const AlbumGridTile = ({ showArtist, record, basePath, ...props }) => {
{showArtist ? (
<ArtistLinkField record={record} className={classes.albumSubtitle} />
) : (
<RangeDoubleField
record={record}
source={'year'}
symbol1={'♫'}
symbol2={'○'}
separator={' · '}
sortBy={'max_year'}
sortByOrder={'DESC'}
className={classes.albumSubtitle}
/>
<AlbumDatesField record={record} className={classes.albumSubtitle} />
)}
</div>
)

View file

@ -20,6 +20,7 @@ import {
ArtistLinkField,
MultiLineTextField,
ParticipantsInfo,
RangeField,
} from '../common'
const useStyles = makeStyles({
@ -47,6 +48,20 @@ const AlbumInfo = (props) => {
</SingleFieldList>
</ArrayField>
),
date:
record?.maxYear && record.maxYear === record.minYear ? (
<TextField source={'date'} />
) : (
<RangeField source={'year'} />
),
originalDate:
record?.maxOriginalYear &&
record.maxOriginalYear === record.minOriginalYear ? (
<TextField source={'originalDate'} />
) : (
<RangeField source={'originalYear'} />
),
releaseDate: <TextField source={'releaseDate'} />,
recordLabel: (
<FunctionField
source={'recordLabel'}

View file

@ -0,0 +1,425 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Details component > Desktop view > renders correctly with all date fields 1`] = `
<div>
<span>
resources.album.fields.originalDate Mar 15, 2018
</span>
·
<span>
resources.album.fields.releaseDate Jun 15, 2020
</span>
·
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-6"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > Desktop view > renders correctly with date 1`] = `
<div>
<span>
May 1, 2020
</span>
·
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-2"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > Desktop view > renders correctly with date and originalDate 1`] = `
<div>
<span>
resources.album.fields.originalDate Mar 15, 2018
</span>
·
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-4"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > Desktop view > renders correctly with just year range 1`] = `
<div>
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-1"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > Desktop view > renders correctly with originalDate 1`] = `
<div>
<span>
resources.album.fields.originalDate Mar 15, 2018
</span>
·
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-3"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > Desktop view > renders correctly with releaseDate 1`] = `
<div>
<span>
resources.album.fields.releaseDate Jun 15, 2020
</span>
·
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-5"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > Mobile view > renders correctly with all date fields 1`] = `
<div>
<span>
♫ Mar 15, 2018
</span>
·
<span>
○ Jun 15, 2020
</span>
·
<span>
12 resources.song.name
</span>
</div>
`;
exports[`Details component > Mobile view > renders correctly with date 1`] = `
<div>
<span>
♫ May 1, 2020
</span>
·
<span>
12 resources.song.name
</span>
</div>
`;
exports[`Details component > Mobile view > renders correctly with date and originalDate 1`] = `
<div>
<span>
♫ Mar 15, 2018
</span>
·
<span>
12 resources.song.name
</span>
</div>
`;
exports[`Details component > Mobile view > renders correctly with just year range 1`] = `
<div>
<span>
12 resources.song.name
</span>
</div>
`;
exports[`Details component > Mobile view > renders correctly with no date fields 1`] = `
<div>
<span>
12 resources.song.name
</span>
</div>
`;
exports[`Details component > Mobile view > renders correctly with originalDate 1`] = `
<div>
<span>
♫ Mar 15, 2018
</span>
·
<span>
12 resources.song.name
</span>
</div>
`;
exports[`Details component > Mobile view > renders correctly with originalYear range 1`] = `
<div>
<span>
12 resources.song.name
</span>
</div>
`;
exports[`Details component > Mobile view > renders correctly with releaseDate 1`] = `
<div>
<span>
Jun 15, 2020
</span>
·
<span>
12 resources.song.name
</span>
</div>
`;
exports[`Details component > Mobile view > renders correctly with year range (start and end years) 1`] = `
<div>
<span>
12 resources.song.name
</span>
</div>
`;
exports[`Details component > renders correctly in mobile view 1`] = `
<div>
<span>
♫ Mar 15, 2018
</span>
·
<span>
○ Jun 15, 2020
</span>
·
<span>
12 resources.song.name
</span>
</div>
`;
exports[`Details component > renders correctly with all date fields 1`] = `
<div>
<span>
resources.album.fields.originalDate Mar 15, 2018
</span>
·
<span>
resources.album.fields.releaseDate Jun 15, 2020
</span>
·
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-6"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > renders correctly with date 1`] = `
<div>
<span>
May 1, 2020
</span>
·
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-2"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > renders correctly with date and originalDate 1`] = `
<div>
<span>
resources.album.fields.originalDate Mar 15, 2018
</span>
·
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-4"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > renders correctly with just year range 1`] = `
<div>
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-1"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > renders correctly with originalDate 1`] = `
<div>
<span>
resources.album.fields.originalDate Mar 15, 2018
</span>
·
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-3"
>
100 KB
</span>
</span>
</div>
`;
exports[`Details component > renders correctly with releaseDate 1`] = `
<div>
<span>
resources.album.fields.releaseDate Jun 15, 2020
</span>
·
<span>
12 resources.song.name
</span>
·
<span>
<span>
01:00:00
</span>
</span>
·
<span>
<span
class="makeStyles-root-5"
>
100 KB
</span>
</span>
</div>
`;

View file

@ -50,7 +50,7 @@ const ArtistDetails = (props) => {
)
}
const AlbumShowLayout = (props) => {
const ArtistShowLayout = (props) => {
const showContext = useShowContext(props)
const record = useRecordContext()
const { width } = props
@ -98,7 +98,7 @@ const ArtistShow = withWidth()((props) => {
const controllerProps = useShowController(props)
return (
<ShowContextProvider value={controllerProps}>
<AlbumShowLayout {...controllerProps} />
<ArtistShowLayout {...controllerProps} />
</ShowContextProvider>
)
})

View file

@ -1,50 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useRecordContext } from 'react-admin'
import { formatRange } from '../common'
export const RangeDoubleField = ({
className,
source,
symbol1,
symbol2,
separator,
...rest
}) => {
const record = useRecordContext(rest)
const yearRange = formatRange(record, source).toString()
const releases = [record.releases]
const releaseDate = [record.releaseDate]
const releaseYear = releaseDate.toString().substring(0, 4)
let subtitle = yearRange
if (releases > 1) {
subtitle = [
[yearRange && symbol1, yearRange].join(' '),
['(', releases, ')))'].join(' '),
].join(separator)
}
if (
yearRange !== releaseYear &&
yearRange.length > 0 &&
releaseYear.length > 0
) {
subtitle = [
[yearRange && symbol1, yearRange].join(' '),
[symbol2, releaseYear].join(' '),
].join(separator)
}
return <span className={className}>{subtitle}</span>
}
RangeDoubleField.propTypes = {
label: PropTypes.string,
record: PropTypes.object,
source: PropTypes.string.isRequired,
}
RangeDoubleField.defaultProps = {
addLabel: true,
}

View file

@ -13,7 +13,6 @@ export * from './Pagination'
export * from './PlayButton'
export * from './QuickFilter'
export * from './RangeField'
export * from './RangeDoubleField'
export * from './ShuffleAllButton'
export * from './SimpleList'
export * from './SizeField'

View file

@ -58,6 +58,7 @@
"genre": "Genre",
"compilation": "Compilation",
"year": "Year",
"date": "Recording Date",
"originalDate": "Original",
"releaseDate": "Released",
"releases": "Release |||| Releases",