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:
parent
4a34ec0cbd
commit
529f009c48
11 changed files with 216 additions and 80 deletions
33
README.md
33
README.md
|
@ -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:
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
organizer.SetRemoteCommonDir(null);
|
||||||
tle.list.RemoveAt(index);
|
tle.list.RemoveAt(index);
|
||||||
|
interceptKeys = false;
|
||||||
|
keyPressed -= onKeyPressed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,11 +909,25 @@ 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();
|
||||||
|
|
||||||
|
bool userCancelled = false;
|
||||||
|
void onKeyPressed(object? sender, ConsoleKey key)
|
||||||
|
{
|
||||||
|
if (key == ConsoleKey.C)
|
||||||
|
{
|
||||||
|
userCancelled = true;
|
||||||
|
cts.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interceptKeys = true;
|
||||||
|
keyPressed += onKeyPressed;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
foreach (var track in tracks)
|
foreach (var track in tracks)
|
||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource();
|
|
||||||
await DownloadTask(config, tle, track, semaphore, fileManager, cts, false, false, false);
|
await DownloadTask(config, tle, track, semaphore, fileManager, cts, false, false, false);
|
||||||
|
|
||||||
if (track.State == TrackState.Downloaded)
|
if (track.State == TrackState.Downloaded)
|
||||||
|
@ -893,6 +935,29 @@ static partial class Program
|
||||||
else
|
else
|
||||||
allSucceeded = false;
|
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
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
interceptKeys = false;
|
||||||
|
keyPressed -= onKeyPressed;
|
||||||
|
}
|
||||||
|
|
||||||
if (allSucceeded)
|
if (allSucceeded)
|
||||||
break;
|
break;
|
||||||
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue