fix: imported FLAC, Ogg and Opus files keep their track and disc numbers (#6171)
release / govulncheck (push) Successful in 14s
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m29s
release / integration (push) Successful in 5m1s
release / android (push) Successful in 5m5s
release / Build signed APK (releases and dev) (push) Successful in 5m21s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped

The importer wrote track=N/M for every format, which ffmpeg stores in a
Vorbis comment as TRACKNUMBER=3/20; dhowden/tag reads a Vorbis TRACKNUMBER
with a bare Atoi, so it came back 0. All 245 tracks imported since
2026-10-10 (Humanz, Plastic Beach, the Gorillaz super deluxe and twelve
more albums) have no track or disc number, and their album pages have no
order.

- The importer writes a Vorbis track and disc number alone, with
  TRACKTOTAL and DISCTOTAL, as Picard does. ID3 and MP4 keep N/M.
- ReadTrackAndDisc takes the number before a slash when the Vorbis reader
  returns 0, for the scanner and for the import's own check of a download.
- tagReadVersion 6 re-reads the files already held (tag reads only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 11:49:44 -04:00
co-authored by Claude Opus 5.5
parent 42e48efb79
commit 80ecc2f7e7
6 changed files with 94 additions and 7 deletions
+19
View File
@@ -242,3 +242,22 @@ func TestReadTagMBIDs_Recording(t *testing.T) {
t.Errorf("release track only: recording = %q, want empty", got)
}
}
// A Vorbis TRACKNUMBER of "3/20", as Minstrel's importer wrote it, is track 3
// (#6171): dhowden/tag reads it as 0.
func TestReadTrackAndDisc(t *testing.T) {
for _, tc := range []struct {
name string
m stubMeta
track, disc int
}{
{"vorbis with counts", stubMeta{format: tag.VORBIS, raw: map[string]interface{}{"tracknumber": "3/20", "discnumber": "2/2"}}, 3, 2},
{"vorbis bare", stubMeta{format: tag.VORBIS, raw: map[string]interface{}{"tracknumber": " 7 "}}, 7, 0},
{"vorbis junk", stubMeta{format: tag.VORBIS, raw: map[string]interface{}{"tracknumber": "A1", "discnumber": "-1"}}, 0, 0},
{"id3 is the library's own reading", stubMeta{format: tag.ID3v2_4, raw: map[string]interface{}{"tracknumber": "3/20"}}, 0, 0},
} {
if track, disc := ReadTrackAndDisc(tc.m); track != tc.track || disc != tc.disc {
t.Errorf("%s: got track %d disc %d, want %d and %d", tc.name, track, disc, tc.track, tc.disc)
}
}
}
+6 -3
View File
@@ -70,7 +70,11 @@ var audioExtensions = map[string]bool{
// release-track name, so every Vorbis-tagged track came in without its
// tag id and waited on an AcoustID lookup. A re-read stores the tag id,
// over a looked-up one (D4).
const tagReadVersion int16 = 5
// 6: a Vorbis TRACKNUMBER or DISCNUMBER written as "3/20" is read (#6171).
// dhowden/tag reads it as 0, and Minstrel's own importer wrote it that
// way, so all 245 FLAC tracks it imported came in with no track or disc
// number and their albums lost their order.
const tagReadVersion int16 = 6
type Stats struct {
Scanned int `json:"scanned"`
@@ -316,8 +320,7 @@ func (s *Scanner) scanFile(
return pgtype.UUID{}, false, fmt.Errorf("album: %w", err)
}
trackNum, _ := meta.Track()
discNum, _ := meta.Disc()
trackNum, discNum := ReadTrackAndDisc(meta)
// An unchanged file being re-read only to refresh tag-derived columns
// doesn't need another ffprobe: the stored duration is still accurate, and
+40
View File
@@ -0,0 +1,40 @@
package library
import (
"strconv"
"strings"
"github.com/dhowden/tag"
)
// ReadTrackAndDisc reads a file's track and disc numbers. dhowden/tag reads a
// Vorbis TRACKNUMBER or DISCNUMBER only as a bare number, but taggers also
// write "3/20" there (Minstrel's own importer did until #6171), and that
// reads as 0. For Vorbis the number before the slash is taken instead.
func ReadTrackAndDisc(m tag.Metadata) (track, disc int) {
track, _ = m.Track()
disc, _ = m.Disc()
if m.Format() != tag.VORBIS {
return track, disc
}
raw := m.Raw()
if track == 0 {
track = leadingNumber(raw["tracknumber"])
}
if disc == 0 {
disc = leadingNumber(raw["discnumber"])
}
return track, disc
}
// leadingNumber is the number in a tag value of "3" or "3/20"; 0 for
// anything else.
func leadingNumber(v any) int {
s, _ := v.(string)
before, _, _ := strings.Cut(s, "/")
n, err := strconv.Atoi(strings.TrimSpace(before))
if err != nil || n < 0 {
return 0
}
return n
}