1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 14:32:40 +00:00
This commit is contained in:
fiso64 2023-11-30 13:17:11 +01:00
parent fd74ac7e93
commit efbdccb765
2 changed files with 80 additions and 113 deletions

View file

@ -96,9 +96,7 @@ Options:
--strict-title Only download if filename contains track title --strict-title Only download if filename contains track title
--strict-artist Only download if filepath contains track artist --strict-artist Only download if filepath contains track artist
--banned-users <list> Comma-separated list of users to ignore --banned-users <list> Comma-separated list of users to ignore
--danger-words <list> Comma-separated list of words that must appear in either
both search result and track title or in neither of the
two. Case-insensitive. (default:"remix, edit,cover")
--pref-format <format> Preferred file format(s), comma-separated (default: mp3) --pref-format <format> Preferred file format(s), comma-separated (default: mp3)
--pref-length-tol <sec> Preferred length tolerance in seconds (default: 2) --pref-length-tol <sec> Preferred length tolerance in seconds (default: 2)
--pref-min-bitrate <rate> Preferred minimum bitrate (default: 200) --pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)
@ -106,9 +104,6 @@ Options:
--pref-max-samplerate <rate> Preferred maximum sample rate (default: 96000) --pref-max-samplerate <rate> Preferred maximum sample rate (default: 96000)
--pref-strict-artist Prefer download if filepath contains track artist --pref-strict-artist Prefer download if filepath contains track artist
--pref-banned-users <list> Comma-separated list of users to deprioritize --pref-banned-users <list> Comma-separated list of users to deprioritize
--pref-danger-words <list> Comma-separated list of words that should appear in either
both search result and track title or in neither of the
two. (default: "mix,dj , edit,cover,(")
-s --skip-existing Skip if a track matching file conditions is found in the -s --skip-existing Skip if a track matching file conditions is found in the
output folder or your music library (if provided) output folder or your music library (if provided)
@ -129,10 +124,6 @@ Options:
--artist-search Also try to find track by searching for the artist only --artist-search Also try to find track by searching for the artist only
--no-diacr-search Also perform a search without diacritics --no-diacr-search Also perform a search without diacritics
--no-regex-search <regex> Also perform a search without a regex pattern --no-regex-search <regex> Also perform a search without a regex pattern
--levenshtein-weight <num> Results are sorted by the distance between the filename
and track title times the weight (among other things). 1
means each differing character will downrank the result, 0
disables this part of the sorting algorithm. (default: 0.5)
--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.

View file

