🏡 index : quine-global/quintodrome.git

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())
		})
	})
})