1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 22:42:41 +00:00
This commit is contained in:
fiso64 2024-06-04 14:12:56 +02:00
parent e1859ad899
commit 2c4ee4309d
3 changed files with 170 additions and 125 deletions

View file

@ -121,7 +121,7 @@ Options:
-r --reverse Download tracks in reverse order
--name-format <format> Name format for downloaded tracks, e.g "{artist} - {title}"
--fast-search Begin downloading as soon as a file satisfying the preferred
conditions is found. Increases chance to download bad files.
conditions is found. Higher chance to download wrong files.
--m3u <option> Create an m3u8 playlist file
'none': Do not create a playlist file
'fails' (default): Write only failed downloads to the m3u

View file

@ -25,7 +25,7 @@ using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string,
// --on-complete
// --artist-col, --title-col, --album-col, --length-col, --yt-desc-col, --yt-id-col, --album-track-count-col
// --input-type, --login, --random-login, --no-modify-share-count --fast-search-delay,
// --fails-to-deprioritize (=1), --fails-to-ignore (=2)
// --fails-to-deprioritize (=1), --fails-to-ignore (=2), --invalid-replace-str
// --cond, --pref, --danger-words, --pref-danger-words, --strict-title, --strict-artist, --strict-album
// --fast-search-delay, --fast-search-min-up-speed
// --min-album-track-count, --max-album-track-count, --extract-max-track-count
@ -97,6 +97,7 @@ static class Program
static string input = "";
static bool preciseSkip = true;
static string nameFormat = "";
static string invalidReplaceStr = " ";
static bool skipNotFound = false;
static bool desperateSearch = false;
static bool noRemoveSpecialChars = false;
@ -190,7 +191,7 @@ static class Program
"\n -r --reverse Download tracks in reverse order" +
"\n --name-format <format> Name format for downloaded tracks, e.g \"{artist} - {title}\"" +
"\n --fast-search Begin downloading as soon as a file satisfying the preferred" +
"\n conditions is found. Increases chance to download bad files." +
"\n conditions is found. Higher chance to download wrong files." +
"\n --m3u <option> Create an m3u8 playlist file" +
"\n 'none': Do not create a playlist file" +
"\n 'fails' (default): Write only failed downloads to the m3u" +
@ -489,6 +490,9 @@ static class Program
case "--name-format":
nameFormat = args[++i];
break;
case "--invalid-replace-str":
invalidReplaceStr = args[++i];
break;
case "--p":
case "--print":
string opt = args[++i];
@ -705,7 +709,9 @@ static class Program
preferredCond.Formats = args[++i].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
break;
case "--plt":
case "--pref-tolerance":
case "--pref-length-tol":
case "--pref-length-tolerance":
preferredCond.LengthTolerance = int.Parse(args[++i]);
break;
case "--pmbr":
@ -758,7 +764,9 @@ static class Program
necessaryCond.Formats = args[++i].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
break;
case "--lt":
case "--tolerance":
case "--length-tol":
case "--length-tolerance":
necessaryCond.LengthTolerance = int.Parse(args[++i]);
break;
case "--mbr":
@ -973,7 +981,7 @@ static class Program
if (folderName == ".")
folderName = "";
folderName = folderName.Replace("\\", "/");
folderName = String.Join('/', folderName.Split("/").Select(x => ReplaceInvalidChars(x, " ").Trim()));
folderName = String.Join('/', folderName.Split("/").Select(x => x.ReplaceInvalidChars(invalidReplaceStr).Trim()));
folderName = folderName.Replace('/', Path.DirectorySeparatorChar);
outputFolder = Path.Combine(parentFolder, folderName);
@ -1049,7 +1057,7 @@ static class Program
if (album || aggregate)
trackLists = TrackLists.FromFlatList(trackLists.Flattened().ToList(), aggregate, album);
defaultFolderName = ReplaceInvalidChars(name, " ");
defaultFolderName = name.ReplaceInvalidChars(invalidReplaceStr);
}
@ -1144,7 +1152,7 @@ static class Program
}
defaultFolderName = ReplaceInvalidChars(playlistName, " ");
defaultFolderName = playlistName.ReplaceInvalidChars(invalidReplaceStr);
}
@ -1179,7 +1187,7 @@ static class Program
}
}
defaultFolderName = ReplaceInvalidChars(track.ToString(true), " ").Trim();
defaultFolderName = track.ToString(true).ReplaceInvalidChars(invalidReplaceStr).Trim();
}
else
{
@ -1243,7 +1251,7 @@ static class Program
}
if (aggregate || isAlbum || album)
defaultFolderName = ReplaceInvalidChars(music.ToString(true), " ").Trim();
defaultFolderName = music.ToString(true).ReplaceInvalidChars(invalidReplaceStr).Trim();
else
defaultFolderName = ".";
}
@ -1255,8 +1263,8 @@ static class Program
{
var (list, type, source) = trackLists.lists[i];
List<Track> existing = new List<Track>();
List<Track> notFound = new List<Track>();
var existing = new List<Track>();
var notFound = new List<Track>();
if (skipNotFound)
{
@ -1382,6 +1390,11 @@ static class Program
{
track.ArtistMaybeWrong = true;
}
track.Artist = track.Artist.Trim();
track.Album = track.Album.Trim();
track.Title = track.Title.Trim();
return track;
}
@ -1936,7 +1949,7 @@ static class Program
if (downloading == 0 && !searchEnded)
{
downloading = 1;
var (r, f) = fsResults.ArgMax(x => x.Value.Item1.UploadSpeed).Value;
var (r, f) = fsResults.MaxBy(x => x.Value.Item1.UploadSpeed).Value;
saveFilePath = GetSavePath(f.Filename);
fsUser = r.Username;
fsFile = f.Filename;
@ -1955,7 +1968,9 @@ static class Program
if (fastSearch && !debugDisableDownload && userSuccessCount.GetValueOrDefault(r.Username, 0) > deprioritizeOn)
{
var f = r.Files.First();
if (r.HasFreeUploadSlot && r.UploadSpeed/1024.0/1024.0 >= fastSearchMinUpSpeed && preferredCond.FileSatisfies(f, track, r))
if (r.HasFreeUploadSlot && r.UploadSpeed/1024.0/1024.0 >= fastSearchMinUpSpeed
&& BracketCheck(track, InferTrack(f.Filename, track)) && preferredCond.FileSatisfies(f, track, r))
{
fsResults.TryAdd(r.Username + "\\" + f.Filename, (r, f));
if (Interlocked.Exchange(ref fsResultsStarted, 1) == 0)
@ -2030,9 +2045,11 @@ static class Program
if (debugDisableDownload)
{
int count = 0;
Console.WriteLine();
foreach (var (response, file) in orderedResults) {
Console.WriteLine(DisplayString(track, file, response,
(printResultsFull ? necessaryCond : null), (printResultsFull ? preferredCond : null), printResultsFull, infoFirst: true));
printResultsFull ? necessaryCond : null, printResultsFull ? preferredCond : null,
fullpath: printResultsFull, infoFirst: true, showSpeed: printResultsFull));
count += 1;
}
WriteLine($"Total: {count}\n", ConsoleColor.Yellow);
@ -2137,7 +2154,7 @@ static class Program
}
}
if (nameFormat != "" && !useYtdlp)
if (nameFormat != "")
saveFilePath = ApplyNamingFormat(saveFilePath, track);
return saveFilePath;
@ -2186,10 +2203,12 @@ static class Program
if (debugDisableDownload && !debugPrintTracks)
{
Console.WriteLine();
foreach (var (response, file) in orderedResults)
{
Console.WriteLine(DisplayString(track, file, response,
(printResultsFull ? necessaryCond : null), (printResultsFull ? preferredCond : null), printResultsFull, infoFirst: true));
printResultsFull ? necessaryCond : null, printResultsFull ? preferredCond : null,
fullpath: printResultsFull, infoFirst: true, showSpeed: printResultsFull));
}
WriteLine($"Total: {orderedResults.Count()}\n", ConsoleColor.Yellow);
return default;
@ -2419,38 +2438,28 @@ static class Program
useInfer = false;
}
Dictionary<string, (Track, int)>? result = null;
Dictionary<string, (Track, int)>? infTracksAndCounts = null;
if (useInfer)
{
var equivalentFiles = EquivalentFiles(track, results.Select(x => x.Value), 1);
result = equivalentFiles
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}",
x => (x.Item1, x.Count));
.ToSafeDictionary(x => $"{x.Username}\\{x.Filename}", y => (y.Item1, y.Count));
}
(Track, int) infTrack((SearchResponse response, Soulseek.File file) x)
(Track, int) inferredTrack((SearchResponse response, Soulseek.File file) x)
{
string key = $"{x.response.Username}\\{x.file.Filename}";
if (result != null && result.ContainsKey(key))
return result[key];
if (infTracksAndCounts != null && infTracksAndCounts.ContainsKey(key))
return infTracksAndCounts[key];
return (new Track(), 0);
}
bool bracketCheck((SearchResponse response, Soulseek.File file) x)
{
Track inferredTrack = infTrack(x).Item1;
string t1 = track.Title.RemoveFt().Replace('[', '(');
string t2 = inferredTrack.Title.RemoveFt().Replace('[', '(');
return track.ArtistMaybeWrong || t1.Contains('(') || !t2.Contains('(');
}
int levenshtein((SearchResponse response, Soulseek.File file) x)
{
Track inferredTrack = infTrack(x).Item1;
string t1 = track.Title.ReplaceInvalidChars("").Replace(" ", "").Replace("_", "").RemoveFt().ToLower();
string t2 = inferredTrack.Title.ReplaceInvalidChars("").Replace(" ", "").Replace("_", "").RemoveFt().ToLower();
Track t = inferredTrack(x).Item1;
string t1 = track.Title.RemoveFt().ReplaceSpecialChars("").Replace(" ", "").Replace("_", "").ToLower();
string t2 = t.Title.RemoveFt().ReplaceSpecialChars("").Replace(" ", "").Replace("_", "").ToLower();
return Utils.Levenshtein(t1, t2);
}
@ -2461,7 +2470,7 @@ static class Program
.ThenByDescending(x => necessaryCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => preferredCond.BannedUsersSatisfies(x.response))
.ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || preferredCond.AcceptNoLength)
.ThenByDescending(x => !useBracketCheck || bracketCheck(x)) // deprioritize result if it contains '(' or '[' and the title does not (avoid remixes)
.ThenByDescending(x => !useBracketCheck || BracketCheck(track, inferredTrack(x).Item1)) // deprioritize result if it contains '(' or '[' and the title does not (avoid remixes)
.ThenByDescending(x => preferredCond.StrictTitleSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => preferredCond.LengthToleranceSatisfies(x.file, track.Length))
.ThenByDescending(x => preferredCond.FormatSatisfies(x.file.Filename))
@ -2473,14 +2482,36 @@ static class Program
.ThenByDescending(x => albumMode || FileConditions.StrictString(x.file.Filename, track.Title))
.ThenByDescending(x => !albumMode || FileConditions.StrictString(GetDirectoryNameSlsk(x.file.Filename), track.Album))
.ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.Artist, boundarySkipWs: false))
.ThenByDescending(x => !useLevenshtein || levenshtein(x) <= 5) // sorts by the distance between the track title and the inferred title of the search result
.ThenByDescending(x => x.response.UploadSpeed / 1024 / 300)
.ThenByDescending(x => (x.file.BitRate ?? 0) / 70)
.ThenByDescending(x => useInfer ? infTrack(x).Item2 : 0) // sorts by the number of occurences of this track
.ThenByDescending(x => useInfer ? inferredTrack(x).Item2 : 0) // sorts by the number of occurences of this track
.ThenByDescending(x => x.response.UploadSpeed / 1024 / 350)
.ThenByDescending(x => (x.file.BitRate ?? 0) / 80)
.ThenByDescending(x => useLevenshtein ? levenshtein(x) / 5 : 0) // sorts by the distance between the track title and the inferred title of the search result
.ThenByDescending(x => random.Next());
}
static bool BracketCheck(Track track, Track other)
{
string t1 = track.Title.RemoveFt().Replace('[', '(');
if (t1.Contains('('))
return true;
string t2 = other.Title.RemoveFt().Replace('[', '(');
if (!t2.Contains('('))
return true;
string ar = track.Artist.Replace('[', '(');
if (ar.Contains('(') && !t2.Replace(ar, "").Contains('('))
return true;
string al = track.Album.Replace('[', '(');
if (al.Contains('(') && !t2.Replace(al, "").Contains('('))
return true;
return false;
}
static async Task RunSearches(Track track, SlDictionary results, Func<int, FileConditions, FileConditions, SearchOptions> getSearchOptions,
Action<SearchResponse> responseHandler, CancellationToken ct, Action? onSearch = null)
{
@ -2697,13 +2728,7 @@ static class Program
if (!parts[0].ContainsIgnoreCase(aname) || !parts[1].ContainsIgnoreCase(tname))
{
t.ArtistMaybeWrong = true;
//if (!maybeRemix && parts[0].ContainsIgnoreCase(tname) && parts[1].ContainsIgnoreCase(aname))
//{
// t.ArtistName = realParts[1];
// t.TrackTitle = realParts[0];
//}
}
}
else if (parts.Length == 3)
{
@ -2752,12 +2777,57 @@ static class Program
t.Title = parts[2];
}
else
{
int artistPos = -1, titlePos = -1;
if (aname != "")
{
var s = parts.Select((p, i) => (p, i)).Where(x => x.p.ContainsIgnoreCase(aname));
if (s.Any())
{
artistPos = s.MinBy(x => Math.Abs(x.p.Length - aname.Length)).i;
if (artistPos != -1)
t.Artist = parts[artistPos];
}
}
if (tname != "")
{
var ss = parts.Select((p, i) => (p, i)).Where(x => x.i != artistPos && x.p.ContainsIgnoreCase(tname));
if (ss.Any())
{
titlePos = ss.MinBy(x => Math.Abs(x.p.Length - tname.Length)).i;
if (titlePos != -1)
t.Title = parts[titlePos];
}
}
}
if (t.Title == "")
{
t.Title = fname;
t.ArtistMaybeWrong = true;
}
else if (t.Artist != "" && !t.Title.ContainsIgnoreCase(defaultTrack.Title) && !t.Artist.ContainsIgnoreCase(defaultTrack.Artist))
{
string[] x = { t.Artist, t.Album, t.Title };
var perm = (0, 1, 2);
(int, int, int)[] permutations = { (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0) };
foreach (var p in permutations)
{
if (x[p.Item1].ContainsIgnoreCase(defaultTrack.Artist) && x[p.Item3].ContainsIgnoreCase(defaultTrack.Title))
{
perm = p;
break;
}
}
t.Artist = x[perm.Item1];
t.Album = x[perm.Item2];
t.Title = x[perm.Item3];
}
t.Title = t.Title.RemoveFt();
t.Artist = t.Artist.RemoveFt();
@ -3172,7 +3242,7 @@ static class Program
fname = regexRemove != "" ? Regex.Replace(fname, regexRemove, "") : fname;
fname = diacrRemove ? fname.RemoveDiacritics() : fname;
fname = fname.Trim().RemoveConsecutiveWs();
tname = tname.Replace("_", " ").ReplaceInvalidChars(" ", true, false);
tname = tname.Replace("_", " ").ReplaceInvalidChars(" ", true, true);
tname = regexRemove != "" ? Regex.Replace(tname, regexRemove, "") : tname;
tname = diacrRemove ? tname.RemoveDiacritics() : tname;
tname = tname.Trim().RemoveConsecutiveWs();
@ -3451,7 +3521,7 @@ static class Program
static string GetSaveName(string sourceFname)
{
string name = GetFileNameWithoutExtSlsk(sourceFname);
return ReplaceInvalidChars(name, " ");
return name.ReplaceInvalidChars(invalidReplaceStr);
}
static string GetAsPathSlsk(string fname)
@ -3541,7 +3611,7 @@ static class Program
TagLib.File? file = null;
try { file = TagLib.File.Create(filepath); }
catch { return filepath; }
catch { }
Regex regex = new Regex(@"(\{(?:\{??[^\{]*?\}))");
MatchCollection matches = regex.Matches(newName);
@ -3550,7 +3620,8 @@ static class Program
{
foreach (Match match in matches.Cast<Match>())
{
string inner = match.Groups[1].Value.Trim('{').Trim('}');
string inner = match.Groups[1].Value;
inner = inner.Substring(1, inner.Length - 2);
var options = inner.Split('|');
string chosenOpt = "";
@ -3559,7 +3630,7 @@ static class Program
{
string[] parts = Regex.Split(opt, @"\([^\)]*\)");
string[] result = parts.Where(part => !string.IsNullOrWhiteSpace(part)).ToArray();
if (result.All(x => GetVarValue(x, file, track) != "")) {
if (result.All(x => GetVarValue(x, file, filepath, track) != "")) {
chosenOpt = opt;
break;
}
@ -3568,10 +3639,11 @@ static class Program
chosenOpt = Regex.Replace(chosenOpt, @"\([^()]*\)|[^()]+", match =>
{
if (match.Value.StartsWith("(") && match.Value.EndsWith(")"))
return match.Value.Substring(1, match.Value.Length-2);
return match.Value.Substring(1, match.Value.Length-2).ReplaceInvalidChars(invalidReplaceStr, removeSlash: false);
else
return GetVarValue(match.Value, file, track);
return GetVarValue(match.Value, file, filepath, track).ReplaceInvalidChars(invalidReplaceStr);
});
string old = match.Groups[1].Value;
old = old.StartsWith("{{") ? old.Substring(1) : old;
newName = newName.Replace(old, chosenOpt);
@ -3580,15 +3652,14 @@ static class Program
matches = regex.Matches(newName);
}
if (newName != format)
{
string directory = Path.GetDirectoryName(filepath);
string dirsep = Path.DirectorySeparatorChar.ToString();
string directory = Path.GetDirectoryName(filepath) ?? "";
string extension = Path.GetExtension(filepath);
newName = newName.Replace(new string[] { "/", "\\" }, dirsep);
char dirsep = Path.DirectorySeparatorChar;
newName = newName.Replace('/', dirsep);
var x = newName.Split(dirsep, StringSplitOptions.RemoveEmptyEntries);
newName = string.Join(dirsep, x.Select(x => ReplaceInvalidChars(x, " ")));
newName = string.Join(dirsep, x.Select(x => x.ReplaceInvalidChars(invalidReplaceStr)));
string newFilePath = Path.Combine(directory, newName + extension);
return newFilePath;
}
@ -3596,22 +3667,22 @@ static class Program
return filepath;
}
static string GetVarValue(string x, TagLib.File file, Track track)
static string GetVarValue(string x, TagLib.File? file, string filepath, Track track)
{
switch (x)
{
case "artist":
return file.Tag.FirstPerformer ?? "";
return file?.Tag.FirstPerformer ?? "";
case "artists":
return string.Join(" & ", file.Tag.Performers);
return file != null ? string.Join(" & ", file.Tag.Performers) : "";
case "albumartist":
return file.Tag.FirstAlbumArtist ?? "";
return file?.Tag.FirstAlbumArtist ?? "";
case "albumartists":
return string.Join(" & ", file.Tag.AlbumArtists);
return file != null ? string.Join(" & ", file.Tag.AlbumArtists) : "";
case "title":
return file.Tag.Title ?? "";
return file?.Tag.Title ?? "";
case "album":
return file.Tag.Album ?? "";
return file?.Tag.Album ?? "";
case "sartist":
case "sartists":
return track.Artist;
@ -3620,13 +3691,13 @@ static class Program
case "salbum":
return track.Album;
case "year":
return file.Tag.Year.ToString() ?? "";
return file?.Tag.Year.ToString() ?? "";
case "track":
return file.Tag.Track.ToString("D2") ?? "";
return file?.Tag.Track.ToString("D2") ?? "";
case "disc":
return file.Tag.Disc.ToString() ?? "";
return file?.Tag.Disc.ToString() ?? "";
case "filename":
return Path.GetFileNameWithoutExtension(file.Name);
return Path.GetFileNameWithoutExtension(filepath);
case "foldername":
return defaultFolderName;
default:
@ -4078,28 +4149,8 @@ static class Program
return totalSeconds;
}
static string ReplaceInvalidChars(this string str, string replaceStr, bool windows = false, bool removeSlash = true)
{
char[] invalidChars = Path.GetInvalidFileNameChars();
if (windows)
invalidChars = new char[] { ':', '|', '?', '>', '<', '*', '"', '/', '\\' };
if (!removeSlash)
invalidChars = invalidChars.Where(c => c != '/' && c != '\\').ToArray();
foreach (char c in invalidChars)
str = str.Replace(c.ToString(), replaceStr);
return str;
}
static string ReplaceSpecialChars(this string str, string replaceStr)
{
string special = ";:'\"|?!<>*/\\[]{}()-–—&%^$#@+=`~_";
foreach (char c in special)
str = str.Replace(c.ToString(), replaceStr);
return str;
}
static string DisplayString(Track t, Soulseek.File? file=null, SearchResponse? response=null, FileConditions? nec=null,
FileConditions? pref=null, bool fullpath=false, string customPath="", bool infoFirst=false, bool showUser=true)
FileConditions? pref=null, bool fullpath=false, string customPath="", bool infoFirst=false, bool showUser=true, bool showSpeed=false)
{
if (file == null)
return t.ToString();
@ -4108,18 +4159,19 @@ static class Program
string bitRate = file.BitRate.HasValue ? $"{file.BitRate}kbps" : "";
string fileSize = $"{file.Size / (float)(1024 * 1024):F1}MB";
string user = showUser && response?.Username != null ? response.Username + "\\" : "";
string speed = showSpeed && response?.Username != null ? $"({response.UploadSpeed / 1024.0 / 1024.0:F2}MB/s) " : "";
string fname = fullpath ? file.Filename : (showUser ? "..\\" : "") + (customPath == "" ? GetFileNameSlsk(file.Filename) : customPath);
string length = Utils.IsMusicFile(file.Filename) ? (file.Length ?? -1).ToString() + "s" : "";
string displayText;
if (!infoFirst)
{
string info = string.Join('/', new string[] { length, sampleRate+bitRate, fileSize }.Where(value => value!=""));
displayText = $"{user}{fname} [{info}]";
displayText = $"{speed}{user}{fname} [{info}]";
}
else
{
string info = string.Join('/', new string[] { length.PadRight(4), (sampleRate+bitRate).PadRight(8), fileSize.PadLeft(6) });
displayText = $"[{info}] {user}{fname}";
displayText = $"[{info}] {speed}{user}{fname}";
}
string necStr = nec != null ? $"nec:{nec.GetNotSatisfiedName(file, t, response)}, " : "";

View file

@ -76,6 +76,32 @@ public static class Utils
return s;
}
public static string ReplaceInvalidChars(this string str, string replaceStr, bool windows = false, bool removeSlash = true, bool alwaysRemoveSlash = false)
{
char[] invalidChars = Path.GetInvalidFileNameChars();
if (windows)
invalidChars = new char[] { ':', '|', '?', '>', '<', '*', '"', '/', '\\' };
if (!removeSlash && !alwaysRemoveSlash)
invalidChars = invalidChars.Where(c => c != '/' && c != '\\').ToArray();
if (alwaysRemoveSlash)
{
var x = invalidChars.ToList();
x.AddRange(new char[] { '/', '\\' });
invalidChars = x.ToArray();
}
foreach (char c in invalidChars)
str = str.Replace(c.ToString(), replaceStr);
return str;
}
public static string ReplaceSpecialChars(this string str, string replaceStr)
{
string special = ";:'\"|?!<>*/\\[]{}()-–—&%^$#@+=`~_";
foreach (char c in special)
str = str.Replace(c.ToString(), replaceStr);
return str;
}
public static string RemoveFt(this string str, bool removeParentheses = true, bool onlyIfNonempty = true)
{
string[] ftStrings = { "feat.", "ft." };
@ -201,39 +227,6 @@ public static class Utils
return d;
}
// https://stackoverflow.com/a/35462350/13157140
public static T ArgMax<T, K>(this IEnumerable<T> source, Func<T, K> map, IComparer<K> comparer = null)
{
if (Object.ReferenceEquals(null, source))
throw new ArgumentNullException("source");
else if (Object.ReferenceEquals(null, map))
throw new ArgumentNullException("map");
T result = default(T);
K maxKey = default(K);
Boolean first = true;
if (null == comparer)
comparer = Comparer<K>.Default;
foreach (var item in source)
{
K key = map(item);
if (first || comparer.Compare(key, maxKey) > 0)
{
first = false;
maxKey = key;
result = item;
}
}
if (!first)
return result;
else
throw new ArgumentException("Can't compute ArgMax on empty sequence.", "source");
}
public static int Levenshtein(string source, string target)
{
if (source.Length == 0)