@ -42,7 +42,6 @@ static class Program
MaxSampleRate = 96000, MaxSampleRate = 96000,
StrictTitle = true, StrictTitle = true,
StrictArtist = false, StrictArtist = false,
DangerWords = new string[] { "mix", " edit", "cover", "karaoke" },
BannedUsers = { }, BannedUsers = { },
AcceptNoLength = false, AcceptNoLength = false,
}; };
@ -55,7 +54,6 @@ static class Program
MaxSampleRate = -1, MaxSampleRate = -1,
StrictTitle = false, StrictTitle = false,
StrictArtist = false, StrictArtist = false,
DangerWords = new string[] { "remix", " edit", "cover", "karaoke" },
BannedUsers = { }, BannedUsers = { },
AcceptNoLength = true, AcceptNoLength = true,
}; };
@ -119,7 +117,6 @@ static class Program
static int maxConcurrentProcesses = 2; static int maxConcurrentProcesses = 2;
static int maxRetriesPerTrack = 30; static int maxRetriesPerTrack = 30;
static int maxResultsPerUser = 30; static int maxResultsPerUser = 30;
static double levenshteinWeight = 0.5;
static bool slowConsoleOutput = false; static bool slowConsoleOutput = false;
static object consoleLock = new object(); static object consoleLock = new object();
@ -146,6 +143,7 @@ static class Program
// --no-modify-share-count, --max-retries, --max-results-per-user, --album-search // --no-modify-share-count, --max-retries, --max-results-per-user, --album-search
// --artist-col, --title-col, --album-col, --length-col, --yt-desc-col, --yt-id-col // --artist-col, --title-col, --album-col, --length-col, --yt-desc-col, --yt-id-col
// --remove-brackets, --spotify, --csv, --string, --youtube, --random-login // --remove-brackets, --spotify, --csv, --string, --youtube, --random-login
// --danger-words, --pref-danger-words
Console.WriteLine("Usage: slsk-batchdl <input> [OPTIONS]" + Console.WriteLine("Usage: slsk-batchdl <input> [OPTIONS]" +
"\n" + "\n" +
"\n <input> <input> is one of the following:" + "\n <input> <input> is one of the following:" +
@ -210,9 +208,7 @@ static class Program
"\n --strict-title Only download if filename contains track title" + "\n --strict-title Only download if filename contains track title" +
"\n --strict-artist Only download if filepath contains track artist" + "\n --strict-artist Only download if filepath contains track artist" +
"\n --banned-users <list> Comma-separated list of users to ignore" + "\n --banned-users <list> Comma-separated list of users to ignore" +
"\n --danger-words <list> Comma-separated list of words that must appear in either" + "\n" +
"\n both search result and track title or in neither of the" +
"\n two. Case-insensitive. (default:\"remix, edit,cover\")" +
"\n --pref-format <format> Preferred file format(s), comma-separated (default: mp3)" + "\n --pref-format <format> Preferred file format(s), comma-separated (default: mp3)" +
"\n --pref-length-tol <sec> Preferred length tolerance in seconds (default: 2)" + "\n --pref-length-tol <sec> Preferred length tolerance in seconds (default: 2)" +
"\n --pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)" + "\n --pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)" +
@ -220,9 +216,6 @@ static class Program
"\n --pref-max-samplerate <rate> Preferred maximum sample rate (default: 96000)" + "\n --pref-max-samplerate <rate> Preferred maximum sample rate (default: 96000)" +
"\n --pref-strict-artist Prefer download if filepath contains track artist" + "\n --pref-strict-artist Prefer download if filepath contains track artist" +
"\n --pref-banned-users <list> Comma-separated list of users to deprioritize" + "\n --pref-banned-users <list> Comma-separated list of users to deprioritize" +
"\n --pref-danger-words <list> Comma-separated list of words that should appear in either" +
"\n both search result and track title or in neither of the" +
"\n two. (default: \"mix,dj , edit,cover,(\")" +
"\n" + "\n" +
"\n -s --skip-existing Skip if a track matching file conditions is found in the" + "\n -s --skip-existing Skip if a track matching file conditions is found in the" +
"\n output folder or your music library (if provided)" + "\n output folder or your music library (if provided)" +
@ -243,10 +236,6 @@ static class Program
"\n --artist-search Also try to find track by searching for the artist only" + "\n --artist-search Also try to find track by searching for the artist only" +
"\n --no-diacr-search Also perform a search without diacritics" + "\n --no-diacr-search Also perform a search without diacritics" +
"\n --no-regex-search <regex> Also perform a search without a regex pattern" + "\n --no-regex-search <regex> Also perform a search without a regex pattern" +
"\n --levenshtein-weight <num> Results are sorted by the distance between the filename" +
"\n and track title times the weight (among other things). 1" +
"\n means each differing character will downrank the result, 0" +
"\n disables this part of the sorting algorithm. (default: 0.5)" +
"\n --yt-dlp Use yt-dlp to download tracks that weren't found on" + "\n --yt-dlp Use yt-dlp to download tracks that weren't found on" +
"\n Soulseek. yt-dlp must be available from the command line." + "\n Soulseek. yt-dlp must be available from the command line." +
"\n" + "\n" +
@ -551,9 +540,6 @@ static class Program
case "--banned-users": case "--banned-users":
necessaryCond.BannedUsers = args[++i].Split(','); necessaryCond.BannedUsers = args[++i].Split(',');
break; break;
case "--levenshtein-weight":
levenshteinWeight = double.Parse(args[++i]);
break;
case "--slow-output": case "--slow-output":
slowConsoleOutput = true; slowConsoleOutput = true;
break; break;
@ -1026,29 +1012,28 @@ static class Program
results.TryAdd(r.Username + "\\" + file.Filename, (r, file)); results.TryAdd(r.Username + "\\" + file.Filename, (r, file));
if (++count >= maxPerUser) break; if (++count >= maxPerUser) break;
} }
//var f = r.Files.First();
var f = r.Files.First(); //if (r.HasFreeUploadSlot && r.UploadSpeed / 1000000 >= 1 && cond.FileSatisfies(f, track, r))
if (cond.FileSatisfies(f, track, r) && r.HasFreeUploadSlot && r.UploadSpeed / 1000000 >= 1) //{
{ // lock (downloadingLocker)
lock (downloadingLocker) // {
{ // if (!downloading)
if (!downloading) // {
{ // downloading = true;
downloading = true; // saveFilePath = GetSavePath(f.Filename, track);
saveFilePath = GetSavePath(f.Filename, track); // downloadTask = DownloadFile(r, f, saveFilePath, track, progress, cts);
downloadTask = DownloadFile(r, f, saveFilePath, track, progress, cts); // downloadTask.ContinueWith(task => {
downloadTask.ContinueWith(task => { // lock (downloadingLocker)
lock (downloadingLocker) // {
{ // downloading = false;
downloading = false; // saveFilePath = "";
saveFilePath = ""; // results.TryRemove(r.Username + "\\" + f.Filename, out _);
results.TryRemove(r.Username + "\\" + f.Filename, out _); // badUsers.Add(r.Username);
badUsers.Add(r.Username); // }
} // }, TaskContinuationOptions.OnlyOnFaulted);
}, TaskContinuationOptions.OnlyOnFaulted); // }
} // }
} //}
}
} }
}; };
} }
@ -1362,44 +1347,48 @@ static class Program
x => (x.Item1, x.Count)); x => (x.Item1, x.Count));
} }
var infTrack = (string fname, string uname) => { var infTrack = ((SearchResponse response, Soulseek.File file) x) => {
string key = $"{uname}\\{fname}"; string key = $"{x.response.Username}\\{x.file.Filename}";
if (result != null && result.ContainsKey(key)) if (result != null && result.ContainsKey(key))
return result[key]; return result[key];
return (new Track(), 0); return (new Track(), 0);
}; };
string t1 = track.TrackTitle.ReplaceInvalidChars("").Replace("—", "-").Replace("_", "").RemoveFt().RemoveDiacritics().Replace(" ", "").ToLower(); var bracketCheck = ((SearchResponse response, Soulseek.File file) x) => {
Track inferredTrack = infTrack(x).Item1;
string t1 = track.TrackTitle.RemoveFt().Replace('[', '(');
string t2 = inferredTrack.TrackTitle.RemoveFt().Replace('[', '(');
return track.ArtistMaybeWrong || t1.Contains('(') || !t2.Contains('(');
};
var value = (Track inferredTrack) => { var levenshtein = ((SearchResponse response, Soulseek.File file) x) => {
if (levenshteinWeight == 0) Track inferredTrack = infTrack(x).Item1;
return 100; string t1 = track.TrackTitle.ReplaceInvalidChars("").Replace(" ", "").Replace("_", "").RemoveFt().ToLower();
string t2 = inferredTrack.TrackTitle.ReplaceInvalidChars("").Replace("—", "-").Replace("_", "").RemoveFt().RemoveDiacritics().Replace(" ", "").ToLower(); string t2 = inferredTrack.TrackTitle.ReplaceInvalidChars("").Replace(" ", "").Replace("_", "").RemoveFt().ToLower();
int val = t2.Contains(t1) || track.ArtistMaybeWrong ? 100 : 50; return Utils.Levenshtein(t1, t2);
return (int)(val - Utils.Levenshtein(t1, t2) * levenshteinWeight);
}; };
var random = new Random(); var random = new Random();
return results.Select(kvp => (response: kvp.Value.Item1, file: kvp.Value.Item2)) return results.Select(kvp => (response: kvp.Value.Item1, file: kvp.Value.Item2))
.OrderByDescending(x => !ignoreUsers.Contains(x.response.Username)) .OrderByDescending(x => !ignoreUsers.Contains(x.response.Username))
.ThenByDescending(x => necessaryCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || preferredCond.AcceptNoLength) .ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || preferredCond.AcceptNoLength)
.ThenByDescending(x => preferredCond.BannedUsersSatisfies(x.response)) .ThenByDescending(x => preferredCond.BannedUsersSatisfies(x.response))
.ThenByDescending(x => value(infTrack(x.file.Filename, x.response.Username).Item1)) .ThenByDescending(x => bracketCheck(x))
.ThenByDescending(x => preferredCond.DangerWordSatisfies(x.file.Filename, track.TrackTitle, track.ArtistName))
.ThenByDescending(x => preferredCond.StrictTitleSatisfies(x.file.Filename, track.TrackTitle)) .ThenByDescending(x => preferredCond.StrictTitleSatisfies(x.file.Filename, track.TrackTitle))
.ThenByDescending(x => preferredCond.LengthToleranceSatisfies(x.file, track.Length)) .ThenByDescending(x => preferredCond.LengthToleranceSatisfies(x.file, track.Length))
.ThenByDescending(x => preferredCond.BitrateSatisfies(x.file)) .ThenByDescending(x => preferredCond.BitrateSatisfies(x.file))
.ThenByDescending(x => preferredCond.FormatSatisfies(x.file.Filename)) .ThenByDescending(x => preferredCond.FormatSatisfies(x.file.Filename))
.ThenByDescending(x => preferredCond.FileSatisfies(x.file, track, x.response)) .ThenByDescending(x => preferredCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => x.response.HasFreeUploadSlot) .ThenByDescending(x => x.response.HasFreeUploadSlot)
.ThenByDescending(x => necessaryCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => x.response.UploadSpeed / 600) .ThenByDescending(x => x.response.UploadSpeed / 600)
.ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.TrackTitle)) .ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.TrackTitle))
.ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.ArtistName)) .ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.ArtistName))
.ThenByDescending(x => -(levenshtein(x) + 1) / 3)
.ThenByDescending(x => track.Length > 0 ? -Math.Max(Math.Abs(track.Length - (x.file.Length ?? -9999)) - 1, 0) / 3 : 0) .ThenByDescending(x => track.Length > 0 ? -Math.Max(Math.Abs(track.Length - (x.file.Length ?? -9999)) - 1, 0) / 3 : 0)
.ThenByDescending(x => x.response.UploadSpeed / 300) .ThenByDescending(x => x.response.UploadSpeed / 300)
.ThenByDescending(x => (x.file.BitRate ?? 0) / 70) .ThenByDescending(x => (x.file.BitRate ?? 0) / 70)
.ThenByDescending(x => infTrack(x.file.Filename, x.response.Username).Item2) .ThenByDescending(x => infTrack(x).Item2)
.ThenByDescending(x => random.Next()); .ThenByDescending(x => random.Next());
} }
@ -1437,47 +1426,31 @@ static class Program
static Track InferTrack(string filename, Track defaultTrack) static Track InferTrack(string filename, Track defaultTrack)
{ {
Track t = new Track(defaultTrack); Track t = new Track(defaultTrack);
filename = GetFileNameWithoutExtSlsk(filename).Replace(" — ", " - ").Replace("_", " ").RemoveConsecutiveWs().Trim();
string artistName = t.ArtistName.Trim();
string trackName = t.TrackTitle.Trim();
string albumName = t.Album.Trim();
string aname = artistName, tname = trackName, alname = albumName;
string fpath = GetAsPathSlsk(filename);
string fname = GetFileNameWithoutExtSlsk(filename).Replace(" — ", " - ").Trim();
var updateIfHelps = (ref string cont, ref string str, string newCont, string newStr) => {
if (!cont.Trim().ContainsIgnoreCase(str.Trim()) && newCont.Trim().ContainsIgnoreCase(newStr.Trim())) {
cont = newCont.Trim();
str = newStr.Trim();
}
};
fname = fname.Replace("_", " ").RemoveConsecutiveWs().Trim();
aname = aname.Replace("_", " ").RemoveConsecutiveWs().Trim();
alname = alname.Replace("_", " ").RemoveConsecutiveWs().Trim();
if (aname != "")
updateIfHelps(ref fname, ref aname, fname.ReplaceInvalidChars(""), aname.ReplaceInvalidChars(""));
if (tname != "")
updateIfHelps(ref fname, ref tname, fname.ReplaceInvalidChars(""), tname.ReplaceInvalidChars(""));
if (alname != "")
updateIfHelps(ref fname, ref alname, fname.ReplaceInvalidChars(""), alname.ReplaceInvalidChars(""));
bool maybeRemix = aname != "" && (fname.ContainsIgnoreCase($"{aname} edit") || fname.ContainsIgnoreCase($"{aname} remix"));
var trackNumStart = @"^(?:(?:[0-9][-\.])?\d{2,3}[. -]|\b\d\.\s|\b\d\s-\s)(?=.+\S)"; var trackNumStart = @"^(?:(?:[0-9][-\.])?\d{2,3}[. -]|\b\d\.\s|\b\d\s-\s)(?=.+\S)";
var trackNumMiddle = @"(-\s*(\d-)?\d{2,3}|\d{2,3}\.?)\s+"; var trackNumMiddle = @"(-\s*(\d-)?\d{2,3}|\d{2,3}\.?)\s+";
if (Regex.Match(fname, trackNumStart).Success || Regex.Match(fname, trackNumMiddle).Success) filename = Regex.Replace(filename, trackNumStart, "").Trim();
{ filename = Regex.Replace(filename, trackNumMiddle, "").Trim();
fname = Regex.Replace(fname, trackNumStart, "").Trim(); if (filename.StartsWith("- ")) filename = filename.Substring(2).Trim();
fname = Regex.Replace(fname, trackNumMiddle, "").Trim();
if (fname.StartsWith("- ")) fname = fname.Substring(2).Trim();
}
string[] parts = fname.Split(new string[] { " - " }, StringSplitOptions.RemoveEmptyEntries); string aname = t.ArtistName.Trim();
string tname = t.TrackTitle.Trim();
string alname = t.Album.Trim();
string fname = filename;
fname = fname.Replace("—", "-").Replace("_", " ").Replace('[', '(').Replace(']', ')').ReplaceInvalidChars("").RemoveFt().RemoveConsecutiveWs().Trim();
tname = tname.Replace("—", "-").Replace("_", " ").Replace('[', '(').Replace(']', ')').ReplaceInvalidChars("").RemoveFt().RemoveConsecutiveWs().Trim();
aname = aname.Replace("—", "-").Replace("_", " ").Replace('[', '(').Replace(']', ')').ReplaceInvalidChars("").RemoveFt().RemoveConsecutiveWs().Trim();
alname = alname.Replace("—", "-").Replace("_", " ").Replace('[', '(').Replace(']', ')').ReplaceInvalidChars("").RemoveFt().RemoveConsecutiveWs().Trim();
bool maybeRemix = aname != "" && Regex.IsMatch(fname, @$"\({Regex.Escape(aname)} .+\)", RegexOptions.IgnoreCase);
string[] parts = fname.Split(new string[] { " - " }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
string[] realParts = filename.Split(new string[] { " - " }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length != realParts.Length)
realParts = parts;
if (parts.Length == 1) if (parts.Length == 1)
{ {
@ -1487,23 +1460,25 @@ static class Program
} }
else if (parts.Length == 2) else if (parts.Length == 2)
{ {
bool hasTitle = tname != "" && parts[1].ContainsIgnoreCase(tname); t.ArtistName = realParts[0];
bool hasArtist = aname != "" && (parts[0].ContainsIgnoreCase(aname) t.TrackTitle = realParts[1];
|| parts[1].ContainsIgnoreCase(aname + " remix") || parts[1].ContainsIgnoreCase(aname + " edit"));
if (!hasArtist && !hasTitle) if (!parts[0].ContainsIgnoreCase(aname) || !parts[1].ContainsIgnoreCase(tname))
{ {
t.ArtistMaybeWrong = true; t.ArtistMaybeWrong = true;
if (!maybeRemix && parts[0].ContainsIgnoreCase(tname) && parts[1].ContainsIgnoreCase(aname))
{
t.ArtistName = realParts[1];
t.TrackTitle = realParts[0];
}
} }
t.ArtistName = parts[0];
t.TrackTitle = parts[1];
} }
else if (parts.Length == 3) else if (parts.Length == 3)
{ {
bool hasTitle = tname != "" && parts[2].ContainsIgnoreCase(tname); bool hasTitle = tname != "" && parts[2].ContainsIgnoreCase(tname);
if (hasTitle) if (hasTitle)
t.TrackTitle = parts[2]; t.TrackTitle = realParts[2];
int artistPos = -1; int artistPos = -1;
if (aname != "") if (aname != "")
@ -1528,18 +1503,16 @@ static class Program
artistPos = 0; artistPos = 0;
albumPos = 1; albumPos = 1;
} }
if (artistPos == -1) if (artistPos == -1 && maybeRemix)
{ {
if (aname != "" && parts[2].ContainsIgnoreCase(aname + " remix") || parts[2].ContainsIgnoreCase(aname + " edit")) t.ArtistMaybeWrong = true;
{ artistPos = 0;
artistPos = 0; albumPos = 1;
albumPos = 1;
}
} }
if (artistPos == -1 && albumPos == -1) if (artistPos == -1 && albumPos == -1)
{ {
t.ArtistMaybeWrong = true; t.ArtistMaybeWrong = true;
t.ArtistName = parts[0] + " - " + parts[1]; t.ArtistName = realParts[0] + " - " + realParts[1];
} }
else if (artistPos >= 0) else if (artistPos >= 0)
{ {
@ -1888,6 +1861,9 @@ static class Program
MaxSampleRate = other.MaxSampleRate; MaxSampleRate = other.MaxSampleRate;
DangerWords = other.DangerWords.ToArray(); DangerWords = other.DangerWords.ToArray();
BannedUsers = other.BannedUsers.ToArray(); BannedUsers = other.BannedUsers.ToArray();
AcceptNoLength = other.AcceptNoLength;
StrictArtist = other.StrictArtist;
StrictTitle = other.StrictTitle;
} }
public bool FileSatisfies(Soulseek.File file, Track track, SearchResponse? response) public bool FileSatisfies(Soulseek.File file, Track track, SearchResponse? response)