mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-21 16:57:14 +02:00
fix(scanner): map movement number and count from ID3 MVIN and MP4 tags (#6357)
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
This commit is contained in:
parent
df77c462e8
commit
56cc32aede
3 changed files with 47 additions and 2 deletions
|
|
@ -6,7 +6,9 @@ import (
|
|||
"encoding/json"
|
||||
"maps"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
|
|
@ -42,6 +44,7 @@ func (md Metadata) ToMediaFile(lib model.Library, folderID string) model.MediaFi
|
|||
}
|
||||
mf.Lyrics = md.mapLyrics()
|
||||
mf.ExplicitStatus = md.mapExplicitStatusTag()
|
||||
md.mapMovementNumbers(mf.Tags)
|
||||
|
||||
// Dates
|
||||
date, origDate, relDate := md.mapDates()
|
||||
|
|
@ -135,6 +138,27 @@ func (md Metadata) mapGain(rg, r128 model.TagName) *float64 {
|
|||
return nil
|
||||
}
|
||||
|
||||
// mapMovementNumbers splits ID3 MVIN "n/N" values, like tuple() does for track and disc numbers.
|
||||
// Totals are only filled when every value has one, so the parallel movement tags stay aligned.
|
||||
func (md Metadata) mapMovementNumbers(tags model.Tags) {
|
||||
values := md.Strings(model.TagMovementNumber)
|
||||
if !slices.ContainsFunc(values, func(v string) bool { return strings.Contains(v, "/") }) {
|
||||
return
|
||||
}
|
||||
numbers := make([]string, len(values))
|
||||
totals := make([]string, len(values))
|
||||
allHaveTotals := true
|
||||
for i, v := range values {
|
||||
var found bool
|
||||
numbers[i], totals[i], found = strings.Cut(v, "/")
|
||||
allHaveTotals = allHaveTotals && found
|
||||
}
|
||||
tags[model.TagMovementNumber] = numbers
|
||||
if allHaveTotals && len(md.Strings(model.TagMovementTotal)) == 0 {
|
||||
tags[model.TagMovementTotal] = totals
|
||||
}
|
||||
}
|
||||
|
||||
func (md Metadata) mapLyrics() string {
|
||||
rawLyrics := md.Pairs(model.TagLyrics)
|
||||
|
||||
|
|
|
|||
|
|
@ -156,6 +156,27 @@ var _ = Describe("ToMediaFile", func() {
|
|||
})
|
||||
})
|
||||
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -231,9 +231,9 @@ additional:
|
|||
movementname:
|
||||
aliases: [ mvnm, movementname, ©mvn ]
|
||||
movementtotal:
|
||||
aliases: [ movementtotal, mvc ]
|
||||
aliases: [ movementtotal, movementcount, mvc ]
|
||||
movement:
|
||||
aliases: [ mvin, movement, mvi ]
|
||||
aliases: [ mvin, movement, movementnumber, mvi ]
|
||||
recordlabel:
|
||||
aliases: [ tpub, label, publisher, ----:com.apple.itunes:label, wm/publisher, organization ]
|
||||
album: true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue