1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 06:22:41 +00:00

refactor config class

This commit is contained in:
fiso64 2024-12-15 17:23:32 +01:00
parent 38c40ec021
commit 85571115cf
19 changed files with 953 additions and 803 deletions

View file

@ -23,5 +23,11 @@ if exist slsk-batchdl\bin\Release\net6.0\linux-x64\publish\*.pdb del /F /Q slsk-
if exist slsk-batchdl\bin\zips\sldl_linux-x64.zip del /F /Q slsk-batchdl\bin\zips\sldl_linux-x64.zip
powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::CreateFromDirectory('slsk-batchdl\bin\Release\net6.0\linux-x64\publish', 'slsk-batchdl\bin\zips\sldl_linux-x64.zip'); }"
REM linux-arm
dotnet publish -c Release -r linux-arm -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true
if exist slsk-batchdl\bin\Release\net6.0\linux-arm\publish\*.pdb del /F /Q slsk-batchdl\bin\Release\net6.0\linux-arm\publish\*.pdb
if exist slsk-batchdl\bin\zips\sldl_linux-arm.zip del /F /Q slsk-batchdl\bin\zips\sldl_linux-arm.zip
powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::CreateFromDirectory('slsk-batchdl\bin\Release\net6.0\linux-arm\publish', 'slsk-batchdl\bin\zips\sldl_linux-arm.zip'); }"
endlocal

View file

@ -1,5 +1,4 @@

using AngleSharp.Css;
using AngleSharp.Css;
using Enums;
using Models;
using System.Text;
@ -89,7 +88,6 @@ public class Config
public int maxTracks = int.MaxValue;
public int offset = 0;
public int maxStaleTime = 50000;
public int updateDelay = 100;
public int searchTimeout = 6000;
public int concurrentProcesses = 2;
public int unknownErrorRetries = 2;
@ -116,36 +114,19 @@ public class Config
public bool DeleteAlbumOnFail => failedAlbumPath == "delete";
public bool IgnoreAlbumFail => failedAlbumPath == "disable";
readonly Dictionary<string, (List<string> args, string? cond)> configProfiles = new();
readonly HashSet<string> appliedProfiles = new();
private Dictionary<string, (List<string> args, string? cond)> configProfiles;
private HashSet<string> appliedProfiles;
private string[] arguments;
bool hasConfiguredIndex = false;
bool confPathChanged = false;
string[] arguments;
FileConditions? undoTempConds = null;
FileConditions? undoTempPrefConds = null;
private static Config Instance = new();
public static Config I { get { return Instance; } }
private Config() { }
private Config(Dictionary<string, (List<string> args, string? cond)> cfg, string[] args)
public Config(string[] args)
{
configProfiles = cfg;
configProfiles = new Dictionary<string, (List<string> args, string? cond)>();
appliedProfiles = new HashSet<string>();
arguments = args;
}
public void LoadAndParse(string[] args)
{
int helpIdx = Array.FindLastIndex(args, x => x == "--help" || x == "-h");
if (args.Length == 0 || helpIdx >= 0)
{
string option = helpIdx + 1 < args.Length ? args[helpIdx + 1] : "";
Help.PrintHelp(option);
Environment.Exit(0);
}
arguments = args.SelectMany(arg =>
{
@ -189,6 +170,28 @@ public class Config
ProcessArgs(arguments);
}
public Config Copy() // deep copies all fields except configProfiles and arguments
{
var copy = (Config)this.MemberwiseClone();
copy.necessaryCond = new FileConditions(necessaryCond);
copy.preferredCond = new FileConditions(preferredCond);
if (undoTempConds != null)
copy.undoTempConds = new FileConditions(undoTempConds);
if (undoTempPrefConds != null)
copy.undoTempPrefConds = new FileConditions(undoTempPrefConds);
copy.regexToReplace = new Track(regexToReplace);
copy.regexReplaceBy = new Track(regexReplaceBy);
copy.appliedProfiles = new HashSet<string>(appliedProfiles);
copy.configProfiles = configProfiles;
copy.arguments = arguments;
return copy;
}
void SetConfigPath(string[] args)
{
@ -308,23 +311,50 @@ public class Config
}
public static bool UpdateProfiles(TrackListEntry tle)
public bool NeedUpdateProfiles(TrackListEntry tle)
{
if (I.DoNotDownload)
if (DoNotDownload)
return false;
if (!I.HasAutoProfiles)
if (!HasAutoProfiles)
return false;
bool needUpdate = false;
var toApply = new List<(string name, List<string> args)>();
foreach ((var key, var val) in I.configProfiles)
foreach ((var key, var val) in configProfiles)
{
if (key == "default" || val.cond == null)
continue;
bool condSatisfied = I.ProfileConditionSatisfied(val.cond, tle);
bool alreadyApplied = I.appliedProfiles.Contains(key);
bool condSatisfied = ProfileConditionSatisfied(val.cond, tle);
bool alreadyApplied = appliedProfiles.Contains(key);
if (condSatisfied && !alreadyApplied)
return true;
if (!condSatisfied && alreadyApplied)
return true;
}
return false;
}
public bool NeedUpdateProfiles(TrackListEntry tle, out List<(string name, List<string> args)>? toApply)
{
toApply = null;
if (DoNotDownload)
return false;
if (!HasAutoProfiles)
return false;
bool needUpdate = false;
toApply = new List<(string name, List<string> args)>();
foreach ((var key, var val) in configProfiles)
{
if (key == "default" || val.cond == null)
continue;
bool condSatisfied = ProfileConditionSatisfied(val.cond, tle);
bool alreadyApplied = appliedProfiles.Contains(key);
if (condSatisfied && !alreadyApplied)
needUpdate = true;
@ -335,26 +365,27 @@ public class Config
toApply.Add((key, val.args));
}
if (!needUpdate)
return false;
return needUpdate;
}
// this means that auto profiles can't change --profile and --config
var profile = I.profile;
Instance = new Config(I.configProfiles, I.arguments);
I.ApplyDefaultConfig();
I.ApplyProfiles(profile);
public void UpdateProfiles(TrackListEntry tle)
{
if (!NeedUpdateProfiles(tle, out var toApply))
return;
ApplyDefaultConfig();
ApplyProfiles(profile);
foreach (var (name, args) in toApply)
{
Console.WriteLine($"Applying auto profile: {name}");
I.ProcessArgs(args);
I.appliedProfiles.Add(name);
ProcessArgs(args);
appliedProfiles.Add(name);
}
I.ProcessArgs(I.arguments);
I.PostProcessArgs();
return true;
ProcessArgs(arguments);
PostProcessArgs();
}
@ -504,6 +535,7 @@ public class Config
public void AddTemporaryConditions(FileConditions? cond, FileConditions? prefCond)
{
throw new NotImplementedException("Code has been refactored; probably does not work.");
if (cond != null)
undoTempConds = necessaryCond.AddConditions(cond);
if (prefCond != null)
@ -513,6 +545,7 @@ public class Config
public void RestoreConditions()
{
throw new NotImplementedException("Code has been refactored; probably does not work.");
if (undoTempConds != null)
necessaryCond.AddConditions(undoTempConds);
if (undoTempPrefConds != null)

View file

@ -11,17 +11,14 @@ using SearchResponse = Soulseek.SearchResponse;
static class Download
{
public static async Task DownloadFile(SearchResponse response, Soulseek.File file, string filePath, Track track, ProgressBar progress, CancellationToken? ct = null, CancellationTokenSource? searchCts = null)
public static async Task DownloadFile(SearchResponse response, Soulseek.File file, string filePath, Track track, ProgressBar progress, Config config, CancellationToken? ct = null, CancellationTokenSource? searchCts = null)
{
if (Config.I.DoNotDownload)
throw new Exception();
await Program.WaitForLogin();
await Program.WaitForLogin(config);
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
string origPath = filePath;
filePath += ".incomplete";
Printing.WriteLine($"Downloading: {track} to '{filePath}'", debugOnly: true);
Printing.WriteLineIf($"Downloading: {track} to '{filePath}'", config.debugInfo);
var transferOptions = new TransferOptions(
stateChanged: (state) =>
@ -63,12 +60,12 @@ static class Download
{
retryCount++;
Printing.WriteLine($"Error while downloading: {e}", ConsoleColor.DarkYellow, debugOnly: true);
Printing.WriteLineIf($"Error while downloading: {e}", config.debugInfo, ConsoleColor.DarkYellow);
if (retryCount >= maxRetries || IsConnectedAndLoggedIn())
throw;
await WaitForLogin();
await WaitForLogin(config);
}
}
}

View file

@ -15,7 +15,7 @@ namespace Extractors
return input.IsInternetUrl() && input.Contains("bandcamp.com");
}
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse)
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config)
{
var trackLists = new TrackLists();
bool isTrack = input.Contains("/track/");
@ -77,15 +77,15 @@ namespace Extractors
var track = new Track() { Artist = artist, Album = name, Type = TrackType.Album };
trackLists.AddEntry(new TrackListEntry(track));
if (Config.I.setAlbumMinTrackCount || Config.I.setAlbumMaxTrackCount)
if (config.setAlbumMinTrackCount || config.setAlbumMaxTrackCount)
{
var trackTable = doc.DocumentNode.SelectSingleNode("//*[@id='track_table']");
int n = trackTable.SelectNodes(".//tr").Count;
if (Config.I.setAlbumMinTrackCount)
if (config.setAlbumMinTrackCount)
track.MinAlbumTrackCount = n;
if (Config.I.setAlbumMaxTrackCount)
if (config.setAlbumMaxTrackCount)
track.MaxAlbumTrackCount = n;
}
}

View file

@ -15,15 +15,15 @@ namespace Extractors
return !input.IsInternetUrl() && input.EndsWith(".csv");
}
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse)
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config)
{
csvFilePath = Utils.ExpandUser(input);
if (!File.Exists(csvFilePath))
throw new FileNotFoundException($"CSV file '{csvFilePath}' not found");
var tracks = await ParseCsvIntoTrackInfo(csvFilePath, Config.I.artistCol, Config.I.titleCol, Config.I.lengthCol,
Config.I.albumCol, Config.I.descCol, Config.I.ytIdCol, Config.I.trackCountCol, Config.I.timeUnit, Config.I.ytParse);
var tracks = await ParseCsvIntoTrackInfo(csvFilePath, config.artistCol, config.titleCol, config.lengthCol,
config.albumCol, config.descCol, config.ytIdCol, config.trackCountCol, config.timeUnit, config.ytParse);
if (reverse)
tracks.Reverse();
@ -58,7 +58,7 @@ namespace Extractors
}
catch (Exception e)
{
Printing.WriteLine($"Error removing from source: {e}", debugOnly: true);
Printing.WriteLine($"Error removing from source: {e}");
}
}
}

View file

@ -6,7 +6,7 @@ namespace Extractors
{
public interface IExtractor
{
Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse);
Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config);
Task RemoveTrackFromSource(Track track) => Task.CompletedTask;
}

View file

@ -14,7 +14,7 @@ namespace Extractors
return !input.IsInternetUrl();
}
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse)
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config)
{
listFilePath = Utils.ExpandUser(input);
@ -44,28 +44,31 @@ namespace Extractors
if (added >= maxTracks)
break;
bool savedVal = Config.I.album;
bool isAlbum = false;
if (line.StartsWith("a:"))
{
line = line[2..];
Config.I.album = true;
isAlbum = true;
}
var fields = ParseLine(line);
if (isAlbum)
{
fields[0] = "album://" + fields[0];
}
var (_, ex) = ExtractorRegistry.GetMatchingExtractor(fields[0]);
var tl = await ex.GetTracks(fields[0], int.MaxValue, 0, false);
Config.I.album = savedVal;
var tl = await ex.GetTracks(fields[0], int.MaxValue, 0, false, config);
foreach (var tle in tl.lists)
{
if (fields.Count >= 2)
tle.additionalConds = Config.ParseConditions(fields[1]);
tle.extractorCond = Config.ParseConditions(fields[1]);
if (fields.Count >= 3)
tle.additionalPrefConds = Config.ParseConditions(fields[2]);
tle.extractorPrefCond = Config.ParseConditions(fields[2]);
tle.defaultFolderName = foldername;
tle.enablesIndexByDefault = true;
@ -143,7 +146,7 @@ namespace Extractors
}
catch (Exception e)
{
Printing.WriteLine($"Error removing from source: {e}", debugOnly: true);
Printing.WriteLine($"Error removing from source: {e}");
}
}
}

View file

