mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 14:32:40 +00:00
update project structure
This commit is contained in:
parent
5cd601ba1b
commit
d13b3307d8
28 changed files with 1010 additions and 1057 deletions
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
using Enums;
|
using Enums;
|
||||||
using Data;
|
using Models;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
|
|
@ -1,404 +0,0 @@
|
||||||
using Enums;
|
|
||||||
using Soulseek;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Data
|
|
||||||
{
|
|
||||||
public class Track
|
|
||||||
{
|
|
||||||
public string Title = "";
|
|
||||||
public string Artist = "";
|
|
||||||
public string Album = "";
|
|
||||||
public string URI = "";
|
|
||||||
public int Length = -1;
|
|
||||||
public bool ArtistMaybeWrong = false;
|
|
||||||
public int MinAlbumTrackCount = -1;
|
|
||||||
public int MaxAlbumTrackCount = -1;
|
|
||||||
public bool IsNotAudio = false;
|
|
||||||
public string DownloadPath = "";
|
|
||||||
public string Other = "";
|
|
||||||
public int CsvRow = -1;
|
|
||||||
public TrackType Type = TrackType.Normal;
|
|
||||||
public FailureReason FailureReason = FailureReason.None;
|
|
||||||
public TrackState State = TrackState.Initial;
|
|
||||||
public List<(SearchResponse, Soulseek.File)>? Downloads = null;
|
|
||||||
|
|
||||||
public bool OutputsDirectory => Type != TrackType.Normal;
|
|
||||||
public Soulseek.File? FirstDownload => Downloads?.FirstOrDefault().Item2;
|
|
||||||
public SearchResponse? FirstResponse => Downloads?.FirstOrDefault().Item1;
|
|
||||||
public string? FirstUsername => Downloads?.FirstOrDefault().Item1?.Username;
|
|
||||||
|
|
||||||
public Track() { }
|
|
||||||
|
|
||||||
public Track(Track other)
|
|
||||||
{
|
|
||||||
Title = other.Title;
|
|
||||||
Artist = other.Artist;
|
|
||||||
Album = other.Album;
|
|
||||||
Length = other.Length;
|
|
||||||
URI = other.URI;
|
|
||||||
ArtistMaybeWrong = other.ArtistMaybeWrong;
|
|
||||||
Downloads = other.Downloads;
|
|
||||||
Type = other.Type;
|
|
||||||
IsNotAudio = other.IsNotAudio;
|
|
||||||
State = other.State;
|
|
||||||
FailureReason = other.FailureReason;
|
|
||||||
DownloadPath = other.DownloadPath;
|
|
||||||
Other = other.Other;
|
|
||||||
MinAlbumTrackCount = other.MinAlbumTrackCount;
|
|
||||||
MaxAlbumTrackCount = other.MaxAlbumTrackCount;
|
|
||||||
//CsvRow = other.CsvRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToKey()
|
|
||||||
{
|
|
||||||
if (Type == TrackType.Album)
|
|
||||||
return $"{Artist};{Album};{(int)Type}";
|
|
||||||
else
|
|
||||||
return $"{Artist};{Album};{Title};{Length};{(int)Type}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return ToString(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ToString(bool noInfo = false)
|
|
||||||
{
|
|
||||||
if (IsNotAudio && Downloads != null && Downloads.Count > 0)
|
|
||||||
return $"{Utils.GetFileNameSlsk(Downloads[0].Item2.Filename)}";
|
|
||||||
|
|
||||||
string str = Artist;
|
|
||||||
if (Type == TrackType.Normal && Title.Length == 0 && Downloads != null && Downloads.Count > 0)
|
|
||||||
{
|
|
||||||
str = $"{Utils.GetFileNameSlsk(Downloads[0].Item2.Filename)}";
|
|
||||||
}
|
|
||||||
else if (Title.Length > 0 || Album.Length > 0)
|
|
||||||
{
|
|
||||||
if (str.Length > 0)
|
|
||||||
str += " - ";
|
|
||||||
if (Type == TrackType.Album)
|
|
||||||
{
|
|
||||||
if (Album.Length > 0)
|
|
||||||
str += Album;
|
|
||||||
else
|
|
||||||
str += Title;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (Title.Length > 0)
|
|
||||||
str += Title;
|
|
||||||
else
|
|
||||||
str += Album;
|
|
||||||
}
|
|
||||||
if (!noInfo)
|
|
||||||
{
|
|
||||||
if (Length > 0)
|
|
||||||
str += $" ({Length}s)";
|
|
||||||
if (Type == TrackType.Album)
|
|
||||||
str += " (album)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!noInfo)
|
|
||||||
{
|
|
||||||
str += " (artist)";
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TrackListEntry
|
|
||||||
{
|
|
||||||
public List<List<Track>>? list;
|
|
||||||
public Track source;
|
|
||||||
public bool needSourceSearch = false;
|
|
||||||
public bool sourceCanBeSkipped = false;
|
|
||||||
public bool needSkipExistingAfterSearch = false;
|
|
||||||
public bool gotoNextAfterSearch = false;
|
|
||||||
public string? defaultFolderName = null;
|
|
||||||
public FileConditionsMod? additionalConds = null;
|
|
||||||
public FileConditionsMod? additionalPrefConds = null;
|
|
||||||
|
|
||||||
public TrackListEntry(TrackType trackType)
|
|
||||||
{
|
|
||||||
list = new List<List<Track>>();
|
|
||||||
this.source = new Track() { Type = trackType };
|
|
||||||
SetDefaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TrackListEntry(Track source)
|
|
||||||
{
|
|
||||||
list = new List<List<Track>>();
|
|
||||||
this.source = source;
|
|
||||||
SetDefaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TrackListEntry(List<List<Track>> list, Track source)
|
|
||||||
{
|
|
||||||
this.list = list;
|
|
||||||
this.source = source;
|
|
||||||
SetDefaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TrackListEntry(List<List<Track>> list, Track source, bool needSourceSearch = false, bool sourceCanBeSkipped = false,
|
|
||||||
bool needSkipExistingAfterSearch = false, bool gotoNextAfterSearch = false, string? defaultFoldername = null)
|
|
||||||
{
|
|
||||||
this.list = list;
|
|
||||||
this.source = source;
|
|
||||||
this.needSourceSearch = needSourceSearch;
|
|
||||||
this.sourceCanBeSkipped = sourceCanBeSkipped;
|
|
||||||
this.needSkipExistingAfterSearch = needSkipExistingAfterSearch;
|
|
||||||
this.gotoNextAfterSearch = gotoNextAfterSearch;
|
|
||||||
this.defaultFolderName = defaultFoldername;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetDefaults()
|
|
||||||
{
|
|
||||||
needSourceSearch = source.Type != TrackType.Normal;
|
|
||||||
needSkipExistingAfterSearch = source.Type == TrackType.Aggregate;
|
|
||||||
gotoNextAfterSearch = source.Type == TrackType.AlbumAggregate;
|
|
||||||
sourceCanBeSkipped = source.Type != TrackType.Normal
|
|
||||||
&& source.Type != TrackType.Aggregate
|
|
||||||
&& source.Type != TrackType.AlbumAggregate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddTrack(Track track)
|
|
||||||
{
|
|
||||||
if (list == null)
|
|
||||||
list = new List<List<Track>>() { new List<Track>() { track } };
|
|
||||||
else if (list.Count == 0)
|
|
||||||
list.Add(new List<Track>() { track });
|
|
||||||
else
|
|
||||||
list[0].Add(track);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TrackLists
|
|
||||||
{
|
|
||||||
public List<TrackListEntry> lists = new();
|
|
||||||
|
|
||||||
public TrackLists() { }
|
|
||||||
|
|
||||||
public static TrackLists FromFlattened(IEnumerable<Track> flatList)
|
|
||||||
{
|
|
||||||
var res = new TrackLists();
|
|
||||||
using var enumerator = flatList.GetEnumerator();
|
|
||||||
|
|
||||||
while (enumerator.MoveNext())
|
|
||||||
{
|
|
||||||
var track = enumerator.Current;
|
|
||||||
|
|
||||||
if (track.Type != TrackType.Normal)
|
|
||||||
{
|
|
||||||
res.AddEntry(new TrackListEntry(track));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
res.AddEntry(new TrackListEntry(TrackType.Normal));
|
|
||||||
res.AddTrackToLast(track);
|
|
||||||
|
|
||||||
bool hasNext;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
hasNext = enumerator.MoveNext();
|
|
||||||
if (!hasNext || enumerator.Current.Type != TrackType.Normal)
|
|
||||||
break;
|
|
||||||
res.AddTrackToLast(enumerator.Current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNext)
|
|
||||||
res.AddEntry(new TrackListEntry(enumerator.Current));
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TrackListEntry this[int index]
|
|
||||||
{
|
|
||||||
get { return lists[index]; }
|
|
||||||
set { lists[index] = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddEntry(TrackListEntry tle)
|
|
||||||
{
|
|
||||||
lists.Add(tle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddTrackToLast(Track track)
|
|
||||||
{
|
|
||||||
if (lists.Count == 0)
|
|
||||||
{
|
|
||||||
AddEntry(new TrackListEntry(new List<List<Track>> { new List<Track>() { track } }, new Track()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int i = lists.Count - 1;
|
|
||||||
|
|
||||||
if (lists[i].list.Count == 0)
|
|
||||||
{
|
|
||||||
lists[i].list.Add(new List<Track>() { track });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int j = lists[i].list.Count - 1;
|
|
||||||
lists[i].list[j].Add(track);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reverse()
|
|
||||||
{
|
|
||||||
lists.Reverse();
|
|
||||||
foreach (var tle in lists)
|
|
||||||
{
|
|
||||||
foreach (var ls in tle.list)
|
|
||||||
{
|
|
||||||
ls.Reverse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpgradeListTypes(bool aggregate, bool album)
|
|
||||||
{
|
|
||||||
if (!aggregate && !album)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var newLists = new List<TrackListEntry>();
|
|
||||||
|
|
||||||
for (int i = 0; i < lists.Count; i++)
|
|
||||||
{
|
|
||||||
var tle = lists[i];
|
|
||||||
|
|
||||||
if (tle.source.Type == TrackType.Album && aggregate)
|
|
||||||
{
|
|
||||||
tle.source.Type = TrackType.AlbumAggregate;
|
|
||||||
tle.SetDefaults();
|
|
||||||
newLists.Add(tle);
|
|
||||||
}
|
|
||||||
else if (tle.source.Type == TrackType.Aggregate && album)
|
|
||||||
{
|
|
||||||
tle.source.Type = TrackType.AlbumAggregate;
|
|
||||||
tle.SetDefaults();
|
|
||||||
newLists.Add(tle);
|
|
||||||
}
|
|
||||||
else if (tle.source.Type == TrackType.Normal && (album || aggregate))
|
|
||||||
{
|
|
||||||
foreach (var track in tle.list[0])
|
|
||||||
{
|
|
||||||
if (album && aggregate)
|
|
||||||
track.Type = TrackType.AlbumAggregate;
|
|
||||||
else if (album)
|
|
||||||
track.Type = TrackType.Album;
|
|
||||||
else if (aggregate)
|
|
||||||
track.Type = TrackType.Aggregate;
|
|
||||||
|
|
||||||
var newTle = new TrackListEntry(track);
|
|
||||||
newTle.defaultFolderName = tle.defaultFolderName;
|
|
||||||
newLists.Add(newTle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
newLists.Add(tle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lists = newLists;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetListEntryOptions()
|
|
||||||
{
|
|
||||||
// aggregate downloads will be placed in subfolders by default
|
|
||||||
foreach (var tle in lists)
|
|
||||||
{
|
|
||||||
if (tle.source.Type == TrackType.Aggregate || tle.source.Type == TrackType.AlbumAggregate)
|
|
||||||
tle.defaultFolderName = Path.Join(tle.defaultFolderName, tle.source.ToString(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Track> Flattened(bool addSources, bool addSpecialSourceTracks, bool sourcesOnly = false)
|
|
||||||
{
|
|
||||||
foreach (var tle in lists)
|
|
||||||
{
|
|
||||||
if ((addSources || sourcesOnly) && tle.source != null && tle.source.Type != TrackType.Normal)
|
|
||||||
yield return tle.source;
|
|
||||||
if (!sourcesOnly && tle.list.Count > 0 && (tle.source.Type == TrackType.Normal || addSpecialSourceTracks))
|
|
||||||
{
|
|
||||||
foreach (var t in tle.list[0])
|
|
||||||
yield return t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TrackComparer : IEqualityComparer<Track>
|
|
||||||
{
|
|
||||||
private bool _ignoreCase = false;
|
|
||||||
private int _lenTol = -1;
|
|
||||||
public TrackComparer(bool ignoreCase = false, int lenTol = -1)
|
|
||||||
{
|
|
||||||
_ignoreCase = ignoreCase;
|
|
||||||
_lenTol = lenTol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Track a, Track b)
|
|
||||||
{
|
|
||||||
if (a.Equals(b))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var comparer = _ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
|
||||||
|
|
||||||
return string.Equals(a.Title, b.Title, comparer)
|
|
||||||
&& string.Equals(a.Artist, b.Artist, comparer)
|
|
||||||
&& string.Equals(a.Album, b.Album, comparer)
|
|
||||||
&& _lenTol == -1 || (a.Length == -1 && b.Length == -1) || (a.Length != -1 && b.Length != -1 && Math.Abs(a.Length - b.Length) <= _lenTol);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetHashCode(Track a)
|
|
||||||
{
|
|
||||||
unchecked
|
|
||||||
{
|
|
||||||
int hash = 17;
|
|
||||||
string trackTitle = _ignoreCase ? a.Title.ToLower() : a.Title;
|
|
||||||
string artistName = _ignoreCase ? a.Artist.ToLower() : a.Artist;
|
|
||||||
string album = _ignoreCase ? a.Album.ToLower() : a.Album;
|
|
||||||
|
|
||||||
hash = hash * 23 + trackTitle.GetHashCode();
|
|
||||||
hash = hash * 23 + artistName.GetHashCode();
|
|
||||||
hash = hash * 23 + album.GetHashCode();
|
|
||||||
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SimpleFile
|
|
||||||
{
|
|
||||||
public string Path;
|
|
||||||
public string? Artists;
|
|
||||||
public string? Title;
|
|
||||||
public string? Album;
|
|
||||||
public int Length;
|
|
||||||
public int Bitrate;
|
|
||||||
public int Samplerate;
|
|
||||||
public int Bitdepth;
|
|
||||||
|
|
||||||
public SimpleFile(TagLib.File file)
|
|
||||||
{
|
|
||||||
Path = file.Name;
|
|
||||||
Artists = file.Tag.JoinedPerformers;
|
|
||||||
Title = file.Tag.Title;
|
|
||||||
Album = file.Tag.Album;
|
|
||||||
Length = (int)file.Length;
|
|
||||||
Bitrate = file.Properties.AudioBitrate;
|
|
||||||
Samplerate = file.Properties.AudioSampleRate;
|
|
||||||
Bitdepth = file.Properties.BitsPerSample;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ResponseData
|
|
||||||
{
|
|
||||||
public int lockedFilesCount;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +1,12 @@
|
||||||
using Soulseek;
|
using Soulseek;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
|
||||||
using static Program;
|
using static Program;
|
||||||
|
|
||||||
using File = System.IO.File;
|
using File = System.IO.File;
|
||||||
using Directory = System.IO.Directory;
|
using Directory = System.IO.Directory;
|
||||||
using ProgressBar = Konsole.ProgressBar;
|
using ProgressBar = Konsole.ProgressBar;
|
||||||
using SearchResponse = Soulseek.SearchResponse;
|
using SearchResponse = Soulseek.SearchResponse;
|
||||||
using SlResponse = Soulseek.SearchResponse;
|
|
||||||
using SlFile = Soulseek.File;
|
|
||||||
using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string, (Soulseek.SearchResponse, Soulseek.File)>;
|
|
||||||
|
|
||||||
|
|
||||||
static class Download
|
static class Download
|
||||||
|
@ -106,134 +99,3 @@ static class Download
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DownloadWrapper
|
|
||||||
{
|
|
||||||
public string savePath;
|
|
||||||
public string displayText = "";
|
|
||||||
public int barState = 0;
|
|
||||||
public Soulseek.File file;
|
|
||||||
public Transfer? transfer;
|
|
||||||
public SearchResponse response;
|
|
||||||
public ProgressBar progress;
|
|
||||||
public Track track;
|
|
||||||
public long bytesTransferred = 0;
|
|
||||||
public bool stalled = false;
|
|
||||||
public bool queued = false;
|
|
||||||
public bool success = false;
|
|
||||||
public CancellationTokenSource cts;
|
|
||||||
public DateTime startTime = DateTime.Now;
|
|
||||||
public DateTime lastChangeTime = DateTime.Now;
|
|
||||||
|
|
||||||
TransferStates? prevTransferState = null;
|
|
||||||
long prevBytesTransferred = 0;
|
|
||||||
bool updatedTextDownload = false;
|
|
||||||
bool updatedTextSuccess = false;
|
|
||||||
readonly char[] bars = { '|', '/', '—', '\\' };
|
|
||||||
|
|
||||||
public DownloadWrapper(string savePath, SearchResponse response, Soulseek.File file, Track track, CancellationTokenSource cts, ProgressBar progress)
|
|
||||||
{
|
|
||||||
this.savePath = savePath;
|
|
||||||
this.response = response;
|
|
||||||
this.file = file;
|
|
||||||
this.cts = cts;
|
|
||||||
this.track = track;
|
|
||||||
this.progress = progress;
|
|
||||||
this.displayText = Printing.DisplayString(track, file, response);
|
|
||||||
|
|
||||||
Printing.RefreshOrPrint(progress, 0, "Initialize: " + displayText, true);
|
|
||||||
Printing.RefreshOrPrint(progress, 0, displayText, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateText()
|
|
||||||
{
|
|
||||||
float? percentage = bytesTransferred / (float)file.Size;
|
|
||||||
queued = (transfer?.State & TransferStates.Queued) != 0;
|
|
||||||
string bar;
|
|
||||||
string state;
|
|
||||||
bool downloading = false;
|
|
||||||
|
|
||||||
if (stalled)
|
|
||||||
{
|
|
||||||
state = "Stalled";
|
|
||||||
bar = " ";
|
|
||||||
}
|
|
||||||
else if (transfer != null)
|
|
||||||
{
|
|
||||||
if (queued)
|
|
||||||
{
|
|
||||||
if ((transfer.State & TransferStates.Remotely) != 0)
|
|
||||||
state = "Queued (R)";
|
|
||||||
else
|
|
||||||
state = "Queued (L)";
|
|
||||||
bar = " ";
|
|
||||||
}
|
|
||||||
else if ((transfer.State & TransferStates.Initializing) != 0)
|
|
||||||
{
|
|
||||||
state = "Initialize";
|
|
||||||
bar = " ";
|
|
||||||
}
|
|
||||||
else if ((transfer.State & TransferStates.Completed) != 0)
|
|
||||||
{
|
|
||||||
var flag = transfer.State & (TransferStates.Succeeded | TransferStates.Cancelled
|
|
||||||
| TransferStates.TimedOut | TransferStates.Errored | TransferStates.Rejected
|
|
||||||
| TransferStates.Aborted);
|
|
||||||
state = flag.ToString();
|
|
||||||
|
|
||||||
if (flag == TransferStates.Succeeded)
|
|
||||||
success = true;
|
|
||||||
|
|
||||||
bar = "";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
state = transfer.State.ToString();
|
|
||||||
if ((transfer.State & TransferStates.InProgress) != 0)
|
|
||||||
{
|
|
||||||
downloading = true;
|
|
||||||
barState = (barState + 1) % bars.Length;
|
|
||||||
bar = bars[barState] + " ";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bar = " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
state = "NullState";
|
|
||||||
bar = " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
string txt = $"{bar}{state}:".PadRight(14) + $" {displayText}";
|
|
||||||
bool needSimplePrintUpdate = (downloading && !updatedTextDownload) || (success && !updatedTextSuccess);
|
|
||||||
updatedTextDownload |= downloading;
|
|
||||||
updatedTextSuccess |= success;
|
|
||||||
|
|
||||||
Console.ResetColor();
|
|
||||||
Printing.RefreshOrPrint(progress, (int)((percentage ?? 0) * 100), txt, needSimplePrintUpdate, needSimplePrintUpdate);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public DateTime UpdateLastChangeTime(bool updateAllFromThisUser = true, bool forceChanged = false)
|
|
||||||
{
|
|
||||||
bool changed = prevTransferState != transfer?.State || prevBytesTransferred != bytesTransferred;
|
|
||||||
if (changed || forceChanged)
|
|
||||||
{
|
|
||||||
lastChangeTime = DateTime.Now;
|
|
||||||
stalled = false;
|
|
||||||
if (updateAllFromThisUser)
|
|
||||||
{
|
|
||||||
foreach (var (_, dl) in downloads)
|
|
||||||
{
|
|
||||||
if (dl != this && dl.response.Username == response.Username)
|
|
||||||
dl.UpdateLastChangeTime(updateAllFromThisUser: false, forceChanged: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prevTransferState = transfer?.State;
|
|
||||||
prevBytesTransferred = bytesTransferred;
|
|
||||||
return lastChangeTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
using Data;
|
using Models;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
using Enums;
|
using Enums;
|
||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Extractors
|
namespace Extractors
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
using System;
|
using System.Text;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
|
||||||
|
|
||||||
namespace Extractors
|
namespace Extractors
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,9 +2,8 @@
|
||||||
using SpotifyAPI.Web.Auth;
|
using SpotifyAPI.Web.Auth;
|
||||||
using Swan;
|
using Swan;
|
||||||
|
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
using Enums;
|
||||||
using System.Security;
|
|
||||||
|
|
||||||
namespace Extractors
|
namespace Extractors
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
using Enums;
|
||||||
|
|
||||||
namespace Extractors
|
namespace Extractors
|
||||||
|
|
|
@ -8,7 +8,7 @@ using System.Diagnostics;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
using Enums;
|
||||||
|
|
||||||
namespace Extractors
|
namespace Extractors
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
using Enums;
|
using Enums;
|
||||||
using Data;
|
using Models;
|
||||||
|
|
||||||
|
|
||||||
namespace Extractors
|
namespace Extractors
|
||||||
|
|
|
@ -1,353 +0,0 @@
|
||||||
|
|
||||||
using Data;
|
|
||||||
|
|
||||||
using SearchResponse = Soulseek.SearchResponse;
|
|
||||||
|
|
||||||
|
|
||||||
public class FileConditions
|
|
||||||
{
|
|
||||||
public int LengthTolerance = -1;
|
|
||||||
public int MinBitrate = -1;
|
|
||||||
public int MaxBitrate = -1;
|
|
||||||
public int MinSampleRate = -1;
|
|
||||||
public int MaxSampleRate = -1;
|
|
||||||
public int MinBitDepth = -1;
|
|
||||||
public int MaxBitDepth = -1;
|
|
||||||
public bool StrictTitle = false;
|
|
||||||
public bool StrictArtist = false;
|
|
||||||
public bool StrictAlbum = false;
|
|
||||||
public string[] Formats = Array.Empty<string>();
|
|
||||||
public string[] BannedUsers = Array.Empty<string>();
|
|
||||||
public bool StrictStringDiacrRemove = true;
|
|
||||||
public bool AcceptNoLength = true;
|
|
||||||
public bool AcceptMissingProps = true;
|
|
||||||
|
|
||||||
public FileConditions() { }
|
|
||||||
|
|
||||||
public FileConditions(FileConditions other)
|
|
||||||
{
|
|
||||||
LengthTolerance = other.LengthTolerance;
|
|
||||||
MinBitrate = other.MinBitrate;
|
|
||||||
MaxBitrate = other.MaxBitrate;
|
|
||||||
MinSampleRate = other.MinSampleRate;
|
|
||||||
MaxSampleRate = other.MaxSampleRate;
|
|
||||||
AcceptNoLength = other.AcceptNoLength;
|
|
||||||
StrictArtist = other.StrictArtist;
|
|
||||||
StrictTitle = other.StrictTitle;
|
|
||||||
StrictAlbum = other.StrictAlbum;
|
|
||||||
MinBitDepth = other.MinBitDepth;
|
|
||||||
MaxBitDepth = other.MaxBitDepth;
|
|
||||||
AcceptMissingProps = other.AcceptMissingProps;
|
|
||||||
StrictStringDiacrRemove = other.StrictStringDiacrRemove;
|
|
||||||
Formats = other.Formats.ToArray();
|
|
||||||
BannedUsers = other.BannedUsers.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public FileConditionsMod ApplyMod(FileConditionsMod mod)
|
|
||||||
{
|
|
||||||
var undoMod = new FileConditionsMod();
|
|
||||||
|
|
||||||
if (mod.LengthTolerance != null)
|
|
||||||
{
|
|
||||||
undoMod.LengthTolerance = LengthTolerance;
|
|
||||||
LengthTolerance = mod.LengthTolerance.Value;
|
|
||||||
}
|
|
||||||
if (mod.MinBitrate != null)
|
|
||||||
{
|
|
||||||
undoMod.MinBitrate = MinBitrate;
|
|
||||||
MinBitrate = mod.MinBitrate.Value;
|
|
||||||
}
|
|
||||||
if (mod.MaxBitrate != null)
|
|
||||||
{
|
|
||||||
undoMod.MaxBitrate = MaxBitrate;
|
|
||||||
MaxBitrate = mod.MaxBitrate.Value;
|
|
||||||
}
|
|
||||||
if (mod.MinSampleRate != null)
|
|
||||||
{
|
|
||||||
undoMod.MinSampleRate = MinSampleRate;
|
|
||||||
MinSampleRate = mod.MinSampleRate.Value;
|
|
||||||
}
|
|
||||||
if (mod.MaxSampleRate != null)
|
|
||||||
{
|
|
||||||
undoMod.MaxSampleRate = MaxSampleRate;
|
|
||||||
MaxSampleRate = mod.MaxSampleRate.Value;
|
|
||||||
}
|
|
||||||
if (mod.MinBitDepth != null)
|
|
||||||
{
|
|
||||||
undoMod.MinBitDepth = MinBitDepth;
|
|
||||||
MinBitDepth = mod.MinBitDepth.Value;
|
|
||||||
}
|
|
||||||
if (mod.MaxBitDepth != null)
|
|
||||||
{
|
|
||||||
undoMod.MaxBitDepth = MaxBitDepth;
|
|
||||||
MaxBitDepth = mod.MaxBitDepth.Value;
|
|
||||||
}
|
|
||||||
if (mod.StrictTitle != null)
|
|
||||||
{
|
|
||||||
undoMod.StrictTitle = StrictTitle;
|
|
||||||
StrictTitle = mod.StrictTitle.Value;
|
|
||||||
}
|
|
||||||
if (mod.StrictArtist != null)
|
|
||||||
{
|
|
||||||
undoMod.StrictArtist = StrictArtist;
|
|
||||||
StrictArtist = mod.StrictArtist.Value;
|
|
||||||
}
|
|
||||||
if (mod.StrictAlbum != null)
|
|
||||||
{
|
|
||||||
undoMod.StrictAlbum = StrictAlbum;
|
|
||||||
StrictAlbum = mod.StrictAlbum.Value;
|
|
||||||
}
|
|
||||||
if (mod.Formats != null)
|
|
||||||
{
|
|
||||||
undoMod.Formats = Formats;
|
|
||||||
Formats = mod.Formats;
|
|
||||||
}
|
|
||||||
if (mod.BannedUsers != null)
|
|
||||||
{
|
|
||||||
undoMod.BannedUsers = BannedUsers;
|
|
||||||
BannedUsers = mod.BannedUsers;
|
|
||||||
}
|
|
||||||
if (mod.StrictStringDiacrRemove != null)
|
|
||||||
{
|
|
||||||
undoMod.StrictStringDiacrRemove = StrictStringDiacrRemove;
|
|
||||||
StrictStringDiacrRemove = mod.StrictStringDiacrRemove.Value;
|
|
||||||
}
|
|
||||||
if (mod.AcceptNoLength != null)
|
|
||||||
{
|
|
||||||
undoMod.AcceptNoLength = AcceptNoLength;
|
|
||||||
AcceptNoLength = mod.AcceptNoLength.Value;
|
|
||||||
}
|
|
||||||
if (mod.AcceptMissingProps != null)
|
|
||||||
{
|
|
||||||
undoMod.AcceptMissingProps = AcceptMissingProps;
|
|
||||||
AcceptMissingProps = mod.AcceptMissingProps.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undoMod;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
if (obj is FileConditions other)
|
|
||||||
{
|
|
||||||
return LengthTolerance == other.LengthTolerance &&
|
|
||||||
MinBitrate == other.MinBitrate &&
|
|
||||||
MaxBitrate == other.MaxBitrate &&
|
|
||||||
MinSampleRate == other.MinSampleRate &&
|
|
||||||
MaxSampleRate == other.MaxSampleRate &&
|
|
||||||
MinBitDepth == other.MinBitDepth &&
|
|
||||||
MaxBitDepth == other.MaxBitDepth &&
|
|
||||||
StrictTitle == other.StrictTitle &&
|
|
||||||
StrictArtist == other.StrictArtist &&
|
|
||||||
StrictAlbum == other.StrictAlbum &&
|
|
||||||
StrictStringDiacrRemove == other.StrictStringDiacrRemove &&
|
|
||||||
AcceptNoLength == other.AcceptNoLength &&
|
|
||||||
AcceptMissingProps == other.AcceptMissingProps &&
|
|
||||||
Formats.SequenceEqual(other.Formats) &&
|
|
||||||
BannedUsers.SequenceEqual(other.BannedUsers);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnsetClientSpecificFields()
|
|
||||||
{
|
|
||||||
MinBitrate = -1;
|
|
||||||
MaxBitrate = -1;
|
|
||||||
MinSampleRate = -1;
|
|
||||||
MaxSampleRate = -1;
|
|
||||||
MinBitDepth = -1;
|
|
||||||
MaxBitDepth = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool FileSatisfies(Soulseek.File file, Track track, SearchResponse? response)
|
|
||||||
{
|
|
||||||
return FormatSatisfies(file.Filename)
|
|
||||||
&& LengthToleranceSatisfies(file, track.Length) && BitrateSatisfies(file) && SampleRateSatisfies(file)
|
|
||||||
&& StrictTitleSatisfies(file.Filename, track.Title) && StrictArtistSatisfies(file.Filename, track.Artist)
|
|
||||||
&& StrictAlbumSatisfies(file.Filename, track.Album) && BannedUsersSatisfies(response) && BitDepthSatisfies(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool FileSatisfies(TagLib.File file, Track track, bool filenameChecks = false)
|
|
||||||
{
|
|
||||||
return FormatSatisfies(file.Name)
|
|
||||||
&& LengthToleranceSatisfies(file, track.Length) && BitrateSatisfies(file) && SampleRateSatisfies(file)
|
|
||||||
&& BitDepthSatisfies(file) && (!filenameChecks || StrictTitleSatisfies(file.Name, track.Title)
|
|
||||||
&& StrictArtistSatisfies(file.Name, track.Artist) && StrictAlbumSatisfies(file.Name, track.Album));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool FileSatisfies(SimpleFile file, Track track, bool filenameChecks = false)
|
|
||||||
{
|
|
||||||
return FormatSatisfies(file.Path)
|
|
||||||
&& LengthToleranceSatisfies(file, track.Length) && BitrateSatisfies(file) && SampleRateSatisfies(file)
|
|
||||||
&& BitDepthSatisfies(file) && (!filenameChecks || StrictTitleSatisfies(file.Path, track.Title)
|
|
||||||
&& StrictArtistSatisfies(file.Path, track.Artist) && StrictAlbumSatisfies(file.Path, track.Album));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool StrictTitleSatisfies(string fname, string tname, bool noPath = true)
|
|
||||||
{
|
|
||||||
if (!StrictTitle || tname.Length == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
fname = noPath ? Utils.GetFileNameWithoutExtSlsk(fname) : fname;
|
|
||||||
return StrictString(fname, tname, StrictStringDiacrRemove, ignoreCase: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool StrictArtistSatisfies(string fname, string aname)
|
|
||||||
{
|
|
||||||
if (!StrictArtist || aname.Length == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return StrictString(fname, aname, StrictStringDiacrRemove, ignoreCase: true, boundarySkipWs: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool StrictAlbumSatisfies(string fname, string alname)
|
|
||||||
{
|
|
||||||
if (!StrictAlbum || alname.Length == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return StrictString(Utils.GetDirectoryNameSlsk(fname), alname, StrictStringDiacrRemove, ignoreCase: true, boundarySkipWs: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string StrictStringPreprocess(string str, bool diacrRemove = true)
|
|
||||||
{
|
|
||||||
str = str.Replace('_', ' ').ReplaceInvalidChars(' ', true, false);
|
|
||||||
str = diacrRemove ? str.RemoveDiacritics() : str;
|
|
||||||
str = str.Trim().RemoveConsecutiveWs();
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool StrictString(string fname, string tname, bool diacrRemove = true, bool ignoreCase = true, bool boundarySkipWs = true)
|
|
||||||
{
|
|
||||||
if (tname.Length == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
fname = StrictStringPreprocess(fname, diacrRemove);
|
|
||||||
tname = StrictStringPreprocess(tname, diacrRemove);
|
|
||||||
|
|
||||||
if (boundarySkipWs)
|
|
||||||
return fname.ContainsWithBoundaryIgnoreWs(tname, ignoreCase, acceptLeftDigit: true);
|
|
||||||
else
|
|
||||||
return fname.ContainsWithBoundary(tname, ignoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool BracketCheck(Track track, Track other)
|
|
||||||
{
|
|
||||||
string t1 = track.Title.RemoveFt().Replace('[', '(');
|
|
||||||
if (t1.Contains('('))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
string t2 = other.Title.RemoveFt().Replace('[', '(');
|
|
||||||
if (!t2.Contains('('))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool FormatSatisfies(string fname)
|
|
||||||
{
|
|
||||||
if (Formats.Length == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
string ext = Path.GetExtension(fname).TrimStart('.').ToLower();
|
|
||||||
return ext.Length > 0 && Formats.Any(f => f == ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool LengthToleranceSatisfies(Soulseek.File file, int wantedLength) => LengthToleranceSatisfies(file.Length, wantedLength);
|
|
||||||
public bool LengthToleranceSatisfies(TagLib.File file, int wantedLength) => LengthToleranceSatisfies((int)file.Properties.Duration.TotalSeconds, wantedLength);
|
|
||||||
public bool LengthToleranceSatisfies(SimpleFile file, int wantedLength) => LengthToleranceSatisfies(file.Length, wantedLength);
|
|
||||||
public bool LengthToleranceSatisfies(int? length, int wantedLength)
|
|
||||||
{
|
|
||||||
if (LengthTolerance < 0 || wantedLength < 0)
|
|
||||||
return true;
|
|
||||||
if (length == null || length < 0)
|
|
||||||
return AcceptNoLength && AcceptMissingProps;
|
|
||||||
return Math.Abs((int)length - wantedLength) <= LengthTolerance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool BitrateSatisfies(Soulseek.File file) => BitrateSatisfies(file.BitRate);
|
|
||||||
public bool BitrateSatisfies(TagLib.File file) => BitrateSatisfies(file.Properties.AudioBitrate);
|
|
||||||
public bool BitrateSatisfies(SimpleFile file) => BitrateSatisfies(file.Bitrate);
|
|
||||||
public bool BitrateSatisfies(int? bitrate)
|
|
||||||
{
|
|
||||||
return BoundCheck(bitrate, MinBitrate, MaxBitrate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool SampleRateSatisfies(Soulseek.File file) => SampleRateSatisfies(file.SampleRate);
|
|
||||||
public bool SampleRateSatisfies(TagLib.File file) => SampleRateSatisfies(file.Properties.AudioSampleRate);
|
|
||||||
public bool SampleRateSatisfies(SimpleFile file) => SampleRateSatisfies(file.Samplerate);
|
|
||||||
public bool SampleRateSatisfies(int? sampleRate)
|
|
||||||
{
|
|
||||||
return BoundCheck(sampleRate, MinSampleRate, MaxSampleRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool BitDepthSatisfies(Soulseek.File file) => BitDepthSatisfies(file.BitDepth);
|
|
||||||
public bool BitDepthSatisfies(TagLib.File file) => BitDepthSatisfies(file.Properties.BitsPerSample);
|
|
||||||
public bool BitDepthSatisfies(SimpleFile file) => BitDepthSatisfies(file.Bitdepth);
|
|
||||||
public bool BitDepthSatisfies(int? bitdepth)
|
|
||||||
{
|
|
||||||
return BoundCheck(bitdepth, MinBitDepth, MaxBitDepth);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool BoundCheck(int? num, int min, int max)
|
|
||||||
{
|
|
||||||
if (max < 0 && min < 0)
|
|
||||||
return true;
|
|
||||||
if (num == null || num < 0)
|
|
||||||
return AcceptMissingProps;
|
|
||||||
if (num < min || max != -1 && num > max)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool BannedUsersSatisfies(SearchResponse? response)
|
|
||||||
{
|
|
||||||
return response == null || !BannedUsers.Any(x => x == response.Username);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetNotSatisfiedName(Soulseek.File file, Track track, SearchResponse? response)
|
|
||||||
{
|
|
||||||
if (!BannedUsersSatisfies(response))
|
|
||||||
return "BannedUsers fails";
|
|
||||||
if (!StrictTitleSatisfies(file.Filename, track.Title))
|
|
||||||
return "StrictTitle fails";
|
|
||||||
if (track.Type == Enums.TrackType.Album && !StrictAlbumSatisfies(file.Filename, track.Artist))
|
|
||||||
return "StrictAlbum fails";
|
|
||||||
if (!StrictArtistSatisfies(file.Filename, track.Artist))
|
|
||||||
return "StrictArtist fails";
|
|
||||||
if (!LengthToleranceSatisfies(file, track.Length))
|
|
||||||
return "LengthTolerance fails";
|
|
||||||
if (!FormatSatisfies(file.Filename))
|
|
||||||
return "Format fails";
|
|
||||||
if (track.Type != Enums.TrackType.Album && !StrictAlbumSatisfies(file.Filename, track.Artist))
|
|
||||||
return "StrictAlbum fails";
|
|
||||||
if (!BitrateSatisfies(file))
|
|
||||||
return "Bitrate fails";
|
|
||||||
if (!SampleRateSatisfies(file))
|
|
||||||
return "SampleRate fails";
|
|
||||||
if (!BitDepthSatisfies(file))
|
|
||||||
return "BitDepth fails";
|
|
||||||
return "Satisfied";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class FileConditionsMod
|
|
||||||
{
|
|
||||||
public int? LengthTolerance = null;
|
|
||||||
public int? MinBitrate = null;
|
|
||||||
public int? MaxBitrate = null;
|
|
||||||
public int? MinSampleRate = null;
|
|
||||||
public int? MaxSampleRate = null;
|
|
||||||
public int? MinBitDepth = null;
|
|
||||||
public int? MaxBitDepth = null;
|
|
||||||
public bool? StrictTitle = null;
|
|
||||||
public bool? StrictArtist = null;
|
|
||||||
public bool? StrictAlbum = null;
|
|
||||||
public string[]? Formats = null;
|
|
||||||
public string[]? BannedUsers = null;
|
|
||||||
public bool? StrictStringDiacrRemove = null;
|
|
||||||
public bool? AcceptNoLength = null;
|
|
||||||
public bool? AcceptMissingProps = null;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
using System;
|
using System.Text.RegularExpressions;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
using Enums;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
|
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
using Enums;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace FileSkippers
|
namespace FileSkippers
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
using Enums;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
|
|
138
slsk-batchdl/Models/DownloadWrapper.cs
Normal file
138
slsk-batchdl/Models/DownloadWrapper.cs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
using Soulseek;
|
||||||
|
using static Program;
|
||||||
|
using ProgressBar = Konsole.ProgressBar;
|
||||||
|
using SearchResponse = Soulseek.SearchResponse;
|
||||||
|
|
||||||
|
namespace Models
|
||||||
|
{
|
||||||
|
public class DownloadWrapper
|
||||||
|
{
|
||||||
|
public string savePath;
|
||||||
|
public string displayText = "";
|
||||||
|
public int barState = 0;
|
||||||
|
public Soulseek.File file;
|
||||||
|
public Transfer? transfer;
|
||||||
|
public SearchResponse response;
|
||||||
|
public ProgressBar progress;
|
||||||
|
public Track track;
|
||||||
|
public long bytesTransferred = 0;
|
||||||
|
public bool stalled = false;
|
||||||
|
public bool queued = false;
|
||||||
|
public bool success = false;
|
||||||
|
public CancellationTokenSource cts;
|
||||||
|
public DateTime startTime = DateTime.Now;
|
||||||
|
public DateTime lastChangeTime = DateTime.Now;
|
||||||
|
|
||||||
|
TransferStates? prevTransferState = null;
|
||||||
|
long prevBytesTransferred = 0;
|
||||||
|
bool updatedTextDownload = false;
|
||||||
|
bool updatedTextSuccess = false;
|
||||||
|
readonly char[] bars = { '|', '/', '—', '\\' };
|
||||||
|
|
||||||
|
public DownloadWrapper(string savePath, SearchResponse response, Soulseek.File file, Track track, CancellationTokenSource cts, ProgressBar progress)
|
||||||
|
{
|
||||||
|
this.savePath = savePath;
|
||||||
|
this.response = response;
|
||||||
|
this.file = file;
|
||||||
|
this.cts = cts;
|
||||||
|
this.track = track;
|
||||||
|
this.progress = progress;
|
||||||
|
this.displayText = Printing.DisplayString(track, file, response);
|
||||||
|
|
||||||
|
Printing.RefreshOrPrint(progress, 0, "Initialize: " + displayText, true);
|
||||||
|
Printing.RefreshOrPrint(progress, 0, displayText, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateText()
|
||||||
|
{
|
||||||
|
float? percentage = bytesTransferred / (float)file.Size;
|
||||||
|
queued = (transfer?.State & TransferStates.Queued) != 0;
|
||||||
|
string bar;
|
||||||
|
string state;
|
||||||
|
bool downloading = false;
|
||||||
|
|
||||||
|
if (stalled)
|
||||||
|
{
|
||||||
|
state = "Stalled";
|
||||||
|
bar = " ";
|
||||||
|
}
|
||||||
|
else if (transfer != null)
|
||||||
|
{
|
||||||
|
if (queued)
|
||||||
|
{
|
||||||
|
if ((transfer.State & TransferStates.Remotely) != 0)
|
||||||
|
state = "Queued (R)";
|
||||||
|
else
|
||||||
|
state = "Queued (L)";
|
||||||
|
bar = " ";
|
||||||
|
}
|
||||||
|
else if ((transfer.State & TransferStates.Initializing) != 0)
|
||||||
|
{
|
||||||
|
state = "Initialize";
|
||||||
|
bar = " ";
|
||||||
|
}
|
||||||
|
else if ((transfer.State & TransferStates.Completed) != 0)
|
||||||
|
{
|
||||||
|
var flag = transfer.State & (TransferStates.Succeeded | TransferStates.Cancelled
|
||||||
|
| TransferStates.TimedOut | TransferStates.Errored | TransferStates.Rejected
|
||||||
|
| TransferStates.Aborted);
|
||||||
|
state = flag.ToString();
|
||||||
|
|
||||||
|
if (flag == TransferStates.Succeeded)
|
||||||
|
success = true;
|
||||||
|
|
||||||
|
bar = "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state = transfer.State.ToString();
|
||||||
|
if ((transfer.State & TransferStates.InProgress) != 0)
|
||||||
|
{
|
||||||
|
downloading = true;
|
||||||
|
barState = (barState + 1) % bars.Length;
|
||||||
|
bar = bars[barState] + " ";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bar = " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state = "NullState";
|
||||||
|
bar = " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
string txt = $"{bar}{state}:".PadRight(14) + $" {displayText}";
|
||||||
|
bool needSimplePrintUpdate = (downloading && !updatedTextDownload) || (success && !updatedTextSuccess);
|
||||||
|
updatedTextDownload |= downloading;
|
||||||
|
updatedTextSuccess |= success;
|
||||||
|
|
||||||
|
Console.ResetColor();
|
||||||
|
Printing.RefreshOrPrint(progress, (int)((percentage ?? 0) * 100), txt, needSimplePrintUpdate, needSimplePrintUpdate);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime UpdateLastChangeTime(bool updateAllFromThisUser = true, bool forceChanged = false)
|
||||||
|
{
|
||||||
|
bool changed = prevTransferState != transfer?.State || prevBytesTransferred != bytesTransferred;
|
||||||
|
if (changed || forceChanged)
|
||||||
|
{
|
||||||
|
lastChangeTime = DateTime.Now;
|
||||||
|
stalled = false;
|
||||||
|
if (updateAllFromThisUser)
|
||||||
|
{
|
||||||
|
foreach (var (_, dl) in downloads)
|
||||||
|
{
|
||||||
|
if (dl != this && dl.response.Username == response.Username)
|
||||||
|
dl.UpdateLastChangeTime(updateAllFromThisUser: false, forceChanged: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prevTransferState = transfer?.State;
|
||||||
|
prevBytesTransferred = bytesTransferred;
|
||||||
|
return lastChangeTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
353
slsk-batchdl/Models/FileConditions.cs
Normal file
353
slsk-batchdl/Models/FileConditions.cs
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
|
||||||
|
|
||||||
|
using SearchResponse = Soulseek.SearchResponse;
|
||||||
|
|
||||||
|
namespace Models
|
||||||
|
{
|
||||||
|
public class FileConditions
|
||||||
|
{
|
||||||
|
public int LengthTolerance = -1;
|
||||||
|
public int MinBitrate = -1;
|
||||||
|
public int MaxBitrate = -1;
|
||||||
|
public int MinSampleRate = -1;
|
||||||
|
public int MaxSampleRate = -1;
|
||||||
|
public int MinBitDepth = -1;
|
||||||
|
public int MaxBitDepth = -1;
|
||||||
|
public bool StrictTitle = false;
|
||||||
|
public bool StrictArtist = false;
|
||||||
|
public bool StrictAlbum = false;
|
||||||
|
public string[] Formats = Array.Empty<string>();
|
||||||
|
public string[] BannedUsers = Array.Empty<string>();
|
||||||
|
public bool StrictStringDiacrRemove = true;
|
||||||
|
public bool AcceptNoLength = true;
|
||||||
|
public bool AcceptMissingProps = true;
|
||||||
|
|
||||||
|
public FileConditions() { }
|
||||||
|
|
||||||
|
public FileConditions(FileConditions other)
|
||||||
|
{
|
||||||
|
LengthTolerance = other.LengthTolerance;
|
||||||
|
MinBitrate = other.MinBitrate;
|
||||||
|
MaxBitrate = other.MaxBitrate;
|
||||||
|
MinSampleRate = other.MinSampleRate;
|
||||||
|
MaxSampleRate = other.MaxSampleRate;
|
||||||
|
AcceptNoLength = other.AcceptNoLength;
|
||||||
|
StrictArtist = other.StrictArtist;
|
||||||
|
StrictTitle = other.StrictTitle;
|
||||||
|
StrictAlbum = other.StrictAlbum;
|
||||||
|
MinBitDepth = other.MinBitDepth;
|
||||||
|
MaxBitDepth = other.MaxBitDepth;
|
||||||
|
AcceptMissingProps = other.AcceptMissingProps;
|
||||||
|
StrictStringDiacrRemove = other.StrictStringDiacrRemove;
|
||||||
|
Formats = other.Formats.ToArray();
|
||||||
|
BannedUsers = other.BannedUsers.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileConditionsMod ApplyMod(FileConditionsMod mod)
|
||||||
|
{
|
||||||
|
var undoMod = new FileConditionsMod();
|
||||||
|
|
||||||
|
if (mod.LengthTolerance != null)
|
||||||
|
{
|
||||||
|
undoMod.LengthTolerance = LengthTolerance;
|
||||||
|
LengthTolerance = mod.LengthTolerance.Value;
|
||||||
|
}
|
||||||
|
if (mod.MinBitrate != null)
|
||||||
|
{
|
||||||
|
undoMod.MinBitrate = MinBitrate;
|
||||||
|
MinBitrate = mod.MinBitrate.Value;
|
||||||
|
}
|
||||||
|
if (mod.MaxBitrate != null)
|
||||||
|
{
|
||||||
|
undoMod.MaxBitrate = MaxBitrate;
|
||||||
|
MaxBitrate = mod.MaxBitrate.Value;
|
||||||
|
}
|
||||||
|
if (mod.MinSampleRate != null)
|
||||||
|
{
|
||||||
|
undoMod.MinSampleRate = MinSampleRate;
|
||||||
|
MinSampleRate = mod.MinSampleRate.Value;
|
||||||
|
}
|
||||||
|
if (mod.MaxSampleRate != null)
|
||||||
|
{
|
||||||
|
undoMod.MaxSampleRate = MaxSampleRate;
|
||||||
|
MaxSampleRate = mod.MaxSampleRate.Value;
|
||||||
|
}
|
||||||
|
if (mod.MinBitDepth != null)
|
||||||
|
{
|
||||||
|
undoMod.MinBitDepth = MinBitDepth;
|
||||||
|
MinBitDepth = mod.MinBitDepth.Value;
|
||||||
|
}
|
||||||
|
if (mod.MaxBitDepth != null)
|
||||||
|
{
|
||||||
|
undoMod.MaxBitDepth = MaxBitDepth;
|
||||||
|
MaxBitDepth = mod.MaxBitDepth.Value;
|
||||||
|
}
|
||||||
|
if (mod.StrictTitle != null)
|
||||||
|
{
|
||||||
|
undoMod.StrictTitle = StrictTitle;
|
||||||
|
StrictTitle = mod.StrictTitle.Value;
|
||||||
|
}
|
||||||
|
if (mod.StrictArtist != null)
|
||||||
|
{
|
||||||
|
undoMod.StrictArtist = StrictArtist;
|
||||||
|
StrictArtist = mod.StrictArtist.Value;
|
||||||
|
}
|
||||||
|
if (mod.StrictAlbum != null)
|
||||||
|
{
|
||||||
|
undoMod.StrictAlbum = StrictAlbum;
|
||||||
|
StrictAlbum = mod.StrictAlbum.Value;
|
||||||
|
}
|
||||||
|
if (mod.Formats != null)
|
||||||
|
{
|
||||||
|
undoMod.Formats = Formats;
|
||||||
|
Formats = mod.Formats;
|
||||||
|
}
|
||||||
|
if (mod.BannedUsers != null)
|
||||||
|
{
|
||||||
|
undoMod.BannedUsers = BannedUsers;
|
||||||
|
BannedUsers = mod.BannedUsers;
|
||||||
|
}
|
||||||
|
if (mod.StrictStringDiacrRemove != null)
|
||||||
|
{
|
||||||
|
undoMod.StrictStringDiacrRemove = StrictStringDiacrRemove;
|
||||||
|
StrictStringDiacrRemove = mod.StrictStringDiacrRemove.Value;
|
||||||
|
}
|
||||||
|
if (mod.AcceptNoLength != null)
|
||||||
|
{
|
||||||
|
undoMod.AcceptNoLength = AcceptNoLength;
|
||||||
|
AcceptNoLength = mod.AcceptNoLength.Value;
|
||||||
|
}
|
||||||
|
if (mod.AcceptMissingProps != null)
|
||||||
|
{
|
||||||
|
undoMod.AcceptMissingProps = AcceptMissingProps;
|
||||||
|
AcceptMissingProps = mod.AcceptMissingProps.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undoMod;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (obj is FileConditions other)
|
||||||
|
{
|
||||||
|
return LengthTolerance == other.LengthTolerance &&
|
||||||
|
MinBitrate == other.MinBitrate &&
|
||||||
|
MaxBitrate == other.MaxBitrate &&
|
||||||
|
MinSampleRate == other.MinSampleRate &&
|
||||||
|
MaxSampleRate == other.MaxSampleRate &&
|
||||||
|
MinBitDepth == other.MinBitDepth &&
|
||||||
|
MaxBitDepth == other.MaxBitDepth &&
|
||||||
|
StrictTitle == other.StrictTitle &&
|
||||||
|
StrictArtist == other.StrictArtist &&
|
||||||
|
StrictAlbum == other.StrictAlbum &&
|
||||||
|
StrictStringDiacrRemove == other.StrictStringDiacrRemove &&
|
||||||
|
AcceptNoLength == other.AcceptNoLength &&
|
||||||
|
AcceptMissingProps == other.AcceptMissingProps &&
|
||||||
|
Formats.SequenceEqual(other.Formats) &&
|
||||||
|
BannedUsers.SequenceEqual(other.BannedUsers);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnsetClientSpecificFields()
|
||||||
|
{
|
||||||
|
MinBitrate = -1;
|
||||||
|
MaxBitrate = -1;
|
||||||
|
MinSampleRate = -1;
|
||||||
|
MaxSampleRate = -1;
|
||||||
|
MinBitDepth = -1;
|
||||||
|
MaxBitDepth = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FileSatisfies(Soulseek.File file, Track track, SearchResponse? response)
|
||||||
|
{
|
||||||
|
return FormatSatisfies(file.Filename)
|
||||||
|
&& LengthToleranceSatisfies(file, track.Length) && BitrateSatisfies(file) && SampleRateSatisfies(file)
|
||||||
|
&& StrictTitleSatisfies(file.Filename, track.Title) && StrictArtistSatisfies(file.Filename, track.Artist)
|
||||||
|
&& StrictAlbumSatisfies(file.Filename, track.Album) && BannedUsersSatisfies(response) && BitDepthSatisfies(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FileSatisfies(TagLib.File file, Track track, bool filenameChecks = false)
|
||||||
|
{
|
||||||
|
return FormatSatisfies(file.Name)
|
||||||
|
&& LengthToleranceSatisfies(file, track.Length) && BitrateSatisfies(file) && SampleRateSatisfies(file)
|
||||||
|
&& BitDepthSatisfies(file) && (!filenameChecks || StrictTitleSatisfies(file.Name, track.Title)
|
||||||
|
&& StrictArtistSatisfies(file.Name, track.Artist) && StrictAlbumSatisfies(file.Name, track.Album));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FileSatisfies(SimpleFile file, Track track, bool filenameChecks = false)
|
||||||
|
{
|
||||||
|
return FormatSatisfies(file.Path)
|
||||||
|
&& LengthToleranceSatisfies(file, track.Length) && BitrateSatisfies(file) && SampleRateSatisfies(file)
|
||||||
|
&& BitDepthSatisfies(file) && (!filenameChecks || StrictTitleSatisfies(file.Path, track.Title)
|
||||||
|
&& StrictArtistSatisfies(file.Path, track.Artist) && StrictAlbumSatisfies(file.Path, track.Album));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StrictTitleSatisfies(string fname, string tname, bool noPath = true)
|
||||||
|
{
|
||||||
|
if (!StrictTitle || tname.Length == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
fname = noPath ? Utils.GetFileNameWithoutExtSlsk(fname) : fname;
|
||||||
|
return StrictString(fname, tname, StrictStringDiacrRemove, ignoreCase: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StrictArtistSatisfies(string fname, string aname)
|
||||||
|
{
|
||||||
|
if (!StrictArtist || aname.Length == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return StrictString(fname, aname, StrictStringDiacrRemove, ignoreCase: true, boundarySkipWs: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StrictAlbumSatisfies(string fname, string alname)
|
||||||
|
{
|
||||||
|
if (!StrictAlbum || alname.Length == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return StrictString(Utils.GetDirectoryNameSlsk(fname), alname, StrictStringDiacrRemove, ignoreCase: true, boundarySkipWs: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string StrictStringPreprocess(string str, bool diacrRemove = true)
|
||||||
|
{
|
||||||
|
str = str.Replace('_', ' ').ReplaceInvalidChars(' ', true, false);
|
||||||
|
str = diacrRemove ? str.RemoveDiacritics() : str;
|
||||||
|
str = str.Trim().RemoveConsecutiveWs();
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool StrictString(string fname, string tname, bool diacrRemove = true, bool ignoreCase = true, bool boundarySkipWs = true)
|
||||||
|
{
|
||||||
|
if (tname.Length == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
fname = StrictStringPreprocess(fname, diacrRemove);
|
||||||
|
tname = StrictStringPreprocess(tname, diacrRemove);
|
||||||
|
|
||||||
|
if (boundarySkipWs)
|
||||||
|
return fname.ContainsWithBoundaryIgnoreWs(tname, ignoreCase, acceptLeftDigit: true);
|
||||||
|
else
|
||||||
|
return fname.ContainsWithBoundary(tname, ignoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool BracketCheck(Track track, Track other)
|
||||||
|
{
|
||||||
|
string t1 = track.Title.RemoveFt().Replace('[', '(');
|
||||||
|
if (t1.Contains('('))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
string t2 = other.Title.RemoveFt().Replace('[', '(');
|
||||||
|
if (!t2.Contains('('))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool FormatSatisfies(string fname)
|
||||||
|
{
|
||||||
|
if (Formats.Length == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
string ext = Path.GetExtension(fname).TrimStart('.').ToLower();
|
||||||
|
return ext.Length > 0 && Formats.Any(f => f == ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool LengthToleranceSatisfies(Soulseek.File file, int wantedLength) => LengthToleranceSatisfies(file.Length, wantedLength);
|
||||||
|
public bool LengthToleranceSatisfies(TagLib.File file, int wantedLength) => LengthToleranceSatisfies((int)file.Properties.Duration.TotalSeconds, wantedLength);
|
||||||
|
public bool LengthToleranceSatisfies(SimpleFile file, int wantedLength) => LengthToleranceSatisfies(file.Length, wantedLength);
|
||||||
|
public bool LengthToleranceSatisfies(int? length, int wantedLength)
|
||||||
|
{
|
||||||
|
if (LengthTolerance < 0 || wantedLength < 0)
|
||||||
|
return true;
|
||||||
|
if (length == null || length < 0)
|
||||||
|
return AcceptNoLength && AcceptMissingProps;
|
||||||
|
return Math.Abs((int)length - wantedLength) <= LengthTolerance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BitrateSatisfies(Soulseek.File file) => BitrateSatisfies(file.BitRate);
|
||||||
|
public bool BitrateSatisfies(TagLib.File file) => BitrateSatisfies(file.Properties.AudioBitrate);
|
||||||
|
public bool BitrateSatisfies(SimpleFile file) => BitrateSatisfies(file.Bitrate);
|
||||||
|
public bool BitrateSatisfies(int? bitrate)
|
||||||
|
{
|
||||||
|
return BoundCheck(bitrate, MinBitrate, MaxBitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SampleRateSatisfies(Soulseek.File file) => SampleRateSatisfies(file.SampleRate);
|
||||||
|
public bool SampleRateSatisfies(TagLib.File file) => SampleRateSatisfies(file.Properties.AudioSampleRate);
|
||||||
|
public bool SampleRateSatisfies(SimpleFile file) => SampleRateSatisfies(file.Samplerate);
|
||||||
|
public bool SampleRateSatisfies(int? sampleRate)
|
||||||
|
{
|
||||||
|
return BoundCheck(sampleRate, MinSampleRate, MaxSampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BitDepthSatisfies(Soulseek.File file) => BitDepthSatisfies(file.BitDepth);
|
||||||
|
public bool BitDepthSatisfies(TagLib.File file) => BitDepthSatisfies(file.Properties.BitsPerSample);
|
||||||
|
public bool BitDepthSatisfies(SimpleFile file) => BitDepthSatisfies(file.Bitdepth);
|
||||||
|
public bool BitDepthSatisfies(int? bitdepth)
|
||||||
|
{
|
||||||
|
return BoundCheck(bitdepth, MinBitDepth, MaxBitDepth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BoundCheck(int? num, int min, int max)
|
||||||
|
{
|
||||||
|
if (max < 0 && min < 0)
|
||||||
|
return true;
|
||||||
|
if (num == null || num < 0)
|
||||||
|
return AcceptMissingProps;
|
||||||
|
if (num < min || max != -1 && num > max)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BannedUsersSatisfies(SearchResponse? response)
|
||||||
|
{
|
||||||
|
return response == null || !BannedUsers.Any(x => x == response.Username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetNotSatisfiedName(Soulseek.File file, Track track, SearchResponse? response)
|
||||||
|
{
|
||||||
|
if (!BannedUsersSatisfies(response))
|
||||||
|
return "BannedUsers fails";
|
||||||
|
if (!StrictTitleSatisfies(file.Filename, track.Title))
|
||||||
|
return "StrictTitle fails";
|
||||||
|
if (track.Type == Enums.TrackType.Album && !StrictAlbumSatisfies(file.Filename, track.Artist))
|
||||||
|
return "StrictAlbum fails";
|
||||||
|
if (!StrictArtistSatisfies(file.Filename, track.Artist))
|
||||||
|
return "StrictArtist fails";
|
||||||
|
if (!LengthToleranceSatisfies(file, track.Length))
|
||||||
|
return "LengthTolerance fails";
|
||||||
|
if (!FormatSatisfies(file.Filename))
|
||||||
|
return "Format fails";
|
||||||
|
if (track.Type != Enums.TrackType.Album && !StrictAlbumSatisfies(file.Filename, track.Artist))
|
||||||
|
return "StrictAlbum fails";
|
||||||
|
if (!BitrateSatisfies(file))
|
||||||
|
return "Bitrate fails";
|
||||||
|
if (!SampleRateSatisfies(file))
|
||||||
|
return "SampleRate fails";
|
||||||
|
if (!BitDepthSatisfies(file))
|
||||||
|
return "BitDepth fails";
|
||||||
|
return "Satisfied";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class FileConditionsMod
|
||||||
|
{
|
||||||
|
public int? LengthTolerance = null;
|
||||||
|
public int? MinBitrate = null;
|
||||||
|
public int? MaxBitrate = null;
|
||||||
|
public int? MinSampleRate = null;
|
||||||
|
public int? MaxSampleRate = null;
|
||||||
|
public int? MinBitDepth = null;
|
||||||
|
public int? MaxBitDepth = null;
|
||||||
|
public bool? StrictTitle = null;
|
||||||
|
public bool? StrictArtist = null;
|
||||||
|
public bool? StrictAlbum = null;
|
||||||
|
public string[]? Formats = null;
|
||||||
|
public string[]? BannedUsers = null;
|
||||||
|
public bool? StrictStringDiacrRemove = null;
|
||||||
|
public bool? AcceptNoLength = null;
|
||||||
|
public bool? AcceptMissingProps = null;
|
||||||
|
}
|
||||||
|
}
|
7
slsk-batchdl/Models/ResponseData.cs
Normal file
7
slsk-batchdl/Models/ResponseData.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Models
|
||||||
|
{
|
||||||
|
public class ResponseData
|
||||||
|
{
|
||||||
|
public int lockedFilesCount;
|
||||||
|
}
|
||||||
|
}
|
20
slsk-batchdl/Models/SearchInfo.cs
Normal file
20
slsk-batchdl/Models/SearchInfo.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using ProgressBar = Konsole.ProgressBar;
|
||||||
|
using SearchResponse = Soulseek.SearchResponse;
|
||||||
|
|
||||||
|
namespace Models
|
||||||
|
{
|
||||||
|
public class SearchInfo
|
||||||
|
{
|
||||||
|
public ConcurrentDictionary<string, (SearchResponse, Soulseek.File)> results;
|
||||||
|
public ProgressBar progress;
|
||||||
|
|
||||||
|
public SearchInfo(ConcurrentDictionary<string, (SearchResponse, Soulseek.File)> results, ProgressBar progress)
|
||||||
|
{
|
||||||
|
this.results = results;
|
||||||
|
this.progress = progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
26
slsk-batchdl/Models/SimpleFile.cs
Normal file
26
slsk-batchdl/Models/SimpleFile.cs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
namespace Models
|
||||||
|
{
|
||||||
|
public class SimpleFile
|
||||||
|
{
|
||||||
|
public string Path;
|
||||||
|
public string? Artists;
|
||||||
|
public string? Title;
|
||||||
|
public string? Album;
|
||||||
|
public int Length;
|
||||||
|
public int Bitrate;
|
||||||
|
public int Samplerate;
|
||||||
|
public int Bitdepth;
|
||||||
|
|
||||||
|
public SimpleFile(TagLib.File file)
|
||||||
|
{
|
||||||
|
Path = file.Name;
|
||||||
|
Artists = file.Tag.JoinedPerformers;
|
||||||
|
Title = file.Tag.Title;
|
||||||
|
Album = file.Tag.Album;
|
||||||
|
Length = (int)file.Length;
|
||||||
|
Bitrate = file.Properties.AudioBitrate;
|
||||||
|
Samplerate = file.Properties.AudioSampleRate;
|
||||||
|
Bitdepth = file.Properties.BitsPerSample;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
150
slsk-batchdl/Models/Track.cs
Normal file
150
slsk-batchdl/Models/Track.cs
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
using Enums;
|
||||||
|
using Soulseek;
|
||||||
|
|
||||||
|
namespace Models
|
||||||
|
{
|
||||||
|
public class Track
|
||||||
|
{
|
||||||
|
public string Title = "";
|
||||||
|
public string Artist = "";
|
||||||
|
public string Album = "";
|
||||||
|
public string URI = "";
|
||||||
|
public int Length = -1;
|
||||||
|
public bool ArtistMaybeWrong = false;
|
||||||
|
public int MinAlbumTrackCount = -1;
|
||||||
|
public int MaxAlbumTrackCount = -1;
|
||||||
|
public bool IsNotAudio = false;
|
||||||
|
public string DownloadPath = "";
|
||||||
|
public string Other = "";
|
||||||
|
public int CsvRow = -1;
|
||||||
|
public TrackType Type = TrackType.Normal;
|
||||||
|
public FailureReason FailureReason = FailureReason.None;
|
||||||
|
public TrackState State = TrackState.Initial;
|
||||||
|
public List<(SearchResponse, Soulseek.File)>? Downloads = null;
|
||||||
|
|
||||||
|
public bool OutputsDirectory => Type != TrackType.Normal;
|
||||||
|
public Soulseek.File? FirstDownload => Downloads?.FirstOrDefault().Item2;
|
||||||
|
public SearchResponse? FirstResponse => Downloads?.FirstOrDefault().Item1;
|
||||||
|
public string? FirstUsername => Downloads?.FirstOrDefault().Item1?.Username;
|
||||||
|
|
||||||
|
public Track() { }
|
||||||
|
|
||||||
|
public Track(Track other)
|
||||||
|
{
|
||||||
|
Title = other.Title;
|
||||||
|
Artist = other.Artist;
|
||||||
|
Album = other.Album;
|
||||||
|
Length = other.Length;
|
||||||
|
URI = other.URI;
|
||||||
|
ArtistMaybeWrong = other.ArtistMaybeWrong;
|
||||||
|
Downloads = other.Downloads;
|
||||||
|
Type = other.Type;
|
||||||
|
IsNotAudio = other.IsNotAudio;
|
||||||
|
State = other.State;
|
||||||
|
FailureReason = other.FailureReason;
|
||||||
|
DownloadPath = other.DownloadPath;
|
||||||
|
Other = other.Other;
|
||||||
|
MinAlbumTrackCount = other.MinAlbumTrackCount;
|
||||||
|
MaxAlbumTrackCount = other.MaxAlbumTrackCount;
|
||||||
|
//CsvRow = other.CsvRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToKey()
|
||||||
|
{
|
||||||
|
if (Type == TrackType.Album)
|
||||||
|
return $"{Artist};{Album};{(int)Type}";
|
||||||
|
else
|
||||||
|
return $"{Artist};{Album};{Title};{Length};{(int)Type}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return ToString(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToString(bool noInfo = false)
|
||||||
|
{
|
||||||
|
if (IsNotAudio && Downloads != null && Downloads.Count > 0)
|
||||||
|
return $"{Utils.GetFileNameSlsk(Downloads[0].Item2.Filename)}";
|
||||||
|
|
||||||
|
string str = Artist;
|
||||||
|
if (Type == TrackType.Normal && Title.Length == 0 && Downloads != null && Downloads.Count > 0)
|
||||||
|
{
|
||||||
|
str = $"{Utils.GetFileNameSlsk(Downloads[0].Item2.Filename)}";
|
||||||
|
}
|
||||||
|
else if (Title.Length > 0 || Album.Length > 0)
|
||||||
|
{
|
||||||
|
if (str.Length > 0)
|
||||||
|
str += " - ";
|
||||||
|
if (Type == TrackType.Album)
|
||||||
|
{
|
||||||
|
if (Album.Length > 0)
|
||||||
|
str += Album;
|
||||||
|
else
|
||||||
|
str += Title;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (Title.Length > 0)
|
||||||
|
str += Title;
|
||||||
|
else
|
||||||
|
str += Album;
|
||||||
|
}
|
||||||
|
if (!noInfo)
|
||||||
|
{
|
||||||
|
if (Length > 0)
|
||||||
|
str += $" ({Length}s)";
|
||||||
|
if (Type == TrackType.Album)
|
||||||
|
str += " (album)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!noInfo)
|
||||||
|
{
|
||||||
|
str += " (artist)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TrackComparer : IEqualityComparer<Track>
|
||||||
|
{
|
||||||
|
private bool _ignoreCase = false;
|
||||||
|
private int _lenTol = -1;
|
||||||
|
public TrackComparer(bool ignoreCase = false, int lenTol = -1)
|
||||||
|
{
|
||||||
|
_ignoreCase = ignoreCase;
|
||||||
|
_lenTol = lenTol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(Track a, Track b)
|
||||||
|
{
|
||||||
|
if (a.Equals(b))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var comparer = _ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||||
|
|
||||||
|
return string.Equals(a.Title, b.Title, comparer)
|
||||||
|
&& string.Equals(a.Artist, b.Artist, comparer)
|
||||||
|
&& string.Equals(a.Album, b.Album, comparer)
|
||||||
|
&& _lenTol == -1 || (a.Length == -1 && b.Length == -1) || (a.Length != -1 && b.Length != -1 && Math.Abs(a.Length - b.Length) <= _lenTol);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(Track a)
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
int hash = 17;
|
||||||
|
string trackTitle = _ignoreCase ? a.Title.ToLower() : a.Title;
|
||||||
|
string artistName = _ignoreCase ? a.Artist.ToLower() : a.Artist;
|
||||||
|
string album = _ignoreCase ? a.Album.ToLower() : a.Album;
|
||||||
|
|
||||||
|
hash = hash * 23 + trackTitle.GetHashCode();
|
||||||
|
hash = hash * 23 + artistName.GetHashCode();
|
||||||
|
hash = hash * 23 + album.GetHashCode();
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
70
slsk-batchdl/Models/TrackListEntry.cs
Normal file
70
slsk-batchdl/Models/TrackListEntry.cs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
using Enums;
|
||||||
|
|
||||||
|
namespace Models
|
||||||
|
{
|
||||||
|
public class TrackListEntry
|
||||||
|
{
|
||||||
|
public List<List<Track>>? list;
|
||||||
|
public Track source;
|
||||||
|
public bool needSourceSearch = false;
|
||||||
|
public bool sourceCanBeSkipped = false;
|
||||||
|
public bool needSkipExistingAfterSearch = false;
|
||||||
|
public bool gotoNextAfterSearch = false;
|
||||||
|
public string? defaultFolderName = null;
|
||||||
|
public FileConditionsMod? additionalConds = null;
|
||||||
|
public FileConditionsMod? additionalPrefConds = null;
|
||||||
|
|
||||||
|
public TrackListEntry(TrackType trackType)
|
||||||
|
{
|
||||||
|
list = new List<List<Track>>();
|
||||||
|
this.source = new Track() { Type = trackType };
|
||||||
|
SetDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackListEntry(Track source)
|
||||||
|
{
|
||||||
|
list = new List<List<Track>>();
|
||||||
|
this.source = source;
|
||||||
|
SetDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackListEntry(List<List<Track>> list, Track source)
|
||||||
|
{
|
||||||
|
this.list = list;
|
||||||
|
this.source = source;
|
||||||
|
SetDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackListEntry(List<List<Track>> list, Track source, bool needSourceSearch = false, bool sourceCanBeSkipped = false,
|
||||||
|
bool needSkipExistingAfterSearch = false, bool gotoNextAfterSearch = false, string? defaultFoldername = null)
|
||||||
|
{
|
||||||
|
this.list = list;
|
||||||
|
this.source = source;
|
||||||
|
this.needSourceSearch = needSourceSearch;
|
||||||
|
this.sourceCanBeSkipped = sourceCanBeSkipped;
|
||||||
|
this.needSkipExistingAfterSearch = needSkipExistingAfterSearch;
|
||||||
|
this.gotoNextAfterSearch = gotoNextAfterSearch;
|
||||||
|
this.defaultFolderName = defaultFoldername;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDefaults()
|
||||||
|
{
|
||||||
|
needSourceSearch = source.Type != TrackType.Normal;
|
||||||
|
needSkipExistingAfterSearch = source.Type == TrackType.Aggregate;
|
||||||
|
gotoNextAfterSearch = source.Type == TrackType.AlbumAggregate;
|
||||||
|
sourceCanBeSkipped = source.Type != TrackType.Normal
|
||||||
|
&& source.Type != TrackType.Aggregate
|
||||||
|
&& source.Type != TrackType.AlbumAggregate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTrack(Track track)
|
||||||
|
{
|
||||||
|
if (list == null)
|
||||||
|
list = new List<List<Track>>() { new List<Track>() { track } };
|
||||||
|
else if (list.Count == 0)
|
||||||
|
list.Add(new List<Track>() { track });
|
||||||
|
else
|
||||||
|
list[0].Add(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
162
slsk-batchdl/Models/TrackLists.cs
Normal file
162
slsk-batchdl/Models/TrackLists.cs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
using Enums;
|
||||||
|
|
||||||
|
namespace Models
|
||||||
|
{
|
||||||
|
public class TrackLists
|
||||||
|
{
|
||||||
|
public List<TrackListEntry> lists = new();
|
||||||
|
|
||||||
|
public TrackLists() { }
|
||||||
|
|
||||||
|
public static TrackLists FromFlattened(IEnumerable<Track> flatList)
|
||||||
|
{
|
||||||
|
var res = new TrackLists();
|
||||||
|
using var enumerator = flatList.GetEnumerator();
|
||||||
|
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
var track = enumerator.Current;
|
||||||
|
|
||||||
|
if (track.Type != TrackType.Normal)
|
||||||
|
{
|
||||||
|
res.AddEntry(new TrackListEntry(track));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
res.AddEntry(new TrackListEntry(TrackType.Normal));
|
||||||
|
res.AddTrackToLast(track);
|
||||||
|
|
||||||
|
bool hasNext;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
hasNext = enumerator.MoveNext();
|
||||||
|
if (!hasNext || enumerator.Current.Type != TrackType.Normal)
|
||||||
|
break;
|
||||||
|
res.AddTrackToLast(enumerator.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNext)
|
||||||
|
res.AddEntry(new TrackListEntry(enumerator.Current));
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackListEntry this[int index]
|
||||||
|
{
|
||||||
|
get { return lists[index]; }
|
||||||
|
set { lists[index] = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEntry(TrackListEntry tle)
|
||||||
|
{
|
||||||
|
lists.Add(tle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTrackToLast(Track track)
|
||||||
|
{
|
||||||
|
if (lists.Count == 0)
|
||||||
|
{
|
||||||
|
AddEntry(new TrackListEntry(new List<List<Track>> { new List<Track>() { track } }, new Track()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = lists.Count - 1;
|
||||||
|
|
||||||
|
if (lists[i].list.Count == 0)
|
||||||
|
{
|
||||||
|
lists[i].list.Add(new List<Track>() { track });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int j = lists[i].list.Count - 1;
|
||||||
|
lists[i].list[j].Add(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reverse()
|
||||||
|
{
|
||||||
|
lists.Reverse();
|
||||||
|
foreach (var tle in lists)
|
||||||
|
{
|
||||||
|
foreach (var ls in tle.list)
|
||||||
|
{
|
||||||
|
ls.Reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpgradeListTypes(bool aggregate, bool album)
|
||||||
|
{
|
||||||
|
if (!aggregate && !album)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var newLists = new List<TrackListEntry>();
|
||||||
|
|
||||||
|
for (int i = 0; i < lists.Count; i++)
|
||||||
|
{
|
||||||
|
var tle = lists[i];
|
||||||
|
|
||||||
|
if (tle.source.Type == TrackType.Album && aggregate)
|
||||||
|
{
|
||||||
|
tle.source.Type = TrackType.AlbumAggregate;
|
||||||
|
tle.SetDefaults();
|
||||||
|
newLists.Add(tle);
|
||||||
|
}
|
||||||
|
else if (tle.source.Type == TrackType.Aggregate && album)
|
||||||
|
{
|
||||||
|
tle.source.Type = TrackType.AlbumAggregate;
|
||||||
|
tle.SetDefaults();
|
||||||
|
newLists.Add(tle);
|
||||||
|
}
|
||||||
|
else if (tle.source.Type == TrackType.Normal && (album || aggregate))
|
||||||
|
{
|
||||||
|
foreach (var track in tle.list[0])
|
||||||
|
{
|
||||||
|
if (album && aggregate)
|
||||||
|
track.Type = TrackType.AlbumAggregate;
|
||||||
|
else if (album)
|
||||||
|
track.Type = TrackType.Album;
|
||||||
|
else if (aggregate)
|
||||||
|
track.Type = TrackType.Aggregate;
|
||||||
|
|
||||||
|
var newTle = new TrackListEntry(track);
|
||||||
|
newTle.defaultFolderName = tle.defaultFolderName;
|
||||||
|
newLists.Add(newTle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newLists.Add(tle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lists = newLists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetListEntryOptions()
|
||||||
|
{
|
||||||
|
// aggregate downloads will be placed in subfolders by default
|
||||||
|
foreach (var tle in lists)
|
||||||
|
{
|
||||||
|
if (tle.source.Type == TrackType.Aggregate || tle.source.Type == TrackType.AlbumAggregate)
|
||||||
|
tle.defaultFolderName = Path.Join(tle.defaultFolderName, tle.source.ToString(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Track> Flattened(bool addSources, bool addSpecialSourceTracks, bool sourcesOnly = false)
|
||||||
|
{
|
||||||
|
foreach (var tle in lists)
|
||||||
|
{
|
||||||
|
if ((addSources || sourcesOnly) && tle.source != null && tle.source.Type != TrackType.Normal)
|
||||||
|
yield return tle.source;
|
||||||
|
if (!sourcesOnly && tle.list.Count > 0 && (tle.source.Type == TrackType.Normal || addSpecialSourceTracks))
|
||||||
|
{
|
||||||
|
foreach (var t in tle.list[0])
|
||||||
|
yield return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
using AngleSharp.Text;
|
using AngleSharp.Text;
|
||||||
using Konsole;
|
|
||||||
using Soulseek;
|
using Soulseek;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
@ -7,7 +6,7 @@ using System.Diagnostics;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
using Enums;
|
||||||
using FileSkippers;
|
using FileSkippers;
|
||||||
using Extractors;
|
using Extractors;
|
||||||
|
|
|
@ -2,13 +2,9 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
using Enums;
|
||||||
using static Program;
|
using static Program;
|
||||||
|
|
||||||
using File = System.IO.File;
|
|
||||||
using Directory = System.IO.Directory;
|
|
||||||
using ProgressBar = Konsole.ProgressBar;
|
|
||||||
using SearchResponse = Soulseek.SearchResponse;
|
using SearchResponse = Soulseek.SearchResponse;
|
||||||
using SlResponse = Soulseek.SearchResponse;
|
using SlResponse = Soulseek.SearchResponse;
|
||||||
using SlFile = Soulseek.File;
|
using SlFile = Soulseek.File;
|
||||||
|
@ -1161,16 +1157,4 @@ public class SearchAndDownloadException : Exception
|
||||||
public SearchAndDownloadException(FailureReason reason, string text = "") : base(text) { this.reason = reason; }
|
public SearchAndDownloadException(FailureReason reason, string text = "") : base(text) { this.reason = reason; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SearchInfo
|
|
||||||
{
|
|
||||||
public ConcurrentDictionary<string, (SearchResponse, Soulseek.File)> results;
|
|
||||||
public ProgressBar progress;
|
|
||||||
|
|
||||||
public SearchInfo(ConcurrentDictionary<string, (SearchResponse, Soulseek.File)> results, ProgressBar progress)
|
|
||||||
{
|
|
||||||
this.results = results;
|
|
||||||
this.progress = progress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
using Enums;
|
||||||
using FileSkippers;
|
using FileSkippers;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
using static Test.Helpers;
|
using static Tests.Helpers;
|
||||||
|
|
||||||
namespace Test
|
namespace Tests
|
||||||
{
|
{
|
||||||
static class Test
|
static class Test
|
||||||
{
|
{
|
||||||
|
@ -26,10 +26,10 @@ namespace Test
|
||||||
SetCurrentTest("TestStringUtils");
|
SetCurrentTest("TestStringUtils");
|
||||||
|
|
||||||
// RemoveFt
|
// RemoveFt
|
||||||
Assert("blah blah ft. blah blah" .RemoveFt() == "blah blah");
|
Assert("blah blah ft. blah blah".RemoveFt() == "blah blah");
|
||||||
Assert("blah blah feat. blah blah" .RemoveFt() == "blah blah");
|
Assert("blah blah feat. blah blah".RemoveFt() == "blah blah");
|
||||||
Assert("blah (feat. blah blah) blah".RemoveFt() == "blah blah");
|
Assert("blah (feat. blah blah) blah".RemoveFt() == "blah blah");
|
||||||
Assert("blah (ft. blah blah) blah" .RemoveFt() == "blah blah");
|
Assert("blah (ft. blah blah) blah".RemoveFt() == "blah blah");
|
||||||
|
|
||||||
// RemoveConsecutiveWs
|
// RemoveConsecutiveWs
|
||||||
Assert(" blah blah blah blah ".RemoveConsecutiveWs() == " blah blah blah blah ");
|
Assert(" blah blah blah blah ".RemoveConsecutiveWs() == " blah blah blah blah ");
|
|
@ -1,21 +1,10 @@
|
||||||
using System;
|
using Konsole;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Konsole;
|
|
||||||
using Soulseek;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
using Data;
|
using Models;
|
||||||
using Enums;
|
using Enums;
|
||||||
|
|
||||||
using Directory = System.IO.Directory;
|
|
||||||
using File = System.IO.File;
|
|
||||||
using ProgressBar = Konsole.ProgressBar;
|
using ProgressBar = Konsole.ProgressBar;
|
||||||
using SearchResponse = Soulseek.SearchResponse;
|
using SearchResponse = Soulseek.SearchResponse;
|
||||||
using SlFile = Soulseek.File;
|
using SlFile = Soulseek.File;
|
||||||
using SlResponse = Soulseek.SearchResponse;
|
|
||||||
|
|
||||||
public static class Printing
|
public static class Printing
|
||||||
{
|
{
|
53
slsk-batchdl/Utilities/RateLimitedSemaphore.cs
Normal file
53
slsk-batchdl/Utilities/RateLimitedSemaphore.cs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
class RateLimitedSemaphore
|
||||||
|
{
|
||||||
|
private readonly int maxCount;
|
||||||
|
private readonly TimeSpan resetTimeSpan;
|
||||||
|
private readonly SemaphoreSlim semaphore;
|
||||||
|
private long nextResetTimeTicks;
|
||||||
|
private readonly object resetTimeLock = new object();
|
||||||
|
|
||||||
|
public RateLimitedSemaphore(int maxCount, TimeSpan resetTimeSpan)
|
||||||
|
{
|
||||||
|
this.maxCount = maxCount;
|
||||||
|
this.resetTimeSpan = resetTimeSpan;
|
||||||
|
this.semaphore = new SemaphoreSlim(maxCount, maxCount);
|
||||||
|
this.nextResetTimeTicks = (DateTimeOffset.UtcNow + this.resetTimeSpan).UtcTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryResetSemaphore()
|
||||||
|
{
|
||||||
|
if (!(DateTimeOffset.UtcNow.UtcTicks > Interlocked.Read(ref this.nextResetTimeTicks)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (this.resetTimeLock)
|
||||||
|
{
|
||||||
|
var currentTime = DateTimeOffset.UtcNow;
|
||||||
|
if (currentTime.UtcTicks > Interlocked.Read(ref this.nextResetTimeTicks))
|
||||||
|
{
|
||||||
|
int releaseCount = this.maxCount - this.semaphore.CurrentCount;
|
||||||
|
if (releaseCount > 0)
|
||||||
|
this.semaphore.Release(releaseCount);
|
||||||
|
|
||||||
|
var newResetTimeTicks = (currentTime + this.resetTimeSpan).UtcTicks;
|
||||||
|
Interlocked.Exchange(ref this.nextResetTimeTicks, newResetTimeTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WaitAsync()
|
||||||
|
{
|
||||||
|
TryResetSemaphore();
|
||||||
|
var semaphoreTask = this.semaphore.WaitAsync();
|
||||||
|
|
||||||
|
while (!semaphoreTask.IsCompleted)
|
||||||
|
{
|
||||||
|
var ticks = Interlocked.Read(ref this.nextResetTimeTicks);
|
||||||
|
var nextResetTime = new DateTimeOffset(new DateTime(ticks, DateTimeKind.Utc));
|
||||||
|
var delayTime = nextResetTime - DateTimeOffset.UtcNow;
|
||||||
|
var delayTask = delayTime >= TimeSpan.Zero ? Task.Delay(delayTime) : Task.CompletedTask;
|
||||||
|
|
||||||
|
await Task.WhenAny(semaphoreTask, delayTask);
|
||||||
|
TryResetSemaphore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Enums;
|
|
||||||
|
|
||||||
public static class Utils
|
public static class Utils
|
||||||
{
|
{
|
||||||
|
@ -714,91 +713,3 @@ public static class Utils
|
||||||
{ 'Ё', 'E' },
|
{ 'Ё', 'E' },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RateLimitedSemaphore
|
|
||||||
{
|
|
||||||
private readonly int maxCount;
|
|
||||||
private readonly TimeSpan resetTimeSpan;
|
|
||||||
private readonly SemaphoreSlim semaphore;
|
|
||||||
private long nextResetTimeTicks;
|
|
||||||
private readonly object resetTimeLock = new object();
|
|
||||||
|
|
||||||
public RateLimitedSemaphore(int maxCount, TimeSpan resetTimeSpan)
|
|
||||||
{
|
|
||||||
this.maxCount = maxCount;
|
|
||||||
this.resetTimeSpan = resetTimeSpan;
|
|
||||||
this.semaphore = new SemaphoreSlim(maxCount, maxCount);
|
|
||||||
this.nextResetTimeTicks = (DateTimeOffset.UtcNow + this.resetTimeSpan).UtcTicks;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryResetSemaphore()
|
|
||||||
{
|
|
||||||
if (!(DateTimeOffset.UtcNow.UtcTicks > Interlocked.Read(ref this.nextResetTimeTicks)))
|
|
||||||
return;
|
|
||||||
|
|
||||||
lock (this.resetTimeLock)
|
|
||||||
{
|
|
||||||
var currentTime = DateTimeOffset.UtcNow;
|
|
||||||
if (currentTime.UtcTicks > Interlocked.Read(ref this.nextResetTimeTicks))
|
|
||||||
{
|
|
||||||
int releaseCount = this.maxCount - this.semaphore.CurrentCount;
|
|
||||||
if (releaseCount > 0)
|
|
||||||
this.semaphore.Release(releaseCount);
|
|
||||||
|
|
||||||
var newResetTimeTicks = (currentTime + this.resetTimeSpan).UtcTicks;
|
|
||||||
Interlocked.Exchange(ref this.nextResetTimeTicks, newResetTimeTicks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WaitAsync()
|
|
||||||
{
|
|
||||||
TryResetSemaphore();
|
|
||||||
var semaphoreTask = this.semaphore.WaitAsync();
|
|
||||||
|
|
||||||
while (!semaphoreTask.IsCompleted)
|
|
||||||
{
|
|
||||||
var ticks = Interlocked.Read(ref this.nextResetTimeTicks);
|
|
||||||
var nextResetTime = new DateTimeOffset(new DateTime(ticks, DateTimeKind.Utc));
|
|
||||||
var delayTime = nextResetTime - DateTimeOffset.UtcNow;
|
|
||||||
var delayTask = delayTime >= TimeSpan.Zero ? Task.Delay(delayTime) : Task.CompletedTask;
|
|
||||||
|
|
||||||
await Task.WhenAny(semaphoreTask, delayTask);
|
|
||||||
TryResetSemaphore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Logger
|
|
||||||
{
|
|
||||||
public static Verbosity verbosity { get; set; } = Verbosity.Normal;
|
|
||||||
|
|
||||||
public static void Log(string message, Verbosity messageLevel)
|
|
||||||
{
|
|
||||||
if (messageLevel <= verbosity)
|
|
||||||
{
|
|
||||||
Console.WriteLine(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Error(string message)
|
|
||||||
{
|
|
||||||
Log(message, Verbosity.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Warning(string message)
|
|
||||||
{
|
|
||||||
Log(message, Verbosity.Warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Info(string message)
|
|
||||||
{
|
|
||||||
Log(message, Verbosity.Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Verbose(string message)
|
|
||||||
{
|
|
||||||
Log(message, Verbosity.Verbose);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue