mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-03 20:47:35 +03:00
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:
parent
010ba0d15c
commit
52b77e4194
24 changed files with 511 additions and 78 deletions
|
@ -103,6 +103,7 @@ type configOptions struct {
|
|||
type scannerOptions struct {
|
||||
Extractor string
|
||||
GenreSeparators string
|
||||
GroupAlbumReleases bool
|
||||
}
|
||||
|
||||
type lastfmOptions struct {
|
||||
|
@ -297,6 +298,7 @@ func init() {
|
|||
|
||||
viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor)
|
||||
viper.SetDefault("scanner.genreseparators", ";/,")
|
||||
viper.SetDefault("scanner.groupalbumreleases", true)
|
||||
|
||||
viper.SetDefault("agents", "lastfm,spotify")
|
||||
viper.SetDefault("lastfm.enabled", true)
|
||||
|
|
49
db/migration/20230515184510_add_release_date.go
Normal file
49
db/migration/20230515184510_add_release_date.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigration(upAddRelRecYear, downAddRelRecYear)
|
||||
}
|
||||
|
||||
func upAddRelRecYear(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
alter table media_file
|
||||
add date varchar(255) default '' not null;
|
||||
alter table media_file
|
||||
add original_year int default 0 not null;
|
||||
alter table media_file
|
||||
add original_date varchar(255) default '' not null;
|
||||
alter table media_file
|
||||
add release_year int default 0 not null;
|
||||
alter table media_file
|
||||
add release_date varchar(255) default '' not null;
|
||||
|
||||
alter table album
|
||||
add date varchar(255) default '' not null;
|
||||
alter table album
|
||||
add min_original_year int default 0 not null;
|
||||
alter table album
|
||||
add max_original_year int default 0 not null;
|
||||
alter table album
|
||||
add original_date varchar(255) default '' not null;
|
||||
alter table album
|
||||
add release_date varchar(255) default '' not null;
|
||||
alter table album
|
||||
add releases integer default 0 not null;
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
notice(tx, "A full rescan needs to be performed to import more tags")
|
||||
return forceFullRescan(tx)
|
||||
}
|
||||
|
||||
func downAddRelRecYear(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
|
@ -20,6 +20,12 @@ type Album struct {
|
|||
AllArtistIDs string `structs:"all_artist_ids" json:"allArtistIds" orm:"column(all_artist_ids)"`
|
||||
MaxYear int `structs:"max_year" json:"maxYear"`
|
||||
MinYear int `structs:"min_year" json:"minYear"`
|
||||
Date string `structs:"date" json:"date,omitempty"`
|
||||
MaxOriginalYear int `structs:"max_original_year" json:"maxOriginalYear"`
|
||||
MinOriginalYear int `structs:"min_original_year" json:"minOriginalYear"`
|
||||
OriginalDate string `structs:"original_date" json:"originalDate,omitempty"`
|
||||
ReleaseDate string `structs:"release_date" json:"releaseDate,omitempty"`
|
||||
Releases int `structs:"releases" json:"releases"`
|
||||
Compilation bool `structs:"compilation" json:"compilation"`
|
||||
Comment string `structs:"comment" json:"comment,omitempty"`
|
||||
SongCount int `structs:"song_count" json:"songCount"`
|
||||
|
@ -56,6 +62,7 @@ func (a Album) CoverArtID() ArtworkID {
|
|||
|
||||
type DiscID struct {
|
||||
AlbumID string `json:"albumId"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
DiscNumber int `json:"discNumber"`
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,11 @@ var fieldMap = map[string]*mappedField{
|
|||
"tracknumber": {field: "media_file.track_number"},
|
||||
"discnumber": {field: "media_file.disc_number"},
|
||||
"year": {field: "media_file.year"},
|
||||
"date": {field: "media_file.date"},
|
||||
"originalyear": {field: "media_file.original_year"},
|
||||
"originaldate": {field: "media_file.original_date"},
|
||||
"releaseyear": {field: "media_file.release_year"},
|
||||
"releasedate": {field: "media_file.release_date"},
|
||||
"size": {field: "media_file.size"},
|
||||
"compilation": {field: "media_file.compilation"},
|
||||
"dateadded": {field: "media_file.created_at"},
|
||||
|
|
|
@ -3,6 +3,7 @@ package model
|
|||
import (
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -32,6 +33,11 @@ type MediaFile struct {
|
|||
DiscNumber int `structs:"disc_number" json:"discNumber"`
|
||||
DiscSubtitle string `structs:"disc_subtitle" json:"discSubtitle,omitempty"`
|
||||
Year int `structs:"year" json:"year"`
|
||||
Date string `structs:"date" json:"date,omitempty"`
|
||||
OriginalYear int `structs:"original_year" json:"originalYear"`
|
||||
OriginalDate string `structs:"original_date" json:"originalDate,omitempty"`
|
||||
ReleaseYear int `structs:"release_year" json:"releaseYear"`
|
||||
ReleaseDate string `structs:"release_date" json:"releaseDate,omitempty"`
|
||||
Size int64 `structs:"size" json:"size"`
|
||||
Suffix string `structs:"suffix" json:"suffix"`
|
||||
Duration float32 `structs:"duration" json:"duration"`
|
||||
|
@ -108,6 +114,11 @@ func (mfs MediaFiles) ToAlbum() Album {
|
|||
var songArtistIds []string
|
||||
var mbzAlbumIds []string
|
||||
var comments []string
|
||||
var years []int
|
||||
var dates []string
|
||||
var originalYears []int
|
||||
var originalDates []string
|
||||
var releaseDates []string
|
||||
for _, m := range mfs {
|
||||
// We assume these attributes are all the same for all songs on an album
|
||||
a.ID = m.AlbumID
|
||||
|
@ -130,12 +141,11 @@ func (mfs MediaFiles) ToAlbum() Album {
|
|||
// Calculated attributes based on aggregations
|
||||
a.Duration += m.Duration
|
||||
a.Size += m.Size
|
||||
if a.MinYear == 0 {
|
||||
a.MinYear = m.Year
|
||||
} else if m.Year > 0 {
|
||||
a.MinYear = number.Min(a.MinYear, m.Year)
|
||||
}
|
||||
a.MaxYear = number.Max(a.MaxYear, m.Year)
|
||||
years = append(years, m.Year)
|
||||
dates = append(dates, m.Date)
|
||||
originalYears = append(originalYears, m.OriginalYear)
|
||||
originalDates = append(originalDates, m.OriginalDate)
|
||||
releaseDates = append(releaseDates, m.ReleaseDate)
|
||||
a.UpdatedAt = newer(a.UpdatedAt, m.UpdatedAt)
|
||||
a.CreatedAt = older(a.CreatedAt, m.CreatedAt)
|
||||
a.Genres = append(a.Genres, m.Genres...)
|
||||
|
@ -151,11 +161,15 @@ func (mfs MediaFiles) ToAlbum() Album {
|
|||
a.EmbedArtPath = m.Path
|
||||
}
|
||||
}
|
||||
|
||||
a.Paths = strings.Join(mfs.Dirs(), consts.Zwsp)
|
||||
comments = slices.Compact(comments)
|
||||
if len(comments) == 1 {
|
||||
a.Comment = comments[0]
|
||||
}
|
||||
a.Date, _ = allOrNothing(dates)
|
||||
a.OriginalDate, _ = allOrNothing(originalDates)
|
||||
a.ReleaseDate, a.Releases = allOrNothing(releaseDates)
|
||||
a.MinYear, a.MaxYear = minMax(years)
|
||||
a.MinOriginalYear, a.MaxOriginalYear = minMax(originalYears)
|
||||
a.Comment, _ = allOrNothing(comments)
|
||||
a.Comment, _ = allOrNothing(comments)
|
||||
a.Genre = slice.MostFrequent(a.Genres).Name
|
||||
slices.SortFunc(a.Genres, func(a, b Genre) bool { return a.ID < b.ID })
|
||||
a.Genres = slices.Compact(a.Genres)
|
||||
|
@ -169,6 +183,32 @@ func (mfs MediaFiles) ToAlbum() Album {
|
|||
return a
|
||||
}
|
||||
|
||||
func allOrNothing(items []string) (string, int) {
|
||||
items = slices.Compact(items)
|
||||
if len(items) == 1 {
|
||||
return items[0], 1
|
||||
}
|
||||
if len(items) > 1 {
|
||||
sort.Strings(items)
|
||||
return "", len(slices.Compact(items))
|
||||
}
|
||||
return "", 0
|
||||
}
|
||||
|
||||
func minMax(items []int) (int, int) {
|
||||
var max int = items[0]
|
||||
var min int = items[0]
|
||||
for _, value := range items {
|
||||
max = number.Max(max, value)
|
||||
if min == 0 {
|
||||
min = value
|
||||
} else if value > 0 {
|
||||
min = number.Min(min, value)
|
||||
}
|
||||
}
|
||||
return min, max
|
||||
}
|
||||
|
||||
func newer(t1, t2 time.Time) time.Time {
|
||||
if t1.After(t2) {
|
||||
return t1
|
||||
|
|
|
@ -26,8 +26,8 @@ func NewMediaFileRepository(ctx context.Context, o orm.QueryExecutor) *mediaFile
|
|||
r.ormer = o
|
||||
r.tableName = "media_file"
|
||||
r.sortMappings = map[string]string{
|
||||
"artist": "order_artist_name asc, order_album_name asc, disc_number asc, track_number asc",
|
||||
"album": "order_album_name asc, disc_number asc, track_number asc, order_artist_name asc, title asc",
|
||||
"artist": "order_artist_name asc, order_album_name asc, release_date asc, disc_number asc, track_number asc",
|
||||
"album": "order_album_name asc, release_date asc, disc_number asc, track_number asc, order_artist_name asc, title asc",
|
||||
"random": "RANDOM()",
|
||||
}
|
||||
r.filterMappings = map[string]filterFunc{
|
||||
|
|
|
@ -123,7 +123,7 @@ func (r *playlistTrackRepository) Add(mediaFileIds []string) (int, error) {
|
|||
}
|
||||
|
||||
func (r *playlistTrackRepository) addMediaFileIds(cond Sqlizer) (int, error) {
|
||||
sq := Select("id").From("media_file").Where(cond).OrderBy("album_artist, album, disc_number, track_number")
|
||||
sq := Select("id").From("media_file").Where(cond).OrderBy("album_artist, album, release_date, disc_number, track_number")
|
||||
var ids []string
|
||||
err := r.queryAll(sq, &ids)
|
||||
if err != nil {
|
||||
|
@ -147,7 +147,7 @@ func (r *playlistTrackRepository) AddDiscs(discs []model.DiscID) (int, error) {
|
|||
}
|
||||
var clauses Or
|
||||
for _, d := range discs {
|
||||
clauses = append(clauses, And{Eq{"album_id": d.AlbumID}, Eq{"disc_number": d.DiscNumber}})
|
||||
clauses = append(clauses, And{Eq{"album_id": d.AlbumID}, Eq{"release_date": d.ReleaseDate}, Eq{"disc_number": d.DiscNumber}})
|
||||
}
|
||||
return r.addMediaFileIds(clauses)
|
||||
}
|
||||
|
|
|
@ -32,9 +32,10 @@ func newMediaFileMapper(rootFolder string, genres model.GenreRepository) *mediaF
|
|||
func (s mediaFileMapper) toMediaFile(md metadata.Tags) model.MediaFile {
|
||||
mf := &model.MediaFile{}
|
||||
mf.ID = s.trackID(md)
|
||||
mf.Year, mf.Date, mf.OriginalYear, mf.OriginalDate, mf.ReleaseYear, mf.ReleaseDate = s.mapDates(md)
|
||||
mf.Title = s.mapTrackTitle(md)
|
||||
mf.Album = md.Album()
|
||||
mf.AlbumID = s.albumID(md)
|
||||
mf.AlbumID = s.albumID(md, mf.ReleaseDate)
|
||||
mf.Album = s.mapAlbumName(md)
|
||||
mf.ArtistID = s.artistID(md)
|
||||
mf.Artist = s.mapArtistName(md)
|
||||
|
@ -42,7 +43,6 @@ func (s mediaFileMapper) toMediaFile(md metadata.Tags) model.MediaFile {
|
|||
mf.AlbumArtist = s.mapAlbumArtistName(md)
|
||||
mf.Genre, mf.Genres = s.mapGenres(md.Genres())
|
||||
mf.Compilation = md.Compilation()
|
||||
mf.Year = md.Year()
|
||||
mf.TrackNumber, _ = md.TrackNumber()
|
||||
mf.DiscNumber, _ = md.DiscNumber()
|
||||
mf.DiscSubtitle = md.DiscSubtitle()
|
||||
|
@ -128,8 +128,13 @@ func (s mediaFileMapper) trackID(md metadata.Tags) string {
|
|||
return fmt.Sprintf("%x", md5.Sum([]byte(md.FilePath())))
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) albumID(md metadata.Tags) string {
|
||||
func (s mediaFileMapper) albumID(md metadata.Tags, releaseDate string) string {
|
||||
albumPath := strings.ToLower(fmt.Sprintf("%s\\%s", s.mapAlbumArtistName(md), s.mapAlbumName(md)))
|
||||
if !conf.Server.Scanner.GroupAlbumReleases {
|
||||
if len(releaseDate) != 0 {
|
||||
albumPath = fmt.Sprintf("%s\\%s", albumPath, releaseDate)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(albumPath)))
|
||||
}
|
||||
|
||||
|
@ -169,3 +174,18 @@ func (s mediaFileMapper) mapGenres(genres []string) (string, model.Genres) {
|
|||
}
|
||||
return result[0].Name, result
|
||||
}
|
||||
|
||||
func (s mediaFileMapper) mapDates(md metadata.Tags) (int, string, int, string, int, string) {
|
||||
year, date := md.Date()
|
||||
originalYear, originalDate := md.OriginalDate()
|
||||
releaseYear, releaseDate := md.ReleaseDate()
|
||||
|
||||
// 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 originalYear, originalDate, originalYear, originalDate, year, date
|
||||
}
|
||||
return year, date, originalYear, originalDate, releaseYear, releaseDate
|
||||
}
|
||||
|
|
|
@ -99,7 +99,9 @@ func (t Tags) SortAlbum() string { return t.getSortTag("", "album") }
|
|||
func (t Tags) SortArtist() string { return t.getSortTag("", "artist") }
|
||||
func (t Tags) SortAlbumArtist() string { return t.getSortTag("tso2", "albumartist", "album_artist") }
|
||||
func (t Tags) Genres() []string { return t.getAllTagValues("genre") }
|
||||
func (t Tags) Year() int { return t.getYear("date") }
|
||||
func (t Tags) Date() (int, string) { return t.getDate("date") }
|
||||
func (t Tags) OriginalDate() (int, string) { return t.getDate("originaldate") }
|
||||
func (t Tags) ReleaseDate() (int, string) { return t.getDate("releasedate") }
|
||||
func (t Tags) Comment() string { return t.getFirstTagValue("comment") }
|
||||
func (t Tags) Lyrics() string { return t.getFirstTagValue("lyrics", "lyrics-eng") }
|
||||
func (t Tags) Compilation() bool { return t.getBool("tcmp", "compilation") }
|
||||
|
@ -217,18 +219,38 @@ func (t Tags) getSortTag(originalTag string, tagNames ...string) string {
|
|||
|
||||
var dateRegex = regexp.MustCompile(`([12]\d\d\d)`)
|
||||
|
||||
func (t Tags) getYear(tagNames ...string) int {
|
||||
func (t Tags) getDate(tagNames ...string) (int, string) {
|
||||
tag := t.getFirstTagValue(tagNames...)
|
||||
if tag == "" {
|
||||
return 0
|
||||
if len(tag) < 4 {
|
||||
return 0, ""
|
||||
}
|
||||
// first get just the year
|
||||
match := dateRegex.FindStringSubmatch(tag)
|
||||
if len(match) == 0 {
|
||||
log.Warn("Error parsing year date field", "file", t.filePath, "date", tag)
|
||||
return 0
|
||||
log.Warn("Error parsing "+tagNames[0]+" field for year", "file", t.filePath, "date", tag)
|
||||
return 0, ""
|
||||
}
|
||||
year, _ := strconv.Atoi(match[1])
|
||||
return year
|
||||
|
||||
if len(tag) < 5 {
|
||||
return year, match[1]
|
||||
}
|
||||
|
||||
//then try YYYY-MM-DD
|
||||
if len(tag) > 10 {
|
||||
tag = tag[:10]
|
||||
}
|
||||
layout := "2006-01-02"
|
||||
_, err := time.Parse(layout, tag)
|
||||
if err != nil {
|
||||
layout = "2006-01"
|
||||
_, err = time.Parse(layout, tag)
|
||||
if err != nil {
|
||||
log.Warn("Error parsing "+tagNames[0]+" field for month + day", "file", t.filePath, "date", tag)
|
||||
return year, match[1]
|
||||
}
|
||||
}
|
||||
return year, tag
|
||||
}
|
||||
|
||||
func (t Tags) getBool(tagNames ...string) bool {
|
||||
|
|
|
@ -6,29 +6,51 @@ import (
|
|||
)
|
||||
|
||||
var _ = Describe("Tags", func() {
|
||||
Describe("getYear", func() {
|
||||
Describe("getDate", func() {
|
||||
It("parses the year correctly", func() {
|
||||
var examples = map[string]int{
|
||||
var examplesYear = map[string]int{
|
||||
"1985": 1985,
|
||||
"2002-01": 2002,
|
||||
"1969.06": 1969,
|
||||
"1980.07.25": 1980,
|
||||
"2004-00-00": 2004,
|
||||
"2016-12-31": 2016,
|
||||
"2013-May-12": 2013,
|
||||
"May 12, 2016": 2016,
|
||||
"01/10/1990": 1990,
|
||||
}
|
||||
for tag, expected := range examples {
|
||||
for tag, expected := range examplesYear {
|
||||
md := &Tags{}
|
||||
md.tags = map[string][]string{"date": {tag}}
|
||||
Expect(md.Year()).To(Equal(expected))
|
||||
testYear, _ := md.Date()
|
||||
Expect(testYear).To(Equal(expected))
|
||||
}
|
||||
})
|
||||
|
||||
It("parses the date correctly", func() {
|
||||
var examplesDate = map[string]string{
|
||||
"1985": "1985",
|
||||
"2002-01": "2002-01",
|
||||
"1969.06": "1969",
|
||||
"1980.07.25": "1980",
|
||||
"2004-00-00": "2004",
|
||||
"2016-12-31": "2016-12-31",
|
||||
"2013-May-12": "2013",
|
||||
"May 12, 2016": "2016",
|
||||
"01/10/1990": "1990",
|
||||
}
|
||||
for tag, expected := range examplesDate {
|
||||
md := &Tags{}
|
||||
md.tags = map[string][]string{"date": {tag}}
|
||||
_, testDate := md.Date()
|
||||
Expect(testDate).To(Equal(expected))
|
||||
}
|
||||
})
|
||||
It("returns 0 if year is invalid", func() {
|
||||
md := &Tags{}
|
||||
md.tags = map[string][]string{"date": {"invalid"}}
|
||||
Expect(md.Year()).To(Equal(0))
|
||||
testYear, _ := md.Date()
|
||||
Expect(testYear).To(Equal(0))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -27,7 +27,15 @@ var _ = Describe("Tags", func() {
|
|||
Expect(m.AlbumArtist()).To(Equal("Album Artist"))
|
||||
Expect(m.Compilation()).To(BeTrue())
|
||||
Expect(m.Genres()).To(Equal([]string{"Rock"}))
|
||||
Expect(m.Year()).To(Equal(2014))
|
||||
y, d := m.Date()
|
||||
Expect(y).To(Equal(2014))
|
||||
Expect(d).To(Equal("2014-05-21"))
|
||||
y, d = m.OriginalDate()
|
||||
Expect(y).To(Equal(1996))
|
||||
Expect(d).To(Equal("1996-11-21"))
|
||||
y, d = m.ReleaseDate()
|
||||
Expect(y).To(Equal(2020))
|
||||
Expect(d).To(Equal("2020-12-31"))
|
||||
n, t := m.TrackNumber()
|
||||
Expect(n).To(Equal(2))
|
||||
Expect(t).To(Equal(10))
|
||||
|
|
|
@ -42,6 +42,8 @@ var _ = Describe("Extractor", func() {
|
|||
Expect(m).To(HaveKeyWithValue("tcmp", []string{"1"})) // Compilation
|
||||
Expect(m).To(HaveKeyWithValue("genre", []string{"Rock"}))
|
||||
Expect(m).To(HaveKeyWithValue("date", []string{"2014-05-21", "2014"}))
|
||||
Expect(m).To(HaveKeyWithValue("originaldate", []string{"1996-11-21"}))
|
||||
Expect(m).To(HaveKeyWithValue("releasedate", []string{"2020-12-31"}))
|
||||
Expect(m).To(HaveKeyWithValue("discnumber", []string{"1/2"}))
|
||||
Expect(m).To(HaveKeyWithValue("has_picture", []string{"true"}))
|
||||
Expect(m).To(HaveKeyWithValue("duration", []string{"1.02"}))
|
||||
|
|
|
@ -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 +
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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} />}</>
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 },
|
||||
}}
|
||||
/>
|
||||
|
|
29
ui/src/common/FormatFullDate.js
Normal file
29
ui/src/common/FormatFullDate.js
Normal 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 ''
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
50
ui/src/common/RangeDoubleField.js
Normal file
50
ui/src/common/RangeDoubleField.js
Normal 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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue