feature: take the disc number from CD1 and Disc 2 folders
Multi-disc rips are very often arranged as CD1 / CD2 folders with nothing in the tags and nothing in the file names. Every disc came out as disc 1, so the two discs' tracks shared numbers and a player would show them interleaved. The folder name is now the last resort, behind a DISCNUMBER tag and a disc number in the file name.
diff --git a/internal/scan/scan.go b/internal/scan/scan.go
@@ -7,6 +7,7 @@ import (
"path/filepath"
"regexp"
"sort"
+ "strconv"
"strings"
"intake/internal/audio"
@@ -69,7 +70,7 @@ func StagingWith(ctx context.Context, dir string, opts Options) ([]Album, error)
func album(ctx context.Context, dir string, opts Options) (Album, error) {
a := Album{Dir: dir, Name: filepath.Base(dir)}
- files, err := collectAudio(dir, &a)
+ files, discHints, err := collectAudio(dir, &a)
if err != nil {
return a, err
}
@@ -95,6 +96,7 @@ func album(ctx context.Context, dir string, opts Options) (Album, error) {
a.Notes = append(a.Notes, fmt.Sprintf("ignoring unreadable file %s", filepath.Base(p)))
continue
}
+ t.DiscHint = discHints[p]
a.Tracks = append(a.Tracks, t)
a.TotalDur += t.Duration
}
@@ -110,11 +112,30 @@ func album(ctx context.Context, dir string, opts Options) (Album, error) {
return a, nil
}
-func collectAudio(dir string, a *Album) ([]string, error) {
+// discFromFolder reads a disc number out of a folder named CD1, Disc 2, Disk_3
+// and the like. Multi-disc rips are very often arranged this way with nothing in
+// the tags, which otherwise leaves every disc numbered 1 and every track number
+// repeated across them.
+func discFromFolder(name string) int {
+ m := discFolder.FindStringSubmatch(name)
+ if m == nil {
+ return 0
+ }
+ n, err := strconv.Atoi(m[1])
+ if err != nil || n < 1 {
+ return 0
+ }
+ return n
+}
+
+var discFolder = regexp.MustCompile(`(?i)^\s*(?:cd|disc|disk)\s*[-_. ]?\s*(\d{1,2})\s*$`)
+
+func collectAudio(dir string, a *Album) ([]string, map[string]int, error) {
var files []string
+ discHints := map[string]int{}
entries, err := os.ReadDir(dir)
if err != nil {
- return nil, err
+ return nil, nil, err
}
for _, e := range entries {
p := filepath.Join(dir, e.Name())
@@ -126,14 +147,19 @@ func collectAudio(dir string, a *Album) ([]string, error) {
a.Subdirs++
sub, err := os.ReadDir(p)
if err != nil {
- return nil, err
+ return nil, nil, err
}
+ hint := discFromFolder(e.Name())
for _, s := range sub {
if s.IsDir() || strings.HasPrefix(s.Name(), ".") {
continue
}
if audio.IsAudio(s.Name()) {
- files = append(files, filepath.Join(p, s.Name()))
+ path := filepath.Join(p, s.Name())
+ files = append(files, path)
+ if hint > 0 {
+ discHints[path] = hint
+ }
}
}
case strings.HasPrefix(e.Name(), "."), strings.HasPrefix(e.Name(), "."):
@@ -144,7 +170,7 @@ func collectAudio(dir string, a *Album) ([]string, error) {
}
}
}
- return files, nil
+ return files, discHints, nil
}
var leadingNumber = regexp.MustCompile(`^\s*(?:(\d{1,2})[.\-_ ]+)?(?:(\d{1,3})\s*[.\-_ ]+)?(.*)$`)
diff --git a/internal/scan/scan_test.go b/internal/scan/scan_test.go
@@ -0,0 +1,23 @@
+package scan
+
+import "testing"
+
+// Multi-disc rips are very often arranged as CD1 / CD2 folders with nothing in
+// the tags. Without reading the folder, every disc came out as disc 1 and every
+// track number was repeated across the discs.
+func TestDiscFromFolder(t *testing.T) {
+ cases := []struct {
+ in string
+ want int
+ }{
+ {"CD1", 1}, {"CD2", 2}, {"cd 3", 3}, {"Disc 2", 2},
+ {"DISK_4", 4}, {"disc-5", 5}, {"CD 10", 10},
+ {"", 0}, {"bonus", 0}, {"CD", 0}, {"cdx", 0}, {"2012", 0},
+ {"CD0", 0}, {"disc one", 0},
+ }
+ for _, c := range cases {
+ if got := discFromFolder(c.in); got != c.want {
+ t.Errorf("discFromFolder(%q) = %d, want %d", c.in, got, c.want)
+ }
+ }
+}