Skip to content

Commit e3ddad2

Browse files
committed
fix: per-line conditions ignored for list.txt inputs in interactive mode
1 parent cde6470 commit e3ddad2

14 files changed

Lines changed: 548 additions & 93 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ A smart and configurable downloader for Soulseek. Built with Soulseek.NET.
3333
> [!NOTE]
3434
> Because it's not possible to run the same account on two separate clients simultaneously, it is recommended to use Sockseek with a **separate Soulseek account** to avoid connection problems.
3535

36+
Also: This project was formerly named `sldl` (and `slsk-batchdl` before that). [Why rename?](https://github.com/fiso64/sldl/releases/)
37+
3638
## Index
3739
- [Options](#options)
3840
- [Input types](#input-types)

Sockseek.Api/Contracts/ServerRequests.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,34 +106,42 @@ public abstract record JobDraftDto;
106106
public sealed record ExtractJobDraftDto(
107107
string Input,
108108
string? InputType = null,
109-
bool? AutoStartExtractedResult = null) : JobDraftDto;
109+
bool? AutoStartExtractedResult = null,
110+
DownloadSettingsPatchDto? DownloadSettings = null) : JobDraftDto;
110111

111112
public sealed record TrackSearchJobDraftDto(
112113
SongQueryDto SongQuery,
113-
bool IncludeFullResults = false) : JobDraftDto;
114+
bool IncludeFullResults = false,
115+
DownloadSettingsPatchDto? DownloadSettings = null) : JobDraftDto;
114116

115117
public sealed record AlbumSearchJobDraftDto(
116-
AlbumQueryDto AlbumQuery) : JobDraftDto;
118+
AlbumQueryDto AlbumQuery,
119+
DownloadSettingsPatchDto? DownloadSettings = null) : JobDraftDto;
117120

118121
public sealed record SongJobDraftDto(
119122
SongQueryDto SongQuery,
120-
DownloadBehaviorPolicyDto? DownloadBehavior = null) : JobDraftDto;
123+
DownloadBehaviorPolicyDto? DownloadBehavior = null,
124+
DownloadSettingsPatchDto? DownloadSettings = null) : JobDraftDto;
121125

122126
public sealed record AlbumJobDraftDto(
123127
AlbumQueryDto AlbumQuery,
124-
DownloadBehaviorPolicyDto? DownloadBehavior = null) : JobDraftDto;
128+
DownloadBehaviorPolicyDto? DownloadBehavior = null,
129+
DownloadSettingsPatchDto? DownloadSettings = null) : JobDraftDto;
125130

126131
public sealed record AggregateJobDraftDto(
127132
SongQueryDto SongQuery,
128-
DownloadBehaviorPolicyDto? DownloadBehavior = null) : JobDraftDto;
133+
DownloadBehaviorPolicyDto? DownloadBehavior = null,
134+
DownloadSettingsPatchDto? DownloadSettings = null) : JobDraftDto;
129135

130136
public sealed record AlbumAggregateJobDraftDto(
131137
AlbumQueryDto AlbumQuery,
132-
DownloadBehaviorPolicyDto? DownloadBehavior = null) : JobDraftDto;
138+
DownloadBehaviorPolicyDto? DownloadBehavior = null,
139+
DownloadSettingsPatchDto? DownloadSettings = null) : JobDraftDto;
133140

134141
public sealed record JobListJobDraftDto(
135142
string? Name,
136-
IReadOnlyList<JobDraftDto> Jobs) : JobDraftDto;
143+
IReadOnlyList<JobDraftDto> Jobs,
144+
DownloadSettingsPatchDto? DownloadSettings = null) : JobDraftDto;
137145

138146
/// <summary>
139147
/// Controls automatic versus caller-selected downloads for download-capable jobs.

Sockseek.Cli.Tests/LocalCliBackendTests.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,101 @@ public async Task LocalCliBackend_ObservesSearchJobsThroughServerShapedModel()
7474
}
7575
}
7676

