A grab that delivers nine of twelve tracks clears the completeness floor and is imported, and the other three were never looked for. On Soulseek that is the commonest way an album ends up almost right: one peer's folder lacks a track, or one file fails. After a successful import the manager now compares the tracks the files were aligned to (ImportResult.Matched, new) with the expected tracklist. When one to three are missing, and fewer than half, it searches for each one on its own, as the track's artist and title with the album kept for ranking. It grabs the first auto-acceptable copy that is not from the source that already failed to supply it, and is not a delegate, which would place it in its own library. The candidate is trimmed to the one file aligned to the track. The import uses the album's own request with ImportOptions.Only, which skips the completeness check and imports only a file aligned to the missing track, so it is tagged and placed as part of the album, and anything else the folder brought is left out. One attempt per track, and nothing here fails the download: the album is already imported, so a track that cannot be found is logged on the job and left. slskd accepts one-file folders for a request that expects one track, which the per-track search needs. Closes #276 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJiuc3ZZhxsPXz3ozTirT
169 lines
4.4 KiB
Go
169 lines
4.4 KiB
Go
package download
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// Filling in the tracks an almost-complete album is missing (#276).
|
|
|
|
func fiveTrackTitles() []string {
|
|
return append(allTitles(), "Let Down")
|
|
}
|
|
|
|
func fiveTrackDownload() Download {
|
|
dl := fourTrackDownload()
|
|
dl.Expected = append(dl.Expected, ExpectedTrack{Position: 5, Title: "Let Down"})
|
|
|
|
return dl
|
|
}
|
|
|
|
func TestMissingTracks(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dl := fiveTrackDownload()
|
|
got := func(positions ...int) []ExpectedTrack {
|
|
out := make([]ExpectedTrack, 0, len(positions))
|
|
for _, p := range positions {
|
|
out = append(out, dl.Expected[p-1])
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
dl Download
|
|
matched []ExpectedTrack
|
|
want int
|
|
}{
|
|
{"complete", dl, got(1, 2, 3, 4, 5), 0},
|
|
{"one short", dl, got(1, 2, 3, 4), 1},
|
|
{"two short", dl, got(1, 2, 3), 2},
|
|
{"half gone is a poor copy", dl, got(1, 2), 0},
|
|
{"a recording is not an album", func() Download {
|
|
d := dl
|
|
d.RecordingMBID = "rec"
|
|
|
|
return d
|
|
}(), got(1, 2, 3, 4), 0},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
if n := len(missingTracks(tc.dl, tc.matched)); n != tc.want {
|
|
t.Errorf("%s: %d missing, want %d", tc.name, n, tc.want)
|
|
}
|
|
}
|
|
|
|
big := fiveTrackDownload()
|
|
for i := 6; i <= 20; i++ {
|
|
big.Expected = append(big.Expected, ExpectedTrack{Position: i, Title: "T" + itoa(i)})
|
|
}
|
|
|
|
if n := len(missingTracks(big, big.Expected[:16])); n != 0 {
|
|
t.Errorf("four of twenty missing: %d filled in, want none past %d", n, maxFillInTracks)
|
|
}
|
|
}
|
|
|
|
// A fill-in import takes only the file that is the missing track, and
|
|
// places it in the album with the album's tags; anything else the grab
|
|
// brought is left out.
|
|
func TestImportOnlyTakesTheMissingTrack(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := newImportFixture(t,
|
|
"05 - Let Down.flac",
|
|
"02 - Paranoid Android.flac",
|
|
)
|
|
|
|
dl := fiveTrackDownload()
|
|
|
|
got, err := f.importer.Import(
|
|
context.Background(), dl,
|
|
Result{Dir: f.dir, Files: f.files},
|
|
ImportOptions{LibraryRoot: f.root, WriteTags: true, Only: dl.Expected[4:]},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Import: %v", err)
|
|
}
|
|
|
|
want := filepath.Join(f.root, "Radiohead", "OK Computer", "05 Let Down.flac")
|
|
if len(got.Paths) != 1 || got.Paths[0] != want {
|
|
t.Errorf("imported %q, want only %s", got.Paths, want)
|
|
}
|
|
|
|
// A file that is not the missing track is not imported at all.
|
|
g := newImportFixture(t, "02 - Paranoid Android.flac")
|
|
|
|
if _, err := g.importer.Import(
|
|
context.Background(), dl,
|
|
Result{Dir: g.dir, Files: g.files},
|
|
ImportOptions{LibraryRoot: g.root, WriteTags: true, Only: dl.Expected[4:]},
|
|
); !errors.Is(err, ErrTooIncomplete) {
|
|
t.Errorf("Import = %v, want nothing matched", err)
|
|
}
|
|
}
|
|
|
|
// The album comes from one source missing its fifth track; the fifth is
|
|
// then found on its own at another and lands in the same album.
|
|
func TestManagerFillsInAMissingTrack(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f := newManagerFixture(t)
|
|
|
|
titles := fiveTrackTitles()
|
|
|
|
album := NewFakeProvider(1, "album", Caps{CanSearch: true, CanTransport: true})
|
|
ac := candidateFor("album-cand", titles, ".flac", 30_000_000)
|
|
ac.ProviderID = 1
|
|
album.Candidates = []Candidate{ac}
|
|
|
|
for i, tt := range titles[:4] {
|
|
album.Written[trackToken(i+1)+" - "+tt+".flac"] = []byte("audio-data")
|
|
}
|
|
|
|
single := NewFakeProvider(2, "single", Caps{CanSearch: true, CanTransport: true})
|
|
sc := Candidate{
|
|
ID: "single-cand",
|
|
Protocol: ProtocolDirect,
|
|
Title: "Radiohead - OK Computer",
|
|
Artist: "Radiohead",
|
|
Files: []CandidateFile{{
|
|
Path: "Radiohead - OK Computer/05 - Let Down.flac",
|
|
Size: 30_000_000,
|
|
}},
|
|
Health: 0.5,
|
|
ProviderID: 2,
|
|
}
|
|
single.Candidates = []Candidate{sc}
|
|
single.Written["05 - Let Down.flac"] = []byte("audio-data")
|
|
|
|
f.manager.installProvider(Config{ID: 1, Priority: 90}, album)
|
|
f.manager.installProvider(Config{ID: 2, Priority: 10}, single)
|
|
|
|
dl := fiveTrackDownload()
|
|
|
|
if _, err := f.manager.Start(context.Background(), dl); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
|
|
waitForDownloadState(t, f.store, dl.ID, StateComplete)
|
|
|
|
if album.GrabCallCount() != 1 || single.GrabCallCount() != 1 {
|
|
t.Errorf(
|
|
"grabs: album=%d single=%d, want 1 and 1",
|
|
album.GrabCallCount(), single.GrabCallCount(),
|
|
)
|
|
}
|
|
|
|
for i, tt := range titles {
|
|
p := filepath.Join(f.root, "Radiohead", "OK Computer", trackToken(i+1)+" "+tt+".flac")
|
|
if _, err := os.Stat(p); err != nil {
|
|
t.Errorf("track %d not in the library: %v", i+1, err)
|
|
}
|
|
}
|
|
}
|