The new Cleanup tab lists the top-most folders in either library that contain no music file (per the pair's extensions). Removing them needs confirmation, plus an explicit acknowledgement when they still hold files; each folder is re-checked for music and moved into the quarantine (undoable from History, purged after the retention period). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
120 lines
3.5 KiB
Go
120 lines
3.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/cdnutter/playlink/internal/store"
|
|
)
|
|
|
|
func (ev *env) noMusic(t *testing.T, side string) *NoMusicScan {
|
|
t.Helper()
|
|
if _, err := ev.e.StartNoMusicScan(ev.pair.ID, side); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ev.wait()
|
|
s := ev.e.NoMusicResult(ev.pair.ID)
|
|
if s == nil {
|
|
t.Fatal("no scan result")
|
|
}
|
|
return s
|
|
}
|
|
|
|
func rels(s *NoMusicScan) []string {
|
|
var out []string
|
|
for _, f := range s.Folders {
|
|
out = append(out, f.Rel)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestFindFoldersWithoutMusic(t *testing.T) {
|
|
ev := setup(t, false)
|
|
s := ev.slave
|
|
ev.write(s, "A/B/song.flac", "music")
|
|
ev.write(s, "A/B/extra/cover.jpg", "art") // inside an album that has music
|
|
os.MkdirAll(filepath.Join(s, "A/Empty"), 0o755)
|
|
ev.write(s, "A/C/cover.jpg", "art")
|
|
ev.write(s, "D/E/F/notes.txt", "text") // only D is listed, not D/E or D/E/F
|
|
ev.write(s, "G/._song.flac", "mac metadata")
|
|
ev.write(s, ".hidden/stuff.txt", "x")
|
|
ev.write(s, "Loose.txt", "x") // a file at the root is not a folder
|
|
scan := ev.noMusic(t, SideSlave)
|
|
got := strings.Join(rels(scan), ",")
|
|
if got != "A/B/extra,A/C,A/Empty,D,G" {
|
|
t.Fatalf("got %s", got)
|
|
}
|
|
for _, f := range scan.Folders {
|
|
if f.Rel == "D" && (f.Files != 1 || f.Folders != 2 || f.Sample[0] != "E/F/notes.txt") {
|
|
t.Fatalf("D: %+v", f)
|
|
}
|
|
}
|
|
// The quarantine never shows up, even when it isn't hidden.
|
|
if strings.Contains(got, "trash") {
|
|
t.Fatal("quarantine listed")
|
|
}
|
|
}
|
|
|
|
func TestRemoveFoldersWithoutMusic(t *testing.T) {
|
|
ev := setup(t, false)
|
|
s := ev.slave
|
|
ev.write(s, "A/B/song.flac", "music")
|
|
os.MkdirAll(filepath.Join(s, "A/Empty"), 0o755)
|
|
ev.write(s, "D/E/notes.txt", "text")
|
|
ev.write(s, "H/cover.jpg", "art")
|
|
ev.noMusic(t, SideSlave)
|
|
|
|
if _, err := ev.e.StartRemoveFolders(ev.pair.ID, []string{"A/B"}); err == nil {
|
|
t.Fatal("a folder that wasn't listed must be refused")
|
|
}
|
|
// Music added after the scan: that folder is skipped.
|
|
ev.write(s, "H/new.flac", "music")
|
|
if _, err := ev.e.StartRemoveFolders(ev.pair.ID, []string{"A/Empty", "D", "H"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
j := ev.wait()
|
|
if j.Results["removed"] != 2 || len(j.Skipped) != 1 || !strings.Contains(j.Skipped[0], "contains music") {
|
|
t.Fatalf("results %+v skipped %v", j.Results, j.Skipped)
|
|
}
|
|
for _, p := range []string{"A/Empty", "D"} {
|
|
if exists(filepath.Join(s, p)) {
|
|
t.Errorf("%s should be gone", p)
|
|
}
|
|
}
|
|
if !exists(filepath.Join(s, "A/B/song.flac")) || !exists(filepath.Join(s, "H/new.flac")) {
|
|
t.Fatal("music must never be touched")
|
|
}
|
|
ops, _, _ := ev.st.Ops(ctx, store.OpFilter{PairID: ev.pair.ID})
|
|
var dOp store.Operation
|
|
for _, o := range ops {
|
|
if o.Action == store.ActRemoveFolder && o.RelSlave == "D" {
|
|
dOp = o
|
|
}
|
|
}
|
|
if dOp.ID == 0 || !exists(filepath.Join(dOp.QuarantinePath, "E/notes.txt")) {
|
|
t.Fatalf("D should be in the quarantine: %+v", dOp)
|
|
}
|
|
// Undo brings it back with its contents; the scan result is stale now.
|
|
if err := ev.e.Undo(ctx, dOp.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if b, _ := os.ReadFile(filepath.Join(s, "D/E/notes.txt")); string(b) != "text" {
|
|
t.Fatal("undo should restore the folder")
|
|
}
|
|
if _, err := ev.e.StartRemoveFolders(ev.pair.ID, []string{"D"}); err == nil {
|
|
t.Fatal("removing needs a fresh scan")
|
|
}
|
|
}
|
|
|
|
func TestRemoveFoldersDryRun(t *testing.T) {
|
|
ev := setup(t, true)
|
|
os.MkdirAll(filepath.Join(ev.master, "Empty"), 0o755)
|
|
ev.noMusic(t, SideMaster)
|
|
ev.e.StartRemoveFolders(ev.pair.ID, []string{"Empty"})
|
|
if j := ev.wait(); j.Results["planned"] != 1 || !exists(filepath.Join(ev.master, "Empty")) {
|
|
t.Fatalf("dry run must only record: %+v", j.Results)
|
|
}
|
|
}
|