mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-01 19:47:37 +03:00
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:
parent
88f87e6c4f
commit
2b84c574ba
20 changed files with 929 additions and 155 deletions
|
@ -51,20 +51,6 @@ func legacyMapAlbumName(md Metadata) string {
|
|||
|
||||
// Keep the TaggedLikePicard logic for backwards compatibility
|
||||
func legacyReleaseDate(md Metadata) string {
|
||||
// Start with defaults
|
||||
date := md.Date(model.TagRecordingDate)
|
||||
year := date.Year()
|
||||
originalDate := md.Date(model.TagOriginalDate)
|
||||
originalYear := originalDate.Year()
|
||||
releaseDate := md.Date(model.TagReleaseDate)
|
||||
releaseYear := releaseDate.Year()
|
||||
|
||||
// MusicBrainz Picard writes the Release Date of an album to the Date tag, and leaves the Release Date tag empty
|
||||
taggedLikePicard := (originalYear != 0) &&
|
||||
(releaseYear == 0) &&
|
||||
(year >= originalYear)
|
||||
if taggedLikePicard {
|
||||
return string(date)
|
||||
}
|
||||
_, _, releaseDate := md.mapDates()
|
||||
return string(releaseDate)
|
||||
}
|
||||
|
|
30
model/metadata/legacy_ids_test.go
Normal file
30
model/metadata/legacy_ids_test.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("legacyReleaseDate", func() {
|
||||
|
||||
DescribeTable("legacyReleaseDate",
|
||||
func(recordingDate, originalDate, releaseDate, expected string) {
|
||||
md := New("", Info{
|
||||
Tags: map[string][]string{
|
||||
"DATE": {recordingDate},
|
||||
"ORIGINALDATE": {originalDate},
|
||||
"RELEASEDATE": {releaseDate},
|
||||
},
|
||||
})
|
||||
|
||||
result := legacyReleaseDate(md)
|
||||
Expect(result).To(Equal(expected))
|
||||
},
|
||||
Entry("regular mapping", "2020-05-15", "2019-02-10", "2021-01-01", "2021-01-01"),
|
||||
Entry("legacy mapping", "2020-05-15", "2019-02-10", "", "2020-05-15"),
|
||||
Entry("legacy mapping, originalYear < year", "2018-05-15", "2019-02-10", "2021-01-01", "2021-01-01"),
|
||||
Entry("legacy mapping, originalYear empty", "2020-05-15", "", "2021-01-01", "2021-01-01"),
|
||||
Entry("legacy mapping, releaseYear", "2020-05-15", "2019-02-10", "2021-01-01", "2021-01-01"),
|
||||
Entry("legacy mapping, same dates", "2020-05-15", "2020-05-15", "", "2020-05-15"),
|
||||
)
|
||||
})
|
|
@ -1,6 +1,7 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"math"
|
||||
|
@ -39,11 +40,9 @@ func (md Metadata) ToMediaFile(libID int, folderID string) model.MediaFile {
|
|||
mf.ExplicitStatus = md.mapExplicitStatusTag()
|
||||
|
||||
// Dates
|
||||
origDate := md.Date(model.TagOriginalDate)
|
||||
date, origDate, relDate := md.mapDates()
|
||||
mf.OriginalYear, mf.OriginalDate = origDate.Year(), string(origDate)
|
||||
relDate := md.Date(model.TagReleaseDate)
|
||||
mf.ReleaseYear, mf.ReleaseDate = relDate.Year(), string(relDate)
|
||||
date := md.Date(model.TagRecordingDate)
|
||||
mf.Year, mf.Date = date.Year(), string(date)
|
||||
|
||||
// MBIDs
|
||||
|
@ -164,3 +163,22 @@ func (md Metadata) mapExplicitStatusTag() string {
|
|||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (md Metadata) mapDates() (date Date, originalDate Date, releaseDate Date) {
|
||||
// Start with defaults
|
||||
date = md.Date(model.TagRecordingDate)
|
||||
originalDate = md.Date(model.TagOriginalDate)
|
||||
releaseDate = md.Date(model.TagReleaseDate)
|
||||
|
||||
// For some historic reason, taggers have been writing the Release Date of an album to the Date tag,
|
||||
// and leave the Release Date tag empty.
|
||||
legacyMappings := (originalDate != "") &&
|
||||
(releaseDate == "") &&
|
||||
(date >= originalDate)
|
||||
if legacyMappings {
|
||||
return originalDate, originalDate, date
|
||||
}
|
||||
// when there's no Date, first fall back to Original Date, then to Release Date.
|
||||
date = cmp.Or(date, originalDate, releaseDate)
|
||||
return date, originalDate, releaseDate
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ var _ = Describe("ToMediaFile", func() {
|
|||
}
|
||||
|
||||
Describe("Dates", func() {
|
||||
It("should parse the dates like Picard", func() {
|
||||
It("should parse properly tagged dates ", func() {
|
||||
mf = toMediaFile(model.RawTags{
|
||||
"ORIGINALDATE": {"1978-09-10"},
|
||||
"DATE": {"1977-03-04"},
|
||||
|
@ -49,6 +49,32 @@ var _ = Describe("ToMediaFile", func() {
|
|||
Expect(mf.ReleaseYear).To(Equal(2002))
|
||||
Expect(mf.ReleaseDate).To(Equal("2002-01-02"))
|
||||
})
|
||||
|
||||
It("should parse dates with only year", func() {
|
||||
mf = toMediaFile(model.RawTags{
|
||||
"ORIGINALYEAR": {"1978"},
|
||||
"DATE": {"1977"},
|
||||
"RELEASEDATE": {"2002"},
|
||||
})
|
||||
|
||||
Expect(mf.Year).To(Equal(1977))
|
||||
Expect(mf.Date).To(Equal("1977"))
|
||||
Expect(mf.OriginalYear).To(Equal(1978))
|
||||
Expect(mf.OriginalDate).To(Equal("1978"))
|
||||
Expect(mf.ReleaseYear).To(Equal(2002))
|
||||
Expect(mf.ReleaseDate).To(Equal("2002"))
|
||||
})
|
||||
|
||||
It("should parse dates tagged the legacy way (no release date)", func() {
|
||||
mf = toMediaFile(model.RawTags{
|
||||
"DATE": {"2014"},
|
||||
"ORIGINALDATE": {"1966"},
|
||||
})
|
||||
|
||||
Expect(mf.Year).To(Equal(1966))
|
||||
Expect(mf.OriginalYear).To(Equal(1966))
|
||||
Expect(mf.ReleaseYear).To(Equal(2014))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Lyrics", func() {
|
||||
|
|
|
@ -90,13 +90,14 @@ var _ = Describe("Metadata", func() {
|
|||
md = metadata.New(filePath, props)
|
||||
|
||||
Expect(md.All()).To(SatisfyAll(
|
||||
HaveLen(5),
|
||||
Not(HaveKey(unknownTag)),
|
||||
HaveKeyWithValue(model.TagTrackArtist, []string{"Artist Name", "Second Artist"}),
|
||||
HaveKeyWithValue(model.TagAlbum, []string{"Album Name"}),
|
||||
HaveKeyWithValue(model.TagRecordingDate, []string{"2022-10-02", "2022"}),
|
||||
HaveKeyWithValue(model.TagRecordingDate, []string{"2022-10-02"}),
|
||||
HaveKeyWithValue(model.TagReleaseDate, []string{"2022"}),
|
||||
HaveKeyWithValue(model.TagGenre, []string{"Pop", "Rock"}),
|
||||
HaveKeyWithValue(model.TagTrackNumber, []string{"1/10"}),
|
||||
HaveLen(6),
|
||||
))
|
||||
})
|
||||
|
||||
|
|
|
@ -97,9 +97,10 @@ func NewAlbumRepository(ctx context.Context, db dbx.Builder) model.AlbumReposito
|
|||
r.tableName = "album"
|
||||
r.registerModel(&model.Album{}, albumFilters())
|
||||
r.setSortMappings(map[string]string{
|
||||
"name": "order_album_name, order_album_artist_name",
|
||||
"artist": "compilation, order_album_artist_name, order_album_name",
|
||||
"album_artist": "compilation, order_album_artist_name, order_album_name",
|
||||
"name": "order_album_name, order_album_artist_name",
|
||||
"artist": "compilation, order_album_artist_name, order_album_name",
|
||||
"album_artist": "compilation, order_album_artist_name, order_album_name",
|
||||
// TODO Rename this to just year (or date)
|
||||
"max_year": "coalesce(nullif(original_date,''), cast(max_year as text)), release_date, name",
|
||||
"random": "random",
|
||||
"recently_added": recentlyAddedSort(),
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"genre": "Gênero",
|
||||
"compilation": "Coletânea",
|
||||
"year": "Ano",
|
||||
"date": "Data de Lançamento",
|
||||
"updatedAt": "Últ. Atualização",
|
||||
"comment": "Comentário",
|
||||
"rating": "Classificação",
|
||||
|
|
|
@ -118,10 +118,10 @@ main:
|
|||
aliases: [ tdor, originaldate, ----:com.apple.itunes:originaldate, wm/originalreleasetime, tory, originalyear, ----:com.apple.itunes:originalyear, wm/originalreleaseyear ]
|
||||
type: date
|
||||
recordingdate:
|
||||
aliases: [ tdrc, date, icrd, ©day, wm/year, year ]
|
||||
aliases: [ tdrc, date, recordingdate, icrd, record date ]
|
||||
type: date
|
||||
releasedate:
|
||||
aliases: [ tdrl, releasedate ]
|
||||
aliases: [ tdrl, releasedate, ©day, wm/year, year ]
|
||||
type: date
|
||||
catalognumber:
|
||||
aliases: [ txxx:catalognumber, catalognumber, ----:com.apple.itunes:catalognumber, wm/catalogno ]
|
||||
|
|
|
@ -62,13 +62,14 @@ func AlbumsByArtistID(artistId string) Options {
|
|||
}
|
||||
|
||||
func AlbumsByYear(fromYear, toYear int) Options {
|
||||
sortOption := "max_year, name"
|
||||
orderOption := ""
|
||||
if fromYear > toYear {
|
||||
fromYear, toYear = toYear, fromYear
|
||||
sortOption = "max_year desc, name"
|
||||
orderOption = "desc"
|
||||
}
|
||||
return addDefaultFilters(Options{
|
||||
Sort: sortOption,
|
||||
Sort: "max_year",
|
||||
Order: orderOption,
|
||||
Filters: Or{
|
||||
And{
|
||||
GtOrEq{"min_year": fromYear},
|
||||
|
@ -118,7 +119,7 @@ func SongWithLyrics(artist, title string) Options {
|
|||
|
||||
func ByGenre(genre string) Options {
|
||||
return addDefaultFilters(Options{
|
||||
Sort: "name asc",
|
||||
Sort: "name",
|
||||
Filters: filterByGenre(genre),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -296,7 +296,7 @@ func childFromAlbum(ctx context.Context, al model.Album) responses.Child {
|
|||
child.Name = al.Name
|
||||
child.Album = al.Name
|
||||
child.Artist = al.AlbumArtist
|
||||
child.Year = int32(al.MaxYear)
|
||||
child.Year = int32(cmp.Or(al.MaxOriginalYear, al.MaxYear))
|
||||
child.Genre = al.Genre
|
||||
child.CoverArt = al.CoverArtID().String()
|
||||
child.Created = &al.CreatedAt
|
||||
|
@ -380,7 +380,7 @@ func buildAlbumID3(ctx context.Context, album model.Album) responses.AlbumID3 {
|
|||
dir.SongCount = int32(album.SongCount)
|
||||
dir.Duration = int32(album.Duration)
|
||||
dir.PlayCount = album.PlayCount
|
||||
dir.Year = int32(album.MaxYear)
|
||||
dir.Year = int32(cmp.Or(album.MaxOriginalYear, album.MaxYear))
|
||||
dir.Genre = album.Genre
|
||||
if !album.CreatedAt.IsZero() {
|
||||
dir.Created = &album.CreatedAt
|
||||
|
|
19
ui/src/album/AlbumDatesField.jsx
Normal file
19
ui/src/album/AlbumDatesField.jsx
Normal 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>
|
||||
}
|
|
@ -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, ' · ')}</>
|
||||
}
|
||||
|
||||
|
|
327
ui/src/album/AlbumDetails.test.jsx
Normal file
327
ui/src/album/AlbumDetails.test.jsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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'}
|
||||
|
|
425
ui/src/album/__snapshots__/AlbumDetails.test.jsx.snap
Normal file
425
ui/src/album/__snapshots__/AlbumDetails.test.jsx.snap
Normal 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>
|
||||
`;
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
"genre": "Genre",
|
||||
"compilation": "Compilation",
|
||||
"year": "Year",
|
||||
"date": "Recording Date",
|
||||
"originalDate": "Original",
|
||||
"releaseDate": "Released",
|
||||
"releases": "Release |||| Releases",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue