diff --git a/README.md b/README.md index f9d7982..9cebebd 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,8 @@ Usage: sldl [OPTIONS] on a per-track basis, so it is best kept off in that case. -d, --desperate Tries harder to find the desired track by searching for the artist/album/title only, then filtering. (slower search) - --fails-to-downrank Number of fails to downrank a user's uploads (default: 1) - --fails-to-ignore Number of fails to ban/ignore a user's uploads (default: 2) + --fails-to-downrank Number of fails to downrank a user's shares (default: 1) + --fails-to-ignore Number of fails to ban/ignore a user's shares (default: 2) --yt-dlp Use yt-dlp to download tracks that weren't found on Soulseek. yt-dlp must be available from the command line. @@ -367,8 +367,12 @@ accept-no-length = false ``` sldl will therefore prefer mp3 files with bitrate between 200 and 2500 kbps, and whose length differs from the supplied length by no more than 3 seconds. It will also prefer files whose -paths contain the supplied artist and album (ignoring case, and bounded by boundary characters) -and which have a non-null length. Changing the last three preferred conditions is not recommended. +paths contain the supplied title and album (ignoring case, and bounded by boundary characters) +and which have non-null length. Changing the last three preferred conditions is not recommended. +Note that files satisfying a subset of the preferred conditions will still be preferred over files +that don't satisfy any condition, but some conditions have precedence over others. For instance, +a file that only satisfies strict-title (if enabled) will always be preferred over a file that +only satisfies the format condition. Run with --print "results-full" to reveal the sorting logic. ### Important note Some info may be unavailable depending on the client used by the peer. For example, the standard diff --git a/slsk-batchdl/Config.cs b/slsk-batchdl/Config.cs index b8613f2..b663f2b 100644 --- a/slsk-batchdl/Config.cs +++ b/slsk-batchdl/Config.cs @@ -537,9 +537,6 @@ static class Config case "bannedusers": cond.BannedUsers = value.Split(',', tr); break; - case "dangerwords": - cond.DangerWords = value.Split(',', tr); - break; case "stricttitle": cond.StrictTitle = bool.Parse(value); break; @@ -967,10 +964,6 @@ static class Config case "--pref-max-bitdepth": preferredCond.MaxBitDepth = int.Parse(args[++i]); break; - case "--pdw": - case "--pref-danger-words": - preferredCond.DangerWords = args[++i].Split(','); - break; case "--pst": case "--pstt": case "--pref-strict-title": @@ -1022,10 +1015,6 @@ static class Config case "--max-bitdepth": necessaryCond.MaxBitDepth = int.Parse(args[++i]); break; - case "--dw": - case "--danger-words": - necessaryCond.DangerWords = args[++i].Split(','); - break; case "--stt": case "--strict-title": setFlag(ref necessaryCond.StrictTitle, ref i); diff --git a/slsk-batchdl/Data.cs b/slsk-batchdl/Data.cs index 298283c..51355de 100644 --- a/slsk-batchdl/Data.cs +++ b/slsk-batchdl/Data.cs @@ -1,6 +1,6 @@ using Enums; +using Soulseek; -using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary; namespace Data { @@ -21,11 +21,11 @@ namespace Data public TrackType Type = TrackType.Normal; public FailureReason FailureReason = FailureReason.None; public TrackState State = TrackState.Initial; - public SlDictionary? Downloads = null; + public List<(SearchResponse, Soulseek.File)>? Downloads = null; public bool OutputsDirectory => Type != TrackType.Normal; - public Soulseek.File? FirstDownload => Downloads?.FirstOrDefault().Value.Item2; - public string? FirstUsername => Downloads?.FirstOrDefault().Value.Item1.Username; + public Soulseek.File? FirstDownload => Downloads?.FirstOrDefault().Item2; + public string? FirstUsername => Downloads?.FirstOrDefault().Item1?.Username; public Track() { } @@ -64,13 +64,13 @@ namespace Data public string ToString(bool noInfo = false) { - if (IsNotAudio && Downloads != null && !Downloads.IsEmpty) - return $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}"; + 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.IsEmpty) + if (Type == TrackType.Normal && Title.Length == 0 && Downloads != null && Downloads.Count > 0) { - str = $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}"; + str = $"{Utils.GetFileNameSlsk(Downloads[0].Item2.Filename)}"; } else if (Title.Length > 0 || Album.Length > 0) { @@ -313,12 +313,14 @@ namespace Data } } - public class TrackStringComparer : IEqualityComparer + public class TrackComparer : IEqualityComparer { private bool _ignoreCase = false; - public TrackStringComparer(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) @@ -330,7 +332,8 @@ namespace Data return string.Equals(a.Title, b.Title, comparer) && string.Equals(a.Artist, b.Artist, comparer) - && string.Equals(a.Album, b.Album, 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) diff --git a/slsk-batchdl/FileConditions.cs b/slsk-batchdl/FileConditions.cs index 90176e6..5fd317c 100644 --- a/slsk-batchdl/FileConditions.cs +++ b/slsk-batchdl/FileConditions.cs @@ -17,10 +17,8 @@ public class FileConditions public bool StrictTitle = false; public bool StrictArtist = false; public bool StrictAlbum = false; - public string[] DangerWords = Array.Empty(); public string[] Formats = Array.Empty(); public string[] BannedUsers = Array.Empty(); - public string StrictStringRegexRemove = string.Empty; public bool StrictStringDiacrRemove = true; public bool AcceptNoLength = true; public bool AcceptMissingProps = true; @@ -40,7 +38,6 @@ public class FileConditions MinBitDepth = other.MinBitDepth; MaxBitDepth = other.MaxBitDepth; Formats = other.Formats.ToArray(); - DangerWords = other.DangerWords.ToArray(); BannedUsers = other.BannedUsers.ToArray(); } @@ -58,12 +55,10 @@ public class FileConditions StrictTitle == other.StrictTitle && StrictArtist == other.StrictArtist && StrictAlbum == other.StrictAlbum && - StrictStringRegexRemove == other.StrictStringRegexRemove && StrictStringDiacrRemove == other.StrictStringDiacrRemove && AcceptNoLength == other.AcceptNoLength && AcceptMissingProps == other.AcceptMissingProps && Formats.SequenceEqual(other.Formats) && - DangerWords.SequenceEqual(other.DangerWords) && BannedUsers.SequenceEqual(other.BannedUsers); } return false; @@ -81,7 +76,7 @@ public class FileConditions public bool FileSatisfies(Soulseek.File file, Track track, SearchResponse? response) { - return DangerWordSatisfies(file.Filename, track.Title, track.Artist) && FormatSatisfies(file.Filename) + 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); @@ -89,7 +84,7 @@ public class FileConditions public bool FileSatisfies(TagLib.File file, Track track, bool filenameChecks = false) { - return DangerWordSatisfies(file.Name, track.Title, track.Artist) && FormatSatisfies(file.Name) + 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)); @@ -97,44 +92,19 @@ public class FileConditions public bool FileSatisfies(SimpleFile file, Track track, bool filenameChecks = false) { - return DangerWordSatisfies(file.Path, track.Title, track.Artist) && FormatSatisfies(file.Path) + 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 DangerWordSatisfies(string fname, string tname, string aname) - { - if (tname.Length == 0) - return true; - - fname = Utils.GetFileNameWithoutExtSlsk(fname).Replace(" — ", " - "); - tname = tname.Replace(" — ", " - "); - - foreach (var word in DangerWords) - { - if (fname.ContainsIgnoreCase(word) ^ tname.ContainsIgnoreCase(word)) - { - if (!(fname.Contains(" - ") && fname.ContainsIgnoreCase(word) && aname.ContainsIgnoreCase(word))) - { - if (word == "mix") - return fname.ContainsIgnoreCase("original mix") || tname.ContainsIgnoreCase("original mix"); - else - return false; - } - } - } - - return true; - } - 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, StrictStringRegexRemove, StrictStringDiacrRemove, ignoreCase: true); + return StrictString(fname, tname, StrictStringDiacrRemove, ignoreCase: true); } public bool StrictArtistSatisfies(string fname, string aname) @@ -142,7 +112,7 @@ public class FileConditions if (!StrictArtist || aname.Length == 0) return true; - return StrictString(fname, aname, StrictStringRegexRemove, StrictStringDiacrRemove, ignoreCase: true, boundarySkipWs: false); + return StrictString(fname, aname, StrictStringDiacrRemove, ignoreCase: true, boundarySkipWs: false); } public bool StrictAlbumSatisfies(string fname, string alname) @@ -150,25 +120,24 @@ public class FileConditions if (!StrictAlbum || alname.Length == 0) return true; - return StrictString(Utils.GetDirectoryNameSlsk(fname), alname, StrictStringRegexRemove, StrictStringDiacrRemove, ignoreCase: true); + return StrictString(Utils.GetDirectoryNameSlsk(fname), alname, StrictStringDiacrRemove, ignoreCase: true); } - public static string StrictStringPreprocess(string str, string regexRemove = "", bool diacrRemove = true) + public static string StrictStringPreprocess(string str, bool diacrRemove = true) { str = str.Replace('_', ' ').ReplaceInvalidChars(' ', true, false); - str = regexRemove.Length > 0 ? Regex.Replace(str, regexRemove, "") : str; str = diacrRemove ? str.RemoveDiacritics() : str; str = str.Trim().RemoveConsecutiveWs(); return str; } - public static bool StrictString(string fname, string tname, string regexRemove = "", bool diacrRemove = true, bool ignoreCase = true, bool boundarySkipWs = true) + 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, regexRemove, diacrRemove); - tname = StrictStringPreprocess(tname, regexRemove, diacrRemove); + fname = StrictStringPreprocess(fname, diacrRemove); + tname = StrictStringPreprocess(tname, diacrRemove); if (boundarySkipWs) return fname.ContainsWithBoundaryIgnoreWs(tname, ignoreCase, acceptLeftDigit: true); @@ -252,43 +221,24 @@ public class FileConditions public string GetNotSatisfiedName(Soulseek.File file, Track track, SearchResponse? response) { - if (!DangerWordSatisfies(file.Filename, track.Title, track.Artist)) - return "DangerWord fails"; - if (!FormatSatisfies(file.Filename)) - return "Format fails"; - if (!LengthToleranceSatisfies(file, track.Length)) - return "Length fails"; - if (!BitrateSatisfies(file)) - return "Bitrate fails"; - if (!SampleRateSatisfies(file)) - return "SampleRate fails"; - if (!StrictTitleSatisfies(file.Filename, track.Title)) - return "StrictTitle fails"; - if (!StrictArtistSatisfies(file.Filename, track.Artist)) - return "StrictArtist fails"; - if (!BitDepthSatisfies(file)) - return "BitDepth fails"; if (!BannedUsersSatisfies(response)) return "BannedUsers fails"; - return "Satisfied"; - } - - public string GetNotSatisfiedName(TagLib.File file, Track track) - { - if (!DangerWordSatisfies(file.Name, track.Title, track.Artist)) - return "DangerWord fails"; - if (!FormatSatisfies(file.Name)) - return "Format 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 "Length fails"; + 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 (!StrictTitleSatisfies(file.Name, track.Title)) - return "StrictTitle fails"; - if (!StrictArtistSatisfies(file.Name, track.Artist)) - return "StrictArtist fails"; if (!BitDepthSatisfies(file)) return "BitDepth fails"; return "Satisfied"; diff --git a/slsk-batchdl/Help.cs b/slsk-batchdl/Help.cs index fc57940..11e2a89 100644 --- a/slsk-batchdl/Help.cs +++ b/slsk-batchdl/Help.cs @@ -83,8 +83,8 @@ public static class Help on a per-track basis, so it is best kept off in that case. -d, --desperate Tries harder to find the desired track by searching for the artist/album/title only, then filtering. (slower search) - --fails-to-downrank Number of fails to downrank a user's uploads (default: 1) - --fails-to-ignore Number of fails to ban/ignore a user's uploads (default: 2) + --fails-to-downrank Number of fails to downrank a user's shares (default: 1) + --fails-to-ignore Number of fails to ban/ignore a user's shares (default: 2) --yt-dlp Use yt-dlp to download tracks that weren't found on Soulseek. yt-dlp must be available from the command line. @@ -339,8 +339,12 @@ public static class Help sldl will therefore prefer mp3 files with bitrate between 200 and 2500 kbps, and whose length differs from the supplied length by no more than 3 seconds. It will also prefer files whose - paths contain the supplied artist and album (ignoring case, and bounded by boundary characters) - and which have a non-null length. Changing the last three preferred conditions is not recommended. + paths contain the supplied title and album (ignoring case, and bounded by boundary characters) + and which have non-null length. Changing the last three preferred conditions is not recommended. + Note that files satisfying a subset of the preferred conditions will still be preferred over files + that don't satisfy any condition, but some conditions have precedence over others. For instance, + a file that only satisfies strict-title (if enabled) will always be preferred over a file that + only satisfies the format condition. Run with --print ""results-full"" to reveal the sorting logic. Important note Some info may be unavailable depending on the client used by the peer. For example, the standard diff --git a/slsk-batchdl/Program.cs b/slsk-batchdl/Program.cs index 9d50ce8..2f6a320 100644 --- a/slsk-batchdl/Program.cs +++ b/slsk-batchdl/Program.cs @@ -350,12 +350,16 @@ static partial class Program else if (tle.source.Type == TrackType.Album) { Console.WriteLine(new string('-', 60)); - Console.WriteLine($"Results for album {tle.source.ToString(true)}:"); + + if (!Config.printOption.HasFlag(PrintOption.Full)) + Console.WriteLine($"Result 1 of {tle.list.Count} for album {tle.source.ToString(true)}:"); + else + Console.WriteLine($"Results ({tle.list.Count}) for album {tle.source.ToString(true)}:"); if (tle.list.Count > 0 && tle.list[0].Count > 0) { if (!Config.noBrowseFolder) - Console.WriteLine("[Skipped full folder retrieval]"); + Console.WriteLine("[Skipping full folder retrieval]"); foreach (var ls in tle.list) { @@ -430,12 +434,12 @@ static partial class Program static void PrintAlbum(List albumTracks, bool retrieveAll = false) { - if (albumTracks.Count == 0 && albumTracks[0].Downloads.IsEmpty) + if (albumTracks.Count == 0 && albumTracks[0].Downloads.Count == 0) return; - var response = albumTracks[0].Downloads.First().Value.Item1; + var response = albumTracks[0].Downloads[0].Item1; string userInfo = $"{response.Username} ({((float)response.UploadSpeed / (1024 * 1024)):F3}MB/s)"; - var (parents, props) = FolderInfo(albumTracks.SelectMany(x => x.Downloads.Select(d => d.Value.Item2))); + var (parents, props) = FolderInfo(albumTracks.SelectMany(x => x.Downloads.Select(d => d.Item2))); Console.WriteLine(); WriteLine($"User : {userInfo}\nFolder: {parents}\nProps : {props}", ConsoleColor.White); @@ -623,23 +627,23 @@ static partial class Program { var albumArtList = list //.Where(tracks => tracks) - .Select(tracks => tracks.Where(t => Utils.IsImageFile(t.Downloads.First().Value.Item2.Filename))) + .Select(tracks => tracks.Where(t => Utils.IsImageFile(t.Downloads[0].Item2.Filename))) .Where(tracks => tracks.Any()); if (option == AlbumArtOption.Largest) { list = albumArtList - .OrderByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Max() / 1024 / 100) - .ThenByDescending(tracks => tracks.First().Downloads.First().Value.Item1.UploadSpeed / 1024 / 300) - .ThenByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Sum() / 1024 / 100) + .OrderByDescending(tracks => tracks.Select(t => t.Downloads[0].Item2.Size).Max() / 1024 / 100) + .ThenByDescending(tracks => tracks.First().Downloads[0].Item1.UploadSpeed / 1024 / 300) + .ThenByDescending(tracks => tracks.Select(t => t.Downloads[0].Item2.Size).Sum() / 1024 / 100) .Select(x => x.ToList()).ToList(); } else if (option == AlbumArtOption.Most) { list = albumArtList .OrderByDescending(tracks => tracks.Count()) - .ThenByDescending(tracks => tracks.First().Downloads.First().Value.Item1.UploadSpeed / 1024 / 300) - .ThenByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Sum() / 1024 / 100) + .ThenByDescending(tracks => tracks.First().Downloads[0].Item1.UploadSpeed / 1024 / 300) + .ThenByDescending(tracks => tracks.Select(t => t.Downloads[0].Item2.Size).Sum() / 1024 / 100) .Select(x => x.ToList()).ToList(); } } @@ -655,7 +659,7 @@ static partial class Program else if (option == AlbumArtOption.Largest) { long curMax = dlFiles.Keys.Where(x => Utils.IsImageFile(x) && File.Exists(x)).Max(x => new FileInfo(x).Length); - need = curMax < list[0].Max(t => t.Downloads.First().Value.Item2.Size) - 1024 * 50; + need = curMax < list[0].Max(t => t.Downloads[0].Item2.Size) - 1024 * 50; } return need; @@ -693,7 +697,7 @@ static partial class Program if (!Config.noBrowseFolder && !Config.interactiveMode && !retrievedFolders.Contains(soulseekFolderPathPrefix)) { Console.WriteLine("Getting all files in folder..."); - var response = tracks[0].Downloads.First().Value.Item1; + var response = tracks[0].Downloads[0].Item1; await CompleteFolder(tracks, response, soulseekFolderPathPrefix); retrievedFolders.Add(soulseekFolderPathPrefix); } @@ -865,9 +869,9 @@ static partial class Program static string GetCommonPathPrefix(List tracks) { if (tracks.Count == 1) - return Utils.GetDirectoryNameSlsk(tracks.First().Downloads.First().Value.Item2.Filename); + return Utils.GetDirectoryNameSlsk(tracks.First().Downloads[0].Item2.Filename); else - return Utils.GreatestCommonPath(tracks.SelectMany(x => x.Downloads.Select(y => y.Value.Item2.Filename)), dirsep: '\\'); + return Utils.GreatestCommonPath(tracks.SelectMany(x => x.Downloads.Select(y => y.Item2.Filename)), dirsep: '\\'); } @@ -897,7 +901,7 @@ static partial class Program { Console.WriteLine(); var tracks = list[aidx]; - var response = tracks[0].Downloads.First().Value.Item1; + var response = tracks[0].Downloads[0].Item1; var folder = GetCommonPathPrefix(tracks); if (retrieveFolder && !Config.noBrowseFolder && !retrievedFolders.Contains(folder)) @@ -908,7 +912,7 @@ static partial class Program } string userInfo = $"{response.Username} ({((float)response.UploadSpeed / (1024 * 1024)):F3}MB/s)"; - var (parents, props) = FolderInfo(tracks.SelectMany(x => x.Downloads.Select(d => d.Value.Item2))); + var (parents, props) = FolderInfo(tracks.SelectMany(x => x.Downloads.Select(d => d.Item2))); WriteLine($"[{aidx + 1} / {list.Count}]", ConsoleColor.DarkGray); WriteLine($"User : {userInfo}\nFolder: {parents}\nProps : {props}", ConsoleColor.White); @@ -1396,7 +1400,7 @@ static partial class Program string ancestor = ""; if (showAncestors) - ancestor = Utils.GreatestCommonPath(tracks.SelectMany(x => x.Downloads.Select(y => y.Value.Item2.Filename)), Path.DirectorySeparatorChar); + ancestor = Utils.GreatestCommonPath(tracks.SelectMany(x => x.Downloads.Select(y => y.Item2.Filename)), Path.DirectorySeparatorChar); if (pathsOnly) { @@ -1405,9 +1409,9 @@ static partial class Program foreach (var x in tracks[i].Downloads) { if (ancestor.Length == 0) - Console.WriteLine(" " + DisplayString(tracks[i], x.Value.Item2, x.Value.Item1, infoFirst: infoFirst, showUser: showUser)); + Console.WriteLine(" " + DisplayString(tracks[i], x.Item2, x.Item1, infoFirst: infoFirst, showUser: showUser)); else - Console.WriteLine(" " + DisplayString(tracks[i], x.Value.Item2, x.Value.Item1, customPath: x.Value.Item2.Filename.Replace(ancestor, ""), infoFirst: infoFirst, showUser: showUser)); + Console.WriteLine(" " + DisplayString(tracks[i], x.Item2, x.Item1, customPath: x.Item2.Filename.Replace(ancestor, ""), infoFirst: infoFirst, showUser: showUser)); } } } @@ -1447,23 +1451,23 @@ static partial class Program foreach (var x in tracks[i].Downloads) { if (ancestor.Length == 0) - Console.WriteLine(" " + DisplayString(tracks[i], x.Value.Item2, x.Value.Item1, infoFirst: infoFirst, showUser: showUser)); + Console.WriteLine(" " + DisplayString(tracks[i], x.Item2, x.Item1, infoFirst: infoFirst, showUser: showUser)); else - Console.WriteLine(" " + DisplayString(tracks[i], x.Value.Item2, x.Value.Item1, customPath: x.Value.Item2.Filename.Replace(ancestor, ""), infoFirst: infoFirst, showUser: showUser)); + Console.WriteLine(" " + DisplayString(tracks[i], x.Item2, x.Item1, customPath: x.Item2.Filename.Replace(ancestor, ""), infoFirst: infoFirst, showUser: showUser)); } if (tracks[i].Downloads?.Count > 0) Console.WriteLine(); } } else { - Console.WriteLine($" File: {Utils.GetFileNameSlsk(tracks[i].Downloads.First().Value.Item2.Filename)}"); + Console.WriteLine($" File: {Utils.GetFileNameSlsk(tracks[i].Downloads[0].Item2.Filename)}"); Console.WriteLine($" Shares: {tracks[i].Downloads.Count}"); foreach (var x in tracks[i].Downloads) { if (ancestor.Length == 0) - Console.WriteLine(" " + DisplayString(tracks[i], x.Value.Item2, x.Value.Item1, infoFirst: infoFirst, showUser: showUser)); + Console.WriteLine(" " + DisplayString(tracks[i], x.Item2, x.Item1, infoFirst: infoFirst, showUser: showUser)); else - Console.WriteLine(" " + DisplayString(tracks[i], x.Value.Item2, x.Value.Item1, customPath: x.Value.Item2.Filename.Replace(ancestor, ""), infoFirst: infoFirst, showUser: showUser)); + Console.WriteLine(" " + DisplayString(tracks[i], x.Item2, x.Item1, customPath: x.Item2.Filename.Replace(ancestor, ""), infoFirst: infoFirst, showUser: showUser)); } Console.WriteLine(); } diff --git a/slsk-batchdl/SearchAndDownload.cs b/slsk-batchdl/SearchAndDownload.cs index 2e824ff..b546867 100644 --- a/slsk-batchdl/SearchAndDownload.cs +++ b/slsk-batchdl/SearchAndDownload.cs @@ -22,6 +22,7 @@ static partial class Program throw new Exception(); responseData ??= new ResponseData(); + IEnumerable<(SlResponse response, SlFile file)>? orderedResults = null; var progress = GetProgressBar(Config.displayMode); var results = new SlDictionary(); var fsResults = new SlDictionary(); @@ -38,7 +39,7 @@ static partial class Program if (track.Downloads != null) { - results = track.Downloads; + orderedResults = track.Downloads; goto downloads; } @@ -141,9 +142,10 @@ static partial class Program downloads: - if (downloading == 0 && !results.IsEmpty) + if (downloading == 0 && (!results.IsEmpty || orderedResults != null)) { - var orderedResults = OrderedResults(results, track, true); + if (orderedResults == null) + orderedResults = OrderedResults(results, track, useInfer: true); int trackTries = Config.maxRetriesPerTrack; async Task process(SlResponse response, SlFile file) @@ -376,13 +378,12 @@ static partial class Program Album = track.Album, Length = x.file.Length ?? -1, IsNotAudio = !Utils.IsMusicFile(x.file.Filename), - Downloads = new ConcurrentDictionary( - new Dictionary { { x.response.Username + '\\' + x.file.Filename, x } }) + Downloads = new() { x }, }; ls.Add(t); } - ls = ls.OrderBy(t => t.IsNotAudio).ThenBy(t => t.Downloads.First().Value.Item2.Filename).ToList(); + ls = ls.OrderBy(t => t.IsNotAudio).ThenBy(t => t.Downloads[0].Item2.Filename).ToList(); result.Add(ls); } @@ -396,9 +397,9 @@ static partial class Program static async Task> GetAggregateTracks(Track track, ResponseData responseData) { - var results = new ConcurrentDictionary(); + var results = new SlDictionary(); SearchOptions getSearchOptions(int timeout, FileConditions nec, FileConditions prf) => - new SearchOptions( + new ( minimumResponseFileCount: 1, minimumPeerUploadSpeed: 1, removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms, @@ -410,9 +411,6 @@ static partial class Program fileFilter: (file) => { return Utils.IsMusicFile(file.Filename) && nec.FileSatisfies(file, track, null); - //&& FileConditions.StrictString(file.Filename, track.ArtistName, ignoreCase: true) - //&& FileConditions.StrictString(file.Filename, track.TrackTitle, ignoreCase: true) - //&& FileConditions.StrictString(file.Filename, track.Album, ignoreCase: true); } ); void handler(SlResponse r) @@ -433,11 +431,8 @@ static partial class Program string trackName = track.Title.Trim(); string albumName = track.Album.Trim(); - //var orderedResults = OrderedResults(results, track, false, false, false); - - var fileResponses = results.Select(x => x.Value); - - var equivalentFiles = EquivalentFiles(track, fileResponses).ToList(); + var equivalentFiles = EquivalentFiles(track, results.Select(x => x.Value)) + .Select(x => (x.Item1, OrderedResults(x.Item2, track, false, false, false))).ToList(); if (!Config.relax) { @@ -452,8 +447,7 @@ static partial class Program var tracks = equivalentFiles .Select(kvp => { - kvp.Item1.Downloads = new SlDictionary( - kvp.Item2.ToDictionary(item => { return item.response.Username + "\\" + item.file.Filename; }, item => item)); + kvp.Item1.Downloads = kvp.Item2.ToList(); return kvp.Item1; }).ToList(); @@ -498,6 +492,7 @@ static partial class Program sortedLengthLists.Add((sortedLengths, album, user)); } + var usernamesList = new List>(); var lengthsList = new List(); var res = new List>>(); @@ -511,8 +506,8 @@ static partial class Program { if (lengths.Length == 1 && lengthsList[i].Length == 1) { - var t1 = InferTrack(album[0].Downloads.First().Value.Item2.Filename, new Track()); - var t2 = InferTrack(res[i][0][0].Downloads.First().Value.Item2.Filename, new Track()); + var t1 = InferTrack(album[0].Downloads[0].Item2.Filename, new Track()); + var t2 = InferTrack(res[i][0][0].Downloads[0].Item2.Filename, new Track()); if ((t2.Artist.ContainsIgnoreCase(t1.Artist) || t1.Artist.ContainsIgnoreCase(t2.Artist)) && (t2.Title.ContainsIgnoreCase(t1.Title) || t1.Title.ContainsIgnoreCase(t2.Title))) @@ -527,6 +522,7 @@ static partial class Program if (found) { + usernamesList[i].Add(user); res[i].Add(album); break; } @@ -539,12 +535,17 @@ static partial class Program } else { + usernamesList.Add(new() { user }); lengthsList.Add(lengths); res.Add(new List> { album }); } } - res = res.Where(x => x.Count >= Config.minSharesAggregate).OrderByDescending(x => x.Count).ToList(); + res = res.Select((x, i) => (x, i)) + .Where(x => usernamesList[x.i].Count >= Config.minSharesAggregate) + .OrderByDescending(x => usernamesList[x.i].Count) + .Select(x => x.x) + .ToList(); return res; // Note: The nested lists are still ordered according to OrderedResults } @@ -586,7 +587,7 @@ static partial class Program if (allFiles.Count > tracks.Count) { - var paths = tracks.Select(x => x.Downloads.First().Value.Item2.Filename).ToHashSet(); + var paths = tracks.Select(x => x.Downloads[0].Item2.Filename).ToHashSet(); var first = tracks[0]; foreach ((var dir, var file) in allFiles) @@ -600,8 +601,7 @@ static partial class Program Artist = first.Artist, Album = first.Album, IsNotAudio = !Utils.IsMusicFile(file.Filename), - Downloads = new ConcurrentDictionary( - new Dictionary { { response.Username + '\\' + fullPath, (response, newFile) } }) + Downloads = new() { (response, newFile) } }; tracks.Add(t); } @@ -615,7 +615,7 @@ static partial class Program } - static IOrderedEnumerable<(Track, IEnumerable<(SlResponse response, SlFile file)>)> EquivalentFiles(Track track, + static IEnumerable<(Track, IEnumerable<(SlResponse response, SlFile file)>)> EquivalentFiles(Track track, IEnumerable<(SlResponse, SlFile)> fileResponses, int minShares = -1) { if (minShares == -1) @@ -628,53 +628,32 @@ static partial class Program return t; } - var res = fileResponses - .GroupBy(inferTrack, new TrackStringComparer(ignoreCase: true)) - .Where(group => group.Select(x => x.Item1.Username).Distinct().Count() >= minShares) - .SelectMany(group => + var groups = fileResponses + .GroupBy(inferTrack, new TrackComparer(ignoreCase: true, Config.necessaryCond.LengthTolerance)) + .Select(x => (x, x.Select(y => y.Item1.Username).Distinct().Count())) + .Where(x => x.Item2 >= minShares) + .OrderByDescending(x => x.Item2) + .Select(x => x.x) + .Select(x => { - var sortedTracks = group.OrderBy(t => t.Item2.Length).Where(x => x.Item2.Length != null).ToList(); - var groups = new List<(Track, List<(SearchResponse, Soulseek.File)>)>(); - var noLengthGroup = group.Where(x => x.Item2.Length == null); - for (int i = 0; i < sortedTracks.Count;) - { - var subGroup = new List<(SearchResponse, Soulseek.File)> { sortedTracks[i] }; - int j = i + 1; - while (j < sortedTracks.Count) - { - int l1 = (int)sortedTracks[j].Item2.Length; - int l2 = (int)sortedTracks[i].Item2.Length; - if (Config.necessaryCond.LengthTolerance == -1 || Math.Abs(l1 - l2) <= Config.necessaryCond.LengthTolerance) - { - subGroup.Add(sortedTracks[j]); - j++; - } - else break; - } - var t = new Track(group.Key); - t.Length = (int)sortedTracks[i].Item2.Length; - groups.Add((t, subGroup)); - i = j; - } + if (x.Key.Length == -1) + x.Key.Length = x.FirstOrDefault(y => y.Item2.Length != null).Item2?.Length ?? -1; + return (x.Key, x.AsEnumerable()); + }); - if (noLengthGroup.Any()) - { - if (groups.Count > 0 && !Config.preferredCond.AcceptNoLength) - groups.First().Item2.AddRange(noLengthGroup); - else - groups.Add((group.Key, noLengthGroup.ToList())); - } - - return groups.Where(subGroup => subGroup.Item2.Select(x => x.Item1.Username).Distinct().Count() >= minShares) - .Select(subGroup => (subGroup.Item1, subGroup.Item2.AsEnumerable())); - }).OrderByDescending(x => x.Item2.Count()); - - return res; + return groups; } static IOrderedEnumerable<(SlResponse response, SlFile file)> OrderedResults(IEnumerable> results, Track track, bool useInfer = false, bool useLevenshtein = true, bool albumMode = false) + { + return OrderedResults(results.Select(x => x.Value), track, useInfer, useLevenshtein, albumMode); + } + + + static IOrderedEnumerable<(SlResponse response, SlFile file)> OrderedResults(IEnumerable<(SlResponse, SlFile)> results, + Track track, bool useInfer = false, bool useLevenshtein = true, bool albumMode = false) { bool useBracketCheck = true; if (albumMode) @@ -685,9 +664,10 @@ static partial class Program } Dictionary? infTracksAndCounts = null; - if (useInfer) + + if (useInfer) // this is very slow { - var equivalentFiles = EquivalentFiles(track, results.Select(x => x.Value), 1); + var equivalentFiles = EquivalentFiles(track, results, 1); infTracksAndCounts = equivalentFiles .SelectMany(t => t.Item2, (t, f) => new { t.Item1, f.response.Username, f.file.Filename, Count = t.Item2.Count() }) .ToSafeDictionary(x => $"{x.Username}\\{x.Filename}", y => (y.Item1, y.Count)); @@ -710,7 +690,7 @@ static partial class Program } var random = new Random(); - return results.Select(kvp => (response: kvp.Value.Item1, file: kvp.Value.Item2)) + return results.Select(x => (response: x.Item1, file: x.Item2)) .Where(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.ignoreOn) .OrderByDescending(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.downrankOn) .ThenByDescending(x => Config.necessaryCond.FileSatisfies(x.file, track, x.response)) @@ -718,10 +698,11 @@ static partial class Program .ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || Config.preferredCond.AcceptNoLength) .ThenByDescending(x => !useBracketCheck || FileConditions.BracketCheck(track, inferredTrack(x).Item1)) // downrank result if it contains '(' or '[' and the title does not (avoid remixes) .ThenByDescending(x => Config.preferredCond.StrictTitleSatisfies(x.file.Filename, track.Title)) + .ThenByDescending(x => !albumMode || Config.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album)) .ThenByDescending(x => Config.preferredCond.StrictArtistSatisfies(x.file.Filename, track.Title)) .ThenByDescending(x => Config.preferredCond.LengthToleranceSatisfies(x.file, track.Length)) .ThenByDescending(x => Config.preferredCond.FormatSatisfies(x.file.Filename)) - .ThenByDescending(x => Config.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album)) + .ThenByDescending(x => albumMode || Config.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album)) .ThenByDescending(x => Config.preferredCond.BitrateSatisfies(x.file)) .ThenByDescending(x => Config.preferredCond.SampleRateSatisfies(x.file)) .ThenByDescending(x => Config.preferredCond.BitDepthSatisfies(x.file)) @@ -901,7 +882,7 @@ static partial class Program } else { - var orderedResults = OrderedResults(results, track, true); + var orderedResults = OrderedResults(results, track, useInfer: true); int count = 0; Console.WriteLine(); foreach (var (response, file) in orderedResults) @@ -971,7 +952,6 @@ static partial class Program filename = Utils.GetFileNameWithoutExtSlsk(filename).Replace(" — ", " - ").Replace('_', ' ').Trim().RemoveConsecutiveWs(); var trackNumStart = new Regex(@"^(?:(?:[0-9][-\.])?\d{2,3}[. -]|\b\d\.\s|\b\d\s-\s)(?=.+\S)"); - //var trackNumMiddle = new Regex(@"\s+-\s+(\d{2,3})(?: -|\.|)\s+|\s+-(\d{2,3})-\s+"); var trackNumMiddle = new Regex(@"(?<= - )((\d-)?\d{2,3}|\d{2,3}\.?)\s+"); var trackNumMiddleAlt = new Regex(@"\s+-(\d{2,3})-\s+");