mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 06:22:41 +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 Data;
|
||||
using Models;
|
||||
using System.Text;
|
||||
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 System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Diagnostics;
|
||||
|
||||
using Data;
|
||||
using Enums;
|
||||
using Models;
|
||||
using static Program;
|
||||
|
||||
using File = System.IO.File;
|
||||
using Directory = System.IO.Directory;
|
||||
using ProgressBar = Konsole.ProgressBar;
|
||||
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
|
||||
|
@ -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 Enums;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.Json;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using Data;
|
||||
using Enums;
|
||||
using Models;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Extractors
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
|
||||
using Data;
|
||||
using Enums;
|
||||
using Models;
|
||||
|
||||
namespace Extractors
|
||||
{
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
using SpotifyAPI.Web.Auth;
|
||||
using Swan;
|
||||
|
||||
using Data;
|
||||
using Models;
|
||||
using Enums;
|
||||
using System.Security;
|
||||
|
||||
namespace Extractors
|
||||
{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
using Data;
|
||||
using Models;
|
||||
using Enums;
|
||||
|
||||
namespace Extractors
|
||||
|
|
|
@ -8,7 +8,7 @@ using System.Diagnostics;
|
|||
using HtmlAgilityPack;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Data;
|
||||
using Models;
|
||||
using Enums;
|
||||
|
||||
namespace Extractors
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
using Enums;
|
||||
using Data;
|
||||
using Models;
|
||||
|
||||
|
||||
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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Data;
|
||||
using Models;
|
||||
using Enums;
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
|
||||
using Data;
|
||||
using Models;
|
||||
using Enums;
|
||||
using System.IO;
|
||||
|
||||
namespace FileSkippers
|
||||
{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using Data;
|
||||
using Models;
|
||||
using Enums;
|
||||
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 Konsole;
|
||||
using Soulseek;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Data;
|
||||
|
@ -7,7 +6,7 @@ using System.Diagnostics;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Net.Sockets;
|
||||
|
||||
using Data;
|
||||
using Models;
|
||||
using Enums;
|
||||
using FileSkippers;
|
||||
using Extractors;
|
||||
|
|
|
@ -2,13 +2,9 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Data;
|
||||
using Models;
|
||||
using Enums;
|
||||
using static Program;
|
||||
|
||||
using File = System.IO.File;
|
||||
using Directory = System.IO.Directory;
|
||||
using ProgressBar = Konsole.ProgressBar;
|
||||
using SearchResponse = Soulseek.SearchResponse;
|
||||
using SlResponse = Soulseek.SearchResponse;
|
||||
using SlFile = Soulseek.File;
|
||||
|
@ -1161,16 +1157,4 @@ public class SearchAndDownloadException : Exception
|
|||
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 FileSkippers;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
using static Test.Helpers;
|
||||
using static Tests.Helpers;
|
||||
|
||||
namespace Test
|
||||
namespace Tests
|
||||
{
|
||||
static class Test
|
||||
{
|
||||
|
@ -26,10 +26,10 @@ namespace Test
|
|||
SetCurrentTest("TestStringUtils");
|
||||
|
||||
// RemoveFt
|
||||
Assert("blah blah ft. blah blah" .RemoveFt() == "blah blah");
|
||||
Assert("blah blah feat. 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 (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
|
||||
Assert(" blah blah blah blah ".RemoveConsecutiveWs() == " blah blah blah blah ");
|
||||
|
@ -79,7 +79,7 @@ namespace Test
|
|||
{
|
||||
SetCurrentTest("TestAutoProfiles");
|
||||
|
||||
ResetConfig();
|
||||
ResetConfig();
|
||||
Config.I.inputType = InputType.YouTube;
|
||||
Config.I.interactiveMode = true;
|
||||
Config.I.aggregate = false;
|
||||
|
@ -87,7 +87,7 @@ namespace Test
|
|||
|
||||
string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf");
|
||||
|
||||
string content =
|
||||
string content =
|
||||
"max-stale-time = 5" +
|
||||
"\nfast-search = true" +
|
||||
"\nformat = flac" +
|
||||
|
@ -122,7 +122,7 @@ namespace Test
|
|||
Config.I.interactiveMode = true;
|
||||
Config.I.useYtdlp = false;
|
||||
Config.I.maxStaleTime = 50000;
|
||||
content =
|
||||
content =
|
||||
"\n[no-stale]" +
|
||||
"\nprofile-cond = interactive && download-mode == \"album\"" +
|
||||
"\nmax-stale-time = 999999" +
|
||||
|
@ -132,7 +132,7 @@ namespace Test
|
|||
|
||||
File.WriteAllText(path, content);
|
||||
|
||||
|
||||
|
||||
Config.I.LoadAndParse(new string[] { "-c", path });
|
||||
Config.UpdateProfiles(tle);
|
||||
Assert(Config.I.maxStaleTime == 999999 && !Config.I.useYtdlp);
|
||||
|
@ -172,7 +172,7 @@ namespace Test
|
|||
Config.I.album = true;
|
||||
Config.I.aggregate = false;
|
||||
|
||||
var conds = new (bool, string)[]
|
||||
var conds = new (bool, string)[]
|
||||
{
|
||||
(true, "input-type == \"youtube\""),
|
||||
(true, "download-mode == \"album\""),
|
||||
|
@ -343,7 +343,7 @@ namespace Test
|
|||
|
||||
Program.m3uEditor.Update();
|
||||
string output = File.ReadAllText(path);
|
||||
string need =
|
||||
string need =
|
||||
"#SLDL:./file1.5,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;,,,,-1,0,0,0;" +
|
||||
"\n" +
|
||||
"\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
|
||||
|
@ -362,7 +362,7 @@ namespace Test
|
|||
|
||||
Program.m3uEditor.Update();
|
||||
output = File.ReadAllText(path);
|
||||
need =
|
||||
need =
|
||||
"#SLDL:/other/new/file/path,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
|
||||
",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
|
||||
"\n" +
|
|
@ -1,21 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Konsole;
|
||||
using Soulseek;
|
||||
using System.Text.RegularExpressions;
|
||||
using Konsole;
|
||||
|
||||
using Data;
|
||||
using Models;
|
||||
using Enums;
|
||||
|
||||
using Directory = System.IO.Directory;
|
||||
using File = System.IO.File;
|
||||
using ProgressBar = Konsole.ProgressBar;
|
||||
using SearchResponse = Soulseek.SearchResponse;
|
||||
using SlFile = Soulseek.File;
|
||||
using SlResponse = Soulseek.SearchResponse;
|
||||
|
||||
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.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Enums;
|
||||
|
||||
public static class Utils
|
||||
{
|
||||
|
@ -714,91 +713,3 @@ public static class Utils
|
|||
{ 'Ё', '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