77+
[TestMethod]
78+
public async Task InteractiveCliCoordinator_FromListPreservesLineConditionsBeforePrompt()
79+
{
80+
string inputPath = Path.Combine(Path.GetTempPath(), "Sockseek-local-interactive-cond-" + Guid.NewGuid() + ".txt");
81+
string musicRoot = Path.Combine(Path.GetTempPath(), "Sockseek-local-interactive-cond-music-" + Guid.NewGuid());
82+
string outputDir = Path.Combine(Path.GetTempPath(), "Sockseek-local-interactive-cond-out-" + Guid.NewGuid());
83+
string albumDir = Path.Combine(musicRoot, "Album Artist", "Album Name");
84+
Directory.CreateDirectory(albumDir);
85+
Directory.CreateDirectory(outputDir);
86+
87+
File.WriteAllText(Path.Combine(albumDir, "01. file1.mp3"), "a");
88+
File.WriteAllLines(inputPath, ["a:\"Album Name\" strict-album=true;format=flac"]);
89+
90+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
91+
92+
try
93+
{
94+
var engineSettings = new EngineSettings
95+
{
96+
MockFilesDir = musicRoot,
97+
MockFilesReadTags = false,
98+
};
99+
var downloadSettings = new DownloadSettings
100+
{
101+
Output =
102+
{
103+
ParentDir = outputDir,
104+
NameFormat = "{foldername}/{filename}",
105+
},
106+
};
107+
108+
var clientManager = new SoulseekClientManager(engineSettings);
109+
string[] args =
110+
[
111+
inputPath,
112+
"--input-type", "list",
113+
"--mock-files-dir", musicRoot,
114+
"--mock-files-no-read-tags",
115+
"-p", outputDir,
116+
"--name-format", "{foldername}/{filename}",
117+
"--no-progress",
118+
"-t",
119+
];
120+
var resolver = new SubmissionOptionsJobSettingsResolver(
121+
ConfigManager.CreateJobSettingsResolver(new ConfigFile("none", []), args, new CliSettings { InteractiveMode = true, NoProgress = true }),
122+
normalize: settings => SettingsNormalizer.NormalizeDownloadPaths(settings, settings.RuntimePathContext));
123+
var engine = new DownloadEngine(engineSettings, clientManager, resolver);
124+
var backend = new LocalCliBackend(engine, downloadSettings, resolver);
125+
var engineTask = engine.RunAsync(cts.Token);
126+
127+
int pickerCalls = 0;
128+
var coordinator = new InteractiveCliCoordinator(
129+
backend,
130+
new CliSettings { InteractiveMode = true, NoProgress = true },
131+
cts.Token,
132+
request =>
133+
{
134+
Interlocked.Increment(ref pickerCalls);
135+
var folder = request.Folders.FirstOrDefault();
136+
return Task.FromResult(new InteractiveModeManager.RunResult(
137+
folder == null ? -1 : 0,
138+
folder,
139+
RetrieveCurrentFolder: true,
140+
ExitInteractiveMode: false,
141+
request.FilterStr));
142+
},
143+
pollInterval: TimeSpan.FromMilliseconds(10));
144+
145+
var summary = await coordinator.StartAsync(
146+
new SubmitExtractJobRequestDto(
147+
inputPath,
148+
"List",
149+
Options: new SubmissionOptionsDto(Guid.NewGuid())),
150+
cts.Token);
151+
152+
await coordinator.RunUntilCompleteAsync(summary.WorkflowId, cts.Token);
153+
154+
Assert.AreEqual(0, pickerCalls, "The MP3 folder must be filtered out by the list-line FLAC condition before prompting.");
155+
Assert.AreEqual(0, Directory.GetFiles(outputDir, "*", SearchOption.AllDirectories).Length);
156+
157+
engine.CompleteEnqueue();
158+
await engineTask;
159+
}
160+
finally
161+
{
162+
cts.Cancel();
163+
if (File.Exists(inputPath))
164+
File.Delete(inputPath);
165+
if (Directory.Exists(musicRoot))
166+
Directory.Delete(musicRoot, true);
167+
if (Directory.Exists(outputDir))
168+
Directory.Delete(outputDir, true);
169+
}
170+
}
171+
77172
[TestMethod]
78173
public async Task LocalCliBackend_RetrieveFolderAndWaitAsync_ReturnsNewFilesFoundCount()
79174
{

Sockseek.Cli.Tests/RemoteCliBackendTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,94 @@ public async Task InteractiveCliCoordinator_FromListSerializesPromptsAndDownload
409409
}
410410
}
411411

