1
0
Fork 0
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:
fiso64 2024-10-06 09:27:16 +02:00
parent 5cd601ba1b
commit d13b3307d8
28 changed files with 1010 additions and 1057 deletions

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -1,5 +1,4 @@
using Data; using Models;
using Enums;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Extractors namespace Extractors

View file

@ -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
{ {

View file

@ -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
{ {

View file

@ -1,5 +1,5 @@
 
using Data; using Models;
using Enums; using Enums;
namespace Extractors namespace Extractors

View file

@ -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

View file

@ -1,5 +1,5 @@
using Enums; using Enums;
using Data; using Models;
namespace Extractors namespace Extractors

View file

@ -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;
}

View file

@ -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;

View file

@ -1,7 +1,6 @@
 
using Data; using Models;
using Enums; using Enums;
using System.IO;
namespace FileSkippers namespace FileSkippers
{ {

View file

@ -1,4 +1,4 @@
using Data; using Models;
using Enums; using Enums;
using System.Text; using System.Text;

View 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;
}
}
}

View 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;
}
}

View file

@ -0,0 +1,7 @@
namespace Models
{
public class ResponseData
{
public int lockedFilesCount;
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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);
}
}
}

View 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;
}
}
}
}
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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 ");
@ -79,7 +79,7 @@ namespace Test
{ {
SetCurrentTest("TestAutoProfiles"); SetCurrentTest("TestAutoProfiles");
ResetConfig(); ResetConfig();
Config.I.inputType = InputType.YouTube; Config.I.inputType = InputType.YouTube;
Config.I.interactiveMode = true; Config.I.interactiveMode = true;
Config.I.aggregate = false; Config.I.aggregate = false;
@ -87,7 +87,7 @@ namespace Test
string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf"); string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf");
string content = string content =
"max-stale-time = 5" + "max-stale-time = 5" +
"\nfast-search = true" + "\nfast-search = true" +
"\nformat = flac" + "\nformat = flac" +
@ -122,7 +122,7 @@ namespace Test
Config.I.interactiveMode = true; Config.I.interactiveMode = true;
Config.I.useYtdlp = false; Config.I.useYtdlp = false;
Config.I.maxStaleTime = 50000; Config.I.maxStaleTime = 50000;
content = content =
"\n[no-stale]" + "\n[no-stale]" +
"\nprofile-cond = interactive && download-mode == \"album\"" + "\nprofile-cond = interactive && download-mode == \"album\"" +
"\nmax-stale-time = 999999" + "\nmax-stale-time = 999999" +
@ -132,7 +132,7 @@ namespace Test
File.WriteAllText(path, content); File.WriteAllText(path, content);
Config.I.LoadAndParse(new string[] { "-c", path }); Config.I.LoadAndParse(new string[] { "-c", path });
Config.UpdateProfiles(tle); Config.UpdateProfiles(tle);
Assert(Config.I.maxStaleTime == 999999 && !Config.I.useYtdlp); Assert(Config.I.maxStaleTime == 999999 && !Config.I.useYtdlp);
@ -172,7 +172,7 @@ namespace Test
Config.I.album = true; Config.I.album = true;
Config.I.aggregate = false; Config.I.aggregate = false;
var conds = new (bool, string)[] var conds = new (bool, string)[]
{ {
(true, "input-type == \"youtube\""), (true, "input-type == \"youtube\""),
(true, "download-mode == \"album\""), (true, "download-mode == \"album\""),
@ -343,7 +343,7 @@ namespace Test
Program.m3uEditor.Update(); Program.m3uEditor.Update();
string output = File.ReadAllText(path); 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;" + "#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" +
"\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" + "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
@ -362,7 +362,7 @@ namespace Test
Program.m3uEditor.Update(); Program.m3uEditor.Update();
output = File.ReadAllText(path); 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;" + "#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;" + ",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
"\n" + "\n" +

View file

@ -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
{ {

View 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();
}
}
}

View file

@ -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);
}
}