@ -18,22 +18,22 @@ namespace Extractors
return input == "spotify-likes" || input.IsInternetUrl() && input.Contains("spotify.com");
}
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse)
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config)
{
var trackLists = new TrackLists();
int max = reverse ? int.MaxValue : maxTracks;
int off = reverse ? 0 : offset;
bool needLogin = input == "spotify-likes" || Config.I.removeTracksFromSource;
bool needLogin = input == "spotify-likes" || config.removeTracksFromSource;
if (needLogin && Config.I.spotifyToken.Length == 0 && (Config.I.spotifyId.Length == 0 || Config.I.spotifySecret.Length == 0))
if (needLogin && config.spotifyToken.Length == 0 && (config.spotifyId.Length == 0 || config.spotifySecret.Length == 0))
{
Console.WriteLine("Error: Credentials are required when downloading liked music or removing from source playlists.");
Environment.Exit(1);
}
spotifyClient = new Spotify(Config.I.spotifyId, Config.I.spotifySecret, Config.I.spotifyToken, Config.I.spotifyRefresh);
await spotifyClient.Authorize(needLogin, Config.I.removeTracksFromSource);
spotifyClient = new Spotify(config.spotifyId, config.spotifySecret, config.spotifyToken, config.spotifyRefresh);
await spotifyClient.Authorize(needLogin, config.removeTracksFromSource);
TrackListEntry? tle = null;
@ -53,10 +53,10 @@ namespace Extractors
tle = new TrackListEntry(TrackType.Album);
tle.source = source;
if (Config.I.setAlbumMinTrackCount)
if (config.setAlbumMinTrackCount)
source.MinAlbumTrackCount = tracks.Count;
if (Config.I.setAlbumMaxTrackCount)
if (config.setAlbumMaxTrackCount)
source.MaxAlbumTrackCount = tracks.Count;
}
else if (input.Contains("/artist/"))
@ -81,7 +81,7 @@ namespace Extractors
{
if (!needLogin && !spotifyClient.UsedDefaultCredentials)
{
await spotifyClient.Authorize(true, Config.I.removeTracksFromSource);
await spotifyClient.Authorize(true, config.removeTracksFromSource);
(playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(input, max, off);
}
else if (!needLogin)
@ -117,7 +117,7 @@ namespace Extractors
}
catch (Exception e)
{
Printing.WriteLine($"Error removing from source: {e}", debugOnly: true);
Printing.WriteLine($"Error removing from source: {e}");
}
}
}

View file

@ -11,13 +11,21 @@ namespace Extractors
return !input.IsInternetUrl();
}
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse)
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config)
{
bool isAlbum = config.album;
if (input.StartsWith("album://"))
{
isAlbum = true;
input = input[8..];
}
var trackLists = new TrackLists();
var music = ParseTrackArg(input, Config.I.album);
var music = ParseTrackArg(input, isAlbum);
TrackListEntry tle;
if (Config.I.album || (music.Title.Length == 0 && music.Album.Length > 0))
if (isAlbum || (music.Title.Length == 0 && music.Album.Length > 0))
{
music.Type = TrackType.Album;
tle = new TrackListEntry(music);

View file

@ -21,24 +21,24 @@ namespace Extractors
return input.IsInternetUrl() && (input.Contains("youtu.be") || input.Contains("youtube.com"));
}
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse)
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse, Config config)
{
var trackLists = new TrackLists();
int max = reverse ? int.MaxValue : maxTracks;
int off = reverse ? 0 : offset;
YouTube.apiKey = Config.I.ytKey;
YouTube.apiKey = config.ytKey;
string name;
List<Track>? deleted = null;
List<Track> tracks = new();
if (Config.I.getDeleted)
if (config.getDeleted)
{
Console.WriteLine("Getting deleted videos..");
var archive = new YouTube.YouTubeArchiveRetriever();
deleted = await archive.RetrieveDeleted(input, printFailed: Config.I.deletedOnly);
deleted = await archive.RetrieveDeleted(input, printFailed: config.deletedOnly);
}
if (!Config.I.deletedOnly)
if (!config.deletedOnly)
{
if (YouTube.apiKey.Length > 0)
{

View file

@ -10,10 +10,12 @@ public class FileManager
readonly HashSet<Track> organized = new();
public string? remoteCommonDir { get; private set; }
public string? defaultFolderName { get; private set; }
private readonly Config config;
public FileManager(TrackListEntry tle)
public FileManager(TrackListEntry tle, Config config)
{
this.tle = tle;
this.config = config;
}
public string GetSavePath(string sourceFname)
@ -23,7 +25,7 @@ public class FileManager
public string GetSavePathNoExt(string sourceFname)
{
string parent = Config.I.parentDir;
string parent = config.parentDir;
string name = Utils.GetFileNameWithoutExtSlsk(sourceFname);
if (tle.defaultFolderName != null)
@ -39,7 +41,7 @@ public class FileManager
parent = Path.Join(parent, dirname, Path.GetDirectoryName(relpath) ?? "");
}
return Path.Join(parent, name).CleanPath(Config.I.invalidReplaceStr);
return Path.Join(parent, name).CleanPath(config.invalidReplaceStr);
}
public void SetRemoteCommonDir(string? remoteCommonDir)
@ -62,7 +64,7 @@ public class FileManager
OrganizeAudio(track, track.FirstDownload);
}
bool onlyAdditionalImages = Config.I.nameFormat.Length == 0;
bool onlyAdditionalImages = config.nameFormat.Length == 0;
var nonAudioToOrganize = onlyAdditionalImages ? additionalImages : tracks.Where(t => t.IsNotAudio);
@ -86,14 +88,14 @@ public class FileManager
if (track.DownloadPath.Length == 0 || !Utils.IsMusicFile(track.DownloadPath))
return;
if (Config.I.nameFormat.Length == 0)
if (config.nameFormat.Length == 0)
{
organized.Add(track);
return;
}
string pathPart = SubstituteValues(Config.I.nameFormat, track, file);
string newFilePath = Path.Join(Config.I.parentDir, pathPart + Path.GetExtension(track.DownloadPath));
string pathPart = SubstituteValues(config.nameFormat, track, file);
string newFilePath = Path.Join(config.parentDir, pathPart + Path.GetExtension(track.DownloadPath));
try
{
@ -132,13 +134,13 @@ public class FileManager
organized.Add(track);
}
static void MoveAndDeleteParent(string oldPath, string newPath)
void MoveAndDeleteParent(string oldPath, string newPath)
{
if (Utils.NormalizedPath(oldPath) != Utils.NormalizedPath(newPath))
{
Directory.CreateDirectory(Path.GetDirectoryName(newPath));
Utils.Move(oldPath, newPath);
Utils.DeleteAncestorsIfEmpty(Path.GetDirectoryName(oldPath), Config.I.parentDir);
Utils.DeleteAncestorsIfEmpty(Path.GetDirectoryName(oldPath), config.parentDir);
}
}
@ -182,7 +184,7 @@ public class FileManager
chosenOpt = Regex.Replace(chosenOpt, @"\([^()]*\)|[^()]+", match =>
{
if (match.Value.StartsWith("(") && match.Value.EndsWith(")"))
return match.Value[1..^1].ReplaceInvalidChars(Config.I.invalidReplaceStr, removeSlash: false);
return match.Value[1..^1].ReplaceInvalidChars(config.invalidReplaceStr, removeSlash: false);
else
{
TryGetVarValue(match.Value, file, slfile, track, out string res);
@ -203,7 +205,7 @@ public class FileManager
char dirsep = Path.DirectorySeparatorChar;
newName = newName.Replace('/', dirsep).Replace('\\', dirsep);
var x = newName.Split(dirsep, StringSplitOptions.RemoveEmptyEntries);
newName = string.Join(dirsep, x.Select(x => x.ReplaceInvalidChars(Config.I.invalidReplaceStr).Trim(' ', '.')));
newName = string.Join(dirsep, x.Select(x => x.ReplaceInvalidChars(config.invalidReplaceStr).Trim(' ', '.')));
return newName;
}
@ -257,15 +259,14 @@ public class FileManager
}
return true;
case "extractor":
res = Config.I.inputType.ToString(); break;
res = config.inputType.ToString(); break;
case "default-folder":
res = tle.defaultFolderName ?? tle.source.ToString(false); break;
default:
res = x; return false;
}
res = res.ReplaceInvalidChars(Config.I.invalidReplaceStr);
res = res.ReplaceInvalidChars(config.invalidReplaceStr);
return true;
}
}

View file

@ -473,7 +473,7 @@ public static class Help
interactive (bool)
";
public static void PrintHelp(string option = "")
public static void PrintHelp(string? option = null)
{
string text = helpText;
@ -487,11 +487,11 @@ public static class Help
{ "config", configHelp },
};
if (dict.ContainsKey(option))
if (option != null && dict.ContainsKey(option))
text = dict[option];
else if (option == "all")
text = $"{helpText}\n{string.Join('\n', dict.Values)}";
else if (option.Length > 0)
else if (option != null)
Console.WriteLine($"Unrecognized help option '{option}'");
var lines = text.Split('\n').Skip(1);
@ -499,5 +499,15 @@ public static class Help
text = string.Join("\n", lines.Select(line => line.Length > minIndent ? line[minIndent..] : line));
Console.WriteLine(text);
}
public static void PrintHelpAndExitIfNeeded(string[] args)
{
int helpIdx = Array.FindLastIndex(args, x => x == "--help" || x == "-h");
if (args.Length == 0 || helpIdx >= 0)
{
PrintHelp(helpIdx + 1 < args.Length ? args[helpIdx + 1] : null);
Environment.Exit(0);
}
}
}

View file

