mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 14:32:40 +00:00
improve result ordering code
This commit is contained in:
parent
d1228110a2
commit
4e3ed2ec46
7 changed files with 131 additions and 197 deletions
12
README.md
12
README.md
|
@ -105,8 +105,8 @@ Usage: sldl <input> [OPTIONS]
|
||||||
on a per-track basis, so it is best kept off in that case.
|
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
|
-d, --desperate Tries harder to find the desired track by searching for the
|
||||||
artist/album/title only, then filtering. (slower search)
|
artist/album/title only, then filtering. (slower search)
|
||||||
--fails-to-downrank <num> Number of fails to downrank a user's uploads (default: 1)
|
--fails-to-downrank <num> Number of fails to downrank a user's shares (default: 1)
|
||||||
--fails-to-ignore <num> Number of fails to ban/ignore a user's uploads (default: 2)
|
--fails-to-ignore <num> 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
|
--yt-dlp Use yt-dlp to download tracks that weren't found on
|
||||||
Soulseek. yt-dlp must be available from the command line.
|
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
|
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
|
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)
|
paths contain the supplied title 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.
|
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
|
### Important note
|
||||||
Some info may be unavailable depending on the client used by the peer. For example, the standard
|
Some info may be unavailable depending on the client used by the peer. For example, the standard
|
||||||
|
|
|
@ -537,9 +537,6 @@ static class Config
|
||||||
case "bannedusers":
|
case "bannedusers":
|
||||||
cond.BannedUsers = value.Split(',', tr);
|
cond.BannedUsers = value.Split(',', tr);
|
||||||
break;
|
break;
|
||||||
case "dangerwords":
|
|
||||||
cond.DangerWords = value.Split(',', tr);
|
|
||||||
break;
|
|
||||||
case "stricttitle":
|
case "stricttitle":
|
||||||
cond.StrictTitle = bool.Parse(value);
|
cond.StrictTitle = bool.Parse(value);
|
||||||
break;
|
break;
|
||||||
|
@ -967,10 +964,6 @@ static class Config
|
||||||
case "--pref-max-bitdepth":
|
case "--pref-max-bitdepth":
|
||||||
preferredCond.MaxBitDepth = int.Parse(args[++i]);
|
preferredCond.MaxBitDepth = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
case "--pdw":
|
|
||||||
case "--pref-danger-words":
|
|
||||||
preferredCond.DangerWords = args[++i].Split(',');
|
|
||||||
break;
|
|
||||||
case "--pst":
|
case "--pst":
|
||||||
case "--pstt":
|
case "--pstt":
|
||||||
case "--pref-strict-title":
|
case "--pref-strict-title":
|
||||||
|
@ -1022,10 +1015,6 @@ static class Config
|
||||||
case "--max-bitdepth":
|
case "--max-bitdepth":
|
||||||
necessaryCond.MaxBitDepth = int.Parse(args[++i]);
|
necessaryCond.MaxBitDepth = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
case "--dw":
|
|
||||||
case "--danger-words":
|
|
||||||
necessaryCond.DangerWords = args[++i].Split(',');
|
|
||||||
break;
|
|
||||||
case "--stt":
|
case "--stt":
|
||||||
case "--strict-title":
|
case "--strict-title":
|
||||||
setFlag(ref necessaryCond.StrictTitle, ref i);
|
setFlag(ref necessaryCond.StrictTitle, ref i);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
using Enums;
|
using Enums;
|
||||||
|
using Soulseek;
|
||||||
|
|
||||||
using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string, (Soulseek.SearchResponse, Soulseek.File)>;
|
|
||||||
|
|
||||||
namespace Data
|
namespace Data
|
||||||
{
|
{
|
||||||
|
@ -21,11 +21,11 @@ namespace Data
|
||||||
public TrackType Type = TrackType.Normal;
|
public TrackType Type = TrackType.Normal;
|
||||||
public FailureReason FailureReason = FailureReason.None;
|
public FailureReason FailureReason = FailureReason.None;
|
||||||
public TrackState State = TrackState.Initial;
|
public TrackState State = TrackState.Initial;
|
||||||
public SlDictionary? Downloads = null;
|
public List<(SearchResponse, Soulseek.File)>? Downloads = null;
|
||||||
|
|
||||||
public bool OutputsDirectory => Type != TrackType.Normal;
|
public bool OutputsDirectory => Type != TrackType.Normal;
|
||||||
public Soulseek.File? FirstDownload => Downloads?.FirstOrDefault().Value.Item2;
|
public Soulseek.File? FirstDownload => Downloads?.FirstOrDefault().Item2;
|
||||||
public string? FirstUsername => Downloads?.FirstOrDefault().Value.Item1.Username;
|
public string? FirstUsername => Downloads?.FirstOrDefault().Item1?.Username;
|
||||||
|
|
||||||
public Track() { }
|
public Track() { }
|
||||||
|
|
||||||
|
@ -64,13 +64,13 @@ namespace Data
|
||||||
|
|
||||||
public string ToString(bool noInfo = false)
|
public string ToString(bool noInfo = false)
|
||||||
{
|
{
|
||||||
if (IsNotAudio && Downloads != null && !Downloads.IsEmpty)
|
if (IsNotAudio && Downloads != null && Downloads.Count > 0)
|
||||||
return $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}";
|
return $"{Utils.GetFileNameSlsk(Downloads[0].Item2.Filename)}";
|
||||||
|
|
||||||
string str = Artist;
|
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)
|
else if (Title.Length > 0 || Album.Length > 0)
|
||||||
{
|
{
|
||||||
|
@ -313,12 +313,14 @@ namespace Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TrackStringComparer : IEqualityComparer<Track>
|
public class TrackComparer : IEqualityComparer<Track>
|
||||||
{
|
{
|
||||||
private bool _ignoreCase = false;
|
private bool _ignoreCase = false;
|
||||||
public TrackStringComparer(bool ignoreCase = false)
|
private int _lenTol = -1;
|
||||||
|
public TrackComparer(bool ignoreCase = false, int lenTol = -1)
|
||||||
{
|
{
|
||||||
_ignoreCase = ignoreCase;
|
_ignoreCase = ignoreCase;
|
||||||
|
_lenTol = lenTol;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(Track a, Track b)
|
public bool Equals(Track a, Track b)
|
||||||
|
@ -330,7 +332,8 @@ namespace Data
|
||||||
|
|
||||||
return string.Equals(a.Title, b.Title, comparer)
|
return string.Equals(a.Title, b.Title, comparer)
|
||||||
&& string.Equals(a.Artist, b.Artist, 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)
|
public int GetHashCode(Track a)
|
||||||
|
|
|
@ -17,10 +17,8 @@ public class FileConditions
|
||||||
public bool StrictTitle = false;
|
public bool StrictTitle = false;
|
||||||
public bool StrictArtist = false;
|
public bool StrictArtist = false;
|
||||||
public bool StrictAlbum = false;
|
public bool StrictAlbum = false;
|
||||||
public string[] DangerWords = Array.Empty<string>();
|
|
||||||
public string[] Formats = Array.Empty<string>();
|
public string[] Formats = Array.Empty<string>();
|
||||||
public string[] BannedUsers = Array.Empty<string>();
|
public string[] BannedUsers = Array.Empty<string>();
|
||||||
public string StrictStringRegexRemove = string.Empty;
|
|
||||||
public bool StrictStringDiacrRemove = true;
|
public bool StrictStringDiacrRemove = true;
|
||||||
public bool AcceptNoLength = true;
|
public bool AcceptNoLength = true;
|
||||||
public bool AcceptMissingProps = true;
|
public bool AcceptMissingProps = true;
|
||||||
|
@ -40,7 +38,6 @@ public class FileConditions
|
||||||
MinBitDepth = other.MinBitDepth;
|
MinBitDepth = other.MinBitDepth;
|
||||||
MaxBitDepth = other.MaxBitDepth;
|
MaxBitDepth = other.MaxBitDepth;
|
||||||
Formats = other.Formats.ToArray();
|
Formats = other.Formats.ToArray();
|
||||||
DangerWords = other.DangerWords.ToArray();
|
|
||||||
BannedUsers = other.BannedUsers.ToArray();
|
BannedUsers = other.BannedUsers.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,12 +55,10 @@ public class FileConditions
|
||||||
StrictTitle == other.StrictTitle &&
|
StrictTitle == other.StrictTitle &&
|
||||||
StrictArtist == other.StrictArtist &&
|
StrictArtist == other.StrictArtist &&
|
||||||
StrictAlbum == other.StrictAlbum &&
|
StrictAlbum == other.StrictAlbum &&
|
||||||
StrictStringRegexRemove == other.StrictStringRegexRemove &&
|
|
||||||
StrictStringDiacrRemove == other.StrictStringDiacrRemove &&
|
StrictStringDiacrRemove == other.StrictStringDiacrRemove &&
|
||||||
AcceptNoLength == other.AcceptNoLength &&
|
AcceptNoLength == other.AcceptNoLength &&
|
||||||
AcceptMissingProps == other.AcceptMissingProps &&
|
AcceptMissingProps == other.AcceptMissingProps &&
|
||||||
Formats.SequenceEqual(other.Formats) &&
|
Formats.SequenceEqual(other.Formats) &&
|
||||||
DangerWords.SequenceEqual(other.DangerWords) &&
|
|
||||||
BannedUsers.SequenceEqual(other.BannedUsers);
|
BannedUsers.SequenceEqual(other.BannedUsers);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -81,7 +76,7 @@ public class FileConditions
|
||||||
|
|
||||||
public bool FileSatisfies(Soulseek.File file, Track track, SearchResponse? response)
|
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)
|
&& LengthToleranceSatisfies(file, track.Length) && BitrateSatisfies(file) && SampleRateSatisfies(file)
|
||||||
&& StrictTitleSatisfies(file.Filename, track.Title) && StrictArtistSatisfies(file.Filename, track.Artist)
|
&& StrictTitleSatisfies(file.Filename, track.Title) && StrictArtistSatisfies(file.Filename, track.Artist)
|
||||||
&& StrictAlbumSatisfies(file.Filename, track.Album) && BannedUsersSatisfies(response) && BitDepthSatisfies(file);
|
&& 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)
|
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)
|
&& LengthToleranceSatisfies(file, track.Length) && BitrateSatisfies(file) && SampleRateSatisfies(file)
|
||||||
&& BitDepthSatisfies(file) && (!filenameChecks || StrictTitleSatisfies(file.Name, track.Title)
|
&& BitDepthSatisfies(file) && (!filenameChecks || StrictTitleSatisfies(file.Name, track.Title)
|
||||||
&& StrictArtistSatisfies(file.Name, track.Artist) && StrictAlbumSatisfies(file.Name, track.Album));
|
&& 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)
|
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)
|
&& LengthToleranceSatisfies(file, track.Length) && BitrateSatisfies(file) && SampleRateSatisfies(file)
|
||||||
&& BitDepthSatisfies(file) && (!filenameChecks || StrictTitleSatisfies(file.Path, track.Title)
|
&& BitDepthSatisfies(file) && (!filenameChecks || StrictTitleSatisfies(file.Path, track.Title)
|
||||||
&& StrictArtistSatisfies(file.Path, track.Artist) && StrictAlbumSatisfies(file.Path, track.Album));
|
&& 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)
|
public bool StrictTitleSatisfies(string fname, string tname, bool noPath = true)
|
||||||
{
|
{
|
||||||
if (!StrictTitle || tname.Length == 0)
|
if (!StrictTitle || tname.Length == 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
fname = noPath ? Utils.GetFileNameWithoutExtSlsk(fname) : fname;
|
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)
|
public bool StrictArtistSatisfies(string fname, string aname)
|
||||||
|
@ -142,7 +112,7 @@ public class FileConditions
|
||||||
if (!StrictArtist || aname.Length == 0)
|
if (!StrictArtist || aname.Length == 0)
|
||||||
return true;
|
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)
|
public bool StrictAlbumSatisfies(string fname, string alname)
|
||||||
|
@ -150,25 +120,24 @@ public class FileConditions
|
||||||
if (!StrictAlbum || alname.Length == 0)
|
if (!StrictAlbum || alname.Length == 0)
|
||||||
return true;
|
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 = str.Replace('_', ' ').ReplaceInvalidChars(' ', true, false);
|
||||||
str = regexRemove.Length > 0 ? Regex.Replace(str, regexRemove, "") : str;
|
|
||||||
str = diacrRemove ? str.RemoveDiacritics() : str;
|
str = diacrRemove ? str.RemoveDiacritics() : str;
|
||||||
str = str.Trim().RemoveConsecutiveWs();
|
str = str.Trim().RemoveConsecutiveWs();
|
||||||
return str;
|
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)
|
if (tname.Length == 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
fname = StrictStringPreprocess(fname, regexRemove, diacrRemove);
|
fname = StrictStringPreprocess(fname, diacrRemove);
|
||||||
tname = StrictStringPreprocess(tname, regexRemove, diacrRemove);
|
tname = StrictStringPreprocess(tname, diacrRemove);
|
||||||
|
|
||||||
if (boundarySkipWs)
|
if (boundarySkipWs)
|
||||||
return fname.ContainsWithBoundaryIgnoreWs(tname, ignoreCase, acceptLeftDigit: true);
|
return fname.ContainsWithBoundaryIgnoreWs(tname, ignoreCase, acceptLeftDigit: true);
|
||||||
|
@ -252,43 +221,24 @@ public class FileConditions
|
||||||
|
|
||||||
public string GetNotSatisfiedName(Soulseek.File file, Track track, SearchResponse? response)
|
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))
|
if (!BannedUsersSatisfies(response))
|
||||||
return "BannedUsers fails";
|
return "BannedUsers fails";
|
||||||
return "Satisfied";
|
if (!StrictTitleSatisfies(file.Filename, track.Title))
|
||||||
}
|
return "StrictTitle fails";
|
||||||
|
if (track.Type == Enums.TrackType.Album && !StrictAlbumSatisfies(file.Filename, track.Artist))
|
||||||
public string GetNotSatisfiedName(TagLib.File file, Track track)
|
return "StrictAlbum fails";
|
||||||
{
|
if (!StrictArtistSatisfies(file.Filename, track.Artist))
|
||||||
if (!DangerWordSatisfies(file.Name, track.Title, track.Artist))
|
return "StrictArtist fails";
|
||||||
return "DangerWord fails";
|
|
||||||
if (!FormatSatisfies(file.Name))
|
|
||||||
return "Format fails";
|
|
||||||
if (!LengthToleranceSatisfies(file, track.Length))
|
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))
|
if (!BitrateSatisfies(file))
|
||||||
return "Bitrate fails";
|
return "Bitrate fails";
|
||||||
if (!SampleRateSatisfies(file))
|
if (!SampleRateSatisfies(file))
|
||||||
return "SampleRate fails";
|
return "SampleRate fails";
|
||||||
if (!StrictTitleSatisfies(file.Name, track.Title))
|
|
||||||
return "StrictTitle fails";
|
|
||||||
if (!StrictArtistSatisfies(file.Name, track.Artist))
|
|
||||||
return "StrictArtist fails";
|
|
||||||
if (!BitDepthSatisfies(file))
|
if (!BitDepthSatisfies(file))
|
||||||
return "BitDepth fails";
|
return "BitDepth fails";
|
||||||
return "Satisfied";
|
return "Satisfied";
|
||||||
|
|
|
@ -83,8 +83,8 @@ public static class Help
|
||||||
on a per-track basis, so it is best kept off in that case.
|
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
|
-d, --desperate Tries harder to find the desired track by searching for the
|
||||||
artist/album/title only, then filtering. (slower search)
|
artist/album/title only, then filtering. (slower search)
|
||||||
--fails-to-downrank <num> Number of fails to downrank a user's uploads (default: 1)
|
--fails-to-downrank <num> Number of fails to downrank a user's shares (default: 1)
|
||||||
--fails-to-ignore <num> Number of fails to ban/ignore a user's uploads (default: 2)
|
--fails-to-ignore <num> 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
|
--yt-dlp Use yt-dlp to download tracks that weren't found on
|
||||||
Soulseek. yt-dlp must be available from the command line.
|
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
|
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
|
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)
|
paths contain the supplied title 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.
|
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
|
Important note
|
||||||
Some info may be unavailable depending on the client used by the peer. For example, the standard
|
Some info may be unavailable depending on the client used by the peer. For example, the standard
|
||||||
|
|
|
@ -350,12 +350,16 @@ static partial class Program
|
||||||
else if (tle.source.Type == TrackType.Album)
|
else if (tle.source.Type == TrackType.Album)
|
||||||
{
|
{
|
||||||
Console.WriteLine(new string('-', 60));
|
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 (tle.list.Count > 0 && tle.list[0].Count > 0)
|
||||||
{
|
{
|
||||||
if (!Config.noBrowseFolder)
|
if (!Config.noBrowseFolder)
|
||||||
Console.WriteLine("[Skipped full folder retrieval]");
|
Console.WriteLine("[Skipping full folder retrieval]");
|
||||||
|
|
||||||
foreach (var ls in tle.list)
|
foreach (var ls in tle.list)
|
||||||
{
|
{
|
||||||
|
@ -430,12 +434,12 @@ static partial class Program
|
||||||
|
|
||||||
static void PrintAlbum(List<Track> albumTracks, bool retrieveAll = false)
|
static void PrintAlbum(List<Track> albumTracks, bool retrieveAll = false)
|
||||||
{
|
{
|
||||||
if (albumTracks.Count == 0 && albumTracks[0].Downloads.IsEmpty)
|
if (albumTracks.Count == 0 && albumTracks[0].Downloads.Count == 0)
|
||||||
return;
|
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)";
|
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();
|
Console.WriteLine();
|
||||||
WriteLine($"User : {userInfo}\nFolder: {parents}\nProps : {props}", ConsoleColor.White);
|
WriteLine($"User : {userInfo}\nFolder: {parents}\nProps : {props}", ConsoleColor.White);
|
||||||
|
@ -623,23 +627,23 @@ static partial class Program
|
||||||
{
|
{
|
||||||
var albumArtList = list
|
var albumArtList = list
|
||||||
//.Where(tracks => tracks)
|
//.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());
|
.Where(tracks => tracks.Any());
|
||||||
|
|
||||||
if (option == AlbumArtOption.Largest)
|
if (option == AlbumArtOption.Largest)
|
||||||
{
|
{
|
||||||
list = albumArtList
|
list = albumArtList
|
||||||
.OrderByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Max() / 1024 / 100)
|
.OrderByDescending(tracks => tracks.Select(t => t.Downloads[0].Item2.Size).Max() / 1024 / 100)
|
||||||
.ThenByDescending(tracks => tracks.First().Downloads.First().Value.Item1.UploadSpeed / 1024 / 300)
|
.ThenByDescending(tracks => tracks.First().Downloads[0].Item1.UploadSpeed / 1024 / 300)
|
||||||
.ThenByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Sum() / 1024 / 100)
|
.ThenByDescending(tracks => tracks.Select(t => t.Downloads[0].Item2.Size).Sum() / 1024 / 100)
|
||||||
.Select(x => x.ToList()).ToList();
|
.Select(x => x.ToList()).ToList();
|
||||||
}
|
}
|
||||||
else if (option == AlbumArtOption.Most)
|
else if (option == AlbumArtOption.Most)
|
||||||
{
|
{
|
||||||
list = albumArtList
|
list = albumArtList
|
||||||
.OrderByDescending(tracks => tracks.Count())
|
.OrderByDescending(tracks => tracks.Count())
|
||||||
.ThenByDescending(tracks => tracks.First().Downloads.First().Value.Item1.UploadSpeed / 1024 / 300)
|
.ThenByDescending(tracks => tracks.First().Downloads[0].Item1.UploadSpeed / 1024 / 300)
|
||||||
.ThenByDescending(tracks => tracks.Select(t => t.Downloads.First().Value.Item2.Size).Sum() / 1024 / 100)
|
.ThenByDescending(tracks => tracks.Select(t => t.Downloads[0].Item2.Size).Sum() / 1024 / 100)
|
||||||
.Select(x => x.ToList()).ToList();
|
.Select(x => x.ToList()).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -655,7 +659,7 @@ static partial class Program
|
||||||
else if (option == AlbumArtOption.Largest)
|
else if (option == AlbumArtOption.Largest)
|
||||||
{
|
{
|
||||||
long curMax = dlFiles.Keys.Where(x => Utils.IsImageFile(x) && File.Exists(x)).Max(x => new FileInfo(x).Length);
|
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;
|
return need;
|
||||||
|
@ -693,7 +697,7 @@ static partial class Program
|
||||||
if (!Config.noBrowseFolder && !Config.interactiveMode && !retrievedFolders.Contains(soulseekFolderPathPrefix))
|
if (!Config.noBrowseFolder && !Config.interactiveMode && !retrievedFolders.Contains(soulseekFolderPathPrefix))
|
||||||
{
|
{
|
||||||
Console.WriteLine("Getting all files in folder...");
|
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);
|
await CompleteFolder(tracks, response, soulseekFolderPathPrefix);
|
||||||
retrievedFolders.Add(soulseekFolderPathPrefix);
|
retrievedFolders.Add(soulseekFolderPathPrefix);
|
||||||
}
|
}
|
||||||
|
@ -865,9 +869,9 @@ static partial class Program
|
||||||
static string GetCommonPathPrefix(List<Track> tracks)
|
static string GetCommonPathPrefix(List<Track> tracks)
|
||||||
{
|
{
|
||||||
if (tracks.Count == 1)
|
if (tracks.Count == 1)
|
||||||
return Utils.GetDirectoryNameSlsk(tracks.First().Downloads.First().Value.Item2.Filename);
|
return Utils.GetDirectoryNameSlsk(tracks.First().Downloads[0].Item2.Filename);
|
||||||
else
|
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();
|
Console.WriteLine();
|
||||||
var tracks = list[aidx];
|
var tracks = list[aidx];
|
||||||
var response = tracks[0].Downloads.First().Value.Item1;
|
var response = tracks[0].Downloads[0].Item1;
|
||||||
|
|
||||||
var folder = GetCommonPathPrefix(tracks);
|
var folder = GetCommonPathPrefix(tracks);
|
||||||
if (retrieveFolder && !Config.noBrowseFolder && !retrievedFolders.Contains(folder))
|
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)";
|
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($"[{aidx + 1} / {list.Count}]", ConsoleColor.DarkGray);
|
||||||
WriteLine($"User : {userInfo}\nFolder: {parents}\nProps : {props}", ConsoleColor.White);
|
WriteLine($"User : {userInfo}\nFolder: {parents}\nProps : {props}", ConsoleColor.White);
|
||||||
|
@ -1396,7 +1400,7 @@ static partial class Program
|
||||||
string ancestor = "";
|
string ancestor = "";
|
||||||
|
|
||||||
if (showAncestors)
|
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)
|
if (pathsOnly)
|
||||||
{
|
{
|
||||||
|
@ -1405,9 +1409,9 @@ static partial class Program
|
||||||
foreach (var x in tracks[i].Downloads)
|
foreach (var x in tracks[i].Downloads)
|
||||||
{
|
{
|
||||||
if (ancestor.Length == 0)
|
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
|
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)
|
foreach (var x in tracks[i].Downloads)
|
||||||
{
|
{
|
||||||
if (ancestor.Length == 0)
|
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
|
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();
|
if (tracks[i].Downloads?.Count > 0) Console.WriteLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
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}");
|
Console.WriteLine($" Shares: {tracks[i].Downloads.Count}");
|
||||||
foreach (var x in tracks[i].Downloads)
|
foreach (var x in tracks[i].Downloads)
|
||||||
{
|
{
|
||||||
if (ancestor.Length == 0)
|
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
|
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();
|
Console.WriteLine();
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ static partial class Program
|
||||||
throw new Exception();
|
throw new Exception();
|
||||||
|
|
||||||
responseData ??= new ResponseData();
|
responseData ??= new ResponseData();
|
||||||
|
IEnumerable<(SlResponse response, SlFile file)>? orderedResults = null;
|
||||||
var progress = GetProgressBar(Config.displayMode);
|
var progress = GetProgressBar(Config.displayMode);
|
||||||
var results = new SlDictionary();
|
var results = new SlDictionary();
|
||||||
var fsResults = new SlDictionary();
|
var fsResults = new SlDictionary();
|
||||||
|
@ -38,7 +39,7 @@ static partial class Program
|
||||||
|
|
||||||
if (track.Downloads != null)
|
if (track.Downloads != null)
|
||||||
{
|
{
|
||||||
results = track.Downloads;
|
orderedResults = track.Downloads;
|
||||||
goto downloads;
|
goto downloads;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,9 +142,10 @@ static partial class Program
|
||||||
|
|
||||||
downloads:
|
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;
|
int trackTries = Config.maxRetriesPerTrack;
|
||||||
async Task<bool> process(SlResponse response, SlFile file)
|
async Task<bool> process(SlResponse response, SlFile file)
|
||||||
|
@ -376,13 +378,12 @@ static partial class Program
|
||||||
Album = track.Album,
|
Album = track.Album,
|
||||||
Length = x.file.Length ?? -1,
|
Length = x.file.Length ?? -1,
|
||||||
IsNotAudio = !Utils.IsMusicFile(x.file.Filename),
|
IsNotAudio = !Utils.IsMusicFile(x.file.Filename),
|
||||||
Downloads = new ConcurrentDictionary<string, (SlResponse, SlFile file)>(
|
Downloads = new() { x },
|
||||||
new Dictionary<string, (SlResponse response, SlFile file)> { { x.response.Username + '\\' + x.file.Filename, x } })
|
|
||||||
};
|
};
|
||||||
ls.Add(t);
|
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);
|
result.Add(ls);
|
||||||
}
|
}
|
||||||
|
@ -396,9 +397,9 @@ static partial class Program
|
||||||
|
|
||||||
static async Task<List<Track>> GetAggregateTracks(Track track, ResponseData responseData)
|
static async Task<List<Track>> GetAggregateTracks(Track track, ResponseData responseData)
|
||||||
{
|
{
|
||||||
var results = new ConcurrentDictionary<string, (SearchResponse, Soulseek.File)>();
|
var results = new SlDictionary();
|
||||||
SearchOptions getSearchOptions(int timeout, FileConditions nec, FileConditions prf) =>
|
SearchOptions getSearchOptions(int timeout, FileConditions nec, FileConditions prf) =>
|
||||||
new SearchOptions(
|
new (
|
||||||
minimumResponseFileCount: 1,
|
minimumResponseFileCount: 1,
|
||||||
minimumPeerUploadSpeed: 1,
|
minimumPeerUploadSpeed: 1,
|
||||||
removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms,
|
removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms,
|
||||||
|
@ -410,9 +411,6 @@ static partial class Program
|
||||||
fileFilter: (file) =>
|
fileFilter: (file) =>
|
||||||
{
|
{
|
||||||
return Utils.IsMusicFile(file.Filename) && nec.FileSatisfies(file, track, null);
|
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)
|
void handler(SlResponse r)
|
||||||
|
@ -433,11 +431,8 @@ static partial class Program
|
||||||
string trackName = track.Title.Trim();
|
string trackName = track.Title.Trim();
|
||||||
string albumName = track.Album.Trim();
|
string albumName = track.Album.Trim();
|
||||||
|
|
||||||
//var orderedResults = OrderedResults(results, track, false, false, false);
|
var equivalentFiles = EquivalentFiles(track, results.Select(x => x.Value))
|
||||||
|
.Select(x => (x.Item1, OrderedResults(x.Item2, track, false, false, false))).ToList();
|
||||||
var fileResponses = results.Select(x => x.Value);
|
|
||||||
|
|
||||||
var equivalentFiles = EquivalentFiles(track, fileResponses).ToList();
|
|
||||||
|
|
||||||
if (!Config.relax)
|
if (!Config.relax)
|
||||||
{
|
{
|
||||||
|
@ -452,8 +447,7 @@ static partial class Program
|
||||||
var tracks = equivalentFiles
|
var tracks = equivalentFiles
|
||||||
.Select(kvp =>
|
.Select(kvp =>
|
||||||
{
|
{
|
||||||
kvp.Item1.Downloads = new SlDictionary(
|
kvp.Item1.Downloads = kvp.Item2.ToList();
|
||||||
kvp.Item2.ToDictionary(item => { return item.response.Username + "\\" + item.file.Filename; }, item => item));
|
|
||||||
return kvp.Item1;
|
return kvp.Item1;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
@ -498,6 +492,7 @@ static partial class Program
|
||||||
sortedLengthLists.Add((sortedLengths, album, user));
|
sortedLengthLists.Add((sortedLengths, album, user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var usernamesList = new List<HashSet<string>>();
|
||||||
var lengthsList = new List<int[]>();
|
var lengthsList = new List<int[]>();
|
||||||
var res = new List<List<List<Track>>>();
|
var res = new List<List<List<Track>>>();
|
||||||
|
|
||||||
|
@ -511,8 +506,8 @@ static partial class Program
|
||||||
{
|
{
|
||||||
if (lengths.Length == 1 && lengthsList[i].Length == 1)
|
if (lengths.Length == 1 && lengthsList[i].Length == 1)
|
||||||
{
|
{
|
||||||
var t1 = InferTrack(album[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.First().Value.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))
|
if ((t2.Artist.ContainsIgnoreCase(t1.Artist) || t1.Artist.ContainsIgnoreCase(t2.Artist))
|
||||||
&& (t2.Title.ContainsIgnoreCase(t1.Title) || t1.Title.ContainsIgnoreCase(t2.Title)))
|
&& (t2.Title.ContainsIgnoreCase(t1.Title) || t1.Title.ContainsIgnoreCase(t2.Title)))
|
||||||
|
@ -527,6 +522,7 @@ static partial class Program
|
||||||
|
|
||||||
if (found)
|
if (found)
|
||||||
{
|
{
|
||||||
|
usernamesList[i].Add(user);
|
||||||
res[i].Add(album);
|
res[i].Add(album);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -539,12 +535,17 @@ static partial class Program
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
usernamesList.Add(new() { user });
|
||||||
lengthsList.Add(lengths);
|
lengthsList.Add(lengths);
|
||||||
res.Add(new List<List<Track>> { album });
|
res.Add(new List<List<Track>> { 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
|
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)
|
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];
|
var first = tracks[0];
|
||||||
|
|
||||||
foreach ((var dir, var file) in allFiles)
|
foreach ((var dir, var file) in allFiles)
|
||||||
|
@ -600,8 +601,7 @@ static partial class Program
|
||||||
Artist = first.Artist,
|
Artist = first.Artist,
|
||||||
Album = first.Album,
|
Album = first.Album,
|
||||||
IsNotAudio = !Utils.IsMusicFile(file.Filename),
|
IsNotAudio = !Utils.IsMusicFile(file.Filename),
|
||||||
Downloads = new ConcurrentDictionary<string, (SlResponse, SlFile file)>(
|
Downloads = new() { (response, newFile) }
|
||||||
new Dictionary<string, (SlResponse response, SlFile file)> { { response.Username + '\\' + fullPath, (response, newFile) } })
|
|
||||||
};
|
};
|
||||||
tracks.Add(t);
|
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)
|
IEnumerable<(SlResponse, SlFile)> fileResponses, int minShares = -1)
|
||||||
{
|
{
|
||||||
if (minShares == -1)
|
if (minShares == -1)
|
||||||
|
@ -628,53 +628,32 @@ static partial class Program
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
var res = fileResponses
|
var groups = fileResponses
|
||||||
.GroupBy(inferTrack, new TrackStringComparer(ignoreCase: true))
|
.GroupBy(inferTrack, new TrackComparer(ignoreCase: true, Config.necessaryCond.LengthTolerance))
|
||||||
.Where(group => group.Select(x => x.Item1.Username).Distinct().Count() >= minShares)
|
.Select(x => (x, x.Select(y => y.Item1.Username).Distinct().Count()))
|
||||||
.SelectMany(group =>
|
.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();
|
if (x.Key.Length == -1)
|
||||||
var groups = new List<(Track, List<(SearchResponse, Soulseek.File)>)>();
|
x.Key.Length = x.FirstOrDefault(y => y.Item2.Length != null).Item2?.Length ?? -1;
|
||||||
var noLengthGroup = group.Where(x => x.Item2.Length == null);
|
return (x.Key, x.AsEnumerable());
|
||||||
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 (noLengthGroup.Any())
|
return groups;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static IOrderedEnumerable<(SlResponse response, SlFile file)> OrderedResults(IEnumerable<KeyValuePair<string, (SlResponse, SlFile)>> results,
|
static IOrderedEnumerable<(SlResponse response, SlFile file)> OrderedResults(IEnumerable<KeyValuePair<string, (SlResponse, SlFile)>> results,
|
||||||
Track track, bool useInfer = false, bool useLevenshtein = true, bool albumMode = false)
|
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;
|
bool useBracketCheck = true;
|
||||||
if (albumMode)
|
if (albumMode)
|
||||||
|
@ -685,9 +664,10 @@ static partial class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
Dictionary<string, (Track, int)>? infTracksAndCounts = null;
|
Dictionary<string, (Track, int)>? 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
|
infTracksAndCounts = equivalentFiles
|
||||||
.SelectMany(t => t.Item2, (t, f) => new { t.Item1, f.response.Username, f.file.Filename, Count = t.Item2.Count() })
|
.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));
|
.ToSafeDictionary(x => $"{x.Username}\\{x.Filename}", y => (y.Item1, y.Count));
|
||||||
|
@ -710,7 +690,7 @@ static partial class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
var random = new Random();
|
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)
|
.Where(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.ignoreOn)
|
||||||
.OrderByDescending(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.downrankOn)
|
.OrderByDescending(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.downrankOn)
|
||||||
.ThenByDescending(x => Config.necessaryCond.FileSatisfies(x.file, track, x.response))
|
.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 => (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 => !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 => 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.StrictArtistSatisfies(x.file.Filename, track.Title))
|
||||||
.ThenByDescending(x => Config.preferredCond.LengthToleranceSatisfies(x.file, track.Length))
|
.ThenByDescending(x => Config.preferredCond.LengthToleranceSatisfies(x.file, track.Length))
|
||||||
.ThenByDescending(x => Config.preferredCond.FormatSatisfies(x.file.Filename))
|
.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.BitrateSatisfies(x.file))
|
||||||
.ThenByDescending(x => Config.preferredCond.SampleRateSatisfies(x.file))
|
.ThenByDescending(x => Config.preferredCond.SampleRateSatisfies(x.file))
|
||||||
.ThenByDescending(x => Config.preferredCond.BitDepthSatisfies(x.file))
|
.ThenByDescending(x => Config.preferredCond.BitDepthSatisfies(x.file))
|
||||||
|
@ -901,7 +882,7 @@ static partial class Program
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var orderedResults = OrderedResults(results, track, true);
|
var orderedResults = OrderedResults(results, track, useInfer: true);
|
||||||
int count = 0;
|
int count = 0;
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
foreach (var (response, file) in orderedResults)
|
foreach (var (response, file) in orderedResults)
|
||||||
|
@ -971,7 +952,6 @@ static partial class Program
|
||||||
filename = Utils.GetFileNameWithoutExtSlsk(filename).Replace(" — ", " - ").Replace('_', ' ').Trim().RemoveConsecutiveWs();
|
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 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 trackNumMiddle = new Regex(@"(?<= - )((\d-)?\d{2,3}|\d{2,3}\.?)\s+");
|
||||||
var trackNumMiddleAlt = new Regex(@"\s+-(\d{2,3})-\s+");
|
var trackNumMiddleAlt = new Regex(@"\s+-(\d{2,3})-\s+");
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue