Support for Original Date, Release Date & splitting/grouping of album editions (#2162)

* Update AlbumGridView.js

* Update AlbumDetails.js

* Update AlbumDetails.js

* Create DoubleRangeField.js

* Update and rename DoubleRangeField.js to RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumGridView.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update index.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update en.json

* Update en.json

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update AlbumGridView.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update ContextMenus.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongList.js

* Update playlist_track_repository.go

* Update 20230113000000_release_year.go

* Update PlayButton.js

* Update mediafile_repository.go

* Update album.go

* Update playlist_track_repository.go

* Update playlist_track_repository.go

* Update SongDatagrid.js

* Update 20230113000000_release_year.go

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update AlbumSongs.js

* Update RangeFieldDouble.js

* Update SongDatagrid.js

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update AlbumSongs.js

* Update AlbumSongs.js

* Update mapping.go

* Update RangeFieldDouble.js

* Update AlbumGridView.js

* Update AlbumSongs.js

* Update en.json

* Update SongDatagrid.js

* Update SongDatagrid.js

* Update metadata.go

* Update mapping.go

* Update AlbumDetails.js

* Update AlbumGridView.js

* Update RangeFieldDouble.js

* Update mapping.go

* Update metadata.go

* Update mapping.go

* Update AlbumDetails.js

* Update 20230113000000_release_year.go

* Update AlbumDetails.js

* Update en.json

* Update configuration.go

* Update mapping.go

* Update configuration.go

* Update mediafile.go

* Update metadata.go

* Update RangeFieldDouble.js

* Update 20230113000000_release_year.go

* Update configuration.go

* Update mapping.go

* Update mediafile.go

* Update mapping.go

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update RangeFieldDouble.js

* Update 20230113000000_release_year.go

* Update AlbumDetails.js

* Update RangeFieldDouble.js

* Update mapping.go

* Update metadata.go

* Update album.go

* Update mediafile.go

* Update mediafile.go

* Update album.go

* Update fields.go

* Update mediafile_repository.go

* Update playlist_track_repository.go

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update PlayButton.js

* Update SongList.js

* Update ContextMenus.js

* Update SongDatagrid.js

* Update metadata.go

* Update ArtistShow.js

* Update mapping.go

* Update configuration.go

* Update mapping.go

* Update metadata.go

* Update metadata.go

* Update mapping.go

* Update metadata.go

* Update metadata.go

* Update mapping.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update mapping.go

* Update metadata.go

* Update metadata.go

* Update album.go

* Update mediafile.go

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update album.go

* Update mediafile.go

* Update metadata.go

* Update mediafile.go

* Update 20230113000000_release_year.go

* Update 20230113000000_release_year.go

* Update album.go

* Update mediafile.go

* Update RangeFieldDouble.js

* Update AlbumDetails.js

* Update AlbumGridView.js

* Update en.json

* Update AlbumGridView.js

* Update RangeFieldDouble.js

* Update and rename 20230113000000_release_year.go to 20230113000000_release_date.go

* Update album.go

* Update mediafile.go

* Update fields.go

* Update playlist_track_repository.go

* Update mediafile_repository.go

* Update mapping.go

* Update metadata.go

* Update mapping.go

* Update SongDatagrid.js

* Update RangeFieldDouble.js

* Update index.js

* Update ContextMenus.js

* Update PlayButton.js

* Create FormatDate.js

* Update SongList.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update AlbumSongs.js

* Update en.json

* Update AlbumDetails.js

* Update album.go

fixed conflict I think?

* Update mediafile.go

fixed conflict

* Format with goimports

* Update SongDatagrid.js

only show Cat # in desktop view

* Update metadata_internal_test.go

* Update metadata_test.go

* Delete test.mp3

* Add files via upload

mp3 test file with Date, Original Date and Release Date

* Update metadata_test.go

* Update metadata_test.go

* Update metadata_test.go

* Update metadata_test.go

* Update taglib_test.go

* Delete test.mp3

* Add files via upload

file with replaygain & dates

* Update AlbumGridView.js

* Update AlbumDetails.js

* Update AlbumSongs.js

* Update ContextMenus.js

* Update FormatDate.js

* Update PlayButton.js

* Update RangeFieldDouble.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update AlbumSongs.js

* Fix formatting

* Update mapping.go

* Update AlbumSongs.js

* Update SongDatagrid.js

* Update SongDatagrid.js

prettier

* Create RangeDoubleField.js

rename of RangeFieldDouble.js

* Update AlbumGridView.js

RangeFieldDouble -> RangeDoubleField

* Update mediafile.go

AllOrNothing() -> allOrNothing()

* Update metadata_internal_test.go

getYear -> getDate

* Update AlbumDetails.js

wrote suggested changes

* Update en.json

Editions -> Releases & fixed the field name

* Update configuration.go

Rename Editions -> Releases

* Update 20230113000000_release_date.go

Editions -> Releases

* Update album.go

Editions -> Releases

* Update mediafile.go

Editions -> Releases

* Update AlbumDetails.js

Editions -> Releases

* Update AlbumSongs.js

Editions -> Releases

* Update RangeDoubleField.js

Editions -> Releases

* Update SongDatagrid.js

Editions -> Releases

* Update index.js

FormatFullDate and RangeDoubleField

* Rename FormatDate.js to FormatFullDate.js

* Delete RangeFieldDouble.js

* Update mediafile.go

AllOrNothing -> allOrNothing

* Update mapping.go

Editions -> Releases

* Update AlbumDetails.js

prettier

* Update SongDatagrid.js

showReleaseRow -> showReleaseDivider

* Update AlbumSongs.js

showReleaseRow -> showReleaseDivider for clarity

* Update and rename 20230113000000_release_date.go to 20230515184510_add_release_date.go

- rename the migration file
- fixed the import to goose/v3
- additional db fields for original date & year

* Update 20230515184510_add_release_date.go

* Update fields.go

* Update album.go

* Update mediafile.go

* Update mapping.go

* Update AlbumDetails.js

* Update en.json

* Update AlbumDetails.js

* Update AlbumDetails.js

now hopefully prettier

* Update mapping.go

---------

Co-authored-by: Deluan <deluan@navidrome.org>
This commit is contained in:
certuna 2023-05-19 21:27:47 +02:00 committed by GitHub
parent 010ba0d15c
commit 52b77e4194
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 511 additions and 78 deletions

View file

@ -25,6 +25,7 @@ import {
ArtistLinkField,
DurationField,
formatRange,
FormatFullDate,
SizeField,
LoveButton,
RatingField,
@ -195,8 +196,59 @@ const Details = (props) => {
details.push(<span key={`detail-${record.id}-${id}`}>{obj}</span>)
}
const year = formatRange(record, 'year')
year && addDetail(<>{year}</>)
const originalYearRange = formatRange(record, 'originalYear')
const originalDate = record.originalDate
? FormatFullDate(record.originalDate)
: originalYearRange
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
showOriginalDate &&
!isXsmall &&
addDetail(
<>
{[translate('resources.album.fields.originalDate'), originalDate].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 +

View file

@ -17,7 +17,7 @@ import {
AlbumContextMenu,
PlayButton,
ArtistLinkField,
RangeField,
RangeDoubleField,
} from '../common'
import { DraggableTypes } from '../consts'
@ -161,9 +161,12 @@ const AlbumGridTile = ({ showArtist, record, basePath, ...props }) => {
{showArtist ? (
<ArtistLinkField record={record} className={classes.albumSubtitle} />
) : (
<RangeField
<RangeDoubleField
record={record}
source={'year'}
symbol1={'♫'}
symbol2={'○'}
separator={' · '}
sortBy={'max_year'}
sortByOrder={'DESC'}
className={classes.albumSubtitle}

View file

@ -99,7 +99,7 @@ const AlbumSongs = (props) => {
trackNumber: isDesktop && (
<TextField
source="trackNumber"
sortBy="discNumber asc, trackNumber asc"
sortBy="releaseDate asc, discNumber asc, trackNumber asc"
label="#"
sortable={false}
/>
@ -172,6 +172,7 @@ const AlbumSongs = (props) => {
{...props}
hasBulkActions={true}
showDiscSubtitles={true}
showReleaseDivider={true}
contextAlwaysVisible={!isDesktop}
classes={{ row: classes.row }}
>
@ -207,7 +208,6 @@ export const removeAlbumCommentsFromSongs = ({ album, data }) => {
const SanitizedAlbumSongs = (props) => {
removeAlbumCommentsFromSongs(props)
const { loaded, loading, total, ...rest } = useListContext(props)
return <>{loaded && <AlbumSongs {...rest} actions={props.actions} />}</>
}

View file

@ -60,7 +60,7 @@ const AlbumShowLayout = (props) => {
addLabel={false}
reference="album"
target="artist_id"
sort={{ field: 'max_year', order: 'ASC' }}
sort={{ field: 'max_year asc,date asc', order: 'ASC' }}
filter={{ artist_id: record?.id }}
perPage={0}
pagination={null}

View file

@ -200,8 +200,12 @@ export const AlbumContextMenu = (props) =>
resource={'album'}
songQueryParams={{
pagination: { page: 1, perPage: -1 },
sort: { field: 'discNumber, trackNumber', order: 'ASC' },
filter: { album_id: props.record.id, disc_number: props.discNumber },
sort: { field: 'releaseDate, discNumber, trackNumber', order: 'ASC' },
filter: {
album_id: props.record.id,
release_date: props.releaseDate,
disc_number: props.discNumber,
},
}}
/>
) : null
@ -226,7 +230,10 @@ export const ArtistContextMenu = (props) =>
resource={'artist'}
songQueryParams={{
pagination: { page: 1, perPage: 200 },
sort: { field: 'album, discNumber, trackNumber', order: 'ASC' },
sort: {
field: 'album, releaseDate, discNumber, trackNumber',
order: 'ASC',
},
filter: { album_artist_id: props.record.id },
}}
/>

View file

@ -0,0 +1,29 @@
export const FormatFullDate = (date) => {
const dashes = date.split('-').length - 1
let options = {
year: 'numeric',
}
switch (dashes) {
case 2:
options = {
year: 'numeric',
month: 'long',
day: 'numeric',
}
return new Date(date).toLocaleDateString(undefined, options)
case 1:
options = {
year: 'numeric',
month: 'long',
}
return new Date(date).toLocaleDateString(undefined, options)
case 0:
if (date.length === 4) {
return new Date(date).toLocaleDateString(undefined, options)
} else {
return ''
}
default:
return ''
}
}

View file

@ -21,8 +21,12 @@ export const PlayButton = ({ record, size, className }) => {
dataProvider
.getList('song', {
pagination: { page: 1, perPage: -1 },
sort: { field: 'discNumber, trackNumber', order: 'ASC' },
filter: { album_id: record.id, disc_number: record.discNumber },
sort: { field: 'releaseDate, discNumber, trackNumber', order: 'ASC' },
filter: {
album_id: record.id,
release_date: record.releaseDate,
disc_number: record.discNumber,
},
})
.then((response) => {
let { data, ids } = extractSongsData(response)

View file

@ -0,0 +1,50 @@
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

@ -1,6 +1,11 @@
import React, { isValidElement, useMemo, useCallback, forwardRef } from 'react'
import { useDispatch } from 'react-redux'
import { Datagrid, PureDatagridBody, PureDatagridRow } from 'react-admin'
import {
Datagrid,
PureDatagridBody,
PureDatagridRow,
useTranslate,
} from 'react-admin'
import {
TableCell,
TableRow,
@ -13,7 +18,7 @@ import AlbumIcon from '@material-ui/icons/Album'
import clsx from 'clsx'
import { useDrag } from 'react-dnd'
import { playTracks } from '../actions'
import { AlbumContextMenu } from '../common'
import { AlbumContextMenu, FormatFullDate } from '../common'
import { DraggableTypes } from '../consts'
const useStyles = makeStyles({
@ -49,12 +54,57 @@ const useStyles = makeStyles({
},
})
const ReleaseRow = forwardRef(
({ record, onClick, colSpan, contextAlwaysVisible }, ref) => {
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
const classes = useStyles({ isDesktop })
const translate = useTranslate()
const handlePlaySubset = (releaseDate) => () => {
onClick(releaseDate)
}
let releaseTitle = []
if (record.releaseDate) {
releaseTitle.push(translate('resources.album.fields.released'))
releaseTitle.push(FormatFullDate(record.releaseDate))
if (record.catalogNum && isDesktop) {
releaseTitle.push('· Cat #')
releaseTitle.push(record.catalogNum)
}
}
return (
<TableRow
hover
ref={ref}
onClick={handlePlaySubset(record.releaseDate)}
className={classes.row}
>
<TableCell colSpan={colSpan}>
<Typography variant="h6" className={classes.subtitle}>
{releaseTitle.join(' ')}
</Typography>
</TableCell>
<TableCell>
<AlbumContextMenu
record={{ id: record.albumId }}
releaseDate={record.releaseDate}
showLove={false}
className={classes.contextMenu}
visible={contextAlwaysVisible}
/>
</TableCell>
</TableRow>
)
}
)
const DiscSubtitleRow = forwardRef(
({ record, onClick, colSpan, contextAlwaysVisible }, ref) => {
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
const classes = useStyles({ isDesktop })
const handlePlayDisc = (discNumber) => () => {
onClick(discNumber)
const handlePlaySubset = (releaseDate, discNumber) => () => {
onClick(releaseDate, discNumber)
}
let subtitle = []
@ -69,7 +119,7 @@ const DiscSubtitleRow = forwardRef(
<TableRow
hover
ref={ref}
onClick={handlePlayDisc(record.discNumber)}
onClick={handlePlaySubset(record.releaseDate, record.discNumber)}
className={classes.row}
>
<TableCell colSpan={colSpan}>
@ -82,6 +132,7 @@ const DiscSubtitleRow = forwardRef(
<AlbumContextMenu
record={{ id: record.albumId }}
discNumber={record.discNumber}
releaseDate={record.releaseDate}
showLove={false}
className={classes.contextMenu}
visible={contextAlwaysVisible}
@ -95,9 +146,10 @@ const DiscSubtitleRow = forwardRef(
export const SongDatagridRow = ({
record,
children,
firstTracks,
firstTracksOfDiscs,
firstTracksOfReleases,
contextAlwaysVisible,
onClickDiscSubtitle,
onClickSubset,
className,
...rest
}) => {
@ -110,7 +162,13 @@ export const SongDatagridRow = ({
() => ({
type: DraggableTypes.DISC,
item: {
discs: [{ albumId: record?.albumId, discNumber: record?.discNumber }],
discs: [
{
albumId: record?.albumId,
releaseDate: record?.releaseDate,
discNumber: record?.discNumber,
},
],
},
options: { dropEffect: 'copy' },
}),
@ -133,11 +191,20 @@ export const SongDatagridRow = ({
const childCount = fields.length
return (
<>
{firstTracks.has(record.id) && (
{firstTracksOfReleases.has(record.id) && (
<ReleaseRow
ref={dragDiscRef}
record={record}
onClick={onClickSubset}
contextAlwaysVisible={contextAlwaysVisible}
colSpan={childCount + (rest.expand ? 1 : 0)}
/>
)}
{firstTracksOfDiscs.has(record.id) && (
<DiscSubtitleRow
ref={dragDiscRef}
record={record}
onClick={onClickDiscSubtitle}
onClick={onClickSubset}
contextAlwaysVisible={contextAlwaysVisible}
colSpan={childCount + (rest.expand ? 1 : 0)}
/>
@ -157,32 +224,43 @@ export const SongDatagridRow = ({
SongDatagridRow.propTypes = {
record: PropTypes.object,
children: PropTypes.node,
firstTracks: PropTypes.instanceOf(Set),
firstTracksOfDiscs: PropTypes.instanceOf(Set),
firstTracksOfReleases: PropTypes.instanceOf(Set),
contextAlwaysVisible: PropTypes.bool,
onClickDiscSubtitle: PropTypes.func,
onClickSubset: PropTypes.func,
}
SongDatagridRow.defaultProps = {
onClickDiscSubtitle: () => {},
onClickSubset: () => {},
}
const SongDatagridBody = ({
contextAlwaysVisible,
showDiscSubtitles,
showReleaseDivider,
...rest
}) => {
const dispatch = useDispatch()
const { ids, data } = rest
const playDisc = useCallback(
(discNumber) => {
const idsToPlay = ids.filter((id) => data[id].discNumber === discNumber)
const playSubset = useCallback(
(releaseDate, discNumber) => {
let idsToPlay = []
if (discNumber !== undefined) {
idsToPlay = ids.filter(
(id) =>
data[id].releaseDate === releaseDate &&
data[id].discNumber === discNumber
)
} else {
idsToPlay = ids.filter((id) => data[id].releaseDate === releaseDate)
}
dispatch(playTracks(data, idsToPlay))
},
[dispatch, data, ids]
)
const firstTracks = useMemo(() => {
const firstTracksOfDiscs = useMemo(() => {
if (!ids) {
return new Set()
}
@ -195,7 +273,8 @@ const SongDatagridBody = ({
foundSubtitle = foundSubtitle || data[id].discSubtitle
if (
acc.length === 0 ||
(last && data[id].discNumber !== data[last].discNumber)
(last && data[id].discNumber !== data[last].discNumber) ||
(last && data[id].releaseDate !== data[last].releaseDate)
) {
acc.push(id)
}
@ -208,14 +287,39 @@ const SongDatagridBody = ({
return set
}, [ids, data, showDiscSubtitles])
const firstTracksOfReleases = useMemo(() => {
if (!ids) {
return new Set()
}
const set = new Set(
ids
.filter((i) => data[i])
.reduce((acc, id) => {
const last = acc && acc[acc.length - 1]
if (
acc.length === 0 ||
(last && data[id].releaseDate !== data[last].releaseDate)
) {
acc.push(id)
}
return acc
}, [])
)
if (!showReleaseDivider || set.size < 2) {
set.clear()
}
return set
}, [ids, data, showReleaseDivider])
return (
<PureDatagridBody
{...rest}
row={
<SongDatagridRow
firstTracks={firstTracks}
firstTracksOfDiscs={firstTracksOfDiscs}
firstTracksOfReleases={firstTracksOfReleases}
contextAlwaysVisible={contextAlwaysVisible}
onClickDiscSubtitle={playDisc}
onClickSubset={playSubset}
/>
}
/>
@ -225,6 +329,7 @@ const SongDatagridBody = ({
export const SongDatagrid = ({
contextAlwaysVisible,
showDiscSubtitles,
showReleaseDivider,
...rest
}) => {
const classes = useStyles()
@ -236,6 +341,7 @@ export const SongDatagrid = ({
<SongDatagridBody
contextAlwaysVisible={contextAlwaysVisible}
showDiscSubtitles={showDiscSubtitles}
showReleaseDivider={showReleaseDivider}
/>
}
/>
@ -245,5 +351,6 @@ export const SongDatagrid = ({
SongDatagrid.propTypes = {
contextAlwaysVisible: PropTypes.bool,
showDiscSubtitles: PropTypes.bool,
showReleaseDivider: PropTypes.bool,
classes: PropTypes.object,
}

View file

@ -4,6 +4,7 @@ export * from './BatchPlayButton'
export * from './BitrateField'
export * from './ContextMenus'
export * from './DateField'
export * from './FormatFullDate'
export * from './DocLink'
export * from './DurationField'
export * from './List'
@ -12,6 +13,7 @@ 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

@ -51,7 +51,9 @@
"name": "Name",
"genre": "Genre",
"compilation": "Compilation",
"year": "Year",
"originalDate": "Original",
"releaseDate": "Released",
"releases": "Release |||| Releases",
"updatedAt": "Updated at",
"comment": "Comment",
"rating": "Rating",

View file

@ -102,7 +102,7 @@ const SongList = (props) => {
<AlbumLinkField
source="album"
sortBy={
'album, order_album_artist_name, disc_number, track_number, title'
'album, order_album_artist_name, release_date, disc_number, track_number, title'
}
sortByOrder={'ASC'}
/>