@ -1,4 +1,5 @@
using Enums;
using FileSkippers;
namespace Models
{
@ -12,8 +13,14 @@ namespace Models
public bool gotoNextAfterSearch = false;
public bool enablesIndexByDefault = false;
public string? defaultFolderName = null;
public FileConditions? additionalConds = null;
public FileConditions? additionalPrefConds = null;
public Config config = null!;
public FileConditions? extractorCond = null;
public FileConditions? extractorPrefCond = null;
public M3uEditor? playlistEditor = null;
public M3uEditor? indexEditor = null;
public FileSkipper? outputDirSkipper = null;
public FileSkipper? musicDirSkipper = null;
public TrackListEntry(TrackType trackType)
{

View file

@ -5,6 +5,7 @@ namespace Models
public class TrackLists
{
public List<TrackListEntry> lists = new();
public int Count => lists.Count;
public TrackLists() { }

View file

@ -18,57 +18,56 @@ using SlFile = Soulseek.File;
static partial class Program
{
const int updateInterval = 100;
private static bool initialized = false;
public static bool skipUpdate = false;
public static bool initialized = false;
public static IExtractor? extractor;
public static SoulseekClient? client;
public static TrackLists? trackLists;
public static M3uEditor? playlistEditor;
public static M3uEditor? indexEditor;
public static FileSkipper? outputDirSkipper = null;
public static FileSkipper? musicDirSkipper = null;
public static IExtractor extractor = null!;
public static TrackLists trackLists = null!;
public static SoulseekClient client = null!;
public static readonly ConcurrentDictionary<Track, SearchInfo> searches = new();
public static readonly ConcurrentDictionary<string, DownloadWrapper> downloads = new();
public static readonly ConcurrentDictionary<string, int> userSuccessCount = new();
public static readonly ConcurrentDictionary<string, int> userSuccessCounts = new();
static async Task Main(string[] args)
{
Console.ResetColor();
Console.OutputEncoding = System.Text.Encoding.UTF8;
Help.PrintHelpAndExitIfNeeded(args);
Config.I.LoadAndParse(args);
var config = new Config(args);
if (Config.I.input.Length == 0)
if (config.input.Length == 0)
throw new ArgumentException($"No input provided");
(Config.I.inputType, extractor) = ExtractorRegistry.GetMatchingExtractor(Config.I.input, Config.I.inputType);
(config.inputType, extractor) = ExtractorRegistry.GetMatchingExtractor(config.input, config.inputType);
WriteLine($"Using extractor: {Config.I.inputType}", debugOnly: true);
WriteLineIf($"Using extractor: {config.inputType}", config.debugInfo);
trackLists = await extractor.GetTracks(Config.I.input, Config.I.maxTracks, Config.I.offset, Config.I.reverse);
trackLists = await extractor.GetTracks(config.input, config.maxTracks, config.offset, config.reverse, config);
WriteLine("Got tracks", debugOnly: true);
WriteLineIf("Got tracks", config.debugInfo);
Config.I.PostProcessArgs();
config.PostProcessArgs();
trackLists.UpgradeListTypes(Config.I.aggregate, Config.I.album);
trackLists.UpgradeListTypes(config.aggregate, config.album);
trackLists.SetListEntryOptions();
playlistEditor = new M3uEditor(trackLists, Config.I.writePlaylist ? M3uOption.Playlist : M3uOption.None, Config.I.offset);
indexEditor = new M3uEditor(trackLists, Config.I.writeIndex ? M3uOption.Index : M3uOption.None);
InitConfigs(config);
await MainLoop();
WriteLine("Mainloop done", debugOnly: true);
WriteLineIf("Mainloop done", config.debugInfo);
}
public static async Task InitClientAndUpdateIfNeeded()
public static async Task InitClientAndUpdateIfNeeded(Config config)
{
if (initialized)
return;
bool needLogin = !Config.I.PrintTracks;
bool needLogin = !config.PrintTracks;
if (needLogin)
{
var connectionOptions = new ConnectionOptions(configureSocket: (socket) =>
@ -82,61 +81,149 @@ static partial class Program
var clientOptions = new SoulseekClientOptions(
transferConnectionOptions: connectionOptions,
serverConnectionOptions: connectionOptions,
listenPort: Config.I.listenPort
listenPort: config.listenPort
);
client = new SoulseekClient(clientOptions);
if (!Config.I.useRandomLogin && (string.IsNullOrEmpty(Config.I.username) || string.IsNullOrEmpty(Config.I.password)))
if (!config.useRandomLogin && (string.IsNullOrEmpty(config.username) || string.IsNullOrEmpty(config.password)))
throw new ArgumentException("No soulseek username or password");
await Login(Config.I.useRandomLogin);
await Login(config, config.useRandomLogin);
Search.searchSemaphore = new RateLimitedSemaphore(Config.I.searchesPerTime, TimeSpan.FromSeconds(Config.I.searchRenewTime));
Search.searchSemaphore = new RateLimitedSemaphore(config.searchesPerTime, TimeSpan.FromSeconds(config.searchRenewTime));
}
bool needUpdate = needLogin;
if (needUpdate)
{
var UpdateTask = Task.Run(() => Update());
WriteLine("Update started", debugOnly: true);
var UpdateTask = Task.Run(() => Update(config));
WriteLineIf("Update started", config.debugInfo);
}
initialized = true;
}
static void InitFileSkippers()
static void InitConfigs(Config defaultConfig)
{
if (Config.I.skipExisting)
if (trackLists.Count == 0)
return;
void initEditors(TrackListEntry tle, Config config)
{
FileConditions? cond = null;
tle.playlistEditor = new M3uEditor(trackLists, config.writePlaylist ? M3uOption.Playlist : M3uOption.None, config.offset);
tle.indexEditor = new M3uEditor(trackLists, config.writeIndex ? M3uOption.Index : M3uOption.None);
}
if (Config.I.skipCheckPrefCond)
void initFileSkippers(TrackListEntry tle, Config config)
{
if (config.skipExisting)
{
cond = Config.I.necessaryCond.With(Config.I.preferredCond);
}
else if (Config.I.skipCheckCond)
{
cond = Config.I.necessaryCond;
}
FileConditions? cond = null;
outputDirSkipper = FileSkipperRegistry.GetSkipper(Config.I.skipMode, Config.I.parentDir, cond, indexEditor);
if (config.skipCheckPrefCond)
{
cond = config.necessaryCond.With(config.preferredCond);
}
else if (config.skipCheckCond)
{
cond = config.necessaryCond;
}
if (Config.I.skipMusicDir.Length > 0)
{
if (!Directory.Exists(Config.I.skipMusicDir))
Console.WriteLine("Error: Music directory does not exist");
else
musicDirSkipper = FileSkipperRegistry.GetSkipper(Config.I.skipModeMusicDir, Config.I.skipMusicDir, cond, indexEditor);
tle.outputDirSkipper = FileSkipperRegistry.GetSkipper(config.skipMode, config.parentDir, cond, tle.indexEditor);
if (config.skipMusicDir.Length > 0)
{
if (!Directory.Exists(config.skipMusicDir))
Console.WriteLine("Error: Music directory does not exist");
else
tle.musicDirSkipper = FileSkipperRegistry.GetSkipper(config.skipModeMusicDir, config.skipMusicDir, cond, tle.indexEditor);
}
}
}
foreach (var tle in trackLists.lists)
{
tle.config = defaultConfig.Copy();
tle.config.UpdateProfiles(tle);
if (tle.extractorCond != null)
{
tle.config.necessaryCond = tle.config.necessaryCond.With(tle.extractorCond);
tle.extractorCond = null;
}
if (tle.extractorPrefCond != null)
{
tle.config.preferredCond = tle.config.preferredCond.With(tle.extractorPrefCond);
tle.extractorPrefCond = null;
}
initEditors(tle, tle.config);
initFileSkippers(tle, tle.config);
}
//defaultConfig.UpdateProfiles(trackLists[0]);
//trackLists[0].config = defaultConfig;
//initEditors(trackLists[0], defaultConfig);
//initFileSkippers(trackLists[0], defaultConfig);
//var configs = new Dictionary<Config, TrackListEntry?>() { { defaultConfig, trackLists[0] } };
//// configs, skippers, and editors are assigned to every individual tle (since they may change based
//// on auto-profiles). This loop re-uses existing configs/skippers/editors whenever autoprofiles
//// don't change. Otherwise, a new file skipper would be created for every tle, and would require
//// indexing every time, even if the directory to be indexed is unchanged.
//foreach (var tle in trackLists.lists.Skip(1))
//{
// bool needUpdate = true;
// foreach (var (config, exampleTle) in configs)
// {
// if (!config.NeedUpdateProfiles(tle))
// {
// tle.config = config;
// if (exampleTle == null)
// {
// initEditors(tle, config);
// initFileSkippers(tle, config);
// configs[config] = tle;
// }
// else
// {
// tle.playlistEditor = exampleTle.playlistEditor;
// tle.indexEditor = exampleTle.indexEditor;
// tle.outputDirSkipper = exampleTle.outputDirSkipper;
// tle.musicDirSkipper = exampleTle.musicDirSkipper;
// }
// needUpdate = false;
// break;
// }
// }
// bool hasExtractorConditions = tle.extractorCond != null || tle.extractorPrefCond != null;
// if (!needUpdate)
// continue;
// var newConfig = defaultConfig.Copy();
// newConfig.UpdateProfiles(tle);
// configs[newConfig] = tle;
// tle.config = newConfig;
// // todo: only create new instances if a relevant config item has changed
// initEditors(tle, newConfig);
// initFileSkippers(tle, newConfig);
//}
}
static void PreprocessTracks(TrackListEntry tle)
static void PreprocessTracks(Config config, TrackListEntry tle)
{
PreprocessTrack(tle.source);
PreprocessTrack(config, tle.source);
for (int k = 0; k < tle.list.Count; k++)
{
@ -144,31 +231,31 @@ static partial class Program
{
for (int i = 0; i < ls.Count; i++)
{
PreprocessTrack(ls[i]);
PreprocessTrack(config, ls[i]);
}
}
}
}
static void PreprocessTrack(Track track)
static void PreprocessTrack(Config config, Track track)
{
if (Config.I.removeFt)
if (config.removeFt)
{
track.Title = track.Title.RemoveFt();
track.Artist = track.Artist.RemoveFt();
}
if (Config.I.removeBrackets)
if (config.removeBrackets)
{
track.Title = track.Title.RemoveSquareBrackets();
}
if (Config.I.regexToReplace.Title.Length + Config.I.regexToReplace.Artist.Length + Config.I.regexToReplace.Album.Length > 0)
if (config.regexToReplace.Title.Length + config.regexToReplace.Artist.Length + config.regexToReplace.Album.Length > 0)
{
track.Title = Regex.Replace(track.Title, Config.I.regexToReplace.Title, Config.I.regexReplaceBy.Title);
track.Artist = Regex.Replace(track.Artist, Config.I.regexToReplace.Artist, Config.I.regexReplaceBy.Artist);
track.Album = Regex.Replace(track.Album, Config.I.regexToReplace.Album, Config.I.regexReplaceBy.Album);
track.Title = Regex.Replace(track.Title, config.regexToReplace.Title, config.regexReplaceBy.Title);
track.Artist = Regex.Replace(track.Artist, config.regexToReplace.Artist, config.regexReplaceBy.Artist);
track.Album = Regex.Replace(track.Album, config.regexToReplace.Album, config.regexReplaceBy.Album);
}
if (Config.I.artistMaybeWrong)
if (config.artistMaybeWrong)
{
track.ArtistMaybeWrong = true;
}
@ -179,45 +266,26 @@ static partial class Program
}
static void PrepareListEntry(TrackListEntry tle, bool isFirstEntry)
static void PrepareListEntry(Config config, TrackListEntry tle, bool isFirstEntry)
{
Config.I.RestoreConditions();
bool changed = Config.UpdateProfiles(tle);
Config.I.AddTemporaryConditions(tle.additionalConds, tle.additionalPrefConds);
string m3uPath, indexPath;
if (Config.I.m3uFilePath.Length > 0)
m3uPath = Config.I.m3uFilePath;
if (config.m3uFilePath.Length > 0)
m3uPath = config.m3uFilePath;
else
m3uPath = Path.Join(Config.I.parentDir, tle.defaultFolderName, "_playlist.m3u8");
m3uPath = Path.Join(config.parentDir, tle.defaultFolderName, "_playlist.m3u8");
if (Config.I.indexFilePath.Length > 0)
indexPath = Config.I.indexFilePath;
if (config.indexFilePath.Length > 0)
indexPath = config.indexFilePath;
else
indexPath = Path.Join(Config.I.parentDir, tle.defaultFolderName, "_index.sldl");
indexPath = Path.Join(config.parentDir, tle.defaultFolderName, "_index.sldl");
indexEditor.option = Config.I.writeIndex ? M3uOption.Index : M3uOption.None;
indexEditor.SetPathAndLoad(indexPath); // does nothing if the path is unchanged
if (config.writePlaylist)
tle.playlistEditor?.SetPathAndLoad(m3uPath);
if (config.writeIndex)
tle.indexEditor?.SetPathAndLoad(indexPath);
if (Config.I.writePlaylist)
{
playlistEditor.option = M3uOption.Playlist;
playlistEditor.SetPathAndLoad(m3uPath);
}
else
{
playlistEditor.option = M3uOption.None;
}
if (changed || isFirstEntry)
{
InitFileSkippers(); // todo: only do this when a relevant config item changes
}
PreprocessTracks(tle);
PreprocessTracks(config, tle);
}
@ -228,47 +296,48 @@ static partial class Program
Console.WriteLine();
var tle = trackLists[i];
var config = tle.config;
PrepareListEntry(tle, isFirstEntry: i == 0);
PrepareListEntry(config, tle, isFirstEntry: i == 0);
var existing = new List<Track>();
var notFound = new List<Track>();
if (Config.I.skipNotFound && !Config.I.PrintResults)
if (config.skipNotFound && !config.PrintResults)
{
if (tle.sourceCanBeSkipped && SetNotFoundLastTime(tle.source))
if (tle.sourceCanBeSkipped && SetNotFoundLastTime(config, tle.source, tle.indexEditor))
notFound.Add(tle.source);
if (tle.source.State != TrackState.NotFoundLastTime && !tle.needSourceSearch)
{
foreach (var tracks in tle.list)
notFound.AddRange(DoSkipNotFound(tracks));
notFound.AddRange(DoSkipNotFound(config, tracks, tle.indexEditor));
}
}
if (Config.I.skipExisting && !Config.I.PrintResults && tle.source.State != TrackState.NotFoundLastTime)
if (config.skipExisting && !config.PrintResults && tle.source.State != TrackState.NotFoundLastTime)
{
if (tle.sourceCanBeSkipped && SetExisting(tle.source))
if (tle.sourceCanBeSkipped && SetExisting(tle, config, tle.source))
existing.Add(tle.source);
if (tle.source.State != TrackState.AlreadyExists && !tle.needSourceSearch)
{
foreach (var tracks in tle.list)
existing.AddRange(DoSkipExisting(tracks));
existing.AddRange(DoSkipExisting(tle, config, tracks));
}
}
if (Config.I.PrintTracks)
if (config.PrintTracks)
{
if (tle.source.Type == TrackType.Normal)
{
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type);
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type, config);
}
else
{
var tl = new List<Track>();
if (tle.source.State == TrackState.Initial) tl.Add(tle.source);
PrintTracksTbd(tl, existing, notFound, tle.source.Type, summary: false);
PrintTracksTbd(tl, existing, notFound, tle.source.Type, config, summary: false);
}
continue;
}
@ -290,7 +359,7 @@ static partial class Program
if (tle.needSourceSearch)
{
await InitClientAndUpdateIfNeeded();
await InitClientAndUpdateIfNeeded(config);
Console.WriteLine($"{tle.source.Type} download: {tle.source.ToString(true)}, searching..");
@ -299,17 +368,17 @@ static partial class Program
if (tle.source.Type == TrackType.Album)
{
tle.list = await Search.GetAlbumDownloads(tle.source, responseData);
tle.list = await Search.GetAlbumDownloads(tle.source, responseData, config);
foundSomething = tle.list.Count > 0 && tle.list[0].Count > 0;
}
else if (tle.source.Type == TrackType.Aggregate)
{
tle.list.Insert(0, await Search.GetAggregateTracks(tle.source, responseData));
tle.list.Insert(0, await Search.GetAggregateTracks(tle.source, responseData, config));
foundSomething = tle.list.Count > 0 && tle.list[0].Count > 0;
}
else if (tle.source.Type == TrackType.AlbumAggregate)
{
var res = await Search.GetAggregateAlbums(tle.source, responseData);
var res = await Search.GetAggregateAlbums(tle.source, responseData, config);
foreach (var item in res)
{
@ -327,20 +396,20 @@ static partial class Program
var lockedFiles = responseData.lockedFilesCount > 0 ? $" (Found {responseData.lockedFilesCount} locked files)" : "";
Console.WriteLine($"No results.{lockedFiles}");
if (!Config.I.PrintResults)
if (!config.PrintResults)
{
tle.source.State = TrackState.Failed;
tle.source.FailureReason = FailureReason.NoSuitableFileFound;
indexEditor.Update();
tle.indexEditor?.Update();
}
continue;
}
if (Config.I.skipExisting && tle.needSkipExistingAfterSearch)
if (config.skipExisting && tle.needSkipExistingAfterSearch)
{
foreach (var tracks in tle.list)
existing.AddRange(DoSkipExisting(tracks));
existing.AddRange(DoSkipExisting(tle, config, tracks));
}
if (tle.gotoNextAfterSearch)
@ -349,18 +418,18 @@ static partial class Program
}
}
if (Config.I.PrintResults)
if (config.PrintResults)
{
await PrintResults(tle, existing, notFound);
await PrintResults(tle, existing, notFound, config);
continue;
}
indexEditor.Update();
playlistEditor.Update();
tle.indexEditor?.Update();
tle.playlistEditor?.Update();
if (tle.source.Type != TrackType.Album)
{
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type);
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type, config);
}
if (notFound.Count + existing.Count >= tle.list.Sum(x => x.Count))
@ -368,35 +437,35 @@ static partial class Program
continue;
}
await InitClientAndUpdateIfNeeded();
await InitClientAndUpdateIfNeeded(config);
if (tle.source.Type == TrackType.Normal)
{
await DownloadNormal(tle);
await DownloadNormal(config, tle);
}
else if (tle.source.Type == TrackType.Album)
{
await DownloadAlbum(tle);
await DownloadAlbum(config, tle);
}
else if (tle.source.Type == TrackType.Aggregate)
{
await DownloadNormal(tle);
await DownloadNormal(config, tle);
}
}
if (!Config.I.DoNotDownload && (trackLists.lists.Count > 0 || trackLists.Flattened(false, false).Skip(1).Any()))
if (!trackLists[^1].config.DoNotDownload && (trackLists.lists.Count > 0 || trackLists.Flattened(false, false).Skip(1).Any()))
{
PrintComplete(trackLists);
}
}
static List<Track> DoSkipExisting(List<Track> tracks)
static List<Track> DoSkipExisting(TrackListEntry tle, Config config, List<Track> tracks)
{
var existing = new List<Track>();
foreach (var track in tracks)
{
if (SetExisting(track))
if (SetExisting(tle, config, track))
{
existing.Add(track);
}
@ -405,27 +474,27 @@ static partial class Program
}
static bool SetExisting(Track track)
static bool SetExisting(TrackListEntry tle, Config config, Track track)
{
string? path = null;
if (outputDirSkipper != null)
if (tle.outputDirSkipper != null)
{
if (!outputDirSkipper.IndexIsBuilt)
outputDirSkipper.BuildIndex();
if (!tle.outputDirSkipper.IndexIsBuilt)
tle.outputDirSkipper.BuildIndex();
outputDirSkipper.TrackExists(track, out path);
tle.outputDirSkipper.TrackExists(track, out path);
}
if (path == null && musicDirSkipper != null)
if (path == null && tle.musicDirSkipper != null)
{
if (!musicDirSkipper.IndexIsBuilt)
if (!tle.musicDirSkipper.IndexIsBuilt)
{
Console.WriteLine($"Building music directory index..");
musicDirSkipper.BuildIndex();
tle.musicDirSkipper.BuildIndex();
}
musicDirSkipper.TrackExists(track, out path);
tle.musicDirSkipper.TrackExists(track, out path);
}
if (path != null)
@ -438,12 +507,12 @@ static partial class Program
}
static List<Track> DoSkipNotFound(List<Track> tracks)
static List<Track> DoSkipNotFound(Config config, List<Track> tracks, M3uEditor indexEditor)
{
var notFound = new List<Track>();
foreach (var track in tracks)
{
if (SetNotFoundLastTime(track))
if (SetNotFoundLastTime(config, track, indexEditor))
{
notFound.Add(track);
}
@ -452,7 +521,7 @@ static partial class Program
}
static bool SetNotFoundLastTime(Track track)
static bool SetNotFoundLastTime(Config config, Track track, M3uEditor indexEditor)
{
if (indexEditor.TryGetPreviousRunResult(track, out var prevTrack))
{
@ -466,47 +535,47 @@ static partial class Program
}
static async Task DownloadNormal(TrackListEntry tle)
static async Task DownloadNormal(Config config, TrackListEntry tle)
{
var tracks = tle.list[0];
var semaphore = new SemaphoreSlim(Config.I.concurrentProcesses);
var semaphore = new SemaphoreSlim(config.concurrentProcesses);
var organizer = new FileManager(tle);
var organizer = new FileManager(tle, config);
var downloadTasks = tracks.Select(async (track, index) =>
{
using var cts = new CancellationTokenSource();
await DownloadTask(tle, track, semaphore, organizer, cts, false, true, true);
indexEditor.Update();
playlistEditor.Update();
await DownloadTask(config, tle, track, semaphore, organizer, cts, false, true, true);
tle.indexEditor?.Update();
tle.playlistEditor?.Update();
});
await Task.WhenAll(downloadTasks);
if (Config.I.removeTracksFromSource && tracks.All(t => t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists))
if (config.removeTracksFromSource && tracks.All(t => t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists))
await extractor.RemoveTrackFromSource(tle.source);
}
static async Task DownloadAlbum(TrackListEntry tle)
static async Task DownloadAlbum(Config config, TrackListEntry tle)
{
var organizer = new FileManager(tle);
var organizer = new FileManager(tle, config);
List<Track>? tracks = null;
var retrievedFolders = new HashSet<string>();
bool succeeded = false;
string? soulseekDir = null;
int index = 0;
while (tle.list.Count > 0 && !Config.I.albumArtOnly)
while (tle.list.Count > 0 && !config.albumArtOnly)
{
bool wasInteractive = Config.I.interactiveMode;
bool wasInteractive = config.interactiveMode;
bool retrieveCurrent = true;
index = 0;
if (Config.I.interactiveMode)
if (config.interactiveMode)
{
(index, tracks, retrieveCurrent) = await InteractiveModeAlbum(tle.list, !Config.I.noBrowseFolder, retrievedFolders);
(index, tracks, retrieveCurrent) = await InteractiveModeAlbum(config, tle.list, !config.noBrowseFolder, retrievedFolders);
if (index == -1) break;
}
else
@ -518,7 +587,7 @@ static partial class Program
organizer.SetRemoteCommonDir(soulseekDir);
if (!Config.I.interactiveMode && !wasInteractive)
if (!config.interactiveMode && !wasInteractive)
{
Console.WriteLine();
PrintAlbum(tracks);
@ -529,9 +598,9 @@ static partial class Program
try
{
await RunAlbumDownloads(tle, organizer, tracks, semaphore, cts);
await RunAlbumDownloads(config, tle, organizer, tracks, semaphore, cts);
if (!Config.I.noBrowseFolder && retrieveCurrent && !retrievedFolders.Contains(soulseekDir))
if (!config.noBrowseFolder && retrieveCurrent && !retrievedFolders.Contains(soulseekDir))
{
Console.WriteLine("Getting all files in folder...");
@ -541,7 +610,7 @@ static partial class Program
if (newFilesFound > 0)
{
Console.WriteLine($"Found {newFilesFound} more files in the directory, downloading:");
await RunAlbumDownloads(tle, organizer, tracks, semaphore, cts);
await RunAlbumDownloads(config, tle, organizer, tracks, semaphore, cts);
}
else
{
@ -554,7 +623,7 @@ static partial class Program
}
catch (OperationCanceledException)
{
OnAlbumFail(tracks);
OnAlbumFail(config, tracks);
}
organizer.SetRemoteCommonDir(null);
@ -563,15 +632,15 @@ static partial class Program
if (succeeded)
{
await OnAlbumSuccess(tle, tracks);
await OnAlbumSuccess(config, tle, tracks);
}
List<Track>? additionalImages = null;
if (Config.I.albumArtOnly || succeeded && Config.I.albumArtOption != AlbumArtOption.Default)
if (config.albumArtOnly || succeeded && config.albumArtOption != AlbumArtOption.Default)
{
Console.WriteLine($"\nDownloading additional images:");
additionalImages = await DownloadImages(tle, tle.list, Config.I.albumArtOption, tle.list[index]);
additionalImages = await DownloadImages(config, tle, tle.list, config.albumArtOption, tle.list[index]);
tracks?.AddRange(additionalImages);
}
@ -580,22 +649,22 @@ static partial class Program
organizer.OrganizeAlbum(tracks, additionalImages);
}
indexEditor.Update();
playlistEditor.Update();
tle.indexEditor?.Update();
tle.playlistEditor?.Update();
}
static async Task RunAlbumDownloads(TrackListEntry tle, FileManager organizer, List<Track> tracks, SemaphoreSlim semaphore, CancellationTokenSource cts)
static async Task RunAlbumDownloads(Config config, TrackListEntry tle, FileManager organizer, List<Track> tracks, SemaphoreSlim semaphore, CancellationTokenSource cts)
{
var downloadTasks = tracks.Select(async track =>
{
await DownloadTask(tle, track, semaphore, organizer, cts, true, true, true);
await DownloadTask(config, tle, track, semaphore, organizer, cts, true, true, true);
});
await Task.WhenAll(downloadTasks);
}
static async Task OnAlbumSuccess(TrackListEntry tle, List<Track>? tracks)
static async Task OnAlbumSuccess(Config config, TrackListEntry tle, List<Track>? tracks)
{
if (tracks == null)
return;
@ -607,7 +676,7 @@ static partial class Program
tle.source.State = TrackState.Downloaded;
tle.source.DownloadPath = Utils.GreatestCommonDirectory(downloadedAudio.Select(t => t.DownloadPath));
if (Config.I.removeTracksFromSource)
if (config.removeTracksFromSource)
{
await extractor.RemoveTrackFromSource(tle.source);
}
@ -615,9 +684,9 @@ static partial class Program
}
static void OnAlbumFail(List<Track>? tracks)
static void OnAlbumFail(Config config, List<Track>? tracks)
{
if (tracks == null || Config.I.IgnoreAlbumFail)
if (tracks == null || config.IgnoreAlbumFail)
return;
foreach (var track in tracks)
@ -626,18 +695,18 @@ static partial class Program
{
try
{
if (Config.I.DeleteAlbumOnFail)
if (config.DeleteAlbumOnFail)
{
File.Delete(track.DownloadPath);
}
else if (Config.I.failedAlbumPath.Length > 0)
else if (config.failedAlbumPath.Length > 0)
{
var newPath = Path.Join(Config.I.failedAlbumPath, Path.GetRelativePath(Config.I.parentDir, track.DownloadPath));
var newPath = Path.Join(config.failedAlbumPath, Path.GetRelativePath(config.parentDir, track.DownloadPath));
Directory.CreateDirectory(Path.GetDirectoryName(newPath));
Utils.Move(track.DownloadPath, newPath);
}
Utils.DeleteAncestorsIfEmpty(Path.GetDirectoryName(track.DownloadPath), Config.I.parentDir);
Utils.DeleteAncestorsIfEmpty(Path.GetDirectoryName(track.DownloadPath), config.parentDir);
}
catch (Exception e)
{
@ -648,13 +717,13 @@ static partial class Program
}
static async Task<List<Track>> DownloadImages(TrackListEntry tle, List<List<Track>> downloads, AlbumArtOption option, List<Track>? chosenAlbum)
static async Task<List<Track>> DownloadImages(Config config, TrackListEntry tle, List<List<Track>> downloads, AlbumArtOption option, List<Track>? chosenAlbum)
{
var downloadedImages = new List<Track>();
long mSize = 0;
int mCount = 0;
var fileManager = new FileManager(tle);
var fileManager = new FileManager(tle, config);
if (chosenAlbum != null)
{
@ -727,12 +796,12 @@ static partial class Program
while (albumArtLists.Count > 0)
{
int index = 0;
bool wasInteractive = Config.I.interactiveMode;
bool wasInteractive = config.interactiveMode;
List<Track> tracks;
if (Config.I.interactiveMode)
if (config.interactiveMode)
{
(index, tracks, _) = await InteractiveModeAlbum(albumArtLists, false, null);
(index, tracks, _) = await InteractiveModeAlbum(config, albumArtLists, false, null);
if (index == -1) break;
}
else
@ -748,7 +817,7 @@ static partial class Program
return downloadedImages;
}
if (!Config.I.interactiveMode && !wasInteractive)
if (!config.interactiveMode && !wasInteractive)
{
Console.WriteLine();
PrintAlbum(tracks);
@ -762,7 +831,7 @@ static partial class Program
foreach (var track in tracks)
{
using var cts = new CancellationTokenSource();
await DownloadTask(null, track, semaphore, fileManager, cts, false, false, false);
await DownloadTask(config, tle, track, semaphore, fileManager, cts, false, false, false);
if (track.State == TrackState.Downloaded)
downloadedImages.Add(track);
@ -778,30 +847,30 @@ static partial class Program
}
static async Task DownloadTask(TrackListEntry? tle, Track track, SemaphoreSlim semaphore, FileManager organizer, CancellationTokenSource? cts, bool cancelOnFail, bool removeFromSource, bool organize)
static async Task DownloadTask(Config config, TrackListEntry? tle, Track track, SemaphoreSlim semaphore, FileManager organizer, CancellationTokenSource? cts, bool cancelOnFail, bool removeFromSource, bool organize)
{
if (track.State != TrackState.Initial)
return;
await semaphore.WaitAsync(cts.Token);
int tries = Config.I.unknownErrorRetries;
int tries = config.unknownErrorRetries;
string savedFilePath = "";
SlFile? chosenFile = null;
while (tries > 0)
{
await WaitForLogin();
await WaitForLogin(config);
cts.Token.ThrowIfCancellationRequested();
try
{
(savedFilePath, chosenFile) = await Search.SearchAndDownload(track, organizer, cts);
(savedFilePath, chosenFile) = await Search.SearchAndDownload(track, organizer, config, cts);
}
catch (Exception ex)
{
WriteLine($"Error: {ex}", debugOnly: true);
WriteLineIf($"Error: {ex}", config.debugInfo);
if (!IsConnectedAndLoggedIn())
{
continue;
@ -844,7 +913,7 @@ static partial class Program
track.DownloadPath = savedFilePath;
}
if (removeFromSource && Config.I.removeTracksFromSource)
if (removeFromSource && config.removeTracksFromSource)
{
try
{
@ -865,16 +934,16 @@ static partial class Program
}
}
if (Config.I.onComplete.Length > 0)
if (config.onComplete.Length > 0)
{
OnComplete(Config.I.onComplete, track);
OnComplete(config, config.onComplete, track);
}
semaphore.Release();
}
static async Task<(int index, List<Track> tracks, bool retrieveFolder)> InteractiveModeAlbum(List<List<Track>> list, bool retrieveFolder, HashSet<string>? retrievedFolders)
static async Task<(int index, List<Track> tracks, bool retrieveFolder)> InteractiveModeAlbum(Config config, List<List<Track>> list, bool retrieveFolder, HashSet<string>? retrievedFolders)
{
int aidx = 0;
static string interactiveModeLoop() // bug: characters don't disappear when backspacing
@ -944,7 +1013,7 @@ static partial class Program
case "s":
return (-1, new List<Track>(), false);
case "q":
Config.I.interactiveMode = false;
config.interactiveMode = false;
return (aidx, tracks, true);
case "r":
if (!retrieveFolder)
@ -1002,7 +1071,7 @@ static partial class Program
}
static async Task Update()
static async Task Update(Config config)
{
while (true)
{
@ -1024,7 +1093,7 @@ static partial class Program
{
lock (val)
{
if ((DateTime.Now - val.UpdateLastChangeTime()).TotalMilliseconds > Config.I.maxStaleTime)
if ((DateTime.Now - val.UpdateLastChangeTime()).TotalMilliseconds > config.maxStaleTime)
{
val.stalled = true;
val.UpdateText();
@ -1051,10 +1120,10 @@ static partial class Program
&& !client.State.HasFlag(SoulseekClientStates.Connecting))
{
WriteLine($"\nDisconnected, logging in\n", ConsoleColor.DarkYellow, true);
try { await Login(Config.I.useRandomLogin); }
try { await Login(config, config.useRandomLogin); }
catch (Exception ex)
{
string banMsg = Config.I.useRandomLogin ? "" : " (possibly a 30-minute ban caused by frequent searches)";
string banMsg = config.useRandomLogin ? "" : " (possibly a 30-minute ban caused by frequent searches)";
WriteLine($"{ex.Message}{banMsg}", ConsoleColor.DarkYellow, true);
}
}
@ -1074,14 +1143,14 @@ static partial class Program
}
}
await Task.Delay(Config.I.updateDelay);
await Task.Delay(updateInterval);
}
}
static async Task Login(bool random = false, int tries = 3)
static async Task Login(Config config, bool random = false, int tries = 3)
{
string user = Config.I.username, pass = Config.I.password;
string user = config.username, pass = config.password;
if (random)
{
var r = new Random();
@ -1096,30 +1165,30 @@ static partial class Program
{
try
{
WriteLine($"Connecting {user}", debugOnly: true);
WriteLineIf($"Connecting {user}", config.debugInfo);
await client.ConnectAsync(user, pass);
if (!Config.I.noModifyShareCount)
if (!config.noModifyShareCount)
{
WriteLine($"Setting share count", debugOnly: true);
WriteLineIf($"Setting share count", config.debugInfo);
await client.SetSharedCountsAsync(20, 100);
}
break;
}
catch (Exception e)
{
WriteLine($"Exception while logging in: {e}", debugOnly: true);
WriteLineIf($"Exception while logging in: {e}", config.debugInfo);
if (!(e is Soulseek.AddressException || e is System.TimeoutException) && --tries == 0)
throw;
}
await Task.Delay(500);
WriteLine($"Retry login {user}", debugOnly: true);
WriteLineIf($"Retry login {user}", config.debugInfo);
}
WriteLine($"Logged in {user}", debugOnly: true);
WriteLineIf($"Logged in {user}", config.debugInfo);
}
static void OnComplete(string onComplete, Track track)
static void OnComplete(Config config, string onComplete, Track track)
{
if (onComplete.Length == 0)
return;
@ -1159,7 +1228,7 @@ static partial class Program
.Replace("{failure-reason}", track.FailureReason.ToString())
.Replace("{path}", track.DownloadPath)
.Replace("{state}", track.State.ToString())
.Replace("{extractor}", Config.I.inputType.ToString())
.Replace("{extractor}", config.inputType.ToString())
.Trim();
if (onComplete[0] == '"')
@ -1191,7 +1260,7 @@ static partial class Program
startInfo.UseShellExecute = useShellExecute;
process.StartInfo = startInfo;
WriteLine($"on-complete: FileName={startInfo.FileName}, Arguments={startInfo.Arguments}", debugOnly: true);
WriteLineIf($"on-complete: FileName={startInfo.FileName}, Arguments={startInfo.Arguments}", config.debugInfo);
process.Start();
@ -1205,11 +1274,11 @@ static partial class Program
}
public static async Task WaitForLogin()
public static async Task WaitForLogin(Config config)
{
while (true)
{
WriteLine($"Wait for login, state: {client.State}", debugOnly: true);
WriteLineIf($"Wait for login, state: {client.State}", config.debugInfo);
if (IsConnectedAndLoggedIn())
break;
await Task.Delay(1000);
@ -1222,5 +1291,3 @@ static partial class Program
return client != null && client.State.HasFlag(SoulseekClientStates.Connected) && client.State.HasFlag(SoulseekClientStates.LoggedIn);
}
}

View file

@ -16,14 +16,14 @@ 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)
public static async Task<(string, SlFile?)> SearchAndDownload(Track track, FileManager organizer, Config config, CancellationTokenSource? cts = null)
{
if (Config.I.DoNotDownload)
if (config.DoNotDownload)
throw new Exception();
IEnumerable<(SlResponse response, SlFile file)>? orderedResults = null;
var responseData = new ResponseData();
var progress = Printing.GetProgressBar();
var progress = Printing.GetProgressBar(config);
var results = new SlDictionary();
var fsResults = new SlDictionary();
using var searchCts = new CancellationTokenSource();
@ -58,7 +58,7 @@ static class Search
saveFilePath = organizer.GetSavePath(f.Filename);
fsUser = r.Username;
chosenFile = f;
downloadTask = Download.DownloadFile(r, f, saveFilePath, track, progress, cts?.Token, searchCts);
downloadTask = Download.DownloadFile(r, f, saveFilePath, track, progress, config, cts?.Token, searchCts);
}
}
}
@ -72,17 +72,17 @@ static class Search
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)
if (config.fastSearch && userSuccessCounts.GetValueOrDefault(r.Username, 0) > config.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))
if (r.HasFreeUploadSlot && r.UploadSpeed / 1024.0 / 1024.0 >= config.fastSearchMinUpSpeed
&& FileConditions.BracketCheck(track, InferTrack(f.Filename, track)) && config.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());
Task.Delay(config.fastSearchDelay).ContinueWith(tt => fastSearchDownload());
}
}
}
@ -94,8 +94,8 @@ static class Search
return new SearchOptions(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
searchTimeout: Config.I.searchTimeout,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
searchTimeout: config.searchTimeout,
removeSingleCharacterSearchTerms: config.removeSingleCharacterSearchTerms,
responseFilter: (response) =>
{
return response.UploadSpeed > 0 && necCond.BannedUsersSatisfies(response);
@ -107,13 +107,13 @@ static class Search
}
void onSearch() => Printing.RefreshOrPrint(progress, 0, $"Searching: {track}", true);
await RunSearches(track, results, getSearchOptions, responseHandler, searchCts.Token, onSearch);
await RunSearches(track, results, getSearchOptions, responseHandler, config, searchCts.Token, onSearch);
searches.TryRemove(track, out _);
searchEnded = true;
lock (fsDownloadLock) { }
if (downloading == 0 && results.IsEmpty && !Config.I.useYtdlp)
if (downloading == 0 && results.IsEmpty && !config.useYtdlp)
{
notFound = true;
}
@ -124,7 +124,7 @@ static class Search
if (downloadTask == null || downloadTask.IsFaulted || downloadTask.IsCanceled)
throw new TaskCanceledException();
await downloadTask;
userSuccessCount.AddOrUpdate(fsUser, 1, (k, v) => v + 1);
userSuccessCounts.AddOrUpdate(fsUser, 1, (k, v) => v + 1);
}
catch
{
@ -133,7 +133,7 @@ static class Search
if (chosenFile != null && fsUser != null)
{
results.TryRemove(fsUser + '\\' + chosenFile.Filename, out _);
userSuccessCount.AddOrUpdate(fsUser, -1, (k, v) => v - 1);
userSuccessCounts.AddOrUpdate(fsUser, -1, (k, v) => v - 1);
}
}
}
@ -145,9 +145,9 @@ static class Search
if (downloading == 0 && (!results.IsEmpty || orderedResults != null))
{
if (orderedResults == null)
orderedResults = OrderedResults(results, track, useInfer: true);
orderedResults = OrderedResults(results, track, config, useInfer: true);
int trackTries = Config.I.maxRetriesPerTrack;
int trackTries = config.maxRetriesPerTrack;
async Task<bool> process(SlResponse response, SlFile file)
{
saveFilePath = organizer.GetSavePath(file.Filename);
@ -155,13 +155,13 @@ static class Search
try
{
downloading = 1;
await Download.DownloadFile(response, file, saveFilePath, track, progress, cts?.Token);
userSuccessCount.AddOrUpdate(response.Username, 1, (k, v) => v + 1);
await Download.DownloadFile(response, file, saveFilePath, track, progress, config, cts?.Token);
userSuccessCounts.AddOrUpdate(response.Username, 1, (k, v) => v + 1);
return true;
}
catch (Exception e)
{
Printing.WriteLine($"Error: Download Error: {e}", ConsoleColor.DarkYellow, debugOnly: true);
Printing.WriteLineIf($"Error: Download Error: {e}", config.debugInfo, ConsoleColor.DarkYellow);
chosenFile = null;
saveFilePath = "";
@ -170,7 +170,7 @@ static class Search
if (!IsConnectedAndLoggedIn())
throw;
userSuccessCount.AddOrUpdate(response.Username, -1, (k, v) => v - 1);
userSuccessCounts.AddOrUpdate(response.Username, -1, (k, v) => v - 1);
if (--trackTries <= 0)
{
Printing.RefreshOrPrint(progress, 0, $"Out of download retries: {track}", true);
@ -190,7 +190,7 @@ static class Search
fr = orderedResults.Skip(1).FirstOrDefault();
if (fr != default)
{
if (userSuccessCount.GetValueOrDefault(fr.response.Username, 0) > Config.I.ignoreOn)
if (userSuccessCounts.GetValueOrDefault(fr.response.Username, 0) > config.ignoreOn)
{
success = await process(fr.response, fr.file);
}
@ -198,7 +198,7 @@ static class Search
{
foreach (var (response, file) in orderedResults.Skip(2))
{
if (userSuccessCount.GetValueOrDefault(response.Username, 0) <= Config.I.ignoreOn)
if (userSuccessCounts.GetValueOrDefault(response.Username, 0) <= config.ignoreOn)
continue;
success = await process(response, file);
if (success) break;
@ -208,7 +208,7 @@ static class Search
}
}
if (downloading == 0 && Config.I.useYtdlp)
if (downloading == 0 && config.useYtdlp)
{
notFound = false;
try
@ -220,12 +220,12 @@ static class Search
{
foreach (var (length, id, title) in ytResults)
{
if (Config.I.necessaryCond.LengthToleranceSatisfies(length, track.Length))
if (config.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);
saveFilePath = await Extractors.YouTube.YtdlpDownload(id, saveFilePathNoExt, config.ytdlpArgument);
Printing.RefreshOrPrint(progress, 100, $"Succeded: yt-dlp completed download for {track}", true);
break;
}
@ -260,14 +260,14 @@ static class Search
}
public static async Task<List<List<Track>>> GetAlbumDownloads(Track track, ResponseData responseData)
public static async Task<List<List<Track>>> GetAlbumDownloads(Track track, ResponseData responseData, Config config)
{
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,
removeSingleCharacterSearchTerms: config.removeSingleCharacterSearchTerms,
searchTimeout: timeout,
responseFilter: (response) =>
{
@ -290,11 +290,11 @@ static class Search
}
using var cts = new CancellationTokenSource();
await RunSearches(track, results, getSearchOptions, handler, cts.Token);
await RunSearches(track, results, getSearchOptions, handler, config, 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 orderedResults = OrderedResults(results, track, config, 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);
@ -354,10 +354,10 @@ static class Search
}
int min, max;
if (Config.I.minAlbumTrackCount > -1 || Config.I.maxAlbumTrackCount > -1)
if (config.minAlbumTrackCount > -1 || config.maxAlbumTrackCount > -1)
{
min = Config.I.minAlbumTrackCount;
max = Config.I.maxAlbumTrackCount;
min = config.minAlbumTrackCount;
max = config.maxAlbumTrackCount;
}
else
{
@ -403,14 +403,14 @@ static class Search
}
public static async Task<List<Track>> GetAggregateTracks(Track track, ResponseData responseData)
public static async Task<List<Track>> GetAggregateTracks(Track track, ResponseData responseData, Config config)
{
var results = new SlDictionary();
SearchOptions getSearchOptions(int timeout, FileConditions nec, FileConditions prf) =>
new(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
removeSingleCharacterSearchTerms: config.removeSingleCharacterSearchTerms,
searchTimeout: timeout,
responseFilter: (response) =>
{
@ -433,16 +433,16 @@ static class Search
}
using var cts = new CancellationTokenSource();
await RunSearches(track, results, getSearchOptions, handler, cts.Token);
await RunSearches(track, results, getSearchOptions, handler, config, 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();
var equivalentFiles = EquivalentFiles(track, results.Select(x => x.Value), config)
.Select(x => (x.Item1, OrderedResults(x.Item2, track, config, false, false, false))).ToList();
if (!Config.I.relax)
if (!config.relax)
{
equivalentFiles = equivalentFiles
.Where(x => FileConditions.StrictString(x.Item1.Title, track.Title, ignoreCase: true)
@ -463,9 +463,9 @@ static class Search
}
public static async Task<List<List<List<Track>>>> GetAggregateAlbums(Track track, ResponseData responseData)
public static async Task<List<List<List<Track>>>> GetAggregateAlbums(Track track, ResponseData responseData, Config config)
{
int maxDiff = Config.I.aggregateLengthTol;
int maxDiff = config.aggregateLengthTol;
bool lengthsAreSimilar(int[] sorted1, int[] sorted2)
{
@ -481,7 +481,7 @@ static class Search
return true;
}
var albums = await GetAlbumDownloads(track, responseData);
var albums = await GetAlbumDownloads(track, responseData, config);
var sortedLengthLists = new List<(int[] lengths, List<Track> album, string username)>();
@ -547,7 +547,7 @@ static class Search
}
res = res.Select((x, i) => (x, i))
.Where(x => usernamesList[x.i].Count >= Config.I.minSharesAggregate)
.Where(x => usernamesList[x.i].Count >= config.minSharesAggregate)
.OrderByDescending(x => usernamesList[x.i].Count)
.Select(x => x.x)
.ToList();
@ -624,11 +624,14 @@ static class Search
}
public static IEnumerable<(Track, IEnumerable<(SlResponse response, SlFile file)>)> EquivalentFiles(Track track,
IEnumerable<(SlResponse, SlFile)> fileResponses, int minShares = -1)
public static IEnumerable<(Track, IEnumerable<(SlResponse response, SlFile file)>)> EquivalentFiles(
Track track,
IEnumerable<(SlResponse, SlFile)> fileResponses,
Config config,
int minShares = -1)
{
if (minShares == -1)
minShares = Config.I.minSharesAggregate;
minShares = config.minSharesAggregate;
Track inferTrack((SearchResponse r, Soulseek.File f) x)
{
@ -638,7 +641,7 @@ static class Search
}
var groups = fileResponses
.GroupBy(inferTrack, new TrackComparer(ignoreCase: true, Config.I.aggregateLengthTol))
.GroupBy(inferTrack, new TrackComparer(ignoreCase: true, config.aggregateLengthTol))
.Select(x => (x, x.Select(y => y.Item1.Username).Distinct().Count()))
.Where(x => x.Item2 >= minShares)
.OrderByDescending(x => x.Item2)
@ -654,15 +657,25 @@ static class Search
}
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)
public static IOrderedEnumerable<(SlResponse response, SlFile file)> OrderedResults(
IEnumerable<KeyValuePair<string, (SlResponse, SlFile)>> results,
Track track,
Config config,
bool useInfer = false,
bool useLevenshtein = true,
bool albumMode = false)
{
return OrderedResults(results.Select(x => x.Value), track, useInfer, useLevenshtein, albumMode);
return OrderedResults(results.Select(x => x.Value), track, config, 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)
public static IOrderedEnumerable<(SlResponse response, SlFile file)> OrderedResults(
IEnumerable<(SlResponse, SlFile)> results,
Track track,
Config config,
bool useInfer = false,
bool useLevenshtein = true,
bool albumMode = false)
{
bool useBracketCheck = true;
if (albumMode)
@ -676,7 +689,7 @@ static class Search
if (useInfer) // this is very slow
{
var equivalentFiles = EquivalentFiles(track, results, 1);
var equivalentFiles = EquivalentFiles(track, results, config, 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));
@ -700,22 +713,22 @@ static class Search
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)
.Where(x => userSuccessCounts.GetValueOrDefault(x.response.Username, 0) > config.ignoreOn)
.OrderByDescending(x => userSuccessCounts.GetValueOrDefault(x.response.Username, 0) > config.downrankOn)
.ThenByDescending(x => config.necessaryCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => config.preferredCond.BannedUsersSatisfies(x.response))
.ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || config.preferredCond.AcceptNoLength == null || config.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 => config.preferredCond.StrictTitleSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => !albumMode || config.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
.ThenByDescending(x => config.preferredCond.StrictArtistSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => config.preferredCond.LengthToleranceSatisfies(x.file, track.Length))
.ThenByDescending(x => config.preferredCond.FormatSatisfies(x.file.Filename))
.ThenByDescending(x => albumMode || config.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
.ThenByDescending(x => config.preferredCond.BitrateSatisfies(x.file))
.ThenByDescending(x => config.preferredCond.SampleRateSatisfies(x.file))
.ThenByDescending(x => config.preferredCond.BitDepthSatisfies(x.file))
.ThenByDescending(x => config.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))
@ -730,7 +743,7 @@ static class Search
public static async Task RunSearches(Track track, SlDictionary results, Func<int, FileConditions, FileConditions, SearchOptions> getSearchOptions,
Action<SearchResponse> responseHandler, CancellationToken? ct = null, Action? onSearch = null)
Action<SearchResponse> responseHandler, Config config, CancellationToken? ct = null, Action? onSearch = null)
{
bool artist = track.Artist.Length > 0;
bool title = track.Title.Length > 0;
@ -739,27 +752,27 @@ static class Search
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));
var defaultSearchOpts = getSearchOptions(config.searchTimeout, config.necessaryCond, config.preferredCond);
searchTasks.Add(DoSearch(search, defaultSearchOpts, responseHandler, config, ct, onSearch));
if (search.RemoveDiacriticsIfExist(out string noDiacrSearch) && !track.ArtistMaybeWrong)
{
searchTasks.Add(DoSearch(noDiacrSearch, defaultSearchOpts, responseHandler, ct, onSearch));
searchTasks.Add(DoSearch(noDiacrSearch, defaultSearchOpts, responseHandler, config, ct, onSearch));
}
await Task.WhenAll(searchTasks);
if (results.IsEmpty && track.ArtistMaybeWrong && title)
{
var cond = new FileConditions(Config.I.necessaryCond);
var cond = new FileConditions(config.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));
var opts = getSearchOptions(Math.Min(config.searchTimeout, 5000), cond, config.preferredCond);
searchTasks.Add(DoSearch($"{infTrack.Artist} {infTrack.Title}", opts, responseHandler, config, ct, onSearch));
}
if (Config.I.desperateSearch)
if (config.desperateSearch)
{
await Task.WhenAll(searchTasks);
@ -767,24 +780,24 @@ static class Search
{
if (artist && album && title)
{
var cond = new FileConditions(Config.I.necessaryCond)
var cond = new FileConditions(config.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));
var opts = getSearchOptions(Math.Min(config.searchTimeout, 5000), cond, config.preferredCond);
searchTasks.Add(DoSearch($"{track.Artist} {track.Album}", opts, responseHandler, config, ct, onSearch));
}
if (artist && title && track.Length != -1 && Config.I.necessaryCond.LengthTolerance != -1)
if (artist && title && track.Length != -1 && config.necessaryCond.LengthTolerance != -1)
{
var cond = new FileConditions(Config.I.necessaryCond)
var cond = new FileConditions(config.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));
var opts = getSearchOptions(Math.Min(config.searchTimeout, 5000), cond, config.preferredCond);
searchTasks.Add(DoSearch($"{track.Artist} {track.Title}", opts, responseHandler, config, ct, onSearch));
}
}
@ -796,37 +809,37 @@ static class Search
if (track.Album.Length > 3 && album)
{
var cond = new FileConditions(Config.I.necessaryCond)
var cond = new FileConditions(config.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));
var opts = getSearchOptions(Math.Min(config.searchTimeout, 5000), cond, config.preferredCond);
searchTasks.Add(DoSearch($"{track.Album}", opts, responseHandler, config, ct, onSearch));
}
if (track2.Title.Length > 3 && artist)
{
var cond = new FileConditions(Config.I.necessaryCond)
var cond = new FileConditions(config.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));
var opts = getSearchOptions(Math.Min(config.searchTimeout, 5000), cond, config.preferredCond);
searchTasks.Add(DoSearch($"{track2.Title}", opts, responseHandler, config, ct, onSearch));
}
if (track2.Artist.Length > 3 && title)
{
var cond = new FileConditions(Config.I.necessaryCond)
var cond = new FileConditions(config.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));
var opts = getSearchOptions(Math.Min(config.searchTimeout, 5000), cond, config.preferredCond);
searchTasks.Add(DoSearch($"{track2.Artist}", opts, responseHandler, config, ct, onSearch));
}
}
}
@ -835,12 +848,12 @@ static class Search
}
static async Task DoSearch(string search, SearchOptions opts, Action<SearchResponse> rHandler, CancellationToken? ct = null, Action? onSearch = null)
static async Task DoSearch(string search, SearchOptions opts, Action<SearchResponse> rHandler, Config config, CancellationToken? ct = null, Action? onSearch = null)
{
await searchSemaphore.WaitAsync();
try
{
search = CleanSearchString(search);
search = CleanSearchString(search, !config.noRemoveSpecialChars);
var q = SearchQuery.FromText(search);
onSearch?.Invoke();
await client.SearchAsync(q, options: opts, cancellationToken: ct, responseHandler: rHandler);
@ -849,7 +862,7 @@ static class Search
}
public static async Task SearchAndPrintResults(List<Track> tracks)
public static async Task SearchAndPrintResults(List<Track> tracks, Config config)
{
foreach (var track in tracks)
{
@ -860,15 +873,15 @@ static class Search
return new SearchOptions(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
searchTimeout: Config.I.searchTimeout,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
searchTimeout: config.searchTimeout,
removeSingleCharacterSearchTerms: config.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);
return Utils.IsMusicFile(file.Filename) && (necCond.FileSatisfies(file, track, null) || config.PrintResultsFull);
});
}
@ -883,22 +896,22 @@ static class Search
}
}
await RunSearches(track, results, getSearchOptions, responseHandler);
await RunSearches(track, results, getSearchOptions, responseHandler, config);
if (Config.I.DoNotDownload && results.IsEmpty)
if (config.DoNotDownload && results.IsEmpty)
{
Printing.WriteLine($"No results", ConsoleColor.Yellow);
}
else
{
var orderedResults = OrderedResults(results, track, useInfer: true);
var orderedResults = OrderedResults(results, track, config, 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));
config.PrintResultsFull ? config.necessaryCond : null, config.PrintResultsFull ? config.preferredCond : null,
fullpath: config.PrintResultsFull, infoFirst: true, showSpeed: config.PrintResultsFull));
count += 1;
}
Printing.WriteLine($"Total: {count}\n", ConsoleColor.Yellow);
@ -930,10 +943,10 @@ static class Search
}
static string CleanSearchString(string str)
static string CleanSearchString(string str, bool removeSpecialChars)
{
string old;
if (!Config.I.noRemoveSpecialChars)
if (removeSpecialChars)
{
old = str;
str = str.ReplaceSpecialChars(" ").Trim().RemoveConsecutiveWs();
@ -1156,5 +1169,3 @@ public class SearchAndDownloadException : Exception
public FailureReason reason;
public SearchAndDownloadException(FailureReason reason, string text = "") : base(text) { this.reason = reason; }
}

View file

@ -13,10 +13,11 @@ namespace Tests
public static async Task RunAllTests()
{
TestStringUtils();
TestAutoProfiles();
TestProfileConditions();
await TestStringExtractor();
TestM3uEditor();
throw new NotImplementedException("Outdated test code");
//TestAutoProfiles();
//TestProfileConditions();
//await TestStringExtractor();
//TestM3uEditor();
Console.WriteLine('\n' + new string('#', 50) + '\n' + "All tests passed.");
}
@ -75,366 +76,366 @@ namespace Tests
Passed();
}
public static void TestAutoProfiles()
{
SetCurrentTest("TestAutoProfiles");
ResetConfig();
Config.I.inputType = InputType.YouTube;
Config.I.interactiveMode = true;
Config.I.aggregate = false;
Config.I.maxStaleTime = 50000;
string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf");
string content =
"max-stale-time = 5" +
"\nfast-search = true" +
"\nformat = flac" +
"\n[profile-true-1]" +
"\nprofile-cond = input-type == \"youtube\" && download-mode == \"album\"" +
"\nmax-stale-time = 10" +
"\n[profile-true-2]" +
"\nprofile-cond = !aggregate" +
"\nfast-search = false" +
"\n[profile-false-1]" +
"\nprofile-cond = input-type == \"string\"" +
"\nformat = mp3" +
"\n[profile-no-cond]" +
"\nformat = opus";
File.WriteAllText(path, content);
Config.I.LoadAndParse(new string[] { "-c", path });
var tle = new TrackListEntry(TrackType.Album);
Config.UpdateProfiles(tle);
Assert(Config.I.maxStaleTime == 10 && !Config.I.fastSearch && Config.I.necessaryCond.Formats[0] == "flac");
ResetConfig();
Config.I.inputType = InputType.CSV;
Config.I.album = true;
Config.I.interactiveMode = true;
Config.I.useYtdlp = false;
Config.I.maxStaleTime = 50000;
content =
"\n[no-stale]" +
"\nprofile-cond = interactive && download-mode == \"album\"" +
"\nmax-stale-time = 999999" +
"\n[youtube]" +
"\nprofile-cond = input-type == \"youtube\"" +
"\nyt-dlp = true";
File.WriteAllText(path, content);
Config.I.LoadAndParse(new string[] { "-c", path });
Config.UpdateProfiles(tle);
Assert(Config.I.maxStaleTime == 999999 && !Config.I.useYtdlp);
ResetConfig();
Config.I.inputType = InputType.YouTube;
Config.I.album = false;
Config.I.interactiveMode = true;
Config.I.useYtdlp = false;
Config.I.maxStaleTime = 50000;
content =
"\n[no-stale]" +
"\nprofile-cond = interactive && download-mode == \"album\"" +
"\nmax-stale-time = 999999" +
"\n[youtube]" +
"\nprofile-cond = input-type == \"youtube\"" +
"\nyt-dlp = true";
File.WriteAllText(path, content);
Config.I.LoadAndParse(new string[] { "-c", path });
Config.UpdateProfiles(new TrackListEntry(TrackType.Normal));
Assert(Config.I.maxStaleTime == 50000 && Config.I.useYtdlp);
if (File.Exists(path))
File.Delete(path);
Passed();
}
public static void TestProfileConditions()
{
SetCurrentTest("TestProfileConditions");
Config.I.inputType = InputType.YouTube;
Config.I.interactiveMode = true;
Config.I.album = true;
Config.I.aggregate = false;
var conds = new (bool, string)[]
{
(true, "input-type == \"youtube\""),
(true, "download-mode == \"album\""),
(false, "aggregate"),
(true, "interactive"),
(true, "album"),
(false, "!interactive"),
(true, "album && input-type == \"youtube\""),
(false, "album && input-type != \"youtube\""),
(false, "(interactive && aggregate)"),
(true, "album && (interactive || aggregate)"),
(true, "input-type == \"spotify\" || aggregate || input-type == \"csv\" || interactive && album"),
(true, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || interactive ) )"),
(false, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || !interactive ) )"),
};
foreach ((var b, var c) in conds)
{
Console.WriteLine(c);
Assert(b == Config.I.ProfileConditionSatisfied(c));
}
Passed();
}
public static async Task TestStringExtractor()
{
SetCurrentTest("TestStringExtractor");
var strings = new List<string>()
{
"Some Title",
"Some, Title",
"artist = Some artist, title = some title",
"Artist - Title, length = 42",
"title=Some, Title, artist=Some, Artist, album = Some, Album, length= 42",
"Some, Artist = a - Some, Title = b, album = Some, Album, length = 42",
"Foo Bar",
"Foo - Bar",
"Artist - Title, length=42",
"title=Title, artist=Artist, length=42",
};
var tracks = new List<Track>()
{
new Track() { Title="Some Title" },
new Track() { Title="Some, Title" },
new Track() { Title = "some title", Artist = "Some artist" },
new Track() { Title = "Title", Artist = "Artist", Length = 42 },
new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42 },
new Track() { Title="Some, Title = b", Artist = "Some, Artist = a", Album = "Some, Album", Length = 42 },
new Track() { Title = "Foo Bar" },
new Track() { Title = "Bar", Artist = "Foo" },
new Track() { Title = "Title", Artist = "Artist", Length = 42 },
new Track() { Title = "Title", Artist = "Artist", Length = 42 },
};
var albums = new List<Track>()
{
new Track() { Album="Some Title", Type = TrackType.Album },
new Track() { Album="Some, Title", Type = TrackType.Album },
new Track() { Title = "some title", Artist = "Some artist", Type = TrackType.Album },
new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42, Type = TrackType.Album },
new Track() { Artist = "Some, Artist = a", Album = "Some, Album", Length = 42, Type = TrackType.Album },
new Track() { Album = "Foo Bar", Type = TrackType.Album },
new Track() { Album = "Bar", Artist = "Foo", Type = TrackType.Album },
new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
new Track() { Title = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
};
var extractor = new Extractors.StringExtractor();
Config.I.aggregate = false;
Config.I.album = false;
Console.WriteLine("Testing songs: ");
for (int i = 0; i < strings.Count; i++)
{
Config.I.input = strings[i];
Console.WriteLine(Config.I.input);
var res = await extractor.GetTracks(Config.I.input, 0, 0, false);
var t = res[0].list[0][0];
Assert(Extractors.StringExtractor.InputMatches(Config.I.input));
Assert(t.ToKey() == tracks[i].ToKey());
}
Console.WriteLine();
Console.WriteLine("Testing albums");
Config.I.album = true;
for (int i = 0; i < strings.Count; i++)
{
Config.I.input = strings[i];
Console.WriteLine(Config.I.input);
var t = (await extractor.GetTracks(Config.I.input, 0, 0, false))[0].source;
Assert(Extractors.StringExtractor.InputMatches(Config.I.input));
Assert(t.ToKey() == albums[i].ToKey());
}
Passed();
}
public static void TestM3uEditor()
{
SetCurrentTest("TestM3uEditor");
Config.I.skipMode = SkipMode.Index;
Config.I.skipMusicDir = "";
Config.I.printOption = PrintOption.Tracks | PrintOption.Full;
Config.I.skipExisting = true;
string path = Path.Join(Directory.GetCurrentDirectory(), "test_m3u.m3u8");
if (File.Exists(path))
File.Delete(path);
File.WriteAllText(path, $"#SLDL:" +
$"{Path.Join(Directory.GetCurrentDirectory(), "file1.5")},\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;" +
$"path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;" +
$"path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;" +
$",\"Artist; ,3\",,Title3 ;a,-1,0,4,0;" +
$",\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
$",,,,-1,0,0,0;");
var notFoundInitial = new List<Track>()
{
new() { Artist = "Artist; ,3", Title = "Title3 ;a" },
new() { Artist = "Artist,,, ;4", Title = "Title4", State = TrackState.Failed, FailureReason = FailureReason.NoSuitableFileFound }
};
var existingInitial = new List<Track>()
{
new() { Artist = "Artist, 1", Title = "Title, , 1", DownloadPath = "path/to/file1", State = TrackState.Downloaded },
new() { Artist = "Artist, 1.5", Title = "Title, , 1.5", DownloadPath = Path.Join(Directory.GetCurrentDirectory(), "file1.5"), State = TrackState.Downloaded },
new() { Artist = "Artist, 2", Title = "Title2", DownloadPath = "path/to/file2", State = TrackState.AlreadyExists }
};
var toBeDownloadedInitial = new List<Track>()
{
new() { Artist = "ArtistA", Album = "Albumm", Title = "TitleA" },
new() { Artist = "ArtistB", Album = "Albumm", Title = "TitleB" }
};
var trackLists = new TrackLists();
trackLists.AddEntry(new TrackListEntry(TrackType.Normal));
foreach (var t in notFoundInitial)
trackLists.AddTrackToLast(t);
foreach (var t in existingInitial)
trackLists.AddTrackToLast(t);
foreach (var t in toBeDownloadedInitial)
trackLists.AddTrackToLast(t);
Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All);
Program.outputDirSkipper = new IndexSkipper(Program.indexEditor, false);
var notFound = (List<Track>)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
var existing = (List<Track>)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0] });
var toBeDownloaded = trackLists[0].list[0].Where(t => t.State == TrackState.Initial).ToList();
Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial));
Assert(existing.SequenceEqualUpToPermutation(existingInitial));
Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
Printing.PrintTracksTbd(toBeDownloaded, existing, notFound, TrackType.Normal);
Program.indexEditor.Update();
string output = File.ReadAllText(path);
string need =
"#SLDL:./file1.5,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;,,,,-1,0,0,0;" +
"\n" +
"\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
"\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
"\npath/to/file1" +
"\nfile1.5" +
"\npath/to/file2" +
"\n";
Assert(output == need);
toBeDownloaded[0].State = TrackState.Downloaded;
toBeDownloaded[0].DownloadPath = "new/file/path";
toBeDownloaded[1].State = TrackState.Failed;
toBeDownloaded[1].FailureReason = FailureReason.NoSuitableFileFound;
existing[1].DownloadPath = "/other/new/file/path";
Program.indexEditor.Update();
output = File.ReadAllText(path);
need =
"#SLDL:/other/new/file/path,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
"\n" +
"\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
"\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
"\npath/to/file1" +
"\n/other/new/file/path" +
"\npath/to/file2" +
"\nnew/file/path" +
"\n#FAIL: ArtistB - TitleB [NoSuitableFileFound]" +
"\n";
Assert(output == need);
Console.WriteLine();
Console.WriteLine(output);
Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All);
foreach (var t in trackLists.Flattened(false, false))
{
Program.indexEditor.TryGetPreviousRunResult(t, out var prev);
Assert(prev != null);
Assert(prev.ToKey() == t.ToKey());
Assert(prev.DownloadPath == t.DownloadPath);
Assert(prev.State == t.State || prev.State == TrackState.NotFoundLastTime);
Assert(prev.FailureReason == t.FailureReason);
}
Program.indexEditor.Update();
output = File.ReadAllText(path);
Assert(output == need);
var test = new List<Track>
{
new() { Artist = "ArtistA", Album = "AlbumA", Type = TrackType.Album },
new() { Artist = "ArtistB", Album = "AlbumB", Type = TrackType.Album },
new() { Artist = "ArtistC", Album = "AlbumC", Type = TrackType.Album },
};
trackLists = new TrackLists();
foreach (var t in test)
trackLists.AddEntry(new TrackListEntry(t));
File.WriteAllText(path, "");
Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index);
Program.indexEditor.Update();
Assert(File.ReadAllText(path) == "");
test[0].State = TrackState.Downloaded;
test[0].DownloadPath = "download/path";
test[1].State = TrackState.Failed;
test[1].FailureReason = FailureReason.NoSuitableFileFound;
test[2].State = TrackState.AlreadyExists;
Program.indexEditor.Update();
Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index);
foreach (var t in test)
{
Program.indexEditor.TryGetPreviousRunResult(t, out var tt);
Assert(tt != null);
Assert(tt.ToKey() == t.ToKey());
t.DownloadPath = "this should not change tt.DownloadPath";
Assert(t.DownloadPath != tt.DownloadPath);
}
File.Delete(path);
Passed();
}
//public static void TestAutoProfiles()
//{
// SetCurrentTest("TestAutoProfiles");
// ResetConfig();
// Config.I.inputType = InputType.YouTube;
// Config.I.interactiveMode = true;
// Config.I.aggregate = false;
// Config.I.maxStaleTime = 50000;
// string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf");
// string content =
// "max-stale-time = 5" +
// "\nfast-search = true" +
// "\nformat = flac" +
// "\n[profile-true-1]" +
// "\nprofile-cond = input-type == \"youtube\" && download-mode == \"album\"" +
// "\nmax-stale-time = 10" +
// "\n[profile-true-2]" +
// "\nprofile-cond = !aggregate" +
// "\nfast-search = false" +
// "\n[profile-false-1]" +
// "\nprofile-cond = input-type == \"string\"" +
// "\nformat = mp3" +
// "\n[profile-no-cond]" +
// "\nformat = opus";
// File.WriteAllText(path, content);
// Config.I.LoadAndParse(new string[] { "-c", path });
// var tle = new TrackListEntry(TrackType.Album);
// Config.UpdateProfiles(tle);
// Assert(Config.I.maxStaleTime == 10 && !Config.I.fastSearch && Config.I.necessaryCond.Formats[0] == "flac");
// ResetConfig();
// Config.I.inputType = InputType.CSV;
// Config.I.album = true;
// Config.I.interactiveMode = true;
// Config.I.useYtdlp = false;
// Config.I.maxStaleTime = 50000;
// content =
// "\n[no-stale]" +
// "\nprofile-cond = interactive && download-mode == \"album\"" +
// "\nmax-stale-time = 999999" +
// "\n[youtube]" +
// "\nprofile-cond = input-type == \"youtube\"" +
// "\nyt-dlp = true";
// File.WriteAllText(path, content);
// Config.I.LoadAndParse(new string[] { "-c", path });
// Config.UpdateProfiles(tle);
// Assert(Config.I.maxStaleTime == 999999 && !Config.I.useYtdlp);
// ResetConfig();
// Config.I.inputType = InputType.YouTube;
// Config.I.album = false;
// Config.I.interactiveMode = true;
// Config.I.useYtdlp = false;
// Config.I.maxStaleTime = 50000;
// content =
// "\n[no-stale]" +
// "\nprofile-cond = interactive && download-mode == \"album\"" +
// "\nmax-stale-time = 999999" +
// "\n[youtube]" +
// "\nprofile-cond = input-type == \"youtube\"" +
// "\nyt-dlp = true";
// File.WriteAllText(path, content);
// Config.I.LoadAndParse(new string[] { "-c", path });
// Config.UpdateProfiles(new TrackListEntry(TrackType.Normal));
// Assert(Config.I.maxStaleTime == 50000 && Config.I.useYtdlp);
// if (File.Exists(path))
// File.Delete(path);
// Passed();
//}
//public static void TestProfileConditions()
//{
// SetCurrentTest("TestProfileConditions");
// Config.I.inputType = InputType.YouTube;
// Config.I.interactiveMode = true;
// Config.I.album = true;
// Config.I.aggregate = false;
// var conds = new (bool, string)[]
// {
// (true, "input-type == \"youtube\""),
// (true, "download-mode == \"album\""),
// (false, "aggregate"),
// (true, "interactive"),
// (true, "album"),
// (false, "!interactive"),
// (true, "album && input-type == \"youtube\""),
// (false, "album && input-type != \"youtube\""),
// (false, "(interactive && aggregate)"),
// (true, "album && (interactive || aggregate)"),
// (true, "input-type == \"spotify\" || aggregate || input-type == \"csv\" || interactive && album"),
// (true, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || interactive ) )"),
// (false, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || !interactive ) )"),
// };
// foreach ((var b, var c) in conds)
// {
// Console.WriteLine(c);
// Assert(b == Config.I.ProfileConditionSatisfied(c));
// }
// Passed();
//}
//public static async Task TestStringExtractor()
//{
// SetCurrentTest("TestStringExtractor");
// var strings = new List<string>()
// {
// "Some Title",
// "Some, Title",
// "artist = Some artist, title = some title",
// "Artist - Title, length = 42",
// "title=Some, Title, artist=Some, Artist, album = Some, Album, length= 42",
// "Some, Artist = a - Some, Title = b, album = Some, Album, length = 42",
// "Foo Bar",
// "Foo - Bar",
// "Artist - Title, length=42",
// "title=Title, artist=Artist, length=42",
// };
// var tracks = new List<Track>()
// {
// new Track() { Title="Some Title" },
// new Track() { Title="Some, Title" },
// new Track() { Title = "some title", Artist = "Some artist" },
// new Track() { Title = "Title", Artist = "Artist", Length = 42 },
// new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42 },
// new Track() { Title="Some, Title = b", Artist = "Some, Artist = a", Album = "Some, Album", Length = 42 },
// new Track() { Title = "Foo Bar" },
// new Track() { Title = "Bar", Artist = "Foo" },
// new Track() { Title = "Title", Artist = "Artist", Length = 42 },
// new Track() { Title = "Title", Artist = "Artist", Length = 42 },
// };
// var albums = new List<Track>()
// {
// new Track() { Album="Some Title", Type = TrackType.Album },
// new Track() { Album="Some, Title", Type = TrackType.Album },
// new Track() { Title = "some title", Artist = "Some artist", Type = TrackType.Album },
// new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
// new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42, Type = TrackType.Album },
// new Track() { Artist = "Some, Artist = a", Album = "Some, Album", Length = 42, Type = TrackType.Album },
// new Track() { Album = "Foo Bar", Type = TrackType.Album },
// new Track() { Album = "Bar", Artist = "Foo", Type = TrackType.Album },
// new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
// new Track() { Title = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
// };
// var extractor = new Extractors.StringExtractor();
// Config.I.aggregate = false;
// Config.I.album = false;
// Console.WriteLine("Testing songs: ");
// for (int i = 0; i < strings.Count; i++)
// {
// Config.I.input = strings[i];
// Console.WriteLine(Config.I.input);
// var res = await extractor.GetTracks(Config.I.input, 0, 0, false);
// var t = res[0].list[0][0];
// Assert(Extractors.StringExtractor.InputMatches(Config.I.input));
// Assert(t.ToKey() == tracks[i].ToKey());
// }
// Console.WriteLine();
// Console.WriteLine("Testing albums");
// Config.I.album = true;
// for (int i = 0; i < strings.Count; i++)
// {
// Config.I.input = strings[i];
// Console.WriteLine(Config.I.input);
// var t = (await extractor.GetTracks(Config.I.input, 0, 0, false))[0].source;
// Assert(Extractors.StringExtractor.InputMatches(Config.I.input));
// Assert(t.ToKey() == albums[i].ToKey());
// }
// Passed();
//}
//public static void TestM3uEditor()
//{
// SetCurrentTest("TestM3uEditor");
// Config.I.skipMode = SkipMode.Index;
// Config.I.skipMusicDir = "";
// Config.I.printOption = PrintOption.Tracks | PrintOption.Full;
// Config.I.skipExisting = true;
// string path = Path.Join(Directory.GetCurrentDirectory(), "test_m3u.m3u8");
// if (File.Exists(path))
// File.Delete(path);
// File.WriteAllText(path, $"#SLDL:" +
// $"{Path.Join(Directory.GetCurrentDirectory(), "file1.5")},\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;" +
// $"path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;" +
// $"path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;" +
// $",\"Artist; ,3\",,Title3 ;a,-1,0,4,0;" +
// $",\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
// $",,,,-1,0,0,0;");
// var notFoundInitial = new List<Track>()
// {
// new() { Artist = "Artist; ,3", Title = "Title3 ;a" },
// new() { Artist = "Artist,,, ;4", Title = "Title4", State = TrackState.Failed, FailureReason = FailureReason.NoSuitableFileFound }
// };
// var existingInitial = new List<Track>()
// {
// new() { Artist = "Artist, 1", Title = "Title, , 1", DownloadPath = "path/to/file1", State = TrackState.Downloaded },
// new() { Artist = "Artist, 1.5", Title = "Title, , 1.5", DownloadPath = Path.Join(Directory.GetCurrentDirectory(), "file1.5"), State = TrackState.Downloaded },
// new() { Artist = "Artist, 2", Title = "Title2", DownloadPath = "path/to/file2", State = TrackState.AlreadyExists }
// };
// var toBeDownloadedInitial = new List<Track>()
// {
// new() { Artist = "ArtistA", Album = "Albumm", Title = "TitleA" },
// new() { Artist = "ArtistB", Album = "Albumm", Title = "TitleB" }
// };
// var trackLists = new TrackLists();
// trackLists.AddEntry(new TrackListEntry(TrackType.Normal));
// foreach (var t in notFoundInitial)
// trackLists.AddTrackToLast(t);
// foreach (var t in existingInitial)
// trackLists.AddTrackToLast(t);
// foreach (var t in toBeDownloadedInitial)
// trackLists.AddTrackToLast(t);
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All);
// Program.outputDirSkipper = new IndexSkipper(Program.indexEditor, false);
// var notFound = (List<Track>)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
// var existing = (List<Track>)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0] });
// var toBeDownloaded = trackLists[0].list[0].Where(t => t.State == TrackState.Initial).ToList();
// Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial));
// Assert(existing.SequenceEqualUpToPermutation(existingInitial));
// Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
// Printing.PrintTracksTbd(toBeDownloaded, existing, notFound, TrackType.Normal);
// Program.indexEditor.Update();
// string output = File.ReadAllText(path);
// string need =
// "#SLDL:./file1.5,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;,,,,-1,0,0,0;" +
// "\n" +
// "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
// "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
// "\npath/to/file1" +
// "\nfile1.5" +
// "\npath/to/file2" +
// "\n";
// Assert(output == need);
// toBeDownloaded[0].State = TrackState.Downloaded;
// toBeDownloaded[0].DownloadPath = "new/file/path";
// toBeDownloaded[1].State = TrackState.Failed;
// toBeDownloaded[1].FailureReason = FailureReason.NoSuitableFileFound;
// existing[1].DownloadPath = "/other/new/file/path";
// Program.indexEditor.Update();
// output = File.ReadAllText(path);
// need =
// "#SLDL:/other/new/file/path,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
// ",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
// "\n" +
// "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
// "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
// "\npath/to/file1" +
// "\n/other/new/file/path" +
// "\npath/to/file2" +
// "\nnew/file/path" +
// "\n#FAIL: ArtistB - TitleB [NoSuitableFileFound]" +
// "\n";
// Assert(output == need);
// Console.WriteLine();
// Console.WriteLine(output);
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All);
// foreach (var t in trackLists.Flattened(false, false))
// {
// Program.indexEditor.TryGetPreviousRunResult(t, out var prev);
// Assert(prev != null);
// Assert(prev.ToKey() == t.ToKey());
// Assert(prev.DownloadPath == t.DownloadPath);
// Assert(prev.State == t.State || prev.State == TrackState.NotFoundLastTime);
// Assert(prev.FailureReason == t.FailureReason);
// }
// Program.indexEditor.Update();
// output = File.ReadAllText(path);
// Assert(output == need);
// var test = new List<Track>
// {
// new() { Artist = "ArtistA", Album = "AlbumA", Type = TrackType.Album },
// new() { Artist = "ArtistB", Album = "AlbumB", Type = TrackType.Album },
// new() { Artist = "ArtistC", Album = "AlbumC", Type = TrackType.Album },
// };
// trackLists = new TrackLists();
// foreach (var t in test)
// trackLists.AddEntry(new TrackListEntry(t));
// File.WriteAllText(path, "");
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index);
// Program.indexEditor.Update();
// Assert(File.ReadAllText(path) == "");
// test[0].State = TrackState.Downloaded;
// test[0].DownloadPath = "download/path";
// test[1].State = TrackState.Failed;
// test[1].FailureReason = FailureReason.NoSuitableFileFound;
// test[2].State = TrackState.AlreadyExists;
// Program.indexEditor.Update();
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index);
// foreach (var t in test)
// {
// Program.indexEditor.TryGetPreviousRunResult(t, out var tt);
// Assert(tt != null);
// Assert(tt.ToKey() == t.ToKey());
// t.DownloadPath = "this should not change tt.DownloadPath";
// Assert(t.DownloadPath != tt.DownloadPath);
// }
// File.Delete(path);
// Passed();
//}
}
static class Helpers