412+
[TestMethod]
413+
public async Task InteractiveCliCoordinator_FromListPreservesLineConditionsBeforePrompt()
414+
{
415+
string inputPath = Path.Combine(Path.GetTempPath(), "Sockseek-remote-interactive-cond-" + Guid.NewGuid() + ".txt");
416+
string musicRoot = Path.Combine(Path.GetTempPath(), "Sockseek-remote-interactive-cond-music-" + Guid.NewGuid());
417+
string outputDir = Path.Combine(Path.GetTempPath(), "Sockseek-remote-interactive-cond-out-" + Guid.NewGuid());
418+
string albumDir = Path.Combine(musicRoot, "Album Artist", "Album Name");
419+
Directory.CreateDirectory(albumDir);
420+
Directory.CreateDirectory(outputDir);
421+
422+
File.WriteAllText(Path.Combine(albumDir, "01. file1.mp3"), "a");
423+
File.WriteAllLines(inputPath, ["a:\"Album Name\" strict-album=true;format=flac"]);
424+
425+
int port = GetFreeTcpPort();
426+
string url = $"http://127.0.0.1:{port}";
427+
await using var app = ServerHost.Build([], new ServerOptions
428+
{
429+
Engine = new EngineSettings
430+
{
431+
MockFilesDir = musicRoot,
432+
MockFilesReadTags = false,
433+
},
434+
DefaultDownload = new DownloadSettings
435+
{
436+
Output =
437+
{
438+
ParentDir = outputDir,
439+
NameFormat = "{foldername}/{filename}",
440+
},
441+
Search =
442+
{
443+
NoBrowseFolder = true,
444+
},
445+
},
446+
Profiles = ProfileCatalog.Empty,
447+
}, url);
448+
449+
try
450+
{
451+
await app.StartAsync();
452+
await using var backend = new RemoteCliBackend(url);
453+
await backend.StartAsync();
454+
455+
int pickerCalls = 0;
456+
var coordinator = new InteractiveCliCoordinator(
457+
backend,
458+
new CliSettings { InteractiveMode = true, NoProgress = true },
459+
CancellationToken.None,
460+
request =>
461+
{
462+
Interlocked.Increment(ref pickerCalls);
463+
var folder = request.Folders.FirstOrDefault();
464+
return Task.FromResult(new InteractiveModeManager.RunResult(
465+
folder == null ? -1 : 0,
466+
folder,
467+
RetrieveCurrentFolder: true,
468+
ExitInteractiveMode: false,
469+
request.FilterStr));
470+
},
471+
pollInterval: TimeSpan.FromMilliseconds(10));
472+
473+
var summary = await coordinator.StartAsync(
474+
new SubmitExtractJobRequestDto(
475+
inputPath,
476+
"List",
477+
Options: new SubmissionOptionsDto(
478+
OutputParentDir: outputDir,
479+
DownloadSettings: ConfigManager.CreateCliDownloadSettingsPatch([inputPath, "--input-type", "list", "--no-browse-folder"]))),
480+
CancellationToken.None);
481+
482+
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));
483+
await coordinator.RunUntilCompleteAsync(summary.WorkflowId, timeout.Token);
484+
485+
Assert.AreEqual(0, pickerCalls, "The MP3 folder must be filtered out by the list-line FLAC condition before prompting.");
486+
Assert.AreEqual(0, Directory.GetFiles(outputDir, "*", SearchOption.AllDirectories).Length);
487+
}
488+
finally
489+
{
490+
await app.StopAsync();
491+
if (File.Exists(inputPath))
492+
File.Delete(inputPath);
493+
if (Directory.Exists(musicRoot))
494+
Directory.Delete(musicRoot, true);
495+
if (Directory.Exists(outputDir))
496+
Directory.Delete(outputDir, true);
497+
}
498+
}
499+
412500
[TestMethod]
413501
public async Task InteractiveCliCoordinator_AlbumAggregatePromptsEachBucket()
414502
{

Sockseek.Cli/Program.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,11 @@ public static async Task Main(string[] args)
153153
return;
154154
}
155155

156-
var engine = new DownloadEngine(engineSettings, clientManager, jobSettingsResolver);
157-
var backend = new LocalCliBackend(engine, rootSettings);
156+
var localSubmissionOptionsResolver = new SubmissionOptionsJobSettingsResolver(
157+
jobSettingsResolver,
158+
normalize: settings => SettingsNormalizer.NormalizeDownloadPaths(settings, settings.RuntimePathContext));
159+
var engine = new DownloadEngine(engineSettings, clientManager, localSubmissionOptionsResolver);
160+
var backend = new LocalCliBackend(engine, rootSettings, localSubmissionOptionsResolver);
158161

159162
CliProgressReporter? cliReporter = null;
160163
if (cliSettings.ProgressJson)

0 commit comments

Comments
 (0)