mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 22:42:41 +00:00
commit
This commit is contained in:
parent
2c4ee4309d
commit
ba9b295e22
2 changed files with 174 additions and 114 deletions
|
@ -144,7 +144,7 @@ Options:
|
||||||
names.
|
names.
|
||||||
|
|
||||||
--format <format> Accepted file format(s), comma-separated
|
--format <format> Accepted file format(s), comma-separated
|
||||||
--length-tol <sec> Length tolerance in seconds (default: 3)
|
--length-tol <sec> Length tolerance in seconds
|
||||||
--min-bitrate <rate> Minimum file bitrate
|
--min-bitrate <rate> Minimum file bitrate
|
||||||
--max-bitrate <rate> Maximum file bitrate
|
--max-bitrate <rate> Maximum file bitrate
|
||||||
--min-samplerate <rate> Minimum file sample rate
|
--min-samplerate <rate> Minimum file sample rate
|
||||||
|
@ -154,7 +154,7 @@ Options:
|
||||||
--banned-users <list> Comma-separated list of users to ignore
|
--banned-users <list> Comma-separated list of users to ignore
|
||||||
|
|
||||||
--pref-format <format> Preferred file format(s), comma-separated (default: mp3)
|
--pref-format <format> Preferred file format(s), comma-separated (default: mp3)
|
||||||
--pref-length-tol <sec> Preferred length tolerance in seconds (default: 2)
|
--pref-length-tol <sec> Preferred length tolerance in seconds (default: 3)
|
||||||
--pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)
|
--pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)
|
||||||
--pref-max-bitrate <rate> Preferred maximum bitrate (default: 2500)
|
--pref-max-bitrate <rate> Preferred maximum bitrate (default: 2500)
|
||||||
--pref-min-samplerate <rate> Preferred minimum sample rate
|
--pref-min-samplerate <rate> Preferred minimum sample rate
|
||||||
|
@ -244,8 +244,7 @@ Options:
|
||||||
```
|
```
|
||||||
|
|
||||||
### File conditions
|
### File conditions
|
||||||
Files not satisfying the conditions will not be downloaded. For example, `--length-tol` is set to 3 by default, meaning that files whose duration differs from the supplied duration by more than 3 seconds will not be downloaded (can be disabled by setting it to -1).
|
Files not satisfying the conditions will not be downloaded. Files satisfying `pref-` conditions will be preferred; setting `--pref-format "flac,wav"` will make it download high quality files if they exist, and only download low quality files if there's nothing else. Conditions can also be supplied as a semicolon-delimited string to `--cond` and `--pref`, e.g `--cond "br>=320;f=mp3,ogg;sr<96000"`. See the start of `Program.cs` for the default file conditions.
|
||||||
Files satisfying `pref-` conditions will be preferred; setting `--pref-format "flac,wav"` will make it download high quality files if they exist, and only download low quality files if there's nothing else. Conditions can also be supplied as a semicolon-delimited string to `--cond` and `--pref`, e.g `--cond "br>=320;f=mp3,ogg;sr<96000"`. See the start of `Program.cs` for the default file conditions.
|
|
||||||
|
|
||||||
**Important note**: Some info may be unavailable depending on the client used by the peer. For example, the default Soulseek client does not share the file bitrate. By default, if `--min-bitrate` is set, then files with unknown bitrate will still be downloaded. You can configure it to reject all files where one of the checked properties is unavailable by enabling `--strict-conditions`. (As a consequence, if `--min-bitrate` is also set then any files shared by users with the default client will be ignored)
|
**Important note**: Some info may be unavailable depending on the client used by the peer. For example, the default Soulseek client does not share the file bitrate. By default, if `--min-bitrate` is set, then files with unknown bitrate will still be downloaded. You can configure it to reject all files where one of the checked properties is unavailable by enabling `--strict-conditions`. (As a consequence, if `--min-bitrate` is also set then any files shared by users with the default client will be ignored)
|
||||||
|
|
||||||
|
@ -262,7 +261,7 @@ The following options will make it go faster, but may decrease search result qua
|
||||||
- `--searches-per-time` increase at the risk of ban, see the notes section for details.
|
- `--searches-per-time` increase at the risk of ban, see the notes section for details.
|
||||||
|
|
||||||
### Quality vs Quantity
|
### Quality vs Quantity
|
||||||
The options `--strict-title`, `--strict-artist` and `--strict-album` will filter any file that does not contain the title/artist/album in the filename (ignoring case, bounded by boundary chars). Since by default such files will be ranked lower anyways and may actually be correct, these options are only recommended when you want to minimize false downloads as much as possible.
|
The options `--strict-title`, `--strict-artist` and `--strict-album` will filter any file that does not contain the title/artist/album in the filename (ignoring case, bounded by boundary chars). Since by default such files will be ranked lower anyways and may actually be correct, these options are only recommended when you want to minimize false downloads as much as possible. Another way to prevent false downloads is to set `--length-tol` to 3 or less to make it ignore any songs that differ from the input by more than 3 seconds.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
Create a file named `sldl.conf` in the same directory as the executable and write your arguments there, e.g:
|
Create a file named `sldl.conf` in the same directory as the executable and write your arguments there, e.g:
|
||||||
|
|
|
@ -15,16 +15,18 @@ using SlFile = Soulseek.File;
|
||||||
using File = System.IO.File;
|
using File = System.IO.File;
|
||||||
using Directory = System.IO.Directory;
|
using Directory = System.IO.Directory;
|
||||||
using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string, (Soulseek.SearchResponse, Soulseek.File)>;
|
using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string, (Soulseek.SearchResponse, Soulseek.File)>;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
|
||||||
// todo
|
// todo
|
||||||
// - Why does it use so much CPU and memory?
|
// - Why does it use so much CPU and memory?
|
||||||
// - Very slow startup time on linux
|
// - Very slow startup time on linux
|
||||||
|
// - Uses more threads than allowed after a hundred or so downloads
|
||||||
|
|
||||||
// undocumented options
|
// undocumented options
|
||||||
// --on-complete
|
// --on-complete
|
||||||
// --artist-col, --title-col, --album-col, --length-col, --yt-desc-col, --yt-id-col, --album-track-count-col
|
// --artist-col, --title-col, --album-col, --length-col, --yt-desc-col, --yt-id-col, --album-track-count-col
|
||||||
// --input-type, --login, --random-login, --no-modify-share-count --fast-search-delay,
|
// --input-type, --login, --random-login, --no-modify-share-count, --unknown-error-retries
|
||||||
// --fails-to-deprioritize (=1), --fails-to-ignore (=2), --invalid-replace-str
|
// --fails-to-deprioritize (=1), --fails-to-ignore (=2), --invalid-replace-str
|
||||||
// --cond, --pref, --danger-words, --pref-danger-words, --strict-title, --strict-artist, --strict-album
|
// --cond, --pref, --danger-words, --pref-danger-words, --strict-title, --strict-artist, --strict-album
|
||||||
// --fast-search-delay, --fast-search-min-up-speed
|
// --fast-search-delay, --fast-search-min-up-speed
|
||||||
|
@ -34,13 +36,13 @@ static class Program
|
||||||
{
|
{
|
||||||
static FileConditions necessaryCond = new()
|
static FileConditions necessaryCond = new()
|
||||||
{
|
{
|
||||||
LengthTolerance = 3,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static FileConditions preferredCond = new()
|
static FileConditions preferredCond = new()
|
||||||
{
|
{
|
||||||
Formats = new string[] { "mp3" },
|
Formats = new string[] { "mp3" },
|
||||||
LengthTolerance = 2,
|
LengthTolerance = 3,
|
||||||
MinBitrate = 200,
|
MinBitrate = 200,
|
||||||
MaxBitrate = 2500,
|
MaxBitrate = 2500,
|
||||||
MaxSampleRate = 48000,
|
MaxSampleRate = 48000,
|
||||||
|
@ -133,6 +135,7 @@ static class Program
|
||||||
static int updateDelay = 100;
|
static int updateDelay = 100;
|
||||||
static int searchTimeout = 5000;
|
static int searchTimeout = 5000;
|
||||||
static int maxConcurrentProcesses = 2;
|
static int maxConcurrentProcesses = 2;
|
||||||
|
static int unknownErrorRetries = 2;
|
||||||
static int maxRetriesPerTrack = 30;
|
static int maxRetriesPerTrack = 30;
|
||||||
static int listenPort = 50000;
|
static int listenPort = 50000;
|
||||||
|
|
||||||
|
@ -214,7 +217,7 @@ static class Program
|
||||||
"\n names." +
|
"\n names." +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\n --format <format> Accepted file format(s), comma-separated" +
|
"\n --format <format> Accepted file format(s), comma-separated" +
|
||||||
"\n --length-tol <sec> Length tolerance in seconds (default: 3)" +
|
"\n --length-tol <sec> Length tolerance in seconds" +
|
||||||
"\n --min-bitrate <rate> Minimum file bitrate" +
|
"\n --min-bitrate <rate> Minimum file bitrate" +
|
||||||
"\n --max-bitrate <rate> Maximum file bitrate" +
|
"\n --max-bitrate <rate> Maximum file bitrate" +
|
||||||
"\n --min-samplerate <rate> Minimum file sample rate" +
|
"\n --min-samplerate <rate> Minimum file sample rate" +
|
||||||
|
@ -224,7 +227,7 @@ static class Program
|
||||||
"\n --banned-users <list> Comma-separated list of users to ignore" +
|
"\n --banned-users <list> Comma-separated list of users to ignore" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\n --pref-format <format> Preferred file format(s), comma-separated (default: mp3)" +
|
"\n --pref-format <format> Preferred file format(s), comma-separated (default: mp3)" +
|
||||||
"\n --pref-length-tol <sec> Preferred length tolerance in seconds (default: 2)" +
|
"\n --pref-length-tol <sec> Preferred length tolerance in seconds (default: 3)" +
|
||||||
"\n --pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)" +
|
"\n --pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)" +
|
||||||
"\n --pref-max-bitrate <rate> Preferred maximum bitrate (default: 2500)" +
|
"\n --pref-max-bitrate <rate> Preferred maximum bitrate (default: 2500)" +
|
||||||
"\n --pref-min-samplerate <rate> Preferred minimum sample rate" +
|
"\n --pref-min-samplerate <rate> Preferred minimum sample rate" +
|
||||||
|
@ -914,6 +917,9 @@ static class Program
|
||||||
case "--fails-to-ignore":
|
case "--fails-to-ignore":
|
||||||
ignoreOn = -int.Parse(args[++i]);
|
ignoreOn = -int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--unknown-error-retries":
|
||||||
|
unknownErrorRetries = int.Parse(args[++i]);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException($"Unknown argument: {args[i]}");
|
throw new ArgumentException($"Unknown argument: {args[i]}");
|
||||||
}
|
}
|
||||||
|
@ -1515,39 +1521,57 @@ static class Program
|
||||||
{
|
{
|
||||||
if (track.TrackState == Track.State.Exists || track.TrackState == Track.State.NotFoundLastTime)
|
if (track.TrackState == Track.State.Exists || track.TrackState == Track.State.NotFoundLastTime)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await semaphore.WaitAsync();
|
await semaphore.WaitAsync();
|
||||||
int tries = 2;
|
|
||||||
retry:
|
|
||||||
await WaitForLogin();
|
|
||||||
|
|
||||||
try
|
int tries = unknownErrorRetries;
|
||||||
|
string savedFilePath = "";
|
||||||
|
|
||||||
|
while (tries > 0)
|
||||||
{
|
{
|
||||||
WriteLine($"Search and download {track}", debugOnly: true);
|
await WaitForLogin();
|
||||||
var savedFilePath = await SearchAndDownload(track);
|
|
||||||
lock (trackLists) { tracks[index] = new Track(track) { TrackState=Track.State.Downloaded, DownloadPath=savedFilePath }; }
|
|
||||||
|
|
||||||
if (removeTracksFromSource && !string.IsNullOrEmpty(spotifyUrl))
|
try
|
||||||
spotifyClient.RemoveTrackFromPlaylist(playlistUri, track.URI);
|
{
|
||||||
|
WriteLine($"Search and download {track}", debugOnly: true);
|
||||||
|
savedFilePath = await SearchAndDownload(track);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
WriteLine($"Exception thrown: {ex}", debugOnly: true);
|
||||||
|
if (!client.State.HasFlag(SoulseekClientStates.LoggedIn))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (ex is SearchAndDownloadException)
|
||||||
|
{
|
||||||
|
lock (trackLists) { tracks[index] = new Track(track) { TrackState = Track.State.Failed, FailureReason = ex.Message }; }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteLine($"\n{ex.Message}\n{ex.StackTrace}\n", ConsoleColor.DarkYellow, true);
|
||||||
|
tries--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
|
if (savedFilePath != "")
|
||||||
{
|
{
|
||||||
WriteLine($"Exception thrown: {ex}", debugOnly: true);
|
try
|
||||||
if (!client.State.HasFlag(SoulseekClientStates.LoggedIn))
|
|
||||||
{
|
{
|
||||||
goto retry;
|
lock (trackLists) { tracks[index] = new Track(track) { TrackState = Track.State.Downloaded, DownloadPath = savedFilePath }; }
|
||||||
|
|
||||||
|
if (removeTracksFromSource && !string.IsNullOrEmpty(spotifyUrl))
|
||||||
|
spotifyClient.RemoveTrackFromPlaylist(playlistUri, track.URI);
|
||||||
}
|
}
|
||||||
else if (ex is SearchAndDownloadException)
|
catch (Exception ex)
|
||||||
{
|
|
||||||
lock (trackLists) { tracks[index] = new Track(track) { TrackState = Track.State.Failed, FailureReason = ex.Message }; }
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
WriteLine($"\n{ex.Message}\n{ex.StackTrace}\n", ConsoleColor.DarkYellow, true);
|
WriteLine($"\n{ex.Message}\n{ex.StackTrace}\n", ConsoleColor.DarkYellow, true);
|
||||||
if (tries-- > 0)
|
|
||||||
goto retry;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally { semaphore.Release(); }
|
|
||||||
|
|
||||||
m3uEditor.Update();
|
m3uEditor.Update();
|
||||||
|
|
||||||
|
@ -1555,6 +1579,8 @@ static class Program
|
||||||
{
|
{
|
||||||
OnComplete(onComplete, tracks[index]);
|
OnComplete(onComplete, tracks[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
semaphore.Release();
|
||||||
});
|
});
|
||||||
|
|
||||||
await Task.WhenAll(downloadTasks);
|
await Task.WhenAll(downloadTasks);
|
||||||
|
@ -1634,15 +1660,62 @@ static class Program
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await semaphore.WaitAsync(mainLoopCts.Token);
|
await semaphore.WaitAsync(mainLoopCts.Token);
|
||||||
int tries = 2;
|
|
||||||
|
|
||||||
retry:
|
int tries = unknownErrorRetries;
|
||||||
await WaitForLogin();
|
string savedFilePath = "";
|
||||||
mainLoopCts.Token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
try
|
while (tries > 0)
|
||||||
|
{
|
||||||
|
await WaitForLogin();
|
||||||
|
mainLoopCts.Token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
savedFilePath = await SearchAndDownload(track);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!client.State.HasFlag(SoulseekClientStates.LoggedIn))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (ex is SearchAndDownloadException)
|
||||||
|
{
|
||||||
|
lock (trackLists)
|
||||||
|
{
|
||||||
|
tracks[index] = new Track(track) { TrackState = Track.State.Failed, FailureReason = ex.Message };
|
||||||
|
if (downloadingImages)
|
||||||
|
ReplaceTrack(listRef, track, tracks[index]); // shitty shortcut
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!albumIgnoreFails)
|
||||||
|
{
|
||||||
|
mainLoopCts.Cancel();
|
||||||
|
foreach (var (key, dl) in downloads)
|
||||||
|
{
|
||||||
|
lock (dl)
|
||||||
|
{
|
||||||
|
dl.cts.Cancel();
|
||||||
|
if (File.Exists(dl.savePath)) File.Delete(dl.savePath);
|
||||||
|
downloads.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new OperationCanceledException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WriteLine($"\n{ex.Message}\n{ex.StackTrace}\n", ConsoleColor.DarkYellow, true);
|
||||||
|
tries--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedFilePath != "")
|
||||||
{
|
{
|
||||||
var savedFilePath = await SearchAndDownload(track);
|
|
||||||
dlFiles.TryAdd(savedFilePath, true);
|
dlFiles.TryAdd(savedFilePath, true);
|
||||||
|
|
||||||
lock (trackLists)
|
lock (trackLists)
|
||||||
|
@ -1655,48 +1728,13 @@ static class Program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (!client.State.HasFlag(SoulseekClientStates.LoggedIn))
|
|
||||||
{
|
|
||||||
goto retry;
|
|
||||||
}
|
|
||||||
else if (ex is SearchAndDownloadException)
|
|
||||||
{
|
|
||||||
lock (trackLists)
|
|
||||||
{
|
|
||||||
tracks[index] = new Track(track) { TrackState = Track.State.Failed, FailureReason = ex.Message };
|
|
||||||
if (downloadingImages)
|
|
||||||
ReplaceTrack(listRef, track, tracks[index]); // shitty shortcut
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WriteLine($"\n{ex.Message}\n{ex.StackTrace}\n", ConsoleColor.DarkYellow, true);
|
|
||||||
if (tries-- > 0)
|
|
||||||
goto retry;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!albumIgnoreFails)
|
|
||||||
{
|
|
||||||
mainLoopCts.Cancel();
|
|
||||||
lock (downloads)
|
|
||||||
{
|
|
||||||
foreach (var (key, dl) in downloads)
|
|
||||||
{
|
|
||||||
dl.cts.Cancel();
|
|
||||||
if (File.Exists(dl.savePath)) File.Delete(dl.savePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new OperationCanceledException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally { semaphore.Release(); }
|
|
||||||
|
|
||||||
if (onComplete != "")
|
if (onComplete != "")
|
||||||
{
|
{
|
||||||
OnComplete(onComplete, tracks[index]);
|
OnComplete(onComplete, tracks[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
semaphore.Release();
|
||||||
});
|
});
|
||||||
|
|
||||||
await Task.WhenAll(downloadTasks);
|
await Task.WhenAll(downloadTasks);
|
||||||
|
@ -2472,10 +2510,13 @@ static class Program
|
||||||
.ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || preferredCond.AcceptNoLength)
|
.ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || preferredCond.AcceptNoLength)
|
||||||
.ThenByDescending(x => !useBracketCheck || BracketCheck(track, inferredTrack(x).Item1)) // deprioritize result if it contains '(' or '[' and the title does not (avoid remixes)
|
.ThenByDescending(x => !useBracketCheck || BracketCheck(track, inferredTrack(x).Item1)) // deprioritize result if it contains '(' or '[' and the title does not (avoid remixes)
|
||||||
.ThenByDescending(x => preferredCond.StrictTitleSatisfies(x.file.Filename, track.Title))
|
.ThenByDescending(x => preferredCond.StrictTitleSatisfies(x.file.Filename, track.Title))
|
||||||
|
.ThenByDescending(x => preferredCond.StrictArtistSatisfies(x.file.Filename, track.Title))
|
||||||
.ThenByDescending(x => preferredCond.LengthToleranceSatisfies(x.file, track.Length))
|
.ThenByDescending(x => preferredCond.LengthToleranceSatisfies(x.file, track.Length))
|
||||||
.ThenByDescending(x => preferredCond.FormatSatisfies(x.file.Filename))
|
.ThenByDescending(x => preferredCond.FormatSatisfies(x.file.Filename))
|
||||||
.ThenByDescending(x => preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
|
.ThenByDescending(x => preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
|
||||||
.ThenByDescending(x => preferredCond.BitrateSatisfies(x.file))
|
.ThenByDescending(x => preferredCond.BitrateSatisfies(x.file))
|
||||||
|
.ThenByDescending(x => preferredCond.SampleRateSatisfies(x.file))
|
||||||
|
.ThenByDescending(x => preferredCond.BitDepthSatisfies(x.file))
|
||||||
.ThenByDescending(x => preferredCond.FileSatisfies(x.file, track, x.response))
|
.ThenByDescending(x => preferredCond.FileSatisfies(x.file, track, x.response))
|
||||||
.ThenByDescending(x => x.response.HasFreeUploadSlot)
|
.ThenByDescending(x => x.response.HasFreeUploadSlot)
|
||||||
.ThenByDescending(x => x.response.UploadSpeed / 1024 / 650)
|
.ThenByDescending(x => x.response.UploadSpeed / 1024 / 650)
|
||||||
|
@ -2500,14 +2541,6 @@ static class Program
|
||||||
if (!t2.Contains('('))
|
if (!t2.Contains('('))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
string ar = track.Artist.Replace('[', '(');
|
|
||||||
if (ar.Contains('(') && !t2.Replace(ar, "").Contains('('))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
string al = track.Album.Replace('[', '(');
|
|
||||||
if (al.Contains('(') && !t2.Replace(al, "").Contains('('))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2842,21 +2875,20 @@ static class Program
|
||||||
throw new Exception();
|
throw new Exception();
|
||||||
|
|
||||||
await WaitForLogin();
|
await WaitForLogin();
|
||||||
System.IO.Directory.CreateDirectory(Path.GetDirectoryName(filePath));
|
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
|
||||||
string origPath = filePath;
|
string origPath = filePath;
|
||||||
filePath += ".incomplete";
|
filePath += ".incomplete";
|
||||||
|
|
||||||
bool transferSet = false;
|
|
||||||
var transferOptions = new TransferOptions(
|
var transferOptions = new TransferOptions(
|
||||||
stateChanged: (state) =>
|
stateChanged: (state) =>
|
||||||
{
|
{
|
||||||
if (downloads.ContainsKey(file.Filename) && !transferSet)
|
if (downloads.TryGetValue(file.Filename, out var x))
|
||||||
downloads[file.Filename].transfer = state.Transfer;
|
x.transfer = state.Transfer;
|
||||||
},
|
},
|
||||||
progressUpdated: (progress) =>
|
progressUpdated: (progress) =>
|
||||||
{
|
{
|
||||||
if (downloads.ContainsKey(file.Filename))
|
if (downloads.TryGetValue(file.Filename, out var x))
|
||||||
downloads[file.Filename].bytesTransferred = progress.PreviousBytesTransferred;
|
x.bytesTransferred = progress.PreviousBytesTransferred;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2869,21 +2901,29 @@ static class Program
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
if (System.IO.File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
try { System.IO.File.Delete(filePath); } catch { }
|
try { File.Delete(filePath); } catch { }
|
||||||
if (downloads.ContainsKey(file.Filename))
|
downloads.TryRemove(file.Filename, out var d);
|
||||||
downloads[file.Filename].UpdateText();
|
if (d != null)
|
||||||
downloads.TryRemove(file.Filename, out _);
|
lock (d) { d.UpdateText(); }
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
try { searchCts?.Cancel(); }
|
try { searchCts?.Cancel(); }
|
||||||
catch { }
|
catch { }
|
||||||
try { System.IO.File.Move(filePath, origPath, true); }
|
|
||||||
|
try { Utils.Move(filePath, origPath); }
|
||||||
catch (IOException) { WriteLine($"Failed to rename .incomplete file", ConsoleColor.DarkYellow, true); }
|
catch (IOException) { WriteLine($"Failed to rename .incomplete file", ConsoleColor.DarkYellow, true); }
|
||||||
downloads[file.Filename].success = true;
|
|
||||||
downloads[file.Filename].UpdateText();
|
downloads.TryRemove(file.Filename, out var x);
|
||||||
downloads.TryRemove(file.Filename, out _);
|
if (x != null)
|
||||||
|
{
|
||||||
|
lock (x)
|
||||||
|
{
|
||||||
|
x.success = true;
|
||||||
|
x.UpdateText();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2897,25 +2937,30 @@ static class Program
|
||||||
{
|
{
|
||||||
if (client.State.HasFlag(SoulseekClientStates.LoggedIn))
|
if (client.State.HasFlag(SoulseekClientStates.LoggedIn))
|
||||||
{
|
{
|
||||||
foreach (var (key, val) in searches) // shouldn't this give "collection was modified" errors? whatever..
|
foreach (var (key, val) in searches)
|
||||||
{
|
{
|
||||||
if (val == null)
|
if (val == null)
|
||||||
searches.TryRemove(key, out _);
|
searches.TryRemove(key, out _); // reminder: removing from a dict in a foreach is allowed in newer .net versions
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (key, val) in downloads)
|
foreach (var (key, val) in downloads)
|
||||||
{
|
{
|
||||||
if (val != null)
|
if (val != null)
|
||||||
{
|
{
|
||||||
val.UpdateText();
|
lock (val)
|
||||||
|
|
||||||
if ((DateTime.Now - val.UpdateLastChangeTime()).TotalMilliseconds > downloadMaxStaleTime)
|
|
||||||
{
|
{
|
||||||
val.stalled = true;
|
if ((DateTime.Now - val.UpdateLastChangeTime()).TotalMilliseconds > downloadMaxStaleTime)
|
||||||
val.UpdateText();
|
{
|
||||||
|
val.stalled = true;
|
||||||
|
val.UpdateText();
|
||||||
|
|
||||||
try { val.cts.Cancel(); } catch { }
|
try { val.cts.Cancel(); } catch { }
|
||||||
downloads.TryRemove(key, out _);
|
downloads.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
val.UpdateText();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -3592,14 +3637,27 @@ static class Program
|
||||||
if (nameFormat == "" || !Utils.IsMusicFile(filepath))
|
if (nameFormat == "" || !Utils.IsMusicFile(filepath))
|
||||||
return filepath;
|
return filepath;
|
||||||
|
|
||||||
string add = Path.GetRelativePath(outputFolder, Path.GetDirectoryName(filepath));
|
string dir = Path.GetDirectoryName(filepath) ?? "";
|
||||||
|
string add = dir != "" ? Path.GetRelativePath(outputFolder, dir) : "";
|
||||||
string newFilePath = NamingFormat(filepath, nameFormat, track);
|
string newFilePath = NamingFormat(filepath, nameFormat, track);
|
||||||
|
|
||||||
if (filepath != newFilePath)
|
if (filepath != newFilePath)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
|
dir = Path.GetDirectoryName(newFilePath) ?? "";
|
||||||
Utils.Move(filepath, newFilePath);
|
if (dir != "") Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Utils.Move(filepath, newFilePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
WriteLine($"\nFailed to move: {ex.Message}\n", ConsoleColor.DarkYellow, true);
|
||||||
|
return filepath;
|
||||||
|
}
|
||||||
|
|
||||||
if (add != "" && add != "." && Utils.GetRecursiveFileCount(Path.Join(outputFolder, add)) == 0)
|
if (add != "" && add != "." && Utils.GetRecursiveFileCount(Path.Join(outputFolder, add)) == 0)
|
||||||
Directory.Delete(Path.Join(outputFolder, add), true);
|
try { Directory.Delete(Path.Join(outputFolder, add), true); } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return newFilePath;
|
return newFilePath;
|
||||||
|
@ -3659,7 +3717,7 @@ static class Program
|
||||||
char dirsep = Path.DirectorySeparatorChar;
|
char dirsep = Path.DirectorySeparatorChar;
|
||||||
newName = newName.Replace('/', dirsep);
|
newName = newName.Replace('/', dirsep);
|
||||||
var x = newName.Split(dirsep, StringSplitOptions.RemoveEmptyEntries);
|
var x = newName.Split(dirsep, StringSplitOptions.RemoveEmptyEntries);
|
||||||
newName = string.Join(dirsep, x.Select(x => x.ReplaceInvalidChars(invalidReplaceStr)));
|
newName = string.Join(dirsep, x.Select(x => x.ReplaceInvalidChars(invalidReplaceStr).Trim(' ', '.')));
|
||||||
string newFilePath = Path.Combine(directory, newName + extension);
|
string newFilePath = Path.Combine(directory, newName + extension);
|
||||||
return newFilePath;
|
return newFilePath;
|
||||||
}
|
}
|
||||||
|
@ -4790,7 +4848,10 @@ class RateLimitedSemaphore
|
||||||
var currentTime = DateTimeOffset.UtcNow;
|
var currentTime = DateTimeOffset.UtcNow;
|
||||||
if (currentTime.UtcTicks > Interlocked.Read(ref this.nextResetTimeTicks))
|
if (currentTime.UtcTicks > Interlocked.Read(ref this.nextResetTimeTicks))
|
||||||
{
|
{
|
||||||
this.semaphore.Release(this.maxCount - this.semaphore.CurrentCount);
|
int releaseCount = this.maxCount - this.semaphore.CurrentCount;
|
||||||
|
if (releaseCount > 0)
|
||||||
|
this.semaphore.Release(releaseCount);
|
||||||
|
|
||||||
var newResetTimeTicks = (currentTime + this.resetTimeSpan).UtcTicks;
|
var newResetTimeTicks = (currentTime + this.resetTimeSpan).UtcTicks;
|
||||||
Interlocked.Exchange(ref this.nextResetTimeTicks, newResetTimeTicks);
|
Interlocked.Exchange(ref this.nextResetTimeTicks, newResetTimeTicks);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue