1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-23 06:52:42 +00:00

c to cancel album download

This commit is contained in:
fiso64 2024-12-22 15:10:42 +01:00
parent 4a34ec0cbd
commit 529f009c48
11 changed files with 216 additions and 80 deletions

View file

@ -23,6 +23,7 @@ See the [usage examples](#examples-1).
- [File conditions](#file-conditions) - [File conditions](#file-conditions)
- [Name format](#name-format) - [Name format](#name-format)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Shortcuts \& interactive mode](#shortcuts--interactive-mode)
- [Examples](#examples-1) - [Examples](#examples-1)
- [Notes](#notes) - [Notes](#notes)
- [Docker](#docker) - [Docker](#docker)
@ -407,7 +408,7 @@ Variables enclosed in {} will be replaced by the corresponding file tag value.
Name format supports subdirectories as well as conditional expressions like {tag1|tag2} - If Name format supports subdirectories as well as conditional expressions like {tag1|tag2} - If
tag1 is null, use tag2. String literals enclosed in parentheses are ignored in the null check. tag1 is null, use tag2. String literals enclosed in parentheses are ignored in the null check.
### Examples: ### Examples
- `{artist} - {title}` - `{artist} - {title}`
Always name it 'Artist - Title'. Because some files on Soulseek are untagged, the Always name it 'Artist - Title'. Because some files on Soulseek are untagged, the
following is generally preferred: following is generally preferred:
@ -418,7 +419,7 @@ tag1 is null, use tag2. String literals enclosed in parentheses are ignored in t
Sort files into artist/album folders if all tags are present, otherwise put them in Sort files into artist/album folders if all tags are present, otherwise put them in
the 'missing-tags' folder. the 'missing-tags' folder.
### Available variables: ### Available variables
``` ```
artist First artist (from the file tags) artist First artist (from the file tags)
sartist Source artist (as on CSV/Spotify/YouTube/etc) sartist Source artist (as on CSV/Spotify/YouTube/etc)
@ -439,7 +440,7 @@ default-folder Default sldl folder name (usually the playlist n
``` ```
## Configuration ## Configuration
### Config Location: ### Config Location
sldl will look for a file named sldl.conf in the following locations: sldl will look for a file named sldl.conf in the following locations:
``` ```
~/AppData/Roaming/sldl/sldl.conf ~/AppData/Roaming/sldl/sldl.conf
@ -447,7 +448,7 @@ default-folder Default sldl folder name (usually the playlist n
``` ```
as well as in the directory of the executable. as well as in the directory of the executable.
### Syntax: ### Syntax
Example config file: Example config file:
``` ```
username = your-username username = your-username
@ -458,7 +459,7 @@ fast-search = true
Lines starting with hashtags (#) will be ignored. Tildes in paths are expanded as the user Lines starting with hashtags (#) will be ignored. Tildes in paths are expanded as the user
directory. The path variable `{bindir}` stores the directory of the sldl binary. directory. The path variable `{bindir}` stores the directory of the sldl binary.
### Configuration profiles: ### Configuration profiles
Profiles are supported: Profiles are supported:
``` ```
[lossless] [lossless]
@ -485,6 +486,28 @@ input-type ("youtube"|"csv"|"string"|"bandcamp"|"spotify")
download-mode ("normal"|"aggregate"|"album"|"album-aggregate") download-mode ("normal"|"aggregate"|"album"|"album-aggregate")
interactive (bool) interactive (bool)
``` ```
## Shortcuts & interactive mode
### Shortcuts
To cancel a running album download, press `C`.
### Interactive mode
Interactive mode for albums can be enabled with `-t`/`--interactive`. It enables users to choose the desired folder or download specific files from it.
Key bindings:
```
Up/p previous folder
Down/n next folder
Enter/d download selected folder
q download folder and disable interactive mode
r retrieve all files in the folder
Esc/s skip current album
d:1,2,3 download specific files
d:start:end download a range of files
```
## Examples ## Examples
Download tracks from a csv file: Download tracks from a csv file:

View file

@ -202,7 +202,7 @@ public class Config
if (confPath == "none") if (confPath == "none")
return; return;
confPath = Utils.ExpandUser(args[idx + 1]); confPath = Utils.ExpandVariables(args[idx + 1]);
if(File.Exists(Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath))) if(File.Exists(Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath)))
confPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath); confPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath);
} }
@ -249,12 +249,12 @@ public class Config
nameFormat = nameFormat.Trim(); nameFormat = nameFormat.Trim();
confPath = Utils.GetFullPath(Utils.ExpandUser(confPath)); confPath = Utils.GetFullPath(Utils.ExpandVariables(confPath));
parentDir = Utils.GetFullPath(Utils.ExpandUser(parentDir)); parentDir = Utils.GetFullPath(Utils.ExpandVariables(parentDir));
m3uFilePath = Utils.GetFullPath(Utils.ExpandUser(m3uFilePath)); m3uFilePath = Utils.GetFullPath(Utils.ExpandVariables(m3uFilePath));
indexFilePath = Utils.GetFullPath(Utils.ExpandUser(indexFilePath)); indexFilePath = Utils.GetFullPath(Utils.ExpandVariables(indexFilePath));
skipMusicDir = Utils.GetFullPath(Utils.ExpandUser(skipMusicDir)); skipMusicDir = Utils.GetFullPath(Utils.ExpandVariables(skipMusicDir));
failedAlbumPath = Utils.GetFullPath(Utils.ExpandUser(failedAlbumPath)); failedAlbumPath = Utils.GetFullPath(Utils.ExpandVariables(failedAlbumPath));
if (failedAlbumPath.Length == 0) if (failedAlbumPath.Length == 0)
failedAlbumPath = Path.Join(parentDir, "failed"); failedAlbumPath = Path.Join(parentDir, "failed");

View file

@ -11,7 +11,7 @@ using SearchResponse = Soulseek.SearchResponse;
static class Download static class Download
{ {
public static async Task DownloadFile(SearchResponse response, Soulseek.File file, string filePath, Track track, ProgressBar progress, Config config, CancellationToken? ct = null, CancellationTokenSource? searchCts = null) public static async Task DownloadFile(SearchResponse response, Soulseek.File file, string filePath, Track track, ProgressBar progress, TrackListEntry tle, Config config, CancellationToken? ct = null, CancellationTokenSource? searchCts = null)
{ {
await Program.WaitForLogin(config); await Program.WaitForLogin(config);
Directory.CreateDirectory(Path.GetDirectoryName(filePath)); Directory.CreateDirectory(Path.GetDirectoryName(filePath));
@ -40,7 +40,7 @@ static class Download
new CancellationTokenSource(); new CancellationTokenSource();
using var outputStream = new FileStream(filePath, FileMode.Create); using var outputStream = new FileStream(filePath, FileMode.Create);
var wrapper = new DownloadWrapper(origPath, response, file, track, downloadCts, progress); var wrapper = new DownloadWrapper(origPath, response, file, track, downloadCts, progress, tle);
downloads.TryAdd(file.Filename, wrapper); downloads.TryAdd(file.Filename, wrapper);
int maxRetries = 3; int maxRetries = 3;

View file

@ -8,6 +8,7 @@ namespace Enums
OutOfDownloadRetries = 2, OutOfDownloadRetries = 2,
NoSuitableFileFound = 3, NoSuitableFileFound = 3,
AllDownloadsFailed = 4, AllDownloadsFailed = 4,
Other = 5,
} }
public enum TrackState public enum TrackState

View file

@ -17,7 +17,7 @@ namespace Extractors
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config) public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config)
{ {
csvFilePath = Utils.ExpandUser(input); csvFilePath = Utils.ExpandVariables(input);
if (!File.Exists(csvFilePath)) if (!File.Exists(csvFilePath))
throw new FileNotFoundException($"CSV file '{csvFilePath}' not found"); throw new FileNotFoundException($"CSV file '{csvFilePath}' not found");

View file

@ -16,7 +16,7 @@ namespace Extractors
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config) public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config)
{ {
listFilePath = Utils.ExpandUser(input); listFilePath = Utils.ExpandVariables(input);
if (!File.Exists(listFilePath)) if (!File.Exists(listFilePath))
throw new FileNotFoundException($"List file '{listFilePath}' not found"); throw new FileNotFoundException($"List file '{listFilePath}' not found");

View file

@ -183,7 +183,7 @@ public static class Help
Help Help
-h, --help [option] [all|input|download-modes|search|name-format| -h, --help [option] [all|input|download-modes|search|name-format|
file-conditions|config] file-conditions|config|shortcuts]
Notes Notes
Acronyms of two- and --three-word-flags are also accepted, e.g. --twf. If the option Acronyms of two- and --three-word-flags are also accepted, e.g. --twf. If the option
@ -473,6 +473,28 @@ public static class Help
interactive (bool) interactive (bool)
"; ";
const string shortcutsHelp = @"
Shortcuts & interactive mode
Shortcuts
To cancel a running album download, press `C`.
Interactive mode
Interactive mode for albums can be enabled with `-t`/`--interactive`. It enables users
to choose the desired folder or download specific files from it.
Key bindings:
Up/p previous folder
Down/n next folder
Enter/d download selected folder
q download folder and disable interactive mode
r retrieve all files in the folder
Esc/s skip current album
d:1,2,3 download specific files
d:start:end download a range of files
";
public static void PrintHelp(string? option = null) public static void PrintHelp(string? option = null)
{ {
string text = helpText; string text = helpText;
@ -485,6 +507,7 @@ public static class Help
{ "file-conditions", fileConditionsHelp }, { "file-conditions", fileConditionsHelp },
{ "name-format", nameFormatHelp }, { "name-format", nameFormatHelp },
{ "config", configHelp }, { "config", configHelp },
{ "shortcuts", shortcutsHelp },
}; };
if (option != null && dict.ContainsKey(option)) if (option != null && dict.ContainsKey(option))

View file

@ -15,6 +15,7 @@ namespace Models
public SearchResponse response; public SearchResponse response;
public ProgressBar progress; public ProgressBar progress;
public Track track; public Track track;
public TrackListEntry tle;
public long bytesTransferred = 0; public long bytesTransferred = 0;
public bool stalled = false; public bool stalled = false;
public bool queued = false; public bool queued = false;
@ -29,12 +30,13 @@ namespace Models
bool updatedTextSuccess = false; bool updatedTextSuccess = false;
readonly char[] bars = { '|', '/', '—', '\\' }; readonly char[] bars = { '|', '/', '—', '\\' };
public DownloadWrapper(string savePath, SearchResponse response, Soulseek.File file, Track track, CancellationTokenSource cts, ProgressBar progress) public DownloadWrapper(string savePath, SearchResponse response, Soulseek.File file, Track track, CancellationTokenSource cts, ProgressBar progress, TrackListEntry tle)
{ {
this.savePath = savePath; this.savePath = savePath;
this.response = response; this.response = response;
this.file = file; this.file = file;
this.cts = cts; this.cts = cts;
this.tle = tle;
this.track = track; this.track = track;
this.progress = progress; this.progress = progress;
this.displayText = Printing.DisplayString(track, file, response); this.displayText = Printing.DisplayString(track, file, response);

View file

@ -22,6 +22,8 @@ static partial class Program
const int updateInterval = 100; const int updateInterval = 100;
private static bool initialized = false; private static bool initialized = false;
public static bool skipUpdate = false; public static bool skipUpdate = false;
public static bool interceptKeys = false;
public static event EventHandler<ConsoleKey>? keyPressed;
public static IExtractor extractor = null!; public static IExtractor extractor = null!;
public static TrackLists trackLists = null!; public static TrackLists trackLists = null!;
@ -615,6 +617,15 @@ static partial class Program
string? soulseekDir = null; string? soulseekDir = null;
int index = 0; int index = 0;
async Task runAlbumDownloads(List<Track> tracks, SemaphoreSlim semaphore, CancellationTokenSource cts)
{
var downloadTasks = tracks.Select(async track =>
{
await DownloadTask(config, tle, track, semaphore, organizer, cts, true, true, true);
});
await Task.WhenAll(downloadTasks);
}
while (tle.list.Count > 0 && !config.albumArtOnly) while (tle.list.Count > 0 && !config.albumArtOnly)
{ {
bool wasInteractive = config.interactiveMode; bool wasInteractive = config.interactiveMode;
@ -641,12 +652,24 @@ static partial class Program
PrintAlbum(tracks); PrintAlbum(tracks);
} }
var semaphore = new SemaphoreSlim(999); // Needs to be uncapped due to a bug that causes album downloads to fail after some time using var semaphore = new SemaphoreSlim(999); // Needs to be uncapped due to a bug that causes album downloads to fail after some time
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
bool userCancelled = false;
void onKeyPressed(object? sender, ConsoleKey key)
{
if (key == ConsoleKey.C)
{
userCancelled = true;
cts.Cancel();
}
}
interceptKeys = true;
keyPressed += onKeyPressed;
try try
{ {
await RunAlbumDownloads(config, tle, organizer, tracks, semaphore, cts); await runAlbumDownloads(tracks, semaphore, cts);
if (!config.noBrowseFolder && retrieveCurrent && !retrievedFolders.Contains(soulseekDir)) if (!config.noBrowseFolder && retrieveCurrent && !retrievedFolders.Contains(soulseekDir))
{ {
@ -657,8 +680,8 @@ static partial class Program
if (newFilesFound > 0) if (newFilesFound > 0)
{ {
Console.WriteLine($"Found {newFilesFound} more files in the directory, downloading:"); Console.WriteLine($"Found {newFilesFound} more files, downloading:");
await RunAlbumDownloads(config, tle, organizer, tracks, semaphore, cts); await runAlbumDownloads(tracks, semaphore, cts);
} }
else else
{ {
@ -671,16 +694,52 @@ static partial class Program
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
OnAlbumFail(config, tracks); if (userCancelled)
{
Console.Write("\nDownload cancelled.");
if (tracks.Any(t => t.State == TrackState.Downloaded && t.DownloadPath.Length > 0))
{
Console.WriteLine("Delete files? [Y/n] (default: album fail action): ");
var res = Console.ReadLine().Trim().ToLower();
if (res == "y")
OnAlbumFail(tracks, true, config);
else if (res == "" && !config.IgnoreAlbumFail)
OnAlbumFail(tracks, config.DeleteAlbumOnFail, config);
}
}
else
{
if (!config.IgnoreAlbumFail)
OnAlbumFail(tracks, config.DeleteAlbumOnFail, config);
}
}
finally
{
organizer.SetRemoteCommonDir(null);
tle.list.RemoveAt(index);
interceptKeys = false;
keyPressed -= onKeyPressed;
} }
organizer.SetRemoteCommonDir(null);
tle.list.RemoveAt(index);
} }
if (succeeded) if (succeeded)
{ {
await OnAlbumSuccess(config, tle, tracks); tle.source.State = TrackState.Downloaded;
var downloadedAudio = tracks.Where(t => !t.IsNotAudio && t.State == TrackState.Downloaded && t.DownloadPath.Length > 0);
if (downloadedAudio.Any())
{
tle.source.DownloadPath = Utils.GreatestCommonDirectory(downloadedAudio.Select(t => t.DownloadPath));
if (config.removeTracksFromSource)
{
await extractor.RemoveTrackFromSource(tle.source);
}
}
}
else if (index != -1)
{
tle.source.State = TrackState.Failed;
} }
List<Track>? additionalImages = null; List<Track>? additionalImages = null;
@ -704,40 +763,9 @@ static partial class Program
} }
static async Task RunAlbumDownloads(Config config, TrackListEntry tle, FileManager organizer, List<Track> tracks, SemaphoreSlim semaphore, CancellationTokenSource cts) static void OnAlbumFail(List<Track>? tracks, bool deleteDownloaded, Config config)
{ {
var downloadTasks = tracks.Select(async track => if (tracks == null) return;
{
await DownloadTask(config, tle, track, semaphore, organizer, cts, true, true, true);
});
await Task.WhenAll(downloadTasks);
}
static async Task OnAlbumSuccess(Config config, TrackListEntry tle, List<Track>? tracks)
{
if (tracks == null)
return;
var downloadedAudio = tracks.Where(t => !t.IsNotAudio && t.State == TrackState.Downloaded && t.DownloadPath.Length > 0);
if (downloadedAudio.Any())
{
tle.source.State = TrackState.Downloaded;
tle.source.DownloadPath = Utils.GreatestCommonDirectory(downloadedAudio.Select(t => t.DownloadPath));
if (config.removeTracksFromSource)
{
await extractor.RemoveTrackFromSource(tle.source);
}
}
}
static void OnAlbumFail(Config config, List<Track>? tracks)
{
if (tracks == null || config.IgnoreAlbumFail)
return;
foreach (var track in tracks) foreach (var track in tracks)
{ {
@ -745,7 +773,7 @@ static partial class Program
{ {
try try
{ {
if (config.DeleteAlbumOnFail) if (deleteDownloaded || track.DownloadPath.EndsWith(".incomplete"))
{ {
File.Delete(track.DownloadPath); File.Delete(track.DownloadPath);
} }
@ -881,17 +909,54 @@ static partial class Program
fileManager.SetRemoteCommonDir(Utils.GreatestCommonDirectorySlsk(tracks.Select(t => t.FirstDownload.Filename))); fileManager.SetRemoteCommonDir(Utils.GreatestCommonDirectorySlsk(tracks.Select(t => t.FirstDownload.Filename)));
bool allSucceeded = true; bool allSucceeded = true;
var semaphore = new SemaphoreSlim(1); using var semaphore = new SemaphoreSlim(1);
using var cts = new CancellationTokenSource();
foreach (var track in tracks) bool userCancelled = false;
void onKeyPressed(object? sender, ConsoleKey key)
{ {
using var cts = new CancellationTokenSource(); if (key == ConsoleKey.C)
await DownloadTask(config, tle, track, semaphore, fileManager, cts, false, false, false); {
userCancelled = true;
cts.Cancel();
}
}
interceptKeys = true;
keyPressed += onKeyPressed;
if (track.State == TrackState.Downloaded) try
downloadedImages.Add(track); {
foreach (var track in tracks)
{
await DownloadTask(config, tle, track, semaphore, fileManager, cts, false, false, false);
if (track.State == TrackState.Downloaded)
downloadedImages.Add(track);
else
allSucceeded = false;
}
}
catch (OperationCanceledException)
{
if (userCancelled)
{
Console.Write("\nDownload cancelled.");
if (tracks.Any(t => t.State == TrackState.Downloaded && t.DownloadPath.Length > 0))
{
Console.WriteLine("Delete files? [Y/n]: ");
if (Console.ReadLine().Trim().ToLower() == "y")
OnAlbumFail(tracks, true, config);
}
}
else else
allSucceeded = false; {
throw;
}
}
finally
{
interceptKeys = false;
keyPressed -= onKeyPressed;
} }
if (allSucceeded) if (allSucceeded)
@ -902,7 +967,7 @@ static partial class Program
} }
static async Task DownloadTask(Config config, TrackListEntry? tle, Track track, SemaphoreSlim semaphore, FileManager organizer, CancellationTokenSource? cts, bool cancelOnFail, bool removeFromSource, bool organize) static async Task DownloadTask(Config config, TrackListEntry tle, Track track, SemaphoreSlim semaphore, FileManager organizer, CancellationTokenSource cts, bool cancelOnFail, bool removeFromSource, bool organize)
{ {
if (track.State != TrackState.Initial) if (track.State != TrackState.Initial)
return; return;
@ -921,7 +986,7 @@ static partial class Program
try try
{ {
(savedFilePath, chosenFile) = await Search.SearchAndDownload(track, organizer, config, cts); (savedFilePath, chosenFile) = await Search.SearchAndDownload(track, organizer, tle, config, cts);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -944,6 +1009,15 @@ static partial class Program
throw new OperationCanceledException(); throw new OperationCanceledException();
} }
} }
else if (ex is OperationCanceledException && cts.IsCancellationRequested)
{
lock (trackLists)
{
track.State = TrackState.Failed;
track.FailureReason = FailureReason.Other;
}
throw;
}
else else
{ {
tries--; tries--;
@ -956,6 +1030,12 @@ static partial class Program
if (tries == 0 && cancelOnFail) if (tries == 0 && cancelOnFail)
{ {
lock (trackLists)
{
track.State = TrackState.Failed;
track.FailureReason = FailureReason.Other;
}
cts.Cancel(); cts.Cancel();
throw new OperationCanceledException(); throw new OperationCanceledException();
} }
@ -1027,8 +1107,6 @@ static partial class Program
WriteLine($" [Up/p] | [Down/n] | [Enter] | [q] {retrieveAll1}| [Esc/s]", ConsoleColor.Green); WriteLine($" [Up/p] | [Down/n] | [Enter] | [q] {retrieveAll1}| [Esc/s]", ConsoleColor.Green);
WriteLine($" Prev | Next | Accept | Accept & Quit Interactive {retrieveAll2}| Skip", ConsoleColor.Green); WriteLine($" Prev | Next | Accept | Accept & Quit Interactive {retrieveAll2}| Skip", ConsoleColor.Green);
Console.WriteLine(); Console.WriteLine();
WriteLine($" d:1,2,3 or d:start:end to download individual files", ConsoleColor.Green);
Console.WriteLine();
} }
writeHelp(); writeHelp();
@ -1123,7 +1201,7 @@ static partial class Program
} }
static async Task Update(Config config) static async Task Update(Config startConfig)
{ {
while (true) while (true)
{ {
@ -1145,7 +1223,7 @@ static partial class Program
{ {
lock (val) lock (val)
{ {
if ((DateTime.Now - val.UpdateLastChangeTime()).TotalMilliseconds > config.maxStaleTime) if ((DateTime.Now - val.UpdateLastChangeTime()).TotalMilliseconds > val.tle.config.maxStaleTime)
{ {
val.stalled = true; val.stalled = true;
val.UpdateText(); val.UpdateText();
@ -1172,10 +1250,10 @@ static partial class Program
&& !client.State.HasFlag(SoulseekClientStates.Connecting)) && !client.State.HasFlag(SoulseekClientStates.Connecting))
{ {
WriteLine($"\nDisconnected, logging in\n", ConsoleColor.DarkYellow, true); WriteLine($"\nDisconnected, logging in\n", ConsoleColor.DarkYellow, true);
try { await Login(config, config.useRandomLogin); } try { await Login(startConfig, startConfig.useRandomLogin); }
catch (Exception ex) catch (Exception ex)
{ {
string banMsg = config.useRandomLogin ? "" : " (possibly a 30-minute ban caused by frequent searches)"; string banMsg = startConfig.useRandomLogin ? "" : " (possibly a 30-minute ban caused by frequent searches)";
WriteLine($"{ex.Message}{banMsg}", ConsoleColor.DarkYellow, true); WriteLine($"{ex.Message}{banMsg}", ConsoleColor.DarkYellow, true);
} }
} }
@ -1193,6 +1271,12 @@ static partial class Program
{ {
WriteLine($"\n{ex.Message}\n", ConsoleColor.DarkYellow, true); WriteLine($"\n{ex.Message}\n", ConsoleColor.DarkYellow, true);
} }
if (interceptKeys && Console.KeyAvailable)
{
var key = Console.ReadKey(intercept: true).Key;
keyPressed?.Invoke(null, key);
}
} }
await Task.Delay(updateInterval); await Task.Delay(updateInterval);

View file

@ -16,7 +16,7 @@ static class Search
public static RateLimitedSemaphore? searchSemaphore; public static RateLimitedSemaphore? searchSemaphore;
// very messy function that does everything // very messy function that does everything
public static async Task<(string, SlFile?)> SearchAndDownload(Track track, FileManager organizer, Config config, CancellationTokenSource? cts = null) public static async Task<(string, SlFile?)> SearchAndDownload(Track track, FileManager organizer, TrackListEntry tle, Config config, CancellationTokenSource? cts = null)
{ {
if (config.DoNotDownload) if (config.DoNotDownload)
throw new Exception(); throw new Exception();
@ -58,7 +58,7 @@ static class Search
saveFilePath = organizer.GetSavePath(f.Filename); saveFilePath = organizer.GetSavePath(f.Filename);
fsUser = r.Username; fsUser = r.Username;
chosenFile = f; chosenFile = f;
downloadTask = Download.DownloadFile(r, f, saveFilePath, track, progress, config, cts?.Token, searchCts); downloadTask = Download.DownloadFile(r, f, saveFilePath, track, progress, tle, config, cts?.Token, searchCts);
} }
} }
} }
@ -155,12 +155,15 @@ static class Search
try try
{ {
downloading = 1; downloading = 1;
await Download.DownloadFile(response, file, saveFilePath, track, progress, config, cts?.Token); await Download.DownloadFile(response, file, saveFilePath, track, progress, tle, config, cts?.Token);
userSuccessCounts.AddOrUpdate(response.Username, 1, (k, v) => v + 1); userSuccessCounts.AddOrUpdate(response.Username, 1, (k, v) => v + 1);
return true; return true;
} }
catch (Exception e) catch (Exception e)
{ {
if (e is OperationCanceledException && cts != null && cts.IsCancellationRequested)
throw;
Printing.WriteLineIf($"Error: Download Error: {e}", config.debugInfo, ConsoleColor.DarkYellow); Printing.WriteLineIf($"Error: Download Error: {e}", config.debugInfo, ConsoleColor.DarkYellow);
chosenFile = null; chosenFile = null;

View file

@ -101,7 +101,7 @@ public static class Utils
return Path.GetDirectoryName(fname); return Path.GetDirectoryName(fname);
} }
public static string ExpandUser(string path) public static string ExpandVariables(string path)
{ {
if (string.IsNullOrWhiteSpace(path)) if (string.IsNullOrWhiteSpace(path))
return path; return path;