Drop Artist/Album/Title depth assumptions from layout checks
Matching has always been by relative path, so any folder layout works as long as both libraries use the same one. Remove the depth and loose-root-file warnings, which flagged valid layouts, and reword the UI and README to say "same folder layout on both sides" with Artist/Album/Title.ext only as an example. The "elsewhere" fallback is now described as same top-level folder, different subfolder. Add a test covering flat and deeply nested layouts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f49274360b
commit
75e0516434
9 files changed
+52
-33
No files matched your search
@@ -293,9 +293,11 @@ func (e *Engine) NormalizePair(p *store.Pair) ([]string, error) {
|
||||
}
|
||||
|
||||
// layoutWarnings does a cheap top-level comparison of the two libraries.
|
||||
// Any folder layout works; what matters is that both libraries use the same
|
||||
// one, so matching files sit at the same relative path.
|
||||
func layoutWarnings(l *libs) []string {
|
||||
var warns []string
|
||||
list := func(root string) (dirs map[string]bool, looseFiles int) {
|
||||
list := func(root string) (dirs map[string]bool, files int) {
|
||||
dirs = map[string]bool{}
|
||||
ents, _ := os.ReadDir(root)
|
||||
for _, en := range ents {
|
||||
@@ -305,20 +307,17 @@ func layoutWarnings(l *libs) []string {
|
||||
if en.IsDir() {
|
||||
dirs[strings.ToLower(en.Name())] = true
|
||||
} else if l.hasExt(en.Name()) {
|
||||
looseFiles++
|
||||
files++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
hd, hl := list(l.host.Path)
|
||||
sd, sl := list(l.slave.Path)
|
||||
if len(sd) == 0 && sl == 0 {
|
||||
hd, _ := list(l.host.Path)
|
||||
sd, sf := list(l.slave.Path)
|
||||
if len(sd) == 0 && sf == 0 {
|
||||
warns = append(warns, "The slave library is empty. Use Build to pick songs from the host library to hardlink into it.")
|
||||
return warns
|
||||
}
|
||||
if hl > 0 || sl > 0 {
|
||||
warns = append(warns, fmt.Sprintf("Found audio files directly in a library root (host: %d, slave: %d). Both libraries should use Artist/Album/Title.ext.", hl, sl))
|
||||
}
|
||||
shared := 0
|
||||
for d := range sd {
|
||||
if hd[d] {
|
||||
@@ -326,8 +325,8 @@ func layoutWarnings(l *libs) []string {
|
||||
}
|
||||
}
|
||||
if len(sd) > 0 && shared*2 < len(sd) {
|
||||
warns = append(warns, fmt.Sprintf("Only %d of %d artist folders in the slave library also exist in the host library. "+
|
||||
"The two libraries should share the same Artist/Album/Title.ext layout, or most files will not match.", shared, len(sd)))
|
||||
warns = append(warns, fmt.Sprintf("Only %d of %d top-level folders in the slave library also exist in the host library. "+
|
||||
"Both libraries should use the same folder layout, or most files will not match.", shared, len(sd)))
|
||||
}
|
||||
return warns
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -479,3 +480,27 @@ func TestNormTitle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnyLayoutMatchesWithoutWarnings(t *testing.T) {
|
||||
ev := setup(t, false)
|
||||
// Flat files, deeper Genre/Artist/Album/Disc nesting: all fine as long as both sides agree.
|
||||
for _, rel := range []string{"loose.flac", "Rock/Band/Album/Disc 1/01 - Song.flac", "Band/Song.flac"} {
|
||||
ev.write(ev.host, rel, "same:"+rel)
|
||||
ev.write(ev.slave, rel, "same:"+rel)
|
||||
}
|
||||
ev.scan()
|
||||
for _, rel := range []string{"loose.flac", "Rock/Band/Album/Disc 1/01 - Song.flac", "Band/Song.flac"} {
|
||||
if m := ev.match(rel); m.Status != store.StatusIdentical || m.Kind != store.KindPath {
|
||||
t.Fatalf("%s: want identical/path, got %s/%s", rel, m.Status, m.Kind)
|
||||
}
|
||||
}
|
||||
p, err := ev.st.Pair(ctx, ev.pair.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, w := range p.LastScanWarnings {
|
||||
if strings.Contains(w, "layout") || strings.Contains(w, "folder") || strings.Contains(w, "depth") {
|
||||
t.Fatalf("matching layouts must not produce layout warnings, got %q", w)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-14
@@ -20,7 +20,6 @@ import (
|
||||
type walkStats struct {
|
||||
files int
|
||||
symlinks int
|
||||
offDepth int // files not at Artist/Album/Title depth
|
||||
}
|
||||
|
||||
// walk lists audio files beneath root. Hidden entries, playlink staging files,
|
||||
@@ -67,9 +66,6 @@ func walk(ctx context.Context, l *libs, root fsops.Root, out map[string]fsops.In
|
||||
}
|
||||
out[rel] = in
|
||||
ws.files++
|
||||
if strings.Count(rel, string(filepath.Separator)) != 2 {
|
||||
ws.offDepth++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return ws, err
|
||||
@@ -107,7 +103,7 @@ type hostIndex struct {
|
||||
files map[string]fsops.Info
|
||||
lower map[string]string // lower(rel) -> rel
|
||||
byDir map[string][]string // lower(dir) -> rels
|
||||
byTitle map[string][]string // lower(artist)|normTitle -> rels
|
||||
byTitle map[string][]string // lower(top folder)|normTitle -> rels
|
||||
}
|
||||
|
||||
func buildIndex(files map[string]fsops.Info) *hostIndex {
|
||||
@@ -167,8 +163,8 @@ func (ix *hostIndex) find(relSlave string) (string, string) {
|
||||
if len(fuzzyHits) == 1 {
|
||||
return fuzzyHits[0], store.KindFuzzy
|
||||
}
|
||||
if artist := firstComponent(relSlave); artist != "" && norm != "" {
|
||||
if hits := ix.byTitle[strings.ToLower(artist)+"|"+norm]; len(hits) == 1 {
|
||||
if top := firstComponent(relSlave); top != "" && norm != "" {
|
||||
if hits := ix.byTitle[strings.ToLower(top)+"|"+norm]; len(hits) == 1 {
|
||||
return hits[0], store.KindElsewhere
|
||||
}
|
||||
}
|
||||
@@ -310,14 +306,8 @@ func (e *Engine) scan(ctx context.Context, j *Job, pairID int64, trigger string)
|
||||
if ss.files > 0 {
|
||||
if n := kinds[store.KindElsewhere] + kinds[store.KindNone]; n*4 > ss.files {
|
||||
warns = append(warns, fmt.Sprintf("%d of %d slave files have no counterpart at the same path in the host library. "+
|
||||
"Check that both libraries use the same Artist/Album/Title.ext layout.", n, ss.files))
|
||||
"Check that both libraries use the same folder layout.", n, ss.files))
|
||||
}
|
||||
if ss.offDepth > 0 {
|
||||
warns = append(warns, fmt.Sprintf("%d slave files are not at Artist/Album/Title.ext depth.", ss.offDepth))
|
||||
}
|
||||
}
|
||||
if hs.offDepth > 0 {
|
||||
warns = append(warns, fmt.Sprintf("%d host files are not at Artist/Album/Title.ext depth.", hs.offDepth))
|
||||
}
|
||||
if hs.symlinks+ss.symlinks > 0 {
|
||||
warns = append(warns, fmt.Sprintf("Ignored %d symlinks (playlink never follows or replaces symlinks).", hs.symlinks+ss.symlinks))
|
||||
|
||||
@@ -220,7 +220,7 @@ const (
|
||||
KindPathCase = "path_case" // relative path differs only by case
|
||||
KindExtension = "extension" // same directory and title, different extension
|
||||
KindFuzzy = "fuzzy" // same directory, normalized title matches
|
||||
KindElsewhere = "elsewhere" // same artist, different album folder
|
||||
KindElsewhere = "elsewhere" // same top-level folder and title, different subfolder
|
||||
KindNone = "none"
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user