diff --git a/slsk-batchdl/Config.cs b/slsk-batchdl/Config.cs index b20aef0..c35cf0c 100644 --- a/slsk-batchdl/Config.cs +++ b/slsk-batchdl/Config.cs @@ -1,6 +1,6 @@  using Enums; -using Data; +using Models; using System.Text; using System.Text.RegularExpressions; diff --git a/slsk-batchdl/Data.cs b/slsk-batchdl/Data.cs deleted file mode 100644 index aa77414..0000000 --- a/slsk-batchdl/Data.cs +++ /dev/null @@ -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; - 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>(); - this.source = new Track() { Type = trackType }; - SetDefaults(); - } - - public TrackListEntry(Track source) - { - list = new List>(); - this.source = source; - SetDefaults(); - } - - public TrackListEntry(List> list, Track source) - { - this.list = list; - this.source = source; - SetDefaults(); - } - - public TrackListEntry(List> 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>() { new List() { track } }; - else if (list.Count == 0) - list.Add(new List() { track }); - else - list[0].Add(track); - } - } - - public class TrackLists - { - public List lists = new(); - - public TrackLists() { } - - public static TrackLists FromFlattened(IEnumerable 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> { new List() { track } }, new Track())); - return; - } - - int i = lists.Count - 1; - - if (lists[i].list.Count == 0) - { - lists[i].list.Add(new List() { 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(); - - 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 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 - { - 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; - } -} diff --git a/slsk-batchdl/Download.cs b/slsk-batchdl/Download.cs index d167f81..7f000d3 100644 --- a/slsk-batchdl/Download.cs +++ b/slsk-batchdl/Download.cs @@ -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; 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; - } -} diff --git a/slsk-batchdl/Extractors/Bandcamp.cs b/slsk-batchdl/Extractors/Bandcamp.cs index 3718bf7..add6709 100644 --- a/slsk-batchdl/Extractors/Bandcamp.cs +++ b/slsk-batchdl/Extractors/Bandcamp.cs @@ -1,8 +1,7 @@ -using Data; +using Models; using HtmlAgilityPack; using Enums; -using System.Net; using System.Text.RegularExpressions; using System.Text.Json; diff --git a/slsk-batchdl/Extractors/Csv.cs b/slsk-batchdl/Extractors/Csv.cs index 64c3f13..4bc8472 100644 --- a/slsk-batchdl/Extractors/Csv.cs +++ b/slsk-batchdl/Extractors/Csv.cs @@ -1,5 +1,4 @@ -using Data; -using Enums; +using Models; using System.Text.RegularExpressions; namespace Extractors diff --git a/slsk-batchdl/Extractors/List.cs b/slsk-batchdl/Extractors/List.cs index 4b35f59..3e0436b 100644 --- a/slsk-batchdl/Extractors/List.cs +++ b/slsk-batchdl/Extractors/List.cs @@ -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 { diff --git a/slsk-batchdl/Extractors/Spotify.cs b/slsk-batchdl/Extractors/Spotify.cs index fea805c..9311a93 100644 --- a/slsk-batchdl/Extractors/Spotify.cs +++ b/slsk-batchdl/Extractors/Spotify.cs @@ -2,9 +2,8 @@ using SpotifyAPI.Web.Auth; using Swan; -using Data; +using Models; using Enums; -using System.Security; namespace Extractors { diff --git a/slsk-batchdl/Extractors/String.cs b/slsk-batchdl/Extractors/String.cs index 2d8ca27..4f668b8 100644 --- a/slsk-batchdl/Extractors/String.cs +++ b/slsk-batchdl/Extractors/String.cs @@ -1,5 +1,5 @@  -using Data; +using Models; using Enums; namespace Extractors diff --git a/slsk-batchdl/Extractors/YouTube.cs b/slsk-batchdl/Extractors/YouTube.cs index 1173835..5a97070 100644 --- a/slsk-batchdl/Extractors/YouTube.cs +++ b/slsk-batchdl/Extractors/YouTube.cs @@ -8,7 +8,7 @@ using System.Diagnostics; using HtmlAgilityPack; using System.Collections.Concurrent; -using Data; +using Models; using Enums; namespace Extractors diff --git a/slsk-batchdl/Extractors/_Extractor.cs b/slsk-batchdl/Extractors/_Extractor.cs index 7e0a0b3..5580228 100644 --- a/slsk-batchdl/Extractors/_Extractor.cs +++ b/slsk-batchdl/Extractors/_Extractor.cs @@ -1,5 +1,5 @@ using Enums; -using Data; +using Models; namespace Extractors diff --git a/slsk-batchdl/FileConditions.cs b/slsk-batchdl/FileConditions.cs deleted file mode 100644 index a23a3e9..0000000 --- a/slsk-batchdl/FileConditions.cs +++ /dev/null @@ -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(); - public string[] BannedUsers = Array.Empty(); - 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; -} - diff --git a/slsk-batchdl/FileManager.cs b/slsk-batchdl/FileManager.cs index 00f8d04..5dd5eb8 100644 --- a/slsk-batchdl/FileManager.cs +++ b/slsk-batchdl/FileManager.cs @@ -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; diff --git a/slsk-batchdl/FileSkipper.cs b/slsk-batchdl/FileSkipper.cs index 627dec5..47c250a 100644 --- a/slsk-batchdl/FileSkipper.cs +++ b/slsk-batchdl/FileSkipper.cs @@ -1,7 +1,6 @@  -using Data; +using Models; using Enums; -using System.IO; namespace FileSkippers { diff --git a/slsk-batchdl/M3uEditor.cs b/slsk-batchdl/M3uEditor.cs index 54db9e1..bdacdd7 100644 --- a/slsk-batchdl/M3uEditor.cs +++ b/slsk-batchdl/M3uEditor.cs @@ -1,4 +1,4 @@ -using Data; +using Models; using Enums; using System.Text; diff --git a/slsk-batchdl/Models/DownloadWrapper.cs b/slsk-batchdl/Models/DownloadWrapper.cs new file mode 100644 index 0000000..af9e50b --- /dev/null +++ b/slsk-batchdl/Models/DownloadWrapper.cs @@ -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; + } + } +} diff --git a/slsk-batchdl/Models/FileConditions.cs b/slsk-batchdl/Models/FileConditions.cs new file mode 100644 index 0000000..12b109a --- /dev/null +++ b/slsk-batchdl/Models/FileConditions.cs @@ -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(); + public string[] BannedUsers = Array.Empty(); + 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; + } +} diff --git a/slsk-batchdl/Models/ResponseData.cs b/slsk-batchdl/Models/ResponseData.cs new file mode 100644 index 0000000..23e56ee --- /dev/null +++ b/slsk-batchdl/Models/ResponseData.cs @@ -0,0 +1,7 @@ +namespace Models +{ + public class ResponseData + { + public int lockedFilesCount; + } +} diff --git a/slsk-batchdl/Models/SearchInfo.cs b/slsk-batchdl/Models/SearchInfo.cs new file mode 100644 index 0000000..c63fd74 --- /dev/null +++ b/slsk-batchdl/Models/SearchInfo.cs @@ -0,0 +1,20 @@ +using System.Collections.Concurrent; +using ProgressBar = Konsole.ProgressBar; +using SearchResponse = Soulseek.SearchResponse; + +namespace Models +{ + public class SearchInfo + { + public ConcurrentDictionary results; + public ProgressBar progress; + + public SearchInfo(ConcurrentDictionary results, ProgressBar progress) + { + this.results = results; + this.progress = progress; + } + } +} + + diff --git a/slsk-batchdl/Models/SimpleFile.cs b/slsk-batchdl/Models/SimpleFile.cs new file mode 100644 index 0000000..a36d744 --- /dev/null +++ b/slsk-batchdl/Models/SimpleFile.cs @@ -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; + } + } +} diff --git a/slsk-batchdl/Models/Track.cs b/slsk-batchdl/Models/Track.cs new file mode 100644 index 0000000..0f525a2 --- /dev/null +++ b/slsk-batchdl/Models/Track.cs @@ -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 + { + 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; + } + } + } +} diff --git a/slsk-batchdl/Models/TrackListEntry.cs b/slsk-batchdl/Models/TrackListEntry.cs new file mode 100644 index 0000000..42714bd --- /dev/null +++ b/slsk-batchdl/Models/TrackListEntry.cs @@ -0,0 +1,70 @@ +using Enums; + +namespace Models +{ + public class TrackListEntry + { + public List>? 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>(); + this.source = new Track() { Type = trackType }; + SetDefaults(); + } + + public TrackListEntry(Track source) + { + list = new List>(); + this.source = source; + SetDefaults(); + } + + public TrackListEntry(List> list, Track source) + { + this.list = list; + this.source = source; + SetDefaults(); + } + + public TrackListEntry(List> 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>() { new List() { track } }; + else if (list.Count == 0) + list.Add(new List() { track }); + else + list[0].Add(track); + } + } +} diff --git a/slsk-batchdl/Models/TrackLists.cs b/slsk-batchdl/Models/TrackLists.cs new file mode 100644 index 0000000..158f284 --- /dev/null +++ b/slsk-batchdl/Models/TrackLists.cs @@ -0,0 +1,162 @@ +using Enums; + +namespace Models +{ + public class TrackLists + { + public List lists = new(); + + public TrackLists() { } + + public static TrackLists FromFlattened(IEnumerable 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> { new List() { track } }, new Track())); + return; + } + + int i = lists.Count - 1; + + if (lists[i].list.Count == 0) + { + lists[i].list.Add(new List() { 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(); + + 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 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; + } + } + } + } +} diff --git a/slsk-batchdl/Program.cs b/slsk-batchdl/Program.cs index 02d9951..0120099 100644 --- a/slsk-batchdl/Program.cs +++ b/slsk-batchdl/Program.cs @@ -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; diff --git a/slsk-batchdl/Search.cs b/slsk-batchdl/Search.cs index 1036339..9a2dde0 100644 --- a/slsk-batchdl/Search.cs +++ b/slsk-batchdl/Search.cs @@ -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 results; - public ProgressBar progress; - - public SearchInfo(ConcurrentDictionary results, ProgressBar progress) - { - this.results = results; - this.progress = progress; - } -} - diff --git a/slsk-batchdl/Test.cs b/slsk-batchdl/Tests/Test.cs similarity index 97% rename from slsk-batchdl/Test.cs rename to slsk-batchdl/Tests/Test.cs index 9171747..cf2fa5b 100644 --- a/slsk-batchdl/Test.cs +++ b/slsk-batchdl/Tests/Test.cs @@ -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" + diff --git a/slsk-batchdl/Printing.cs b/slsk-batchdl/Utilities/Printing.cs similarity index 97% rename from slsk-batchdl/Printing.cs rename to slsk-batchdl/Utilities/Printing.cs index 1b2cbe2..3ada28a 100644 --- a/slsk-batchdl/Printing.cs +++ b/slsk-batchdl/Utilities/Printing.cs @@ -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 { diff --git a/slsk-batchdl/Utilities/RateLimitedSemaphore.cs b/slsk-batchdl/Utilities/RateLimitedSemaphore.cs new file mode 100644 index 0000000..198a44a --- /dev/null +++ b/slsk-batchdl/Utilities/RateLimitedSemaphore.cs @@ -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(); + } + } +} diff --git a/slsk-batchdl/Utils.cs b/slsk-batchdl/Utilities/Utils.cs similarity index 90% rename from slsk-batchdl/Utils.cs rename to slsk-batchdl/Utilities/Utils.cs index b61a1b9..bc902c3 100644 --- a/slsk-batchdl/Utils.cs +++ b/slsk-batchdl/Utilities/Utils.cs @@ -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); - } -}