mirror of
https://github.com/navidrome/navidrome.git
synced 2026-11-04 17:28:39 +01:00
TagLib reports ID3 MVIN and MP4 ©mvi as movementnumber, and MP4 ©mvc as movementcount. Neither name was aliased in mappings.yaml, so getSong returned movements with the name only. Add both aliases. ID3 MVIN stores the value as n/N (e.g. 1/4), which parsed as 0. Split it at scan time, like track and disc numbers, so the stored movement tag holds the number and movementtotal gets the count when the file has no explicit total. Storing the clean value also lets smart playlist rules on the movement tag match. Existing files need a full scan to pick this up. Fixes #6356
192 lines
6.9 KiB
Go
192 lines
6.9 KiB
Go
package metadata_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"sort"
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/metadata"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("ToMediaFile", func() {
|
|
var (
|
|
props metadata.Info
|
|
md metadata.Metadata
|
|
mf model.MediaFile
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
_, filePath, _ := tests.TempFile(GinkgoT(), "test", ".mp3")
|
|
fileInfo, _ := os.Stat(filePath)
|
|
props = metadata.Info{
|
|
FileInfo: testFileInfo{fileInfo},
|
|
}
|
|
})
|
|
|
|
var toMediaFile = func(tags model.RawTags) model.MediaFile {
|
|
props.Tags = tags
|
|
md = metadata.New("filepath", props)
|
|
return md.ToMediaFile(model.Library{ID: 1}, "folderID")
|
|
}
|
|
|
|
Describe("Persistent IDs", func() {
|
|
It("uses the library PID config for the album ID and for albumid in the track spec", func() {
|
|
props.Tags = model.RawTags{"ALBUM": {"Kind of Blue"}, "TITLE": {"So What"}}
|
|
md = metadata.New("Jazz/Loose/01.mp3", props)
|
|
|
|
byTags := md.ToMediaFile(model.Library{ID: 1, PIDAlbum: "album", PIDTrack: "albumid,title"}, "folderID")
|
|
byFolder := md.ToMediaFile(model.Library{ID: 1, PIDAlbum: "folder", PIDTrack: "albumid,title"}, "folderID")
|
|
|
|
Expect(byFolder.AlbumID).ToNot(Equal(byTags.AlbumID))
|
|
Expect(byFolder.AlbumID).To(Equal(md.AlbumID(byFolder, "folder")))
|
|
Expect(byFolder.PID).ToNot(Equal(byTags.PID))
|
|
})
|
|
})
|
|
|
|
Describe("Dates", func() {
|
|
It("should parse properly tagged dates ", func() {
|
|
mf = toMediaFile(model.RawTags{
|
|
"ORIGINALDATE": {"1978-09-10"},
|
|
"DATE": {"1977-03-04"},
|
|
"RELEASEDATE": {"2002-01-02"},
|
|
})
|
|
|
|
Expect(mf.Year).To(Equal(1977))
|
|
Expect(mf.Date).To(Equal("1977-03-04"))
|
|
Expect(mf.OriginalYear).To(Equal(1978))
|
|
Expect(mf.OriginalDate).To(Equal("1978-09-10"))
|
|
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))
|
|
})
|
|
DescribeTable("legacyReleaseDate (TaggedLikePicard old behavior)",
|
|
func(recordingDate, originalDate, releaseDate, expected string) {
|
|
mf := toMediaFile(model.RawTags{
|
|
"DATE": {recordingDate},
|
|
"ORIGINALDATE": {originalDate},
|
|
"RELEASEDATE": {releaseDate},
|
|
})
|
|
|
|
Expect(mf.ReleaseDate).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"),
|
|
)
|
|
})
|
|
|
|
Describe("Lyrics", func() {
|
|
It("should parse the lyrics", func() {
|
|
mf = toMediaFile(model.RawTags{
|
|
"LYRICS:XXX": {"Lyrics"},
|
|
"LYRICS:ENG": {
|
|
"[00:00.00]This is\n[00:02.50]English SYLT\n",
|
|
},
|
|
})
|
|
var actual model.LyricList
|
|
err := json.Unmarshal([]byte(mf.Lyrics), &actual)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
expected := model.LyricList{
|
|
{Lang: "eng", Line: []model.Line{
|
|
{Value: "This is", Start: new(int64(0))},
|
|
{Value: "English SYLT", Start: new(int64(2500))},
|
|
}, Synced: true},
|
|
{Lang: "xxx", Line: []model.Line{{Value: "Lyrics"}}, Synced: false},
|
|
}
|
|
sort.Slice(actual, func(i, j int) bool { return actual[i].Lang < actual[j].Lang })
|
|
sort.Slice(expected, func(i, j int) bool { return expected[i].Lang < expected[j].Lang })
|
|
Expect(actual).To(Equal(expected))
|
|
})
|
|
})
|
|
|
|
Describe("BPM", func() {
|
|
It("maps the BPM tag rounded to the nearest integer", func() {
|
|
mf = toMediaFile(model.RawTags{"BPM": {"120.6"}})
|
|
Expect(mf.BPM).To(Equal(new(121)))
|
|
})
|
|
It("leaves BPM nil when the tag is absent", func() {
|
|
mf = toMediaFile(model.RawTags{})
|
|
Expect(mf.BPM).To(BeNil())
|
|
})
|
|
It("leaves BPM nil when the tag is zero or unparseable", func() {
|
|
Expect(toMediaFile(model.RawTags{"BPM": {"0"}}).BPM).To(BeNil())
|
|
Expect(toMediaFile(model.RawTags{"BPM": {"fast"}}).BPM).To(BeNil())
|
|
})
|
|
It("leaves BPM nil when the tag does not fit in 32 bits", func() {
|
|
Expect(toMediaFile(model.RawTags{"BPM": {"4294967295"}}).BPM).To(BeNil())
|
|
})
|
|
It("leaves BPM nil when the tag is negative", func() {
|
|
Expect(toMediaFile(model.RawTags{"BPM": {"-120"}}).BPM).To(BeNil())
|
|
})
|
|
It("keeps the largest 32-bit BPM value", func() {
|
|
Expect(toMediaFile(model.RawTags{"BPM": {"2147483647"}}).BPM).To(Equal(new(2147483647)))
|
|
})
|
|
})
|
|
|
|
Describe("Movements", func() {
|
|
It("splits an ID3 MVIN n/N value into movement number and total", func() {
|
|
mf = toMediaFile(model.RawTags{"movementname": {"Allegro"}, "movementnumber": {"1/4"}})
|
|
Expect(mf.Tags).To(HaveKeyWithValue(model.TagMovementNumber, []string{"1"}))
|
|
Expect(mf.Tags).To(HaveKeyWithValue(model.TagMovementTotal, []string{"4"}))
|
|
Expect(mf.Movements()).To(Equal([]model.Movement{{Name: "Allegro", Number: 1, Count: 4}}))
|
|
})
|
|
It("maps MP4 movement number and count", func() {
|
|
mf = toMediaFile(model.RawTags{"movementname": {"Andante"}, "movementnumber": {"2"}, "movementcount": {"4"}})
|
|
Expect(mf.Movements()).To(Equal([]model.Movement{{Name: "Andante", Number: 2, Count: 4}}))
|
|
})
|
|
It("prefers an explicit movement total over the n/N total", func() {
|
|
mf = toMediaFile(model.RawTags{"movementname": {"Allegro"}, "movement": {"2/4"}, "movementtotal": {"5"}})
|
|
Expect(mf.Movements()).To(Equal([]model.Movement{{Name: "Allegro", Number: 2, Count: 5}}))
|
|
})
|
|
It("does not add a total when only some values have one", func() {
|
|
mf = toMediaFile(model.RawTags{"movementname": {"I", "II"}, "movement": {"1/4", "2"}})
|
|
Expect(mf.Movements()).To(Equal([]model.Movement{{Name: "I", Number: 1}, {Name: "II", Number: 2}}))
|
|
})
|
|
})
|
|
|
|
Describe("BitDepth", func() {
|
|
It("maps the bit depth when present", func() {
|
|
props.AudioProperties = metadata.AudioProperties{BitDepth: 24}
|
|
mf = toMediaFile(model.RawTags{})
|
|
Expect(mf.BitDepth).To(Equal(new(24)))
|
|
})
|
|
It("leaves BitDepth nil when zero (lossy codecs have no bit depth)", func() {
|
|
props.AudioProperties = metadata.AudioProperties{BitDepth: 0}
|
|
mf = toMediaFile(model.RawTags{})
|
|
Expect(mf.BitDepth).To(BeNil())
|
|
})
|
|
})
|
|
})
|