View file

@ -141,39 +141,39 @@ public static class Printing
}
public static async Task PrintResults(TrackListEntry tle, List<Track> existing, List<Track> notFound)
public static async Task PrintResults(TrackListEntry tle, List<Track> existing, List<Track> notFound, Config config)
{
await Program.InitClientAndUpdateIfNeeded();
await Program.InitClientAndUpdateIfNeeded(config);
if (tle.source.Type == TrackType.Normal)
{
await Search.SearchAndPrintResults(tle.list[0]);
await Search.SearchAndPrintResults(tle.list[0], config);
}
else if (tle.source.Type == TrackType.Aggregate)
{
Console.WriteLine(new string('-', 60));
Console.WriteLine($"Results for aggregate {tle.source.ToString(true)}:");
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type);
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type, config);
}
else if (tle.source.Type == TrackType.Album)
{
Console.WriteLine(new string('-', 60));
if (!Config.I.printOption.HasFlag(PrintOption.Full))
if (!config.printOption.HasFlag(PrintOption.Full))
Console.WriteLine($"Result 1 of {tle.list.Count} for album {tle.source.ToString(true)}:");
else
Console.WriteLine($"Results ({tle.list.Count}) for album {tle.source.ToString(true)}:");
if (tle.list.Count > 0 && tle.list[0].Count > 0)
{
if (!Config.I.noBrowseFolder)
if (!config.noBrowseFolder)
Console.WriteLine("[Skipping full folder retrieval]");
foreach (var ls in tle.list)
{
PrintAlbum(ls);
if (!Config.I.printOption.HasFlag(PrintOption.Full))
if (!config.printOption.HasFlag(PrintOption.Full))
break;
}
}
@ -201,16 +201,16 @@ public static class Printing
}
public static void PrintTracksTbd(List<Track> toBeDownloaded, List<Track> existing, List<Track> notFound, TrackType type, bool summary = true)
public static void PrintTracksTbd(List<Track> toBeDownloaded, List<Track> existing, List<Track> notFound, TrackType type, Config config, bool summary = true)
{
if (type == TrackType.Normal && !Config.I.PrintTracks && toBeDownloaded.Count == 1 && existing.Count + notFound.Count == 0)
if (type == TrackType.Normal && !config.PrintTracks && toBeDownloaded.Count == 1 && existing.Count + notFound.Count == 0)
return;
string notFoundLastTime = notFound.Count > 0 ? $"{notFound.Count} not found" : "";
string alreadyExist = existing.Count > 0 ? $"{existing.Count} already exist" : "";
notFoundLastTime = alreadyExist.Length > 0 && notFoundLastTime.Length > 0 ? ", " + notFoundLastTime : notFoundLastTime;
string skippedTracks = alreadyExist.Length + notFoundLastTime.Length > 0 ? $" ({alreadyExist}{notFoundLastTime})" : "";
bool full = Config.I.printOption.HasFlag(PrintOption.Full);
bool full = config.printOption.HasFlag(PrintOption.Full);
bool allSkipped = existing.Count + notFound.Count > toBeDownloaded.Count;
if (summary && (type == TrackType.Normal || skippedTracks.Length > 0))
@ -218,24 +218,24 @@ public static class Printing
if (toBeDownloaded.Count > 0)
{
bool showAll = type != TrackType.Normal || Config.I.PrintTracks || Config.I.PrintResults;
PrintTracks(toBeDownloaded, showAll ? int.MaxValue : 10, full, infoFirst: Config.I.PrintTracks);
bool showAll = type != TrackType.Normal || config.PrintTracks || config.PrintResults;
PrintTracks(toBeDownloaded, showAll ? int.MaxValue : 10, full, infoFirst: config.PrintTracks);
if (full && (existing.Count > 0 || notFound.Count > 0))
Console.WriteLine("\n-----------------------------------------------\n");
}
if (Config.I.PrintTracks || Config.I.PrintResults)
if (config.PrintTracks || config.PrintResults)
{
if (existing.Count > 0)
{
Console.WriteLine($"\nThe following tracks already exist:");
PrintTracks(existing, fullInfo: full, infoFirst: Config.I.PrintTracks);
PrintTracks(existing, fullInfo: full, infoFirst: config.PrintTracks);
}
if (notFound.Count > 0)
{
Console.WriteLine($"\nThe following tracks were not found during a prior run:");
PrintTracks(notFound, fullInfo: full, infoFirst: Config.I.PrintTracks);
PrintTracks(notFound, fullInfo: full, infoFirst: config.PrintTracks);
}
}
}
@ -307,17 +307,15 @@ public static class Printing
try { progress.Refresh(current, item); }
catch { }
}
else if ((Config.I.noProgress || Console.IsOutputRedirected) && print)
else if ((progress == null || Console.IsOutputRedirected) && print)
{
Console.WriteLine(item);
}
}
public static void WriteLine(string value, ConsoleColor color = ConsoleColor.Gray, bool safe = false, bool debugOnly = false)
public static void WriteLine(string value, ConsoleColor color = ConsoleColor.Gray, bool safe = false)
{
if (debugOnly && !Config.I.debugInfo)
return;
if (!safe)
{
Console.ForegroundColor = color;
@ -339,11 +337,18 @@ public static class Printing
}
public static ProgressBar? GetProgressBar()
public static void WriteLineIf(string value, bool condition, ConsoleColor color = ConsoleColor.Gray)
{
if (condition)
Printing.WriteLine(value, color);
}
public static ProgressBar? GetProgressBar(Config config)
{
lock (consoleLock)
{
if (!Config.I.noProgress)
if (!config.noProgress)
{
return new ProgressBar(PbStyle.SingleLine, 100, Console.WindowWidth - 10, character: ' ');
}

View file

@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>sldl</AssemblyName>
<VersionPrefix>2.3.1</VersionPrefix>
<VersionPrefix>2.4.0</VersionPrefix>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -27,7 +27,7 @@
<PackageReference Include="SpotifyAPI.Web" Version="7.1.1" />
<PackageReference Include="SpotifyAPI.Web.Auth" Version="7.1.1" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="YoutubeExplode" Version="6.4.3" />
<PackageReference Include="YoutubeExplode" Version="6.5.0" />
</ItemGroup>
<ItemGroup>