1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 14:32:40 +00:00
slsk-batchdl/slsk-batchdl/Search.cs
2024-10-12 13:07:49 +02:00

1160 lines
46 KiB
C#

using Soulseek;
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using Models;
using Enums;
using static Program;
using SearchResponse = Soulseek.SearchResponse;
using SlResponse = Soulseek.SearchResponse;
using SlFile = Soulseek.File;
using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string, (Soulseek.SearchResponse, Soulseek.File)>;
static class Search
{
public static RateLimitedSemaphore? searchSemaphore;
// very messy function that does everything
public static async Task<(string, SlFile?)> SearchAndDownload(Track track, FileManager organizer, CancellationTokenSource? cts = null)
{
if (Config.I.DoNotDownload)
throw new Exception();
IEnumerable<(SlResponse response, SlFile file)>? orderedResults = null;
var responseData = new ResponseData();
var progress = Printing.GetProgressBar();
var results = new SlDictionary();
var fsResults = new SlDictionary();
using var searchCts = new CancellationTokenSource();
var saveFilePath = "";
SlFile? chosenFile = null;
Task? downloadTask = null;
var fsDownloadLock = new object();
int fsResultsStarted = 0;
int downloading = 0;
bool notFound = false;
bool searchEnded = false;
string? fsUser = null;
if (track.Downloads != null)
{
orderedResults = track.Downloads;
goto downloads;
}
Printing.RefreshOrPrint(progress, 0, $"Waiting: {track}", false);
searches.TryAdd(track, new SearchInfo(results, progress));
void fastSearchDownload()
{
lock (fsDownloadLock)
{
if (downloading == 0 && !searchEnded)
{
downloading = 1;
var (r, f) = fsResults.MaxBy(x => x.Value.Item1.UploadSpeed).Value;
saveFilePath = organizer.GetSavePath(f.Filename);
fsUser = r.Username;
chosenFile = f;
downloadTask = Download.DownloadFile(r, f, saveFilePath, track, progress, cts?.Token, searchCts);
}
}
}
void responseHandler(SearchResponse r)
{
if (r.Files.Count > 0)
{
responseData.lockedFilesCount += r.LockedFileCount;
foreach (var file in r.Files)
results.TryAdd(r.Username + '\\' + file.Filename, (r, file));
if (Config.I.fastSearch && userSuccessCount.GetValueOrDefault(r.Username, 0) > Config.I.downrankOn)
{
var f = r.Files.First();
if (r.HasFreeUploadSlot && r.UploadSpeed / 1024.0 / 1024.0 >= Config.I.fastSearchMinUpSpeed
&& FileConditions.BracketCheck(track, InferTrack(f.Filename, track)) && Config.I.preferredCond.FileSatisfies(f, track, r))
{
fsResults.TryAdd(r.Username + '\\' + f.Filename, (r, f));
if (Interlocked.Exchange(ref fsResultsStarted, 1) == 0)
{
Task.Delay(Config.I.fastSearchDelay).ContinueWith(tt => fastSearchDownload());
}
}
}
}
}
SearchOptions getSearchOptions(int timeout, FileConditions necCond, FileConditions prfCond)
{
return new SearchOptions(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
searchTimeout: Config.I.searchTimeout,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
responseFilter: (response) =>
{
return response.UploadSpeed > 0 && necCond.BannedUsersSatisfies(response);
},
fileFilter: (file) =>
{
return Utils.IsMusicFile(file.Filename) && necCond.FileSatisfies(file, track, null);
});
}
void onSearch() => Printing.RefreshOrPrint(progress, 0, $"Searching: {track}", true);
await RunSearches(track, results, getSearchOptions, responseHandler, searchCts.Token, onSearch);
searches.TryRemove(track, out _);
searchEnded = true;
lock (fsDownloadLock) { }
if (downloading == 0 && results.IsEmpty && !Config.I.useYtdlp)
{
notFound = true;
}
else if (downloading == 1)
{
try
{
if (downloadTask == null || downloadTask.IsFaulted || downloadTask.IsCanceled)
throw new TaskCanceledException();
await downloadTask;
userSuccessCount.AddOrUpdate(fsUser, 1, (k, v) => v + 1);
}
catch
{
saveFilePath = "";
downloading = 0;
if (chosenFile != null && fsUser != null)
{
results.TryRemove(fsUser + '\\' + chosenFile.Filename, out _);
userSuccessCount.AddOrUpdate(fsUser, -1, (k, v) => v - 1);
}
}
}
searchCts.Dispose();
downloads:
if (downloading == 0 && (!results.IsEmpty || orderedResults != null))
{
if (orderedResults == null)
orderedResults = OrderedResults(results, track, useInfer: true);
int trackTries = Config.I.maxRetriesPerTrack;
async Task<bool> process(SlResponse response, SlFile file)
{
saveFilePath = organizer.GetSavePath(file.Filename);
chosenFile = file;
try
{
downloading = 1;
await Download.DownloadFile(response, file, saveFilePath, track, progress, cts?.Token);
userSuccessCount.AddOrUpdate(response.Username, 1, (k, v) => v + 1);
return true;
}
catch (Exception e)
{
Printing.WriteLine($"Error: Download Error: {e}", ConsoleColor.DarkYellow, debugOnly: true);
chosenFile = null;
saveFilePath = "";
downloading = 0;
if (!IsConnectedAndLoggedIn())
throw;
userSuccessCount.AddOrUpdate(response.Username, -1, (k, v) => v - 1);
if (--trackTries <= 0)
{
Printing.RefreshOrPrint(progress, 0, $"Out of download retries: {track}", true);
Printing.WriteLine("Last error was: " + e.Message, ConsoleColor.DarkYellow);
throw new SearchAndDownloadException(FailureReason.OutOfDownloadRetries);
}
return false;
}
}
// the first result is usually fine, no need to sort the entire sequence
var fr = orderedResults.First();
bool success = await process(fr.response, fr.file);
if (!success)
{
fr = orderedResults.Skip(1).FirstOrDefault();
if (fr != default)
{
if (userSuccessCount.GetValueOrDefault(fr.response.Username, 0) > Config.I.ignoreOn)
{
success = await process(fr.response, fr.file);
}
if (!success)
{
foreach (var (response, file) in orderedResults.Skip(2))
{
if (userSuccessCount.GetValueOrDefault(response.Username, 0) <= Config.I.ignoreOn)
continue;
success = await process(response, file);
if (success) break;
}
}
}
}
}
if (downloading == 0 && Config.I.useYtdlp)
{
notFound = false;
try
{
Printing.RefreshOrPrint(progress, 0, $"yt-dlp search: {track}", true);
var ytResults = await Extractors.YouTube.YtdlpSearch(track);
if (ytResults.Count > 0)
{
foreach (var (length, id, title) in ytResults)
{
if (Config.I.necessaryCond.LengthToleranceSatisfies(length, track.Length))
{
string saveFilePathNoExt = organizer.GetSavePathNoExt(title);
downloading = 1;
Printing.RefreshOrPrint(progress, 0, $"yt-dlp download: {track}", true);
saveFilePath = await Extractors.YouTube.YtdlpDownload(id, saveFilePathNoExt, Config.I.ytdlpArgument);
Printing.RefreshOrPrint(progress, 100, $"Succeded: yt-dlp completed download for {track}", true);
break;
}
}
}
}
catch (Exception e)
{
saveFilePath = "";
downloading = 0;
Printing.RefreshOrPrint(progress, 0, $"{e.Message}", true);
throw new SearchAndDownloadException(FailureReason.NoSuitableFileFound);
}
}
if (downloading == 0)
{
if (notFound)
{
string lockedFilesStr = responseData.lockedFilesCount > 0 ? $" (Found {responseData.lockedFilesCount} locked files)" : "";
Printing.RefreshOrPrint(progress, 0, $"Not found: {track}{lockedFilesStr}", true);
throw new SearchAndDownloadException(FailureReason.NoSuitableFileFound);
}
else
{
Printing.RefreshOrPrint(progress, 0, $"All downloads failed: {track}", true);
throw new SearchAndDownloadException(FailureReason.AllDownloadsFailed);
}
}
return (Utils.GetFullPath(saveFilePath), chosenFile);
}
public static async Task<List<List<Track>>> GetAlbumDownloads(Track track, ResponseData responseData)
{
var results = new ConcurrentDictionary<string, (SearchResponse, Soulseek.File)>();
SearchOptions getSearchOptions(int timeout, FileConditions nec, FileConditions prf) =>
new SearchOptions(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
searchTimeout: timeout,
responseFilter: (response) =>
{
return response.UploadSpeed > 0 && nec.BannedUsersSatisfies(response);
},
fileFilter: (file) =>
{
return !Utils.IsMusicFile(file.Filename) || nec.FileSatisfies(file, track, null);
}
);
void handler(SlResponse r)
{
responseData.lockedFilesCount += r.LockedFileCount;
if (r.Files.Count > 0)
{
foreach (var file in r.Files)
results.TryAdd(r.Username + '\\' + file.Filename, (r, file));
}
}
using var cts = new CancellationTokenSource();
await RunSearches(track, results, getSearchOptions, handler, cts.Token);
string fullPath((SearchResponse r, Soulseek.File f) x) { return x.r.Username + '\\' + x.f.Filename; }
var orderedResults = OrderedResults(results, track, false, false, albumMode: true);
var discPattern = new Regex(@"^(?i)(dis[c|k]|cd)\s*\d{1,2}$");
bool canMatchDiscPattern = !discPattern.IsMatch(track.Album) && !discPattern.IsMatch(track.Artist);
var directoryStructure = new Dictionary<string, List<(SlResponse response, SlFile file)>>();
foreach (var x in orderedResults)
{
var path = fullPath(x);
var dirpath = path[..path.LastIndexOf('\\')];
if (!directoryStructure.ContainsKey(dirpath))
{
directoryStructure[dirpath] = new() { x };
}
else
{
directoryStructure[dirpath].Add(x);
}
}
if (canMatchDiscPattern)
{
foreach (var key in directoryStructure.Keys.ToArray())
{
var dirname = key[(key.LastIndexOf('\\') + 1)..];
if (discPattern.IsMatch(dirname))
{
directoryStructure.Remove(key, out var val);
var newKey = key[..key.LastIndexOf('\\')];
if (directoryStructure.ContainsKey(newKey))
{
directoryStructure[newKey].AddRange(val);
}
else
{
directoryStructure[newKey] = val;
}
}
}
}
foreach ((var key, var val) in directoryStructure)
{
foreach ((var key2, var val2) in directoryStructure)
{
if (key == key2)
continue;
if ((key2 + '\\').StartsWith(key + '\\'))
{
val.AddRange(val2);
directoryStructure.Remove(key2);
}
}
}
int min, max;
if (Config.I.minAlbumTrackCount > -1 || Config.I.maxAlbumTrackCount > -1)
{
min = Config.I.minAlbumTrackCount;
max = Config.I.maxAlbumTrackCount;
}
else
{
min = track.MinAlbumTrackCount;
max = track.MaxAlbumTrackCount;
}
bool countIsGood(int count) => count >= min && (max == -1 || count <= max);
var result = new List<List<Track>>();
foreach ((var key, var val) in directoryStructure)
{
int musicFileCount = val.Count(x => Utils.IsMusicFile(x.file.Filename));
if (musicFileCount == 0 || !countIsGood(musicFileCount))
continue;
var ls = new List<Track>();
foreach (var x in val)
{
var t = new Track
{
Artist = track.Artist,
Album = track.Album,
Length = x.file.Length ?? -1,
IsNotAudio = !Utils.IsMusicFile(x.file.Filename),
Downloads = new() { x },
};
ls.Add(t);
}
ls = ls.OrderBy(t => t.IsNotAudio).ThenBy(t => t.Downloads[0].Item2.Filename).ToList();
result.Add(ls);
}
if (result.Count == 0)
result.Add(new List<Track>());
return result;
}
public static async Task<List<Track>> GetAggregateTracks(Track track, ResponseData responseData)
{
var results = new SlDictionary();
SearchOptions getSearchOptions(int timeout, FileConditions nec, FileConditions prf) =>
new(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
searchTimeout: timeout,
responseFilter: (response) =>
{
return response.UploadSpeed > 0 && nec.BannedUsersSatisfies(response);
},
fileFilter: (file) =>
{
return Utils.IsMusicFile(file.Filename) && nec.FileSatisfies(file, track, null);
}
);
void handler(SlResponse r)
{
responseData.lockedFilesCount += r.LockedFileCount;
if (r.Files.Count > 0)
{
foreach (var file in r.Files)
results.TryAdd(r.Username + "\\" + file.Filename, (r, file));
}
}
using var cts = new CancellationTokenSource();
await RunSearches(track, results, getSearchOptions, handler, cts.Token);
string artistName = track.Artist.Trim();
string trackName = track.Title.Trim();
string albumName = track.Album.Trim();
var equivalentFiles = EquivalentFiles(track, results.Select(x => x.Value))
.Select(x => (x.Item1, OrderedResults(x.Item2, track, false, false, false))).ToList();
if (!Config.I.relax)
{
equivalentFiles = equivalentFiles
.Where(x => FileConditions.StrictString(x.Item1.Title, track.Title, ignoreCase: true)
&& (FileConditions.StrictString(x.Item1.Artist, track.Artist, ignoreCase: true, boundarySkipWs: false)
|| FileConditions.StrictString(x.Item1.Title, track.Artist, ignoreCase: true, boundarySkipWs: false)
&& x.Item1.Title.ContainsInBrackets(track.Artist, ignoreCase: true)))
.ToList();
}
var tracks = equivalentFiles
.Select(kvp =>
{
kvp.Item1.Downloads = kvp.Item2.ToList();
return kvp.Item1;
}).ToList();
return tracks;
}
public static async Task<List<List<List<Track>>>> GetAggregateAlbums(Track track, ResponseData responseData)
{
int maxDiff = Config.I.aggregateLengthTol;
bool lengthsAreSimilar(int[] sorted1, int[] sorted2)
{
if (sorted1.Length != sorted2.Length)
return false;
for (int i = 0; i < sorted1.Length; i++)
{
if (Math.Abs(sorted1[i] - sorted2[i]) > maxDiff)
return false;
}
return true;
}
var albums = await GetAlbumDownloads(track, responseData);
var sortedLengthLists = new List<(int[] lengths, List<Track> album, string username)>();
foreach (var album in albums)
{
if (album.Count == 0)
{
continue;
}
var sortedLengths = album.Where(x => !x.IsNotAudio).Select(x => x.Length).OrderBy(x => x).ToArray();
string user = album[0].FirstUsername;
sortedLengthLists.Add((sortedLengths, album, user));
}
var usernamesList = new List<HashSet<string>>();
var lengthsList = new List<int[]>();
var res = new List<List<List<Track>>>();
foreach ((var lengths, var album, var user) in sortedLengthLists)
{
bool found = false;
for (int i = 0; i < lengthsList.Count; i++)
{
if (lengthsAreSimilar(lengths, lengthsList[i]))
{
if (lengths.Length == 1 && lengthsList[i].Length == 1)
{
var t1 = InferTrack(album[0].Downloads[0].Item2.Filename, new Track());
var t2 = InferTrack(res[i][0][0].Downloads[0].Item2.Filename, new Track());
if ((t2.Artist.ContainsIgnoreCase(t1.Artist) || t1.Artist.ContainsIgnoreCase(t2.Artist))
&& (t2.Title.ContainsIgnoreCase(t1.Title) || t1.Title.ContainsIgnoreCase(t2.Title)))
{
found = true;
}
}
else
{
found = true;
}
if (found)
{
usernamesList[i].Add(user);
res[i].Add(album);
break;
}
}
}
if (found)
{
continue;
}
else
{
usernamesList.Add(new() { user });
lengthsList.Add(lengths);
res.Add(new List<List<Track>> { album });
}
}
res = res.Select((x, i) => (x, i))
.Where(x => usernamesList[x.i].Count >= Config.I.minSharesAggregate)
.OrderByDescending(x => usernamesList[x.i].Count)
.Select(x => x.x)
.ToList();
return res; // Note: The nested lists are still ordered according to OrderedResults
}
public static async Task<List<(string dir, SlFile file)>> GetAllFilesInFolder(string user, string folderPrefix, CancellationToken? cancellationToken = null)
{
var browseOptions = new BrowseOptions();
var res = new List<(string dir, SlFile file)>();
folderPrefix = folderPrefix.TrimEnd('\\') + '\\';
var userFileList = await client.BrowseAsync(user, browseOptions, cancellationToken);
foreach (var dir in userFileList.Directories)
{
string dirname = dir.Name.TrimEnd('\\') + '\\';
if (dirname.StartsWith(folderPrefix))
{
res.AddRange(dir.Files.Select(x => (dir.Name, x)));
}
}
return res;
}
public static async Task<int> CompleteFolder(List<Track> tracks, SearchResponse response, string folder, CancellationToken? cancellationToken = null)
{
int newFiles = 0;
try
{
List<(string dir, SlFile file)> allFiles;
try
{
allFiles = await GetAllFilesInFolder(response.Username, folder, cancellationToken);
}
catch (Exception e)
{
Console.WriteLine($"Error: Error getting all files in directory '{folder}: {e}'");
return 0;
}
if (allFiles.Count > tracks.Count)
{
var paths = tracks.Select(x => x.Downloads[0].Item2.Filename).ToHashSet();
var first = tracks[0];
foreach ((var dir, var file) in allFiles)
{
var fullPath = dir + '\\' + file.Filename;
if (!paths.Contains(fullPath))
{
newFiles++;
var newFile = new SlFile(file.Code, fullPath, file.Size, file.Extension, file.Attributes);
var t = new Track
{
Artist = first.Artist,
Album = first.Album,
IsNotAudio = !Utils.IsMusicFile(file.Filename),
Downloads = new() { (response, newFile) }
};
tracks.Add(t);
}
}
}
}
catch (Exception ex)
{
Printing.WriteLine($"Error getting complete list of files: {ex}", ConsoleColor.DarkYellow);
}
return newFiles;
}
public static IEnumerable<(Track, IEnumerable<(SlResponse response, SlFile file)>)> EquivalentFiles(Track track,
IEnumerable<(SlResponse, SlFile)> fileResponses, int minShares = -1)
{
if (minShares == -1)
minShares = Config.I.minSharesAggregate;
Track inferTrack((SearchResponse r, Soulseek.File f) x)
{
var t = InferTrack(x.f.Filename, track);
t.Length = x.f.Length ?? -1;
return t;
}
var groups = fileResponses
.GroupBy(inferTrack, new TrackComparer(ignoreCase: true, Config.I.aggregateLengthTol))
.Select(x => (x, x.Select(y => y.Item1.Username).Distinct().Count()))
.Where(x => x.Item2 >= minShares)
.OrderByDescending(x => x.Item2)
.Select(x => x.x)
.Select(x =>
{
if (x.Key.Length == -1)
x.Key.Length = x.FirstOrDefault(y => y.Item2.Length != null).Item2?.Length ?? -1;
return (x.Key, x.AsEnumerable());
});
return groups;
}
public static IOrderedEnumerable<(SlResponse response, SlFile file)> OrderedResults(IEnumerable<KeyValuePair<string, (SlResponse, SlFile)>> results,
Track track, bool useInfer = false, bool useLevenshtein = true, bool albumMode = false)
{
return OrderedResults(results.Select(x => x.Value), track, useInfer, useLevenshtein, albumMode);
}
public static IOrderedEnumerable<(SlResponse response, SlFile file)> OrderedResults(IEnumerable<(SlResponse, SlFile)> results,
Track track, bool useInfer = false, bool useLevenshtein = true, bool albumMode = false)
{
bool useBracketCheck = true;
if (albumMode)
{
useBracketCheck = false;
useLevenshtein = false;
useInfer = false;
}
Dictionary<string, (Track, int)>? infTracksAndCounts = null;
if (useInfer) // this is very slow
{
var equivalentFiles = EquivalentFiles(track, results, 1);
infTracksAndCounts = equivalentFiles
.SelectMany(t => t.Item2, (t, f) => new { t.Item1, f.response.Username, f.file.Filename, Count = t.Item2.Count() })
.ToSafeDictionary(x => $"{x.Username}\\{x.Filename}", y => (y.Item1, y.Count));
}
(Track, int) inferredTrack((SearchResponse response, Soulseek.File file) x)
{
string key = $"{x.response.Username}\\{x.file.Filename}";
if (infTracksAndCounts != null && infTracksAndCounts.ContainsKey(key))
return infTracksAndCounts[key];
return (new Track(), 0);
}
int levenshtein((SearchResponse response, Soulseek.File file) x)
{
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);
}
var random = new Random();
return results.Select(x => (response: x.Item1, file: x.Item2))
.Where(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.I.ignoreOn)
.OrderByDescending(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.I.downrankOn)
.ThenByDescending(x => Config.I.necessaryCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => Config.I.preferredCond.BannedUsersSatisfies(x.response))
.ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || Config.I.preferredCond.AcceptNoLength == null || Config.I.preferredCond.AcceptNoLength.Value)
.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.I.preferredCond.StrictTitleSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => !albumMode || Config.I.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
.ThenByDescending(x => Config.I.preferredCond.StrictArtistSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => Config.I.preferredCond.LengthToleranceSatisfies(x.file, track.Length))
.ThenByDescending(x => Config.I.preferredCond.FormatSatisfies(x.file.Filename))
.ThenByDescending(x => albumMode || Config.I.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
.ThenByDescending(x => Config.I.preferredCond.BitrateSatisfies(x.file))
.ThenByDescending(x => Config.I.preferredCond.SampleRateSatisfies(x.file))
.ThenByDescending(x => Config.I.preferredCond.BitDepthSatisfies(x.file))
.ThenByDescending(x => Config.I.preferredCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => x.response.HasFreeUploadSlot)
.ThenByDescending(x => x.response.UploadSpeed / 1024 / 650)
.ThenByDescending(x => albumMode || FileConditions.StrictString(x.file.Filename, track.Title))
.ThenByDescending(x => !albumMode || FileConditions.StrictString(Utils.GetDirectoryNameSlsk(x.file.Filename), track.Album))
.ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.Artist, boundarySkipWs: false))
.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());
}
public static async Task RunSearches(Track track, SlDictionary results, Func<int, FileConditions, FileConditions, SearchOptions> getSearchOptions,
Action<SearchResponse> responseHandler, CancellationToken? ct = null, Action? onSearch = null)
{
bool artist = track.Artist.Length > 0;
bool title = track.Title.Length > 0;
bool album = track.Album.Length > 0;
string search = GetSearchString(track);
var searchTasks = new List<Task>();
var defaultSearchOpts = getSearchOptions(Config.I.searchTimeout, Config.I.necessaryCond, Config.I.preferredCond);
searchTasks.Add(DoSearch(search, defaultSearchOpts, responseHandler, ct, onSearch));
if (search.RemoveDiacriticsIfExist(out string noDiacrSearch) && !track.ArtistMaybeWrong)
{
searchTasks.Add(DoSearch(noDiacrSearch, defaultSearchOpts, responseHandler, ct, onSearch));
}
await Task.WhenAll(searchTasks);
if (results.IsEmpty && track.ArtistMaybeWrong && title)
{
var cond = new FileConditions(Config.I.necessaryCond);
var infTrack = InferTrack(track.Title, new Track());
cond.StrictTitle = infTrack.Title == track.Title;
cond.StrictArtist = false;
var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{infTrack.Artist} {infTrack.Title}", opts, responseHandler, ct, onSearch));
}
if (Config.I.desperateSearch)
{
await Task.WhenAll(searchTasks);
if (results.IsEmpty && !track.ArtistMaybeWrong)
{
if (artist && album && title)
{
var cond = new FileConditions(Config.I.necessaryCond)
{
StrictTitle = true,
StrictAlbum = true
};
var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{track.Artist} {track.Album}", opts, responseHandler, ct, onSearch));
}
if (artist && title && track.Length != -1 && Config.I.necessaryCond.LengthTolerance != -1)
{
var cond = new FileConditions(Config.I.necessaryCond)
{
LengthTolerance = -1,
StrictTitle = true,
StrictArtist = true
};
var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{track.Artist} {track.Title}", opts, responseHandler, ct, onSearch));
}
}
await Task.WhenAll(searchTasks);
if (results.IsEmpty)
{
var track2 = track.ArtistMaybeWrong ? InferTrack(track.Title, new Track()) : track;
if (track.Album.Length > 3 && album)
{
var cond = new FileConditions(Config.I.necessaryCond)
{
StrictAlbum = true,
StrictTitle = !track.ArtistMaybeWrong,
StrictArtist = !track.ArtistMaybeWrong,
LengthTolerance = -1
};
var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{track.Album}", opts, responseHandler, ct, onSearch));
}
if (track2.Title.Length > 3 && artist)
{
var cond = new FileConditions(Config.I.necessaryCond)
{
StrictTitle = !track.ArtistMaybeWrong,
StrictArtist = !track.ArtistMaybeWrong,
LengthTolerance = -1
};
var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{track2.Title}", opts, responseHandler, ct, onSearch));
}
if (track2.Artist.Length > 3 && title)
{
var cond = new FileConditions(Config.I.necessaryCond)
{
StrictTitle = !track.ArtistMaybeWrong,
StrictArtist = !track.ArtistMaybeWrong,
LengthTolerance = -1
};
var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{track2.Artist}", opts, responseHandler, ct, onSearch));
}
}
}
await Task.WhenAll(searchTasks);
}
static async Task DoSearch(string search, SearchOptions opts, Action<SearchResponse> rHandler, CancellationToken? ct = null, Action? onSearch = null)
{
await searchSemaphore.WaitAsync();
try
{
search = CleanSearchString(search);
var q = SearchQuery.FromText(search);
onSearch?.Invoke();
await client.SearchAsync(q, options: opts, cancellationToken: ct, responseHandler: rHandler);
}
catch (OperationCanceledException) { }
}
public static async Task SearchAndPrintResults(List<Track> tracks)
{
foreach (var track in tracks)
{
Console.WriteLine($"Results for {track}:");
SearchOptions getSearchOptions(int timeout, FileConditions necCond, FileConditions prfCond)
{
return new SearchOptions(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
searchTimeout: Config.I.searchTimeout,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
responseFilter: (response) =>
{
return response.UploadSpeed > 0 && necCond.BannedUsersSatisfies(response);
},
fileFilter: (file) =>
{
return Utils.IsMusicFile(file.Filename) && (necCond.FileSatisfies(file, track, null) || Config.I.PrintResultsFull);
});
}
var results = new SlDictionary();
void responseHandler(SearchResponse r)
{
if (r.Files.Count > 0)
{
foreach (var file in r.Files)
results.TryAdd(r.Username + '\\' + file.Filename, (r, file));
}
}
await RunSearches(track, results, getSearchOptions, responseHandler);
if (Config.I.DoNotDownload && results.IsEmpty)
{
Printing.WriteLine($"No results", ConsoleColor.Yellow);
}
else
{
var orderedResults = OrderedResults(results, track, useInfer: true);
int count = 0;
Console.WriteLine();
foreach (var (response, file) in orderedResults)
{
Console.WriteLine(Printing.DisplayString(track, file, response,
Config.I.PrintResultsFull ? Config.I.necessaryCond : null, Config.I.PrintResultsFull ? Config.I.preferredCond : null,
fullpath: Config.I.PrintResultsFull, infoFirst: true, showSpeed: Config.I.PrintResultsFull));
count += 1;
}
Printing.WriteLine($"Total: {count}\n", ConsoleColor.Yellow);
}
Console.WriteLine();
}
}
static string GetSearchString(Track track)
{
if (track.Type == TrackType.Album)
{
if (track.Album.Length > 0)
return (track.Artist + " " + track.Album).Trim();
if (track.Title.Length > 0)
return (track.Artist + " " + track.Title).Trim();
return track.Artist.Trim();
}
else
{
if (track.Title.Length > 0)
return (track.Artist + " " + track.Title).Trim();
else if (track.Album.Length > 0)
return (track.Artist + " " + track.Album).Trim();
return track.Artist.Trim();
}
}
static string CleanSearchString(string str)
{
string old;
if (!Config.I.noRemoveSpecialChars)
{
old = str;
str = str.ReplaceSpecialChars(" ").Trim().RemoveConsecutiveWs();
if (str.Length == 0) str = old;
}
foreach (var banned in bannedTerms)
{
string b1 = banned;
string b2 = banned.Replace(" ", "-");
string b3 = banned.Replace(" ", "_");
string b4 = banned.Replace(" ", "");
foreach (var s in new string[] { b1, b2, b3, b4 })
str = str.Replace(s, string.Concat("*", s.AsSpan(1)), StringComparison.OrdinalIgnoreCase);
}
return str.Trim();
}
public static Track InferTrack(string filename, Track defaultTrack, TrackType type = TrackType.Normal)
{
var t = new Track(defaultTrack);
t.Type = type;
filename = Utils.GetFileNameWithoutExtSlsk(filename).Replace(" — ", " - ").Replace('_', ' ').Trim().RemoveConsecutiveWs();
var trackNumStart = new Regex(@"^(?:(?:[0-9][-\.])?\d{2,3}[. -]|\b\d\.\s|\b\d\s-\s)(?=.+\S)");
var trackNumMiddle = new Regex(@"(?<= - )((\d-)?\d{2,3}|\d{2,3}\.?)\s+");
var trackNumMiddleAlt = new Regex(@"\s+-(\d{2,3})-\s+");
if (trackNumStart.IsMatch(filename))
{
filename = trackNumStart.Replace(filename, "", 1).Trim();
if (filename.StartsWith("- "))
filename = filename[2..].Trim();
}
else
{
var reg = trackNumMiddle.IsMatch(filename) ? trackNumMiddle : (trackNumMiddleAlt.IsMatch(filename) ? trackNumMiddleAlt : null);
if (reg != null && !reg.IsMatch(defaultTrack.ToString(noInfo: true)))
{
filename = reg.Replace(filename, "<<tracknum>>", 1).Trim();
filename = Regex.Replace(filename, @"-\s*<<tracknum>>\s*-", "-");
filename = filename.Replace("<<tracknum>>", "");
}
}
string aname = t.Artist.Trim();
string tname = t.Title.Trim();
string alname = t.Album.Trim();
string fname = filename;
fname = fname.Replace('—', '-').Replace('_', ' ').Replace('[', '(').Replace(']', ')').ReplaceInvalidChars("", true).RemoveConsecutiveWs().Trim();
tname = tname.Replace('—', '-').Replace('_', ' ').Replace('[', '(').Replace(']', ')').ReplaceInvalidChars("", true).RemoveFt().RemoveConsecutiveWs().Trim();
aname = aname.Replace('—', '-').Replace('_', ' ').Replace('[', '(').Replace(']', ')').ReplaceInvalidChars("", true).RemoveFt().RemoveConsecutiveWs().Trim();
alname = alname.Replace('—', '-').Replace('_', ' ').Replace('[', '(').Replace(']', ')').ReplaceInvalidChars("", true).RemoveFt().RemoveConsecutiveWs().Trim();
bool maybeRemix = aname.Length > 0 && 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 (maybeRemix)
t.ArtistMaybeWrong = true;
t.Title = parts[0];
}
else if (parts.Length == 2)
{
t.Artist = realParts[0];
t.Title = realParts[1];
if (!parts[0].ContainsIgnoreCase(aname) || !parts[1].ContainsIgnoreCase(tname))
{
t.ArtistMaybeWrong = true;
}
}
else if (parts.Length == 3)
{
bool hasTitle = tname.Length > 0 && parts[2].ContainsIgnoreCase(tname);
if (hasTitle)
t.Title = realParts[2];
int artistPos = -1;
if (aname.Length > 0)
{
if (parts[0].ContainsIgnoreCase(aname))
artistPos = 0;
else if (parts[1].ContainsIgnoreCase(aname))
artistPos = 1;
else
t.ArtistMaybeWrong = true;
}
int albumPos = -1;
if (alname.Length > 0)
{
if (parts[0].ContainsIgnoreCase(alname))
albumPos = 0;
else if (parts[1].ContainsIgnoreCase(alname))
albumPos = 1;
}
if (artistPos >= 0 && artistPos == albumPos)
{
artistPos = 0;
albumPos = 1;
}
if (artistPos == -1 && maybeRemix)
{
t.ArtistMaybeWrong = true;
artistPos = 0;
albumPos = 1;
}
if (artistPos == -1 && albumPos == -1)
{
t.ArtistMaybeWrong = true;
t.Artist = realParts[0] + " - " + realParts[1];
}
else if (artistPos >= 0)
{
t.Artist = parts[artistPos];
}
t.Title = parts[2];
}
else
{
int artistPos = -1, titlePos = -1;
if (aname.Length > 0)
{
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.Length > 0)
{
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.Length == 0)
{
t.Title = fname;
t.ArtistMaybeWrong = true;
}
else if (t.Artist.Length > 0 && !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();
return t;
}
public static bool AlbumsAreSimilar(List<Track> album1, List<Track> album2, int[]? album1SortedLengths = null, int tolerance = 3)
{
if (album1SortedLengths != null && album1SortedLengths.Length != album2.Count(t => !t.IsNotAudio))
return false;
else if (album1.Count(t => !t.IsNotAudio) != album2.Count(t => !t.IsNotAudio))
return false;
if (album1SortedLengths == null)
album1SortedLengths = album1.Where(t => !t.IsNotAudio).Select(t => t.Length).OrderBy(x => x).ToArray();
var album2SortedLengths = album2.Where(t => !t.IsNotAudio).Select(t => t.Length).OrderBy(x => x).ToArray();
for (int i = 0; i < album1SortedLengths.Length; i++)
{
if (Math.Abs(album1SortedLengths[i] - album2SortedLengths[i]) > tolerance)
return false;
}
return true;
}
static readonly List<string> bannedTerms = new()
{
"depeche mode", "beatles", "prince revolutions", "michael jackson", "coexist", "bob dylan", "enter shikari",
"village people", "lenny kravitz", "beyonce", "beyoncé", "lady gaga", "jay z", "kanye west", "rihanna",
"adele", "kendrick lamar", "bad romance", "born this way", "weeknd", "broken hearted", "highway 61 revisited",
"west gold digger", "west good life"
};
}
public class SearchAndDownloadException : Exception
{
public FailureReason reason;
public SearchAndDownloadException(FailureReason reason, string text = "") : base(text) { this.reason = reason; }
}