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 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 -r --reverse Download tracks in reverse order
--name-format <format> Name format for downloaded tracks, e.g "{artist} - {title}" --name-format <format> Name format for downloaded tracks, e.g "{artist} - {title}"
--fast-search Begin downloading as soon as a file satisfying the preferred --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 --m3u <option> Create an m3u8 playlist file
'none': Do not create a playlist file 'none': Do not create a playlist file
'fails' (default): Write only failed downloads to the m3u 'fails' (default): Write only failed downloads to the m3u

View file

@ -25,7 +25,7 @@ using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string,
// --on-complete // --on-complete
// --artist-col, --title-col, --album-col, --length-col, --yt-desc-col, --yt-id-col, --album-track-count-col // --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, // --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 // --cond, --pref, --danger-words, --pref-danger-words, --strict-title, --strict-artist, --strict-album
// --fast-search-delay, --fast-search-min-up-speed // --fast-search-delay, --fast-search-min-up-speed
// --min-album-track-count, --max-album-track-count, --extract-max-track-count // --min-album-track-count, --max-album-track-count, --extract-max-track-count
@ -97,6 +97,7 @@ static class Program
static string input = ""; static string input = "";
static bool preciseSkip = true; static bool preciseSkip = true;
static string nameFormat = ""; static string nameFormat = "";
static string invalidReplaceStr = " ";
static bool skipNotFound = false; static bool skipNotFound = false;
static bool desperateSearch = false; static bool desperateSearch = false;
static bool noRemoveSpecialChars = false; static bool noRemoveSpecialChars = false;
@ -168,7 +169,7 @@ static class Program
"\n Provide a --youtube-key to include unavailabe uploads." + "\n Provide a --youtube-key to include unavailabe uploads." +
"\n" + "\n" +
"\n Path to a local CSV file: Use a csv file containing track" + "\n Path to a local CSV file: Use a csv file containing track" +
"\n info to download. The names of the columns should be Artist, " + "\n info to download. The names of the columns should be Artist," +
"\n Title, Album, Length. Only the title or album column is" + "\n Title, Album, Length. Only the title or album column is" +
"\n required, but extra info may improve search results." + "\n required, but extra info may improve search results." +
"\n" + "\n" +
@ -190,7 +191,7 @@ static class Program
"\n -r --reverse Download tracks in reverse order" + "\n -r --reverse Download tracks in reverse order" +
"\n --name-format <format> Name format for downloaded tracks, e.g \"{artist} - {title}\"" + "\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 --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 --m3u <option> Create an m3u8 playlist file" +
"\n 'none': Do not create a playlist file" + "\n 'none': Do not create a playlist file" +
"\n 'fails' (default): Write only failed downloads to the m3u" + "\n 'fails' (default): Write only failed downloads to the m3u" +
@ -489,6 +490,9 @@ static class Program
case "--name-format": case "--name-format":
nameFormat = args[++i]; nameFormat = args[++i];
break; break;
case "--invalid-replace-str":
invalidReplaceStr = args[++i];
break;
case "--p": case "--p":
case "--print": case "--print":
string opt = args[++i]; string opt = args[++i];
@ -705,7 +709,9 @@ static class Program
preferredCond.Formats = args[++i].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); preferredCond.Formats = args[++i].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
break; break;
case "--plt": case "--plt":
case "--pref-tolerance":
case "--pref-length-tol": case "--pref-length-tol":
case "--pref-length-tolerance":
preferredCond.LengthTolerance = int.Parse(args[++i]); preferredCond.LengthTolerance = int.Parse(args[++i]);
break; break;
case "--pmbr": case "--pmbr":
@ -758,7 +764,9 @@ static class Program
necessaryCond.Formats = args[++i].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); necessaryCond.Formats = args[++i].Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
break; break;
case "--lt": case "--lt":
case "--tolerance":
case "--length-tol": case "--length-tol":
case "--length-tolerance":
necessaryCond.LengthTolerance = int.Parse(args[++i]); necessaryCond.LengthTolerance = int.Parse(args[++i]);
break; break;
case "--mbr": case "--mbr":
@ -973,7 +981,7 @@ static class Program
if (folderName == ".") if (folderName == ".")
folderName = ""; folderName = "";
folderName = folderName.Replace("\\", "/"); 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); folderName = folderName.Replace('/', Path.DirectorySeparatorChar);
outputFolder = Path.Combine(parentFolder, folderName); outputFolder = Path.Combine(parentFolder, folderName);
@ -1049,7 +1057,7 @@ static class Program
if (album || aggregate) if (album || aggregate)
trackLists = TrackLists.FromFlatList(trackLists.Flattened().ToList(), aggregate, album); 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 else
{ {
@ -1243,7 +1251,7 @@ static class Program
} }
if (aggregate || isAlbum || album) if (aggregate || isAlbum || album)
defaultFolderName = ReplaceInvalidChars(music.ToString(true), " ").Trim(); defaultFolderName = music.ToString(true).ReplaceInvalidChars(invalidReplaceStr).Trim();
else else
defaultFolderName = "."; defaultFolderName = ".";
} }
@ -1255,8 +1263,8 @@ static class Program
{ {
var (list, type, source) = trackLists.lists[i]; var (list, type, source) = trackLists.lists[i];
List<Track> existing = new List<Track>(); var existing = new List<Track>();
List<Track> notFound = new List<Track>(); var notFound = new List<Track>();
if (skipNotFound) if (skipNotFound)
{ {
@ -1382,6 +1390,11 @@ static class Program
{ {
track.ArtistMaybeWrong = true; track.ArtistMaybeWrong = true;
} }
track.Artist = track.Artist.Trim();
track.Album = track.Album.Trim();
track.Title = track.Title.Trim();
return track; return track;
} }
@ -1936,7 +1949,7 @@ static class Program
if (downloading == 0 && !searchEnded) if (downloading == 0 && !searchEnded)
{ {
downloading = 1; 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); saveFilePath = GetSavePath(f.Filename);
fsUser = r.Username; fsUser = r.Username;
fsFile = f.Filename; fsFile = f.Filename;
@ -1955,7 +1968,9 @@ static class Program
if (fastSearch && !debugDisableDownload && userSuccessCount.GetValueOrDefault(r.Username, 0) > deprioritizeOn) if (fastSearch && !debugDisableDownload && userSuccessCount.GetValueOrDefault(r.Username, 0) > deprioritizeOn)
{ {
var f = r.Files.First(); 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)); fsResults.TryAdd(r.Username + "\\" + f.Filename, (r, f));
if (Interlocked.Exchange(ref fsResultsStarted, 1) == 0) if (Interlocked.Exchange(ref fsResultsStarted, 1) == 0)
@ -2030,9 +2045,11 @@ static class Program
if (debugDisableDownload) if (debugDisableDownload)
{ {
int count = 0; int count = 0;
Console.WriteLine();
foreach (var (response, file) in orderedResults) { foreach (var (response, file) in orderedResults) {
Console.WriteLine(DisplayString(track, file, response, 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; count += 1;
} }
WriteLine($"Total: {count}\n", ConsoleColor.Yellow); WriteLine($"Total: {count}\n", ConsoleColor.Yellow);
@ -2137,7 +2154,7 @@ static class Program
} }
} }
if (nameFormat != "" && !useYtdlp) if (nameFormat != "")
saveFilePath = ApplyNamingFormat(saveFilePath, track); saveFilePath = ApplyNamingFormat(saveFilePath, track);
return saveFilePath; return saveFilePath;
@ -2186,10 +2203,12 @@ static class Program
if (debugDisableDownload && !debugPrintTracks) if (debugDisableDownload && !debugPrintTracks)
{ {
Console.WriteLine();
foreach (var (response, file) in orderedResults) foreach (var (response, file) in orderedResults)
{ {
Console.WriteLine(DisplayString(track, file, response, 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); WriteLine($"Total: {orderedResults.Count()}\n", ConsoleColor.Yellow);
return default; return default;
@ -2419,38 +2438,28 @@ static class Program
useInfer = false; useInfer = false;
} }
Dictionary<string, (Track, int)>? result = null; Dictionary<string, (Track, int)>? infTracksAndCounts = null;
if (useInfer) if (useInfer)
{ {
var equivalentFiles = EquivalentFiles(track, results.Select(x => x.Value), 1); 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() }) .SelectMany(t => t.Item2, (t, f) => new { t.Item1, f.response.Username, f.file.Filename, Count = t.Item2.Count() })
.ToSafeDictionary( .ToSafeDictionary(x => $"{x.Username}\\{x.Filename}", y => (y.Item1, y.Count));
x => $"{x.Username}\\{x.Filename}",
x => (x.Item1, x.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}"; string key = $"{x.response.Username}\\{x.file.Filename}";
if (result != null && result.ContainsKey(key)) if (infTracksAndCounts != null && infTracksAndCounts.ContainsKey(key))
return result[key]; return infTracksAndCounts[key];
return (new Track(), 0); 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) int levenshtein((SearchResponse response, Soulseek.File file) x)
{ {
Track inferredTrack = infTrack(x).Item1; Track t = inferredTrack(x).Item1;
string t1 = track.Title.ReplaceInvalidChars("").Replace(" ", "").Replace("_", "").RemoveFt().ToLower(); string t1 = track.Title.RemoveFt().ReplaceSpecialChars("").Replace(" ", "").Replace("_", "").ToLower();
string t2 = inferredTrack.Title.ReplaceInvalidChars("").Replace(" ", "").Replace("_", "").RemoveFt().ToLower(); string t2 = t.Title.RemoveFt().ReplaceSpecialChars("").Replace(" ", "").Replace("_", "").ToLower();
return Utils.Levenshtein(t1, t2); return Utils.Levenshtein(t1, t2);
} }
@ -2461,7 +2470,7 @@ static class Program
.ThenByDescending(x => necessaryCond.FileSatisfies(x.file, track, x.response)) .ThenByDescending(x => necessaryCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => preferredCond.BannedUsersSatisfies(x.response)) .ThenByDescending(x => preferredCond.BannedUsersSatisfies(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 => !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.StrictTitleSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => preferredCond.LengthToleranceSatisfies(x.file, track.Length)) .ThenByDescending(x => preferredCond.LengthToleranceSatisfies(x.file, track.Length))
.ThenByDescending(x => preferredCond.FormatSatisfies(x.file.Filename)) .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(x.file.Filename, track.Title))
.ThenByDescending(x => !albumMode || FileConditions.StrictString(GetDirectoryNameSlsk(x.file.Filename), track.Album)) .ThenByDescending(x => !albumMode || FileConditions.StrictString(GetDirectoryNameSlsk(x.file.Filename), track.Album))
.ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.Artist, boundarySkipWs: false)) .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 => useInfer ? inferredTrack(x).Item2 : 0) // sorts by the number of occurences of this track
.ThenByDescending(x => x.response.UploadSpeed / 1024 / 300) .ThenByDescending(x => x.response.UploadSpeed / 1024 / 350)
.ThenByDescending(x => (x.file.BitRate ?? 0) / 70) .ThenByDescending(x => (x.file.BitRate ?? 0) / 80)
.ThenByDescending(x => useInfer ? infTrack(x).Item2 : 0) // sorts by the number of occurences of this track .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()); .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, static async Task RunSearches(Track track, SlDictionary results, Func<int, FileConditions, FileConditions, SearchOptions> getSearchOptions,
Action<SearchResponse> responseHandler, CancellationToken ct, Action? onSearch = null) Action<SearchResponse> responseHandler, CancellationToken ct, Action? onSearch = null)
{ {
@ -2697,13 +2728,7 @@ static class Program
if (!parts[0].ContainsIgnoreCase(aname) || !parts[1].ContainsIgnoreCase(tname)) 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];
//}
} }
} }
else if (parts.Length == 3) else if (parts.Length == 3)
{ {
@ -2752,12 +2777,57 @@ static class Program
t.Title = parts[2]; 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 == "") if (t.Title == "")
{ {
t.Title = fname; t.Title = fname;
t.ArtistMaybeWrong = true; 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.Title = t.Title.RemoveFt();
t.Artist = t.Artist.RemoveFt(); t.Artist = t.Artist.RemoveFt();
@ -3172,7 +3242,7 @@ static class Program
fname = regexRemove != "" ? Regex.Replace(fname, regexRemove, "") : fname; fname = regexRemove != "" ? Regex.Replace(fname, regexRemove, "") : fname;
fname = diacrRemove ? fname.RemoveDiacritics() : fname; fname = diacrRemove ? fname.RemoveDiacritics() : fname;
fname = fname.Trim().RemoveConsecutiveWs(); fname = fname.Trim().RemoveConsecutiveWs();
tname = tname.Replace("_", " ").ReplaceInvalidChars(" ", true, false); tname = tname.Replace("_", " ").ReplaceInvalidChars(" ", true, true);
tname = regexRemove != "" ? Regex.Replace(tname, regexRemove, "") : tname; tname = regexRemove != "" ? Regex.Replace(tname, regexRemove, "") : tname;
tname = diacrRemove ? tname.RemoveDiacritics() : tname; tname = diacrRemove ? tname.RemoveDiacritics() : tname;
tname = tname.Trim().RemoveConsecutiveWs(); tname = tname.Trim().RemoveConsecutiveWs();
@ -3451,7 +3521,7 @@ static class Program
static string GetSaveName(string sourceFname) static string GetSaveName(string sourceFname)
{ {
string name = GetFileNameWithoutExtSlsk(sourceFname); string name = GetFileNameWithoutExtSlsk(sourceFname);
return ReplaceInvalidChars(name, " "); return name.ReplaceInvalidChars(invalidReplaceStr);
} }
static string GetAsPathSlsk(string fname) static string GetAsPathSlsk(string fname)
@ -3541,7 +3611,7 @@ static class Program
TagLib.File? file = null; TagLib.File? file = null;
try { file = TagLib.File.Create(filepath); } try { file = TagLib.File.Create(filepath); }
catch { return filepath; } catch { }
Regex regex = new Regex(@"(\{(?:\{??[^\{]*?\}))"); Regex regex = new Regex(@"(\{(?:\{??[^\{]*?\}))");
MatchCollection matches = regex.Matches(newName); MatchCollection matches = regex.Matches(newName);
@ -3550,7 +3620,8 @@ static class Program
{ {
foreach (Match match in matches.Cast<Match>()) 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('|'); var options = inner.Split('|');
string chosenOpt = ""; string chosenOpt = "";
@ -3559,7 +3630,7 @@ static class Program
{ {
string[] parts = Regex.Split(opt, @"\([^\)]*\)"); string[] parts = Regex.Split(opt, @"\([^\)]*\)");
string[] result = parts.Where(part => !string.IsNullOrWhiteSpace(part)).ToArray(); 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; chosenOpt = opt;
break; break;
} }
@ -3568,10 +3639,11 @@ static class Program
chosenOpt = Regex.Replace(chosenOpt, @"\([^()]*\)|[^()]+", match => chosenOpt = Regex.Replace(chosenOpt, @"\([^()]*\)|[^()]+", match =>
{ {
if (match.Value.StartsWith("(") && match.Value.EndsWith(")")) 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 else
return GetVarValue(match.Value, file, track); return GetVarValue(match.Value, file, filepath, track).ReplaceInvalidChars(invalidReplaceStr);
}); });
string old = match.Groups[1].Value; string old = match.Groups[1].Value;
old = old.StartsWith("{{") ? old.Substring(1) : old; old = old.StartsWith("{{") ? old.Substring(1) : old;
newName = newName.Replace(old, chosenOpt); newName = newName.Replace(old, chosenOpt);
@ -3580,15 +3652,14 @@ static class Program
matches = regex.Matches(newName); matches = regex.Matches(newName);
} }
if (newName != format) if (newName != format)
{ {
string directory = Path.GetDirectoryName(filepath); string directory = Path.GetDirectoryName(filepath) ?? "";
string dirsep = Path.DirectorySeparatorChar.ToString();
string extension = Path.GetExtension(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); 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); string newFilePath = Path.Combine(directory, newName + extension);
return newFilePath; return newFilePath;
} }
@ -3596,22 +3667,22 @@ static class Program
return filepath; 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) switch (x)
{ {
case "artist": case "artist":
return file.Tag.FirstPerformer ?? ""; return file?.Tag.FirstPerformer ?? "";
case "artists": case "artists":
return string.Join(" & ", file.Tag.Performers); return file != null ? string.Join(" & ", file.Tag.Performers) : "";
case "albumartist": case "albumartist":
return file.Tag.FirstAlbumArtist ?? ""; return file?.Tag.FirstAlbumArtist ?? "";
case "albumartists": case "albumartists":
return string.Join(" & ", file.Tag.AlbumArtists); return file != null ? string.Join(" & ", file.Tag.AlbumArtists) : "";
case "title": case "title":
return file.Tag.Title ?? ""; return file?.Tag.Title ?? "";
case "album": case "album":
return file.Tag.Album ?? ""; return file?.Tag.Album ?? "";
case "sartist": case "sartist":
case "sartists": case "sartists":
return track.Artist; return track.Artist;
@ -3620,13 +3691,13 @@ static class Program
case "salbum": case "salbum":
return track.Album; return track.Album;
case "year": case "year":
return file.Tag.Year.ToString() ?? ""; return file?.Tag.Year.ToString() ?? "";
case "track": case "track":
return file.Tag.Track.ToString("D2") ?? ""; return file?.Tag.Track.ToString("D2") ?? "";
case "disc": case "disc":
return file.Tag.Disc.ToString() ?? ""; return file?.Tag.Disc.ToString() ?? "";
case "filename": case "filename":
return Path.GetFileNameWithoutExtension(file.Name); return Path.GetFileNameWithoutExtension(filepath);
case "foldername": case "foldername":
return defaultFolderName; return defaultFolderName;
default: default:
@ -4078,28 +4149,8 @@ static class Program
return totalSeconds; 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, 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) if (file == null)
return t.ToString(); return t.ToString();
@ -4108,18 +4159,19 @@ static class Program
string bitRate = file.BitRate.HasValue ? $"{file.BitRate}kbps" : ""; string bitRate = file.BitRate.HasValue ? $"{file.BitRate}kbps" : "";
string fileSize = $"{file.Size / (float)(1024 * 1024):F1}MB"; string fileSize = $"{file.Size / (float)(1024 * 1024):F1}MB";
string user = showUser && response?.Username != null ? response.Username + "\\" : ""; 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 fname = fullpath ? file.Filename : (showUser ? "..\\" : "") + (customPath == "" ? GetFileNameSlsk(file.Filename) : customPath);
string length = Utils.IsMusicFile(file.Filename) ? (file.Length ?? -1).ToString() + "s" : ""; string length = Utils.IsMusicFile(file.Filename) ? (file.Length ?? -1).ToString() + "s" : "";
string displayText; string displayText;
if (!infoFirst) if (!infoFirst)
{ {
string info = string.Join('/', new string[] { length, sampleRate+bitRate, fileSize }.Where(value => value!="")); string info = string.Join('/', new string[] { length, sampleRate+bitRate, fileSize }.Where(value => value!=""));
displayText = $"{user}{fname} [{info}]"; displayText = $"{speed}{user}{fname} [{info}]";
} }
else else
{ {
string info = string.Join('/', new string[] { length.PadRight(4), (sampleRate+bitRate).PadRight(8), fileSize.PadLeft(6) }); 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)}, " : ""; string necStr = nec != null ? $"nec:{nec.GetNotSatisfiedName(file, t, response)}, " : "";

View file

@ -76,6 +76,32 @@ public static class Utils
return s; 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) public static string RemoveFt(this string str, bool removeParentheses = true, bool onlyIfNonempty = true)
{ {
string[] ftStrings = { "feat.", "ft." }; string[] ftStrings = { "feat.", "ft." };
@ -201,39 +227,6 @@ public static class Utils
return d; 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) public static int Levenshtein(string source, string target)
{ {
if (source.Length == 0) if (source.Length == 0)