1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 06:22:41 +00:00
This commit is contained in:
fiso64 2024-09-02 09:28:38 +02:00
parent 923cd2cbf5
commit 149a27115a
19 changed files with 567 additions and 526 deletions

View file

@ -11,6 +11,7 @@ See the [examples](#examples-1).
- [Spotify](#spotify) - [Spotify](#spotify)
- [Bandcamp](#bandcamp) - [Bandcamp](#bandcamp)
- [Search string](#search-string) - [Search string](#search-string)
- [List](#list)
- [Download modes](#download-modes) - [Download modes](#download-modes)
- [Normal](#normal) - [Normal](#normal)
- [Album](#album) - [Album](#album)
@ -51,7 +52,7 @@ Usage: sldl <input> [OPTIONS]
--profile <names> Configuration profile(s) to use. See --help "config". --profile <names> Configuration profile(s) to use. See --help "config".
--concurrent-downloads <num> Max concurrent downloads (default: 2) --concurrent-downloads <num> Max concurrent downloads (default: 2)
--m3u <option> Create an m3u8 playlist file in the output directory --m3u <option> Create an m3u8 playlist file in the output directory
'none' (default for single inputs): Do not create 'none' (default for string input): Do not create
'index' (default): Write a line indexing all downloaded 'index' (default): Write a line indexing all downloaded
files, required for skip-not-found or skip-existing=m3u files, required for skip-not-found or skip-existing=m3u
'all': Write the index and a list of paths and fails 'all': Write the index and a list of paths and fails
@ -234,8 +235,8 @@ The input type is usually determined automatically. To force a specific input ty
Path to a local CSV file: Use a csv file containing track info of the songs to download. Path to a local CSV file: Use a csv file containing track info of the songs to download.
The names of the columns should be Artist, Title, Album, Length, although alternative names The names of the columns should be Artist, Title, Album, Length, although alternative names
are usually detected as well. Only the title or album column is required, but extra info may are usually detected as well. Only the title or album column is required, but extra info may
improve search results. Every row that does not have a title column text will be treated as an improve search result ranking. Every row that does not have a title column text will be treated
album download. as an album download.
### YouTube ### YouTube
A playlist url: Download songs from a youtube playlist. A playlist url: Download songs from a youtube playlist.
@ -244,10 +245,6 @@ the ones which are unavailable. To get all video titles, you can use the officia
providing a key with --youtube-key. Get it here https://console.cloud.google.com. Create a providing a key with --youtube-key. Get it here https://console.cloud.google.com. Create a
new project, click "Enable Api" and search for "youtube data", then follow the prompts. new project, click "Enable Api" and search for "youtube data", then follow the prompts.
Tip: For playlists containing music videos, it may be better to remove all text in parentheses
(to remove (Lyrics), (Official), etc) and disable song duration checking:
--regex "[\[\(].*?[\]\)]" --pref-length-tol -1
### Spotify ### Spotify
A playlist/album url or 'spotify-likes': Download a spotify playlist, album, or your A playlist/album url or 'spotify-likes': Download a spotify playlist, album, or your
liked songs. Credentials are required when downloading a private playlist or liked music. liked songs. Credentials are required when downloading a private playlist or liked music.
@ -285,7 +282,7 @@ Name of the track, album, or artist to search for: Can either be any typical sea
(like what you would enter into the soulseek search bar), or a comma-separated list of (like what you would enter into the soulseek search bar), or a comma-separated list of
properties like 'title=Song Name, artist=Artist Name, length=215'. properties like 'title=Song Name, artist=Artist Name, length=215'.
The following properties are allowed: The following properties are accepted:
``` ```
title title
artist artist
@ -336,7 +333,7 @@ one user will be ignored.
### Album Aggregate ### Album Aggregate
Activated when both --album and --aggregate are enabled. sldl will group shares and download Activated when both --album and --aggregate are enabled. sldl will group shares and download
one of each distinct album, starting with the one shared by the most users. It's one of each distinct album, starting with the one shared by the most users. It's
recommended to pair this with --interactive. recommended to pair this with --interactive.
Note that --min-shares-aggregate is 2 by default, which means that albums shared by only Note that --min-shares-aggregate is 2 by default, which means that albums shared by only
one user will be ignored. one user will be ignored.
@ -452,6 +449,7 @@ disc Disc number
filename Soulseek filename without extension filename Soulseek filename without extension
foldername Soulseek folder name foldername Soulseek folder name
extractor Name of the extractor used (CSV/Spotify/YouTube/etc) extractor Name of the extractor used (CSV/Spotify/YouTube/etc)
default-folder Default sldl folder name (usually the playlist name)
``` ```
## Skip existing ## Skip existing
@ -547,20 +545,20 @@ sldl spotify-likes
Download albums for every song in a spotify playlist: Download albums for every song in a spotify playlist:
``` ```
sldl https://spotify/playlist/url --album --skip-existing sldl https://spotify/playlist/id --album --skip-existing
``` ```
<br> <br>
Retrieve deleted video names, then download from a youtube playlist with fallback to yt-dlp: Retrieve deleted video names, then download from a youtube playlist with fallback to yt-dlp:
``` ```
sldl "https://www.youtube.com/playlist?list=PLI_eFW8NAFzYAXZ5DrU6E6mQ_XfhaLBUX" --get-deleted --yt-dlp sldl https://www.youtube.com/playlist/id --get-deleted --yt-dlp
``` ```
<br> <br>
Search & download a specific song, preferring lossless: Search & download a specific song, preferring lossless:
``` ```
sldl "title=MC MENTAL @ HIS BEST,length=242" --pref-format "flac,wav" sldl "MC MENTAL @ HIS BEST, length=242" --pref-format "flac,wav"
``` ```
<br> <br>
@ -578,23 +576,31 @@ sldl "artist=MC MENTAL" --aggregate --skip-existing --music-dir "path/to/music"
Download all albums by an artist found on soulseek: Download all albums by an artist found on soulseek:
``` ```
sldl "artist=MC MENTAL" --aggregate --album sldl "artist=MC MENTAL" --aggregate --album --interactive
``` ```
<hr style="height:0px; visibility:hidden;" />
#### Advanced example: Automatic wishlist downloader #### Advanced example: Automatic wishlist downloader
Create a file named `wishlist.txt`, and add some wishlist items: Create a file named `wishlist.txt`, and add some items as detailed in [Input types: List](#list):
```bash ```bash
echo title=My Favorite Song, artist=Artist >> wishlist.txt echo "title=My Favorite Song, artist=Artist" >> wishlist.txt
echo https://spotify/album/url >> wishlist.txt echo "album=Album" "format=mp3" >> wishlist.txt
``` ```
Set up a cron job (or scheduled task on windows) to periodically run sldl on every line of the wishlist file with the following options: Add a profile to your `sldl.conf`:
``` ```
--skip-existing --skip-mode m3u --m3u index --m3u-path wishlist-archive.sldl [wishlist]
input = wishlist.txt
input-type = list
skip-existing = true
skip-mode = m3u
m3u = index
m3u-path = wishlist-archive.sldl
``` ```
This will create a global archive file `wishlist-archive.sldl` which will be scanned every time sldl is run to skip wishlist items that have already been downloaded. This will create a global archive file `wishlist-archive.sldl` which will be scanned every time sldl is run to skip wishlist items that have already been downloaded. You can also use `--skip-mode m3u-cond` together with `--skip-existing-pref-cond` and specify some preferred conditions to (e.g) only stop searching for an item once a lossless version is downloaded.
You can also use `--skip-mode m3u-cond` together with `--skip-existing-pref-cond` and specify some preferred conditions to (e.g) only stop searching for an item once a lossless version is downloaded. Finally, set up a cron job (or a scheduled task on windows) to periodically run sldl with the following option:
If you expect to have a lot of individual songs in your wishlist, it may be better to use a csv file as that will allow sldl to use concurrency when downloading. ```
sldl --profile wishlist
```
## Notes ## Notes
- For macOS builds you can use publish.sh to build the app. Download dotnet from https://dotnet.microsoft.com/en-us/download/dotnet/6.0, then run `chmod +x publish.sh && sh publish.sh`. For intel macs, uncomment the x64 and comment the arm64 section in publish.sh. - For macOS builds you can use publish.sh to build the app. Download dotnet from https://dotnet.microsoft.com/en-us/download/dotnet/6.0, then run `chmod +x publish.sh && sh publish.sh`. For intel macs, uncomment the x64 and comment the arm64 section in publish.sh.

View file

@ -5,11 +5,11 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
static class Config public class Config
{ {
public static FileConditions necessaryCond = new(); public FileConditions necessaryCond = new();
public static FileConditions preferredCond = new() public FileConditions preferredCond = new()
{ {
Formats = new string[] { "mp3" }, Formats = new string[] { "mp3" },
LengthTolerance = 3, LengthTolerance = 3,
@ -21,180 +21,174 @@ static class Config
AcceptNoLength = false, AcceptNoLength = false,
}; };
public static string parentDir = Directory.GetCurrentDirectory(); public string parentDir = Directory.GetCurrentDirectory();
public static string input = ""; public string input = "";
public static string m3uFilePath = ""; public string m3uFilePath = "";
public static string musicDir = ""; public string musicDir = "";
public static string spotifyId = ""; public string spotifyId = "";
public static string spotifySecret = ""; public string spotifySecret = "";
public static string spotifyToken = ""; public string spotifyToken = "";
public static string spotifyRefresh = ""; public string spotifyRefresh = "";
public static string ytKey = ""; public string ytKey = "";
public static string username = ""; public string username = "";
public static string password = ""; public string password = "";
public static string artistCol = ""; public string artistCol = "";
public static string albumCol = ""; public string albumCol = "";
public static string trackCol = ""; public string trackCol = "";
public static string ytIdCol = ""; public string ytIdCol = "";
public static string descCol = ""; public string descCol = "";
public static string trackCountCol = ""; public string trackCountCol = "";
public static string lengthCol = ""; public string lengthCol = "";
public static string timeUnit = "s"; public string timeUnit = "s";
public static string nameFormat = ""; public string nameFormat = "";
public static string invalidReplaceStr = " "; public string invalidReplaceStr = " ";
public static string ytdlpArgument = ""; public string ytdlpArgument = "";
public static string onComplete = ""; public string onComplete = "";
public static string confPath = ""; public string confPath = "";
public static string profile = ""; public string profile = "";
public static string failedAlbumPath = ""; public string failedAlbumPath = "";
public static bool aggregate = false; public bool aggregate = false;
public static bool album = false; public bool album = false;
public static bool albumArtOnly = false; public bool albumArtOnly = false;
public static bool interactiveMode = false; public bool interactiveMode = false;
public static bool setAlbumMinTrackCount = true; public bool setAlbumMinTrackCount = true;
public static bool setAlbumMaxTrackCount = false; public bool setAlbumMaxTrackCount = false;
public static bool skipNotFound = false; public bool skipNotFound = false;
public static bool desperateSearch = false; public bool desperateSearch = false;
public static bool noRemoveSpecialChars = false; public bool noRemoveSpecialChars = false;
public static bool artistMaybeWrong = false; public bool artistMaybeWrong = false;
public static bool fastSearch = false; public bool fastSearch = false;
public static bool ytParse = false; public bool ytParse = false;
public static bool removeFt = false; public bool removeFt = false;
public static bool removeBrackets = false; public bool removeBrackets = false;
public static bool reverse = false; public bool reverse = false;
public static bool useYtdlp = false; public bool useYtdlp = false;
public static bool skipExisting = false; public bool skipExisting = false;
public static bool removeTracksFromSource = false; public bool removeTracksFromSource = false;
public static bool getDeleted = false; public bool getDeleted = false;
public static bool deletedOnly = false; public bool deletedOnly = false;
public static bool removeSingleCharacterSearchTerms = false; public bool removeSingleCharacterSearchTerms = false;
public static bool relax = false; public bool relax = false;
public static bool debugInfo = false; public bool debugInfo = false;
public static bool noModifyShareCount = false; public bool noModifyShareCount = false;
public static bool useRandomLogin = false; public bool useRandomLogin = false;
public static bool noBrowseFolder = false; public bool noBrowseFolder = false;
public static bool skipExistingPrefCond = false; public bool skipExistingPrefCond = false;
public static int downrankOn = -1; public int downrankOn = -1;
public static int ignoreOn = -2; public int ignoreOn = -2;
public static int minAlbumTrackCount = -1; public int minAlbumTrackCount = -1;
public static int maxAlbumTrackCount = -1; public int maxAlbumTrackCount = -1;
public static int fastSearchDelay = 300; public int fastSearchDelay = 300;
public static int minSharesAggregate = 2; public int minSharesAggregate = 2;
public static int maxTracks = int.MaxValue; public int maxTracks = int.MaxValue;
public static int offset = 0; public int offset = 0;
public static int maxStaleTime = 50000; public int maxStaleTime = 50000;
public static int updateDelay = 100; public int updateDelay = 100;
public static int searchTimeout = 6000; public int searchTimeout = 6000;
public static int concurrentProcesses = 2; public int concurrentProcesses = 2;
public static int unknownErrorRetries = 2; public int unknownErrorRetries = 2;
public static int maxRetriesPerTrack = 30; public int maxRetriesPerTrack = 30;
public static int listenPort = 49998; public int listenPort = 49998;
public static int searchesPerTime = 34; public int searchesPerTime = 34;
public static int searchRenewTime = 220; public int searchRenewTime = 220;
public static int aggregateLengthTol = 3; public int aggregateLengthTol = 3;
public static double fastSearchMinUpSpeed = 1.0; public double fastSearchMinUpSpeed = 1.0;
public static Track regexToReplace = new(); public Track regexToReplace = new();
public static Track regexReplaceBy = new(); public Track regexReplaceBy = new();
public static AlbumArtOption albumArtOption = AlbumArtOption.Default; public AlbumArtOption albumArtOption = AlbumArtOption.Default;
public static M3uOption m3uOption = M3uOption.Index; public M3uOption m3uOption = M3uOption.Index;
public static DisplayMode displayMode = DisplayMode.Single; public DisplayMode displayMode = DisplayMode.Single;
public static InputType inputType = InputType.None; public InputType inputType = InputType.None;
public static SkipMode skipMode = SkipMode.M3u; public SkipMode skipMode = SkipMode.M3u;
public static SkipMode skipModeMusicDir = SkipMode.Name; public SkipMode skipModeMusicDir = SkipMode.Name;
public static PrintOption printOption = PrintOption.None; public PrintOption printOption = PrintOption.None;
public static bool HasAutoProfiles { get; private set; } = false; public bool HasAutoProfiles { get; private set; } = false;
public static bool DoNotDownload => (printOption & (PrintOption.Results | PrintOption.Tracks)) != 0; public bool DoNotDownload => (printOption & (PrintOption.Results | PrintOption.Tracks)) != 0;
public static bool PrintTracks => (printOption & PrintOption.Tracks) != 0; public bool PrintTracks => (printOption & PrintOption.Tracks) != 0;
public static bool PrintResults => (printOption & PrintOption.Results) != 0; public bool PrintResults => (printOption & PrintOption.Results) != 0;
public static bool PrintTracksFull => (printOption & PrintOption.Tracks) != 0 && (printOption & PrintOption.Full) != 0; public bool PrintTracksFull => (printOption & PrintOption.Tracks) != 0 && (printOption & PrintOption.Full) != 0;
public static bool PrintResultsFull => (printOption & PrintOption.Results) != 0 && (printOption & PrintOption.Full) != 0; public bool PrintResultsFull => (printOption & PrintOption.Results) != 0 && (printOption & PrintOption.Full) != 0;
public static bool DeleteAlbumOnFail => failedAlbumPath == "delete"; public bool DeleteAlbumOnFail => failedAlbumPath == "delete";
public static bool IgnoreAlbumFail => failedAlbumPath == "disable"; public bool IgnoreAlbumFail => failedAlbumPath == "disable";
static readonly Dictionary<string, (List<string> args, string? cond)> profiles = new(); readonly Dictionary<string, (List<string> args, string? cond)> configProfiles = new();
static readonly HashSet<string> appliedProfiles = new(); readonly HashSet<string> appliedProfiles = new();
static bool hasConfiguredM3uMode = false; bool hasConfiguredM3uMode = false;
static bool confPathChanged = false; bool confPathChanged = false;
static string[] arguments; string[] arguments;
static FileConditions? prevConds = null; FileConditions? prevConds = null;
static FileConditions? prevPrefConds = null; FileConditions? prevPrefConds = null;
public static bool ParseArgsAndReadConfig(string[] args) 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)
{
configProfiles = cfg;
arguments = args;
}
public void Load(string[] args)
{ {
args = args.SelectMany(arg => arguments = args.SelectMany(arg =>
{ {
if (arg.Length > 3 && arg.StartsWith("--") && arg.Contains('=')) if (arg.Length > 2 && arg[0] == '-')
{ {
var parts = arg.Split('=', 2); if (arg[1] == '-')
return new[] { parts[0], parts[1] }; {
} if (arg.Length > 3 && arg.Contains('='))
return arg.Split('=', 2); // --arg=val becomes --arg val
}
else if (!arg.Contains(' '))
{
return arg[1..].Select(c => $"-{c}"); // -abc becomes -a -b -c
}
}
return new[] { arg }; return new[] { arg };
}).ToArray(); }).ToArray();
SetConfigPath(args); SetConfigPath(arguments);
if (confPath != "none" && (confPathChanged || File.Exists(confPath))) if (confPath != "none" && (confPathChanged || File.Exists(confPath)))
{ {
if (File.Exists(Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath)))
confPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath);
ParseConfig(confPath); ParseConfig(confPath);
ApplyDefaultConfig();
} }
args = args.SelectMany(arg => int profileIndex = Array.FindLastIndex(arguments, x => x == "--profile");
if (profileIndex != -1)
{ {
if (arg.Length > 2 && arg[0] == '-' && arg[1] != '-' && !arg.Contains(' ')) profile = arguments[profileIndex + 1];
return arg[1..].Select(c => $"-{c}");
return new[] { arg };
}).ToArray();
arguments = args;
int profileIndex = Array.FindLastIndex(args, x => x == "--profile");
if (profileIndex != -1 && profileIndex < args.Length - 1)
{
profile = args[profileIndex + 1];
if (profile == "help") if (profile == "help")
{ {
ListProfiles(); ListProfiles();
return false; Environment.Exit(0);
} }
} }
if (profiles.ContainsKey("default"))
{
ProcessArgs(profiles["default"].args);
appliedProfiles.Add("default");
}
if (HasAutoProfiles)
{
ProcessArgs(args);
ApplyAutoProfiles();
}
ApplyProfiles(profile); ApplyProfiles(profile);
ProcessArgs(args); ProcessArgs(arguments);
return true;
} }
static void SetConfigPath(string[] args) void SetConfigPath(string[] args)
{ {
int idx = Array.LastIndexOf(args, "-c"); int idx = Array.FindLastIndex(args, x => x == "-c" || x == "--config");
int idx2 = Array.LastIndexOf(args, "--config");
idx = idx > idx2 ? idx : idx2;
if (idx != -1) if (idx != -1)
{ {
confPath = Utils.ExpandUser(args[idx + 1]); confPath = Utils.ExpandUser(args[idx + 1]);
}
if (confPath.Length > 0)
{
confPathChanged = true; confPathChanged = true;
if(File.Exists(Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath)))
confPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath);
} }
if (!confPathChanged) if (!confPathChanged)
@ -204,7 +198,6 @@ static class Config
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "sldl", "sldl.conf"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "sldl", "sldl.conf"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "sldl", "sldl.conf"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "sldl", "sldl.conf"),
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sldl.conf"), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sldl.conf"),
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "slsk-batchdl.conf"),
}; };
foreach (var path in configPaths) foreach (var path in configPaths)
@ -219,7 +212,7 @@ static class Config
} }
public static void PostProcessArgs() public void PostProcessArgs() // must be run after extracting tracklist
{ {
if (DoNotDownload || debugInfo) if (DoNotDownload || debugInfo)
concurrentProcesses = 1; concurrentProcesses = 1;
@ -230,6 +223,8 @@ static class Config
m3uOption = M3uOption.None; m3uOption = M3uOption.None;
else if (!hasConfiguredM3uMode && inputType == InputType.String) else if (!hasConfiguredM3uMode && inputType == InputType.String)
m3uOption = M3uOption.None; m3uOption = M3uOption.None;
else if (!hasConfiguredM3uMode && !Program.trackLists.Flattened(true, true).Skip(1).Any())
m3uOption = M3uOption.None;
if (albumArtOnly && albumArtOption == AlbumArtOption.Default) if (albumArtOnly && albumArtOption == AlbumArtOption.Default)
albumArtOption = AlbumArtOption.Largest; albumArtOption = AlbumArtOption.Largest;
@ -246,7 +241,7 @@ static class Config
} }
static void ParseConfig(string path) void ParseConfig(string path)
{ {
var lines = File.ReadAllLines(path); var lines = File.ReadAllLines(path);
var curProfile = "default"; var curProfile = "default";
@ -274,13 +269,13 @@ static class Config
if (val[0] == '"' && val[^1] == '"') if (val[0] == '"' && val[^1] == '"')
val = val[1..^1]; val = val[1..^1];
if (!profiles.ContainsKey(curProfile)) if (!configProfiles.ContainsKey(curProfile))
profiles[curProfile] = (new List<string>(), null); configProfiles[curProfile] = (new List<string>(), null);
if (key == "profile-cond" && curProfile != "default") if (key == "profile-cond" && curProfile != "default")
{ {
var a = profiles[curProfile].args; var a = configProfiles[curProfile].args;
profiles[curProfile] = (a, val); configProfiles[curProfile] = (a, val);
HasAutoProfiles = true; HasAutoProfiles = true;
} }
else else
@ -290,8 +285,8 @@ static class Config
else else
key = "--" + key; key = "--" + key;
profiles[curProfile].args.Add(key); configProfiles[curProfile].args.Add(key);
profiles[curProfile].args.Add(val); configProfiles[curProfile].args.Add(val);
} }
} }
} }
@ -299,33 +294,70 @@ static class Config
public static void UpdateProfiles(TrackListEntry tle) public static void UpdateProfiles(TrackListEntry tle)
{ {
if (DoNotDownload) if (I.DoNotDownload)
return; return;
if (!HasAutoProfiles) if (!I.HasAutoProfiles)
return; return;
var newProfiles = ApplyAutoProfiles(tle); bool needUpdate = false;
var toApply = new List<(string name, List<string> args)>();
if (newProfiles.Count > 0) foreach ((var key, var val) in I.configProfiles)
{ {
//appliedProfiles.Clear(); if (key == "default" || val.cond == null)
appliedProfiles.UnionWith(newProfiles); continue;
ApplyProfiles(profile);
ProcessArgs(arguments); bool condSatisfied = I.ProfileConditionSatisfied(val.cond, tle);
PostProcessArgs(); bool alreadyApplied = I.appliedProfiles.Contains(key);
if (condSatisfied && !alreadyApplied)
needUpdate = true;
if (!condSatisfied && alreadyApplied)
needUpdate = true;
if (condSatisfied)
toApply.Add((key, val.args));
}
if (!needUpdate)
return;
// 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);
foreach (var (name, args) in toApply)
{
Console.WriteLine($"Applying auto profile: {name}");
I.ProcessArgs(args);
I.appliedProfiles.Add(name);
}
I.ProcessArgs(I.arguments);
}
void ApplyDefaultConfig()
{
if (configProfiles.ContainsKey("default"))
{
ProcessArgs(configProfiles["default"].args);
appliedProfiles.Add("default");
} }
} }
static void ApplyProfiles(string names) void ApplyProfiles(string names)
{ {
foreach (var name in names.Split(',')) foreach (var name in names.Split(','))
{ {
if (name.Length > 0 && name != "default") if (name.Length > 0 && name != "default")
{ {
if (profiles.ContainsKey(name)) if (configProfiles.ContainsKey(name))
{ {
ProcessArgs(profiles[name].args); ProcessArgs(configProfiles[name].args);
appliedProfiles.Add(name); appliedProfiles.Add(name);
} }
else else
@ -335,31 +367,7 @@ static class Config
} }
static HashSet<string> ApplyAutoProfiles(TrackListEntry? tle = null) object GetVarValue(string var, TrackListEntry? tle = null)
{
var applied = new HashSet<string>();
if (!HasAutoProfiles)
return applied;
foreach ((var key, var val) in profiles)
{
if (key == "default" || appliedProfiles.Contains(key))
continue;
if (val.cond != null && ProfileConditionSatisfied(val.cond, tle))
{
Console.WriteLine($"Applying auto profile: {key}");
ProcessArgs(val.args);
appliedProfiles.Add(key);
applied.Add(key);
}
}
return applied;
}
static object GetVarValue(string var, TrackListEntry? tle = null)
{ {
static string toKebab(string input) static string toKebab(string input)
{ {
@ -380,7 +388,7 @@ static class Config
} }
public static bool ProfileConditionSatisfied(string cond, TrackListEntry? tle = null) public bool ProfileConditionSatisfied(string cond, TrackListEntry? tle = null)
{ {
var tokens = new Queue<string>(Regex.Split(cond, @"(\s+|\(|\)|&&|\|\||==|!=|!|\"".*?\"")").Where(t => !string.IsNullOrWhiteSpace(t))); var tokens = new Queue<string>(Regex.Split(cond, @"(\s+|\(|\)|&&|\|\||==|!=|!|\"".*?\"")").Where(t => !string.IsNullOrWhiteSpace(t)));
@ -456,10 +464,10 @@ static class Config
} }
static void ListProfiles() void ListProfiles()
{ {
Console.WriteLine("Available profiles:"); Console.WriteLine("Available profiles:");
foreach ((var key, var val) in profiles) foreach ((var key, var val) in configProfiles)
{ {
if (key == "default") if (key == "default")
continue; continue;
@ -475,7 +483,7 @@ static class Config
} }
public static void AddTemporaryConditions(FileConditionsPatch? cond, FileConditionsPatch? prefCond) public void AddTemporaryConditions(FileConditionsMod? cond, FileConditionsMod? prefCond)
{ {
if (cond != null) if (cond != null)
{ {
@ -489,7 +497,8 @@ static class Config
} }
} }
public static void RestoreConditions()
public void RestoreConditions()
{ {
if (prevConds != null) if (prevConds != null)
necessaryCond = prevConds; necessaryCond = prevConds;
@ -498,7 +507,7 @@ static class Config
} }
public static FileConditionsPatch ParseConditions(string input) public static FileConditionsMod ParseConditions(string input)
{ {
static void UpdateMinMax(string value, string condition, ref int? min, ref int? max) static void UpdateMinMax(string value, string condition, ref int? min, ref int? max)
{ {
@ -514,7 +523,7 @@ static class Config
min = max = int.Parse(value); min = max = int.Parse(value);
} }
var cond = new FileConditionsPatch(); var cond = new FileConditionsMod();
var tr = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries; var tr = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries;
string[] conditions = input.Split(';', tr); string[] conditions = input.Split(';', tr);
@ -583,7 +592,7 @@ static class Config
} }
static void ProcessArgs(IReadOnlyList<string> args) void ProcessArgs(IReadOnlyList<string> args)
{ {
void setFlag(ref bool flag, ref int i, bool trueVal = true) void setFlag(ref bool flag, ref int i, bool trueVal = true)
{ {

View file

@ -107,8 +107,8 @@ namespace Data
public bool needSkipExistingAfterSearch = false; public bool needSkipExistingAfterSearch = false;
public bool gotoNextAfterSearch = false; public bool gotoNextAfterSearch = false;
public string? defaultFolderName = null; public string? defaultFolderName = null;
public FileConditionsPatch? additionalConds = null; public FileConditionsMod? additionalConds = null;
public FileConditionsPatch? additionalPrefConds = null; public FileConditionsMod? additionalPrefConds = null;
public TrackListEntry(TrackType trackType) public TrackListEntry(TrackType trackType)
{ {

View file

@ -1,6 +1,7 @@
using Soulseek; using Soulseek;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Diagnostics;
using Data; using Data;
using Enums; using Enums;
@ -17,9 +18,9 @@ using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string,
static class Download static class Download
{ {
public static async Task DownloadFile(SearchResponse response, Soulseek.File file, string filePath, Track track, ProgressBar progress, CancellationTokenSource cts, CancellationTokenSource? searchCts = null) public static async Task DownloadFile(SearchResponse response, Soulseek.File file, string filePath, Track track, ProgressBar progress, CancellationToken? ct = null, CancellationTokenSource? searchCts = null)
{ {
if (Config.DoNotDownload) if (Config.I.DoNotDownload)
throw new Exception(); throw new Exception();
await Program.WaitForLogin(); await Program.WaitForLogin();
@ -42,8 +43,12 @@ static class Download
try try
{ {
using var downloadCts = ct != null ?
CancellationTokenSource.CreateLinkedTokenSource((CancellationToken)ct) :
new CancellationTokenSource();
using var outputStream = new FileStream(filePath, FileMode.Create); using var outputStream = new FileStream(filePath, FileMode.Create);
var wrapper = new DownloadWrapper(origPath, response, file, track, cts, progress); var wrapper = new DownloadWrapper(origPath, response, file, track, downloadCts, progress);
downloads.TryAdd(file.Filename, wrapper); downloads.TryAdd(file.Filename, wrapper);
int maxRetries = 3; int maxRetries = 3;
@ -55,7 +60,7 @@ static class Download
await client.DownloadAsync(response.Username, file.Filename, await client.DownloadAsync(response.Username, file.Filename,
() => Task.FromResult((Stream)outputStream), () => Task.FromResult((Stream)outputStream),
file.Size, startOffset: outputStream.Position, file.Size, startOffset: outputStream.Position,
options: transferOptions, cancellationToken: cts.Token); options: transferOptions, cancellationToken: downloadCts.Token);
break; break;
} }
@ -156,7 +161,12 @@ public class DownloadWrapper
else if (transfer != null) else if (transfer != null)
{ {
if (queued) if (queued)
state = "Queued"; {
if ((transfer.State & TransferStates.Remotely) != 0)
state = "Queued (R)";
else
state = "Queued (L)";
}
else if ((transfer.State & TransferStates.Initializing) != 0) else if ((transfer.State & TransferStates.Initializing) != 0)
state = "Initialize"; state = "Initialize";
else if ((transfer.State & TransferStates.Completed) != 0) else if ((transfer.State & TransferStates.Completed) != 0)

View file

@ -77,15 +77,15 @@ namespace Extractors
var track = new Track() { Artist = artist, Album = name, Type = TrackType.Album }; var track = new Track() { Artist = artist, Album = name, Type = TrackType.Album };
trackLists.AddEntry(new TrackListEntry(track)); trackLists.AddEntry(new TrackListEntry(track));
if (Config.setAlbumMinTrackCount || Config.setAlbumMaxTrackCount) if (Config.I.setAlbumMinTrackCount || Config.I.setAlbumMaxTrackCount)
{ {
var trackTable = doc.DocumentNode.SelectSingleNode("//*[@id='track_table']"); var trackTable = doc.DocumentNode.SelectSingleNode("//*[@id='track_table']");
int n = trackTable.SelectNodes(".//tr").Count; int n = trackTable.SelectNodes(".//tr").Count;
if (Config.setAlbumMinTrackCount) if (Config.I.setAlbumMinTrackCount)
track.MinAlbumTrackCount = n; track.MinAlbumTrackCount = n;
if (Config.setAlbumMaxTrackCount) if (Config.I.setAlbumMaxTrackCount)
track.MaxAlbumTrackCount = n; track.MaxAlbumTrackCount = n;
} }
} }

View file

@ -23,8 +23,8 @@ namespace Extractors
csvFilePath = input; csvFilePath = input;
var tracks = await ParseCsvIntoTrackInfo(input, Config.artistCol, Config.trackCol, Config.lengthCol, var tracks = await ParseCsvIntoTrackInfo(input, Config.I.artistCol, Config.I.trackCol, Config.I.lengthCol,
Config.albumCol, Config.descCol, Config.ytIdCol, Config.trackCountCol, Config.timeUnit, Config.ytParse); Config.I.albumCol, Config.I.descCol, Config.I.ytIdCol, Config.I.trackCountCol, Config.I.timeUnit, Config.I.ytParse);
if (reverse) if (reverse)
tracks.Reverse(); tracks.Reverse();

View file

@ -49,12 +49,22 @@ namespace Extractors
if (added >= maxTracks) if (added >= maxTracks)
break; break;
bool savedVal = Config.I.album;
if (line.StartsWith("a:"))
{
line = line[2..];
Config.I.album = true;
}
var fields = ParseLine(line); var fields = ParseLine(line);
var (_, ex) = ExtractorRegistry.GetMatchingExtractor(fields[0]); var (_, ex) = ExtractorRegistry.GetMatchingExtractor(fields[0]);
var tl = await ex.GetTracks(fields[0], int.MaxValue, 0, false); var tl = await ex.GetTracks(fields[0], int.MaxValue, 0, false);
Config.I.album = savedVal;
foreach (var tle in tl.lists) foreach (var tle in tl.lists)
{ {
if (fields.Count >= 2) if (fields.Count >= 2)

View file

@ -25,22 +25,24 @@ namespace Extractors
int max = reverse ? int.MaxValue : maxTracks; int max = reverse ? int.MaxValue : maxTracks;
int off = reverse ? 0 : offset; int off = reverse ? 0 : offset;
bool needLogin = input == "spotify-likes" || Config.removeTracksFromSource; bool needLogin = input == "spotify-likes" || Config.I.removeTracksFromSource;
var tle = new TrackListEntry(TrackType.Normal);
if (needLogin && Config.spotifyToken.Length == 0 && (Config.spotifyId.Length == 0 || Config.spotifySecret.Length == 0)) if (needLogin && Config.I.spotifyToken.Length == 0 && (Config.I.spotifyId.Length == 0 || Config.I.spotifySecret.Length == 0))
{ {
Console.WriteLine("Error: Credentials are required when downloading liked music or removing from source playlists."); Console.WriteLine("Error: Credentials are required when downloading liked music or removing from source playlists.");
Environment.Exit(1); Environment.Exit(1);
} }
spotifyClient = new Spotify(Config.spotifyId, Config.spotifySecret, Config.spotifyToken, Config.spotifyRefresh); spotifyClient = new Spotify(Config.I.spotifyId, Config.I.spotifySecret, Config.I.spotifyToken, Config.I.spotifyRefresh);
await spotifyClient.Authorize(needLogin, Config.removeTracksFromSource); await spotifyClient.Authorize(needLogin, Config.I.removeTracksFromSource);
TrackListEntry? tle = null;
if (input == "spotify-likes") if (input == "spotify-likes")
{ {
Console.WriteLine("Loading Spotify likes.."); Console.WriteLine("Loading Spotify likes..");
var tracks = await spotifyClient.GetLikes(max, off); var tracks = await spotifyClient.GetLikes(max, off);
tle = new TrackListEntry(TrackType.Normal);
tle.defaultFolderName = "Spotify Likes"; tle.defaultFolderName = "Spotify Likes";
tle.list.Add(tracks); tle.list.Add(tracks);
} }
@ -48,12 +50,13 @@ namespace Extractors
{ {
Console.WriteLine("Loading Spotify album.."); Console.WriteLine("Loading Spotify album..");
(var source, var tracks) = await spotifyClient.GetAlbum(input); (var source, var tracks) = await spotifyClient.GetAlbum(input);
tle = new TrackListEntry(TrackType.Album);
tle.source = source; tle.source = source;
if (Config.setAlbumMinTrackCount) if (Config.I.setAlbumMinTrackCount)
source.MinAlbumTrackCount = tracks.Count; source.MinAlbumTrackCount = tracks.Count;
if (Config.setAlbumMaxTrackCount) if (Config.I.setAlbumMaxTrackCount)
source.MaxAlbumTrackCount = tracks.Count; source.MaxAlbumTrackCount = tracks.Count;
} }
else if (input.Contains("/artist/")) else if (input.Contains("/artist/"))
@ -65,6 +68,7 @@ namespace Extractors
else else
{ {
var tracks = new List<Track>(); var tracks = new List<Track>();
tle = new TrackListEntry(TrackType.Normal);
try try
{ {
@ -76,7 +80,7 @@ namespace Extractors
{ {
if (!needLogin && !spotifyClient.UsedDefaultCredentials) if (!needLogin && !spotifyClient.UsedDefaultCredentials)
{ {
await spotifyClient.Authorize(true, Config.removeTracksFromSource); await spotifyClient.Authorize(true, Config.I.removeTracksFromSource);
(var playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(input, max, off); (var playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(input, max, off);
tle.defaultFolderName = playlistName; tle.defaultFolderName = playlistName;
} }
@ -219,7 +223,7 @@ namespace Extractors
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Could not make an API call with existing token: {ex}"); Console.WriteLine($"Could not make an API call with existing token: {ex.Message}");
} }
} }
if (_clientRefreshToken.Length != 0) if (_clientRefreshToken.Length != 0)

View file

@ -14,10 +14,10 @@ namespace Extractors
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)
{ {
var trackLists = new TrackLists(); var trackLists = new TrackLists();
var music = ParseTrackArg(input, Config.album); var music = ParseTrackArg(input, Config.I.album);
TrackListEntry tle; TrackListEntry tle;
if (Config.album || (music.Title.Length == 0 && music.Album.Length > 0)) if (Config.I.album || (music.Title.Length == 0 && music.Album.Length > 0))
{ {
music.Type = TrackType.Album; music.Type = TrackType.Album;
tle = new TrackListEntry(music); tle = new TrackListEntry(music);

View file

@ -26,19 +26,19 @@ namespace Extractors
var trackLists = new TrackLists(); var trackLists = new TrackLists();
int max = reverse ? int.MaxValue : maxTracks; int max = reverse ? int.MaxValue : maxTracks;
int off = reverse ? 0 : offset; int off = reverse ? 0 : offset;
YouTube.apiKey = Config.ytKey; YouTube.apiKey = Config.I.ytKey;
string name; string name;
List<Track>? deleted = null; List<Track>? deleted = null;
List<Track> tracks = new(); List<Track> tracks = new();
if (Config.getDeleted) if (Config.I.getDeleted)
{ {
Console.WriteLine("Getting deleted videos.."); Console.WriteLine("Getting deleted videos..");
var archive = new YouTube.YouTubeArchiveRetriever(); var archive = new YouTube.YouTubeArchiveRetriever();
deleted = await archive.RetrieveDeleted(input, printFailed: Config.deletedOnly); deleted = await archive.RetrieveDeleted(input, printFailed: Config.I.deletedOnly);
} }
if (!Config.deletedOnly) if (!Config.I.deletedOnly)
{ {
if (YouTube.apiKey.Length > 0) if (YouTube.apiKey.Length > 0)
{ {

View file

@ -1,5 +1,4 @@
using System.Text.RegularExpressions; 
using Data; using Data;
using SearchResponse = Soulseek.SearchResponse; using SearchResponse = Soulseek.SearchResponse;
@ -41,52 +40,38 @@ public class FileConditions
BannedUsers = other.BannedUsers.ToArray(); BannedUsers = other.BannedUsers.ToArray();
} }
public FileConditions With(FileConditionsPatch patch) public FileConditions With(FileConditionsMod patch)
{ {
var cond = new FileConditions(this); var cond = new FileConditions(this);
if (patch.LengthTolerance != null) if (patch.LengthTolerance != null)
cond.LengthTolerance = patch.LengthTolerance.Value; cond.LengthTolerance = patch.LengthTolerance.Value;
if (patch.MinBitrate != null) if (patch.MinBitrate != null)
cond.MinBitrate = patch.MinBitrate.Value; cond.MinBitrate = patch.MinBitrate.Value;
if (patch.MaxBitrate != null) if (patch.MaxBitrate != null)
cond.MaxBitrate = patch.MaxBitrate.Value; cond.MaxBitrate = patch.MaxBitrate.Value;
if (patch.MinSampleRate != null) if (patch.MinSampleRate != null)
cond.MinSampleRate = patch.MinSampleRate.Value; cond.MinSampleRate = patch.MinSampleRate.Value;
if (patch.MaxSampleRate != null) if (patch.MaxSampleRate != null)
cond.MaxSampleRate = patch.MaxSampleRate.Value; cond.MaxSampleRate = patch.MaxSampleRate.Value;
if (patch.MinBitDepth != null) if (patch.MinBitDepth != null)
cond.MinBitDepth = patch.MinBitDepth.Value; cond.MinBitDepth = patch.MinBitDepth.Value;
if (patch.MaxBitDepth != null) if (patch.MaxBitDepth != null)
cond.MaxBitDepth = patch.MaxBitDepth.Value; cond.MaxBitDepth = patch.MaxBitDepth.Value;
if (patch.StrictTitle != null) if (patch.StrictTitle != null)
cond.StrictTitle = patch.StrictTitle.Value; cond.StrictTitle = patch.StrictTitle.Value;
if (patch.StrictArtist != null) if (patch.StrictArtist != null)
cond.StrictArtist = patch.StrictArtist.Value; cond.StrictArtist = patch.StrictArtist.Value;
if (patch.StrictAlbum != null) if (patch.StrictAlbum != null)
cond.StrictAlbum = patch.StrictAlbum.Value; cond.StrictAlbum = patch.StrictAlbum.Value;
if (patch.Formats != null) if (patch.Formats != null)
cond.Formats = patch.Formats; cond.Formats = patch.Formats;
if (patch.BannedUsers != null) if (patch.BannedUsers != null)
cond.BannedUsers = patch.BannedUsers; cond.BannedUsers = patch.BannedUsers;
if (patch.StrictStringDiacrRemove != null) if (patch.StrictStringDiacrRemove != null)
cond.StrictStringDiacrRemove = patch.StrictStringDiacrRemove.Value; cond.StrictStringDiacrRemove = patch.StrictStringDiacrRemove.Value;
if (patch.AcceptNoLength != null) if (patch.AcceptNoLength != null)
cond.AcceptNoLength = patch.AcceptNoLength.Value; cond.AcceptNoLength = patch.AcceptNoLength.Value;
if (patch.AcceptMissingProps != null) if (patch.AcceptMissingProps != null)
cond.AcceptMissingProps = patch.AcceptMissingProps.Value; cond.AcceptMissingProps = patch.AcceptMissingProps.Value;
@ -298,7 +283,7 @@ public class FileConditions
} }
public class FileConditionsPatch public class FileConditionsMod
{ {
public int? LengthTolerance = null; public int? LengthTolerance = null;
public int? MinBitrate = null; public int? MinBitrate = null;

View file

@ -7,14 +7,13 @@ using System.Text.RegularExpressions;
using Data; using Data;
using Enums; using Enums;
using System.ComponentModel;
public class FileManager public class FileManager
{ {
readonly TrackListEntry tle; readonly TrackListEntry tle;
readonly HashSet<Track> organized = new(); readonly HashSet<Track> organized = new();
string? remoteCommonDir; public string? remoteCommonDir { get; private set; }
public FileManager(TrackListEntry tle) public FileManager(TrackListEntry tle)
{ {
@ -28,19 +27,16 @@ public class FileManager
public string GetSavePathNoExt(string sourceFname) public string GetSavePathNoExt(string sourceFname)
{ {
string parent = Config.parentDir; string parent = Config.I.parentDir;
string name = Utils.GetFileNameWithoutExtSlsk(sourceFname); string name = Utils.GetFileNameWithoutExtSlsk(sourceFname);
if (tle.defaultFolderName != null) if (tle.defaultFolderName != null)
{ {
parent = Path.Join(parent, tle.defaultFolderName.ReplaceInvalidChars(Config.invalidReplaceStr, removeSlash: false)); parent = Path.Join(parent, tle.defaultFolderName.ReplaceInvalidChars(Config.I.invalidReplaceStr, removeSlash: false));
} }
if (tle.source.Type == TrackType.Album) if (tle.source.Type == TrackType.Album && !string.IsNullOrEmpty(remoteCommonDir))
{ {
if (remoteCommonDir == null)
throw new NullReferenceException("Remote common dir needs to be configured to organize album files");
string dirname = Path.GetFileName(remoteCommonDir); string dirname = Path.GetFileName(remoteCommonDir);
string relpath = Path.GetRelativePath(remoteCommonDir, Utils.NormalizedPath(sourceFname)); string relpath = Path.GetRelativePath(remoteCommonDir, Utils.NormalizedPath(sourceFname));
parent = Path.Join(parent, dirname, Path.GetDirectoryName(relpath)); parent = Path.Join(parent, dirname, Path.GetDirectoryName(relpath));
@ -64,7 +60,7 @@ public class FileManager
OrganizeAudio(track, track.FirstDownload); OrganizeAudio(track, track.FirstDownload);
} }
bool onlyAdditionalImages = Config.nameFormat.Length == 0; bool onlyAdditionalImages = Config.I.nameFormat.Length == 0;
var nonAudioToOrganize = onlyAdditionalImages ? additionalImages : tracks.Where(t => t.IsNotAudio); var nonAudioToOrganize = onlyAdditionalImages ? additionalImages : tracks.Where(t => t.IsNotAudio);
@ -88,14 +84,14 @@ public class FileManager
if (track.DownloadPath.Length == 0 || !Utils.IsMusicFile(track.DownloadPath)) if (track.DownloadPath.Length == 0 || !Utils.IsMusicFile(track.DownloadPath))
return; return;
if (Config.nameFormat.Length == 0) if (Config.I.nameFormat.Length == 0)
{ {
organized.Add(track); organized.Add(track);
return; return;
} }
string pathPart = SubstituteValues(Config.nameFormat, track, file); string pathPart = SubstituteValues(Config.I.nameFormat, track, file);
string newFilePath = Path.Join(Config.parentDir, pathPart + Path.GetExtension(track.DownloadPath)); string newFilePath = Path.Join(Config.I.parentDir, pathPart + Path.GetExtension(track.DownloadPath));
try try
{ {
@ -140,7 +136,7 @@ public class FileManager
{ {
Directory.CreateDirectory(Path.GetDirectoryName(newPath)); Directory.CreateDirectory(Path.GetDirectoryName(newPath));
Utils.Move(oldPath, newPath); Utils.Move(oldPath, newPath);
Utils.DeleteAncestorsIfEmpty(Path.GetDirectoryName(oldPath), Config.parentDir); Utils.DeleteAncestorsIfEmpty(Path.GetDirectoryName(oldPath), Config.I.parentDir);
} }
} }
@ -184,7 +180,7 @@ public class FileManager
chosenOpt = Regex.Replace(chosenOpt, @"\([^()]*\)|[^()]+", match => chosenOpt = Regex.Replace(chosenOpt, @"\([^()]*\)|[^()]+", match =>
{ {
if (match.Value.StartsWith("(") && match.Value.EndsWith(")")) if (match.Value.StartsWith("(") && match.Value.EndsWith(")"))
return match.Value[1..^1].ReplaceInvalidChars(Config.invalidReplaceStr, removeSlash: false); return match.Value[1..^1].ReplaceInvalidChars(Config.I.invalidReplaceStr, removeSlash: false);
else else
{ {
TryGetVarValue(match.Value, file, slfile, track, out string res); TryGetVarValue(match.Value, file, slfile, track, out string res);
@ -205,7 +201,7 @@ public class FileManager
char dirsep = Path.DirectorySeparatorChar; char dirsep = Path.DirectorySeparatorChar;
newName = newName.Replace('/', dirsep).Replace('\\', dirsep); newName = newName.Replace('/', dirsep).Replace('\\', dirsep);
var x = newName.Split(dirsep, StringSplitOptions.RemoveEmptyEntries); var x = newName.Split(dirsep, StringSplitOptions.RemoveEmptyEntries);
newName = string.Join(dirsep, x.Select(x => x.ReplaceInvalidChars(Config.invalidReplaceStr).Trim(' ', '.'))); newName = string.Join(dirsep, x.Select(x => x.ReplaceInvalidChars(Config.I.invalidReplaceStr).Trim(' ', '.')));
return newName; return newName;
} }
@ -257,12 +253,14 @@ public class FileManager
return true; return true;
} }
case "extractor": case "extractor":
res = Config.inputType.ToString(); break; res = Config.I.inputType.ToString(); break;
case "default-folder":
res = tle.defaultFolderName ?? tle.source.ToString(false); break;
default: default:
res = x; return false; res = x; return false;
} }
res = res.ReplaceInvalidChars(Config.invalidReplaceStr); res = res.ReplaceInvalidChars(Config.I.invalidReplaceStr);
return true; return true;
} }
} }

View file

@ -29,7 +29,7 @@ public static class Help
--profile <names> Configuration profile(s) to use. See --help ""config"". --profile <names> Configuration profile(s) to use. See --help ""config"".
--concurrent-downloads <num> Max concurrent downloads (default: 2) --concurrent-downloads <num> Max concurrent downloads (default: 2)
--m3u <option> Create an m3u8 playlist file in the output directory --m3u <option> Create an m3u8 playlist file in the output directory
'none' (default for single inputs): Do not create 'none' (default for string inputs): Do not create
'index' (default): Write a line indexing all downloaded 'index' (default): Write a line indexing all downloaded
files, required for skip-not-found or skip-existing=m3u files, required for skip-not-found or skip-existing=m3u
'all': Write the index and a list of paths and fails 'all': Write the index and a list of paths and fails
@ -204,8 +204,8 @@ public static class Help
Path to a local CSV file: Use a csv file containing track info of the songs to download. Path to a local CSV file: Use a csv file containing track info of the songs to download.
The names of the columns should be Artist, Title, Album, Length, although alternative names The names of the columns should be Artist, Title, Album, Length, although alternative names
are usually detected as well. Only the title or album column is required, but extra info may are usually detected as well. Only the title or album column is required, but extra info may
improve search results. Every row that does not have a title column text will be treated as an improve search result ranking. Every row that does not have a title column text will be treated
album download. as an album download.
YouTube YouTube
A playlist url: Download songs from a youtube playlist. A playlist url: Download songs from a youtube playlist.
@ -213,10 +213,6 @@ public static class Help
the ones which are unavailable. To get all video titles, you can use the official API by the ones which are unavailable. To get all video titles, you can use the official API by
providing a key with --youtube-key. Get it here https://console.cloud.google.com. Create a providing a key with --youtube-key. Get it here https://console.cloud.google.com. Create a
new project, click ""Enable Api"" and search for ""youtube data"", then follow the prompts. new project, click ""Enable Api"" and search for ""youtube data"", then follow the prompts.
Tip: For playlists containing music videos, it may be better to remove all text in parentheses
(to remove (Lyrics), (Official), etc) and disable song duration checking:
--regex ""[\[\(].*?[\]\)]"" --pref-length-tol -1
Spotify Spotify
A playlist/album url or 'spotify-likes': Download a spotify playlist, album, or your A playlist/album url or 'spotify-likes': Download a spotify playlist, album, or your
@ -255,7 +251,7 @@ public static class Help
(like what you would enter into the soulseek search bar), or a comma-separated list of (like what you would enter into the soulseek search bar), or a comma-separated list of
properties like 'title=Song Name, artist=Artist Name, length=215'. properties like 'title=Song Name, artist=Artist Name, length=215'.
The following properties are allowed: The following properties are accepted:
title title
artist artist
album album
@ -423,6 +419,7 @@ public static class Help
filename Soulseek filename without extension filename Soulseek filename without extension
foldername Soulseek folder name foldername Soulseek folder name
extractor Name of the extractor used (CSV/Spotify/YouTube/etc) extractor Name of the extractor used (CSV/Spotify/YouTube/etc)
default-folder Default sldl folder name (usually the playlist name)
"; ";
const string skipExistingHelp = @" const string skipExistingHelp = @"

View file

@ -32,6 +32,7 @@ public class M3uEditor
this.path = Path.GetFullPath(path); this.path = Path.GetFullPath(path);
parent = Utils.NormalizedPath(Path.GetDirectoryName(this.path)); parent = Utils.NormalizedPath(Path.GetDirectoryName(this.path));
lines = ReadAllLines().ToList(); lines = ReadAllLines().ToList();
LoadPreviousResults(); LoadPreviousResults();
} }
@ -81,7 +82,7 @@ public class M3uEditor
if (field == 0) if (field == 0)
{ {
if (x.StartsWith("./")) if (x.StartsWith("./"))
x = System.IO.Path.Join(parent, x[2..]); x = Path.Join(parent, x[2..]);
track.DownloadPath = x; track.DownloadPath = x;
} }
else if (field == 1) else if (field == 1)

View file

@ -164,21 +164,21 @@ public static class Printing
{ {
Console.WriteLine(new string('-', 60)); Console.WriteLine(new string('-', 60));
if (!Config.printOption.HasFlag(PrintOption.Full)) if (!Config.I.printOption.HasFlag(PrintOption.Full))
Console.WriteLine($"Result 1 of {tle.list.Count} for album {tle.source.ToString(true)}:"); Console.WriteLine($"Result 1 of {tle.list.Count} for album {tle.source.ToString(true)}:");
else else
Console.WriteLine($"Results ({tle.list.Count}) for album {tle.source.ToString(true)}:"); Console.WriteLine($"Results ({tle.list.Count}) for album {tle.source.ToString(true)}:");
if (tle.list.Count > 0 && tle.list[0].Count > 0) if (tle.list.Count > 0 && tle.list[0].Count > 0)
{ {
if (!Config.noBrowseFolder) if (!Config.I.noBrowseFolder)
Console.WriteLine("[Skipping full folder retrieval]"); Console.WriteLine("[Skipping full folder retrieval]");
foreach (var ls in tle.list) foreach (var ls in tle.list)
{ {
PrintAlbum(ls); PrintAlbum(ls);
if (!Config.printOption.HasFlag(PrintOption.Full)) if (!Config.I.printOption.HasFlag(PrintOption.Full))
break; break;
} }
} }
@ -208,14 +208,14 @@ 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, bool summary = true)
{ {
if (type == TrackType.Normal && !Config.PrintTracks && toBeDownloaded.Count == 1 && existing.Count + notFound.Count == 0) if (type == TrackType.Normal && !Config.I.PrintTracks && toBeDownloaded.Count == 1 && existing.Count + notFound.Count == 0)
return; return;
string notFoundLastTime = notFound.Count > 0 ? $"{notFound.Count} not found" : ""; string notFoundLastTime = notFound.Count > 0 ? $"{notFound.Count} not found" : "";
string alreadyExist = existing.Count > 0 ? $"{existing.Count} already exist" : ""; string alreadyExist = existing.Count > 0 ? $"{existing.Count} already exist" : "";
notFoundLastTime = alreadyExist.Length > 0 && notFoundLastTime.Length > 0 ? ", " + notFoundLastTime : notFoundLastTime; notFoundLastTime = alreadyExist.Length > 0 && notFoundLastTime.Length > 0 ? ", " + notFoundLastTime : notFoundLastTime;
string skippedTracks = alreadyExist.Length + notFoundLastTime.Length > 0 ? $" ({alreadyExist}{notFoundLastTime})" : ""; string skippedTracks = alreadyExist.Length + notFoundLastTime.Length > 0 ? $" ({alreadyExist}{notFoundLastTime})" : "";
bool full = Config.printOption.HasFlag(PrintOption.Full); bool full = Config.I.printOption.HasFlag(PrintOption.Full);
bool allSkipped = existing.Count + notFound.Count > toBeDownloaded.Count; bool allSkipped = existing.Count + notFound.Count > toBeDownloaded.Count;
if (summary && (type == TrackType.Normal || skippedTracks.Length > 0)) if (summary && (type == TrackType.Normal || skippedTracks.Length > 0))
@ -223,27 +223,26 @@ public static class Printing
if (toBeDownloaded.Count > 0) if (toBeDownloaded.Count > 0)
{ {
bool showAll = type != TrackType.Normal || Config.PrintTracks || Config.PrintResults; bool showAll = type != TrackType.Normal || Config.I.PrintTracks || Config.I.PrintResults;
PrintTracks(toBeDownloaded, showAll ? int.MaxValue : 10, full, infoFirst: Config.PrintTracks); PrintTracks(toBeDownloaded, showAll ? int.MaxValue : 10, full, infoFirst: Config.I.PrintTracks);
if (full && (existing.Count > 0 || notFound.Count > 0)) if (full && (existing.Count > 0 || notFound.Count > 0))
Console.WriteLine("\n-----------------------------------------------\n"); Console.WriteLine("\n-----------------------------------------------\n");
} }
if (Config.PrintTracks || Config.PrintResults) if (Config.I.PrintTracks || Config.I.PrintResults)
{ {
if (existing.Count > 0) if (existing.Count > 0)
{ {
Console.WriteLine($"\nThe following tracks already exist:"); Console.WriteLine($"\nThe following tracks already exist:");
PrintTracks(existing, fullInfo: full, infoFirst: Config.PrintTracks); PrintTracks(existing, fullInfo: full, infoFirst: Config.I.PrintTracks);
} }
if (notFound.Count > 0) if (notFound.Count > 0)
{ {
Console.WriteLine($"\nThe following tracks were not found during a prior run:"); Console.WriteLine($"\nThe following tracks were not found during a prior run:");
PrintTracks(notFound, fullInfo: full, infoFirst: Config.PrintTracks); PrintTracks(notFound, fullInfo: full, infoFirst: Config.I.PrintTracks);
} }
} }
Console.WriteLine();
} }
@ -294,9 +293,7 @@ public static class Printing
res += $" / {totalFileSizeInMB:F2} MB]"; res += $" / {totalFileSizeInMB:F2} MB]";
string gcp = Utils.GreatestCommonDirectory(files.Select(x => x.Filename)).TrimEnd('\\'); string gcp = Utils.GreatestCommonDirectorySlsk(files.Select(x => x.Filename)).TrimEnd('\\');
var discPattern = new Regex(@"^(?i)(dis[c|k]|cd)\s*\d{1,2}$");
int lastIndex = gcp.LastIndexOf('\\'); int lastIndex = gcp.LastIndexOf('\\');
if (lastIndex != -1) if (lastIndex != -1)
{ {
@ -315,14 +312,14 @@ public static class Printing
try { progress.Refresh(current, item); } try { progress.Refresh(current, item); }
catch { } catch { }
} }
else if ((Config.displayMode == DisplayMode.Simple || Console.IsOutputRedirected) && print) else if ((Config.I.displayMode == DisplayMode.Simple || Console.IsOutputRedirected) && print)
Console.WriteLine(item); 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, bool debugOnly = false)
{ {
if (debugOnly && !Config.debugInfo) if (debugOnly && !Config.I.debugInfo)
return; return;
if (!safe) if (!safe)
{ {

View file

@ -15,17 +15,13 @@ using static Printing;
using Directory = System.IO.Directory; using Directory = System.IO.Directory;
using File = System.IO.File; using File = System.IO.File;
using ProgressBar = Konsole.ProgressBar;
using SearchResponse = Soulseek.SearchResponse;
using SlFile = Soulseek.File; using SlFile = Soulseek.File;
using SlResponse = Soulseek.SearchResponse;
static partial class Program static partial class Program
{ {
public static bool skipUpdate = false; public static bool skipUpdate = false;
public static bool initialized = false; public static bool initialized = false;
public static Extractors.IExtractor? extractor; public static IExtractor? extractor;
public static FileSkipper? outputDirSkipper; public static FileSkipper? outputDirSkipper;
public static FileSkipper? musicDirSkipper; public static FileSkipper? musicDirSkipper;
public static SoulseekClient? client; public static SoulseekClient? client;
@ -48,33 +44,30 @@ static partial class Program
return; return;
} }
bool doContinue = Config.ParseArgsAndReadConfig(args); Config.I.Load(args);
if (!doContinue) if (Config.I.input.Length == 0)
return;
if (Config.input.Length == 0)
throw new ArgumentException($"No input provided"); throw new ArgumentException($"No input provided");
(Config.inputType, extractor) = ExtractorRegistry.GetMatchingExtractor(Config.input, Config.inputType); (Config.I.inputType, extractor) = ExtractorRegistry.GetMatchingExtractor(Config.I.input, Config.I.inputType);
WriteLine($"Using extractor: {Config.inputType}", debugOnly: true); WriteLine($"Using extractor: {Config.I.inputType}", debugOnly: true);
trackLists = await extractor.GetTracks(Config.input, Config.maxTracks, Config.offset, Config.reverse); trackLists = await extractor.GetTracks(Config.I.input, Config.I.maxTracks, Config.I.offset, Config.I.reverse);
WriteLine("Got tracks", debugOnly: true); WriteLine("Got tracks", debugOnly: true);
trackLists.UpgradeListTypes(Config.aggregate, Config.album); Config.I.PostProcessArgs();
trackLists.UpgradeListTypes(Config.I.aggregate, Config.I.album);
trackLists.SetListEntryOptions(); trackLists.SetListEntryOptions();
Config.PostProcessArgs(); m3uEditor = new M3uEditor(trackLists, Config.I.m3uOption);
m3uEditor = new M3uEditor(trackLists, Config.m3uOption);
InitFileSkippers(); InitFileSkippers();
await MainLoop(); await MainLoop();
WriteLine("Mainloop done", debugOnly: true); WriteLine("Mainloop done", debugOnly: true);
} }
@ -84,7 +77,7 @@ static partial class Program
if (initialized) if (initialized)
return; return;
bool needLogin = !Config.PrintTracks; bool needLogin = !Config.I.PrintTracks;
if (needLogin) if (needLogin)
{ {
var connectionOptions = new ConnectionOptions(configureSocket: (socket) => var connectionOptions = new ConnectionOptions(configureSocket: (socket) =>
@ -98,17 +91,17 @@ static partial class Program
var clientOptions = new SoulseekClientOptions( var clientOptions = new SoulseekClientOptions(
transferConnectionOptions: connectionOptions, transferConnectionOptions: connectionOptions,
serverConnectionOptions: connectionOptions, serverConnectionOptions: connectionOptions,
listenPort: Config.listenPort listenPort: Config.I.listenPort
); );
client = new SoulseekClient(clientOptions); client = new SoulseekClient(clientOptions);
if (!Config.useRandomLogin && (string.IsNullOrEmpty(Config.username) || string.IsNullOrEmpty(Config.password))) if (!Config.I.useRandomLogin && (string.IsNullOrEmpty(Config.I.username) || string.IsNullOrEmpty(Config.I.password)))
throw new ArgumentException("No soulseek username or password"); throw new ArgumentException("No soulseek username or password");
await Login(Config.useRandomLogin); await Login(Config.I.useRandomLogin);
Search.searchSemaphore = new RateLimitedSemaphore(Config.searchesPerTime, TimeSpan.FromSeconds(Config.searchRenewTime)); Search.searchSemaphore = new RateLimitedSemaphore(Config.I.searchesPerTime, TimeSpan.FromSeconds(Config.I.searchRenewTime));
} }
bool needUpdate = needLogin; bool needUpdate = needLogin;
@ -124,19 +117,19 @@ static partial class Program
static void InitFileSkippers() static void InitFileSkippers()
{ {
if (Config.skipExisting) if (Config.I.skipExisting)
{ {
var cond = Config.skipExistingPrefCond ? Config.preferredCond : Config.necessaryCond; var cond = Config.I.skipExistingPrefCond ? Config.I.preferredCond : Config.I.necessaryCond;
if (Config.musicDir.Length == 0 || !Config.parentDir.StartsWith(Config.musicDir, StringComparison.OrdinalIgnoreCase)) if (Config.I.musicDir.Length == 0 || !Config.I.parentDir.StartsWith(Config.I.musicDir, StringComparison.OrdinalIgnoreCase))
outputDirSkipper = FileSkipperRegistry.GetChecker(Config.skipMode, Config.parentDir, cond, m3uEditor); outputDirSkipper = FileSkipperRegistry.GetChecker(Config.I.skipMode, Config.I.parentDir, cond, m3uEditor);
if (Config.musicDir.Length > 0) if (Config.I.musicDir.Length > 0)
{ {
if (!Directory.Exists(Config.musicDir)) if (!Directory.Exists(Config.I.musicDir))
Console.WriteLine("Error: Music directory does not exist"); Console.WriteLine("Error: Music directory does not exist");
else else
musicDirSkipper = FileSkipperRegistry.GetChecker(Config.skipModeMusicDir, Config.musicDir, cond, m3uEditor); musicDirSkipper = FileSkipperRegistry.GetChecker(Config.I.skipModeMusicDir, Config.I.musicDir, cond, m3uEditor);
} }
} }
} }
@ -160,22 +153,22 @@ static partial class Program
static void PreprocessTrack(Track track) static void PreprocessTrack(Track track)
{ {
if (Config.removeFt) if (Config.I.removeFt)
{ {
track.Title = track.Title.RemoveFt(); track.Title = track.Title.RemoveFt();
track.Artist = track.Artist.RemoveFt(); track.Artist = track.Artist.RemoveFt();
} }
if (Config.removeBrackets) if (Config.I.removeBrackets)
{ {
track.Title = track.Title.RemoveSquareBrackets(); track.Title = track.Title.RemoveSquareBrackets();
} }
if (Config.regexToReplace.Title.Length + Config.regexToReplace.Artist.Length + Config.regexToReplace.Album.Length > 0) if (Config.I.regexToReplace.Title.Length + Config.I.regexToReplace.Artist.Length + Config.I.regexToReplace.Album.Length > 0)
{ {
track.Title = Regex.Replace(track.Title, Config.regexToReplace.Title, Config.regexReplaceBy.Title); track.Title = Regex.Replace(track.Title, Config.I.regexToReplace.Title, Config.I.regexReplaceBy.Title);
track.Artist = Regex.Replace(track.Artist, Config.regexToReplace.Artist, Config.regexReplaceBy.Artist); track.Artist = Regex.Replace(track.Artist, Config.I.regexToReplace.Artist, Config.I.regexReplaceBy.Artist);
track.Album = Regex.Replace(track.Album, Config.regexToReplace.Album, Config.regexReplaceBy.Album); track.Album = Regex.Replace(track.Album, Config.I.regexToReplace.Album, Config.I.regexReplaceBy.Album);
} }
if (Config.artistMaybeWrong) if (Config.I.artistMaybeWrong)
{ {
track.ArtistMaybeWrong = true; track.ArtistMaybeWrong = true;
} }
@ -188,18 +181,18 @@ static partial class Program
static void PrepareListEntry(TrackListEntry tle) static void PrepareListEntry(TrackListEntry tle)
{ {
Config.RestoreConditions(); Config.I.RestoreConditions();
Config.UpdateProfiles(tle); Config.UpdateProfiles(tle);
Config.AddTemporaryConditions(tle.additionalConds, tle.additionalPrefConds); Config.I.AddTemporaryConditions(tle.additionalConds, tle.additionalPrefConds);
string m3uPath; string m3uPath;
if (Config.m3uFilePath.Length > 0) if (Config.I.m3uFilePath.Length > 0)
m3uPath = Config.m3uFilePath; m3uPath = Config.I.m3uFilePath;
else else
m3uPath = Path.Join(Config.parentDir, tle.defaultFolderName, "sldl.m3u"); m3uPath = Path.Join(Config.I.parentDir, tle.defaultFolderName, "sldl.m3u8");
m3uEditor.SetPathAndLoad(m3uPath); m3uEditor.SetPathAndLoad(m3uPath);
@ -211,7 +204,7 @@ static partial class Program
{ {
for (int i = 0; i < trackLists.lists.Count; i++) for (int i = 0; i < trackLists.lists.Count; i++)
{ {
if (i > 0) Console.WriteLine(); Console.WriteLine();
var tle = trackLists[i]; var tle = trackLists[i];
@ -220,7 +213,7 @@ static partial class Program
var existing = new List<Track>(); var existing = new List<Track>();
var notFound = new List<Track>(); var notFound = new List<Track>();
if (Config.skipNotFound && !Config.PrintResults) if (Config.I.skipNotFound && !Config.I.PrintResults)
{ {
if (tle.sourceCanBeSkipped && SetNotFoundLastTime(tle.source)) if (tle.sourceCanBeSkipped && SetNotFoundLastTime(tle.source))
notFound.Add(tle.source); notFound.Add(tle.source);
@ -232,7 +225,7 @@ static partial class Program
} }
} }
if (Config.skipExisting && !Config.PrintResults && tle.source.State != TrackState.NotFoundLastTime) if (Config.I.skipExisting && !Config.I.PrintResults && tle.source.State != TrackState.NotFoundLastTime)
{ {
if (tle.sourceCanBeSkipped && SetExisting(tle.source)) if (tle.sourceCanBeSkipped && SetExisting(tle.source))
existing.Add(tle.source); existing.Add(tle.source);
@ -244,7 +237,7 @@ static partial class Program
} }
} }
if (Config.PrintTracks) if (Config.I.PrintTracks)
{ {
if (tle.source.Type == TrackType.Normal) if (tle.source.Type == TrackType.Normal)
{ {
@ -286,12 +279,12 @@ static partial class Program
if (tle.source.Type == TrackType.Album) if (tle.source.Type == TrackType.Album)
{ {
tle.list = await Search.GetAlbumDownloads(tle.source, responseData); tle.list = await Search.GetAlbumDownloads(tle.source, responseData);
foundSomething = tle.list.Count > 0; foundSomething = tle.list.Count > 0 && tle.list[0].Count > 0;
} }
else if (tle.source.Type == TrackType.Aggregate) 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));
foundSomething = tle.list.Count > 0; foundSomething = tle.list.Count > 0 && tle.list[0].Count > 0;
} }
else if (tle.source.Type == TrackType.AlbumAggregate) else if (tle.source.Type == TrackType.AlbumAggregate)
{ {
@ -313,7 +306,7 @@ static partial class Program
var lockedFiles = responseData.lockedFilesCount > 0 ? $" (Found {responseData.lockedFilesCount} locked files)" : ""; var lockedFiles = responseData.lockedFilesCount > 0 ? $" (Found {responseData.lockedFilesCount} locked files)" : "";
Console.WriteLine($"No results.{lockedFiles}"); Console.WriteLine($"No results.{lockedFiles}");
if (!Config.PrintResults) if (!Config.I.PrintResults)
{ {
tle.source.State = TrackState.Failed; tle.source.State = TrackState.Failed;
tle.source.FailureReason = FailureReason.NoSuitableFileFound; tle.source.FailureReason = FailureReason.NoSuitableFileFound;
@ -323,7 +316,7 @@ static partial class Program
continue; continue;
} }
if (Config.skipExisting && tle.needSkipExistingAfterSearch) if (Config.I.skipExisting && tle.needSkipExistingAfterSearch)
{ {
foreach (var tracks in tle.list) foreach (var tracks in tle.list)
existing.AddRange(DoSkipExisting(tracks)); existing.AddRange(DoSkipExisting(tracks));
@ -335,7 +328,7 @@ static partial class Program
} }
} }
if (Config.PrintResults) if (Config.I.PrintResults)
{ {
await PrintResults(tle, existing, notFound); await PrintResults(tle, existing, notFound);
continue; continue;
@ -369,7 +362,7 @@ static partial class Program
} }
} }
if (!Config.DoNotDownload && (trackLists.lists.Count > 0 || trackLists.Flattened(false, false).Skip(1).Any())) if (!Config.I.DoNotDownload && (trackLists.lists.Count > 0 || trackLists.Flattened(false, false).Skip(1).Any()))
{ {
PrintComplete(trackLists); PrintComplete(trackLists);
} }
@ -455,7 +448,7 @@ static partial class Program
{ {
var tracks = tle.list[0]; var tracks = tle.list[0];
var semaphore = new SemaphoreSlim(Config.concurrentProcesses); var semaphore = new SemaphoreSlim(Config.I.concurrentProcesses);
var organizer = new FileManager(tle); var organizer = new FileManager(tle);
@ -468,7 +461,7 @@ static partial class Program
await Task.WhenAll(downloadTasks); await Task.WhenAll(downloadTasks);
if (Config.removeTracksFromSource && tracks.All(t => t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists)) if (Config.I.removeTracksFromSource && tracks.All(t => t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists))
await extractor.RemoveTrackFromSource(tle.source); await extractor.RemoveTrackFromSource(tle.source);
} }
@ -481,14 +474,14 @@ static partial class Program
bool succeeded = false; bool succeeded = false;
string? soulseekDir = null; string? soulseekDir = null;
while (tle.list.Count > 0 && !Config.albumArtOnly) while (tle.list.Count > 0 && !Config.I.albumArtOnly)
{ {
int index = 0; int index = 0;
bool wasInteractive = Config.interactiveMode; bool wasInteractive = Config.I.interactiveMode;
if (Config.interactiveMode) if (Config.I.interactiveMode)
{ {
index = await InteractiveModeAlbum(tle.list, !Config.noBrowseFolder, retrievedFolders); index = await InteractiveModeAlbum(tle.list, !Config.I.noBrowseFolder, retrievedFolders);
if (index == -1) break; if (index == -1) break;
} }
@ -498,20 +491,20 @@ static partial class Program
organizer.SetRemoteCommonDir(soulseekDir); organizer.SetRemoteCommonDir(soulseekDir);
if (!Config.interactiveMode && !wasInteractive) if (!Config.I.interactiveMode && !wasInteractive)
{ {
Console.WriteLine(); Console.WriteLine();
PrintAlbum(tracks); PrintAlbum(tracks);
} }
var semaphore = new SemaphoreSlim(Config.concurrentProcesses); var semaphore = new SemaphoreSlim(Config.I.concurrentProcesses);
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
try try
{ {
await RunAlbumDownloads(tle, organizer, tracks, semaphore, cts); await RunAlbumDownloads(tle, organizer, tracks, semaphore, cts);
if (!Config.noBrowseFolder && !retrievedFolders.Contains(soulseekDir)) if (!Config.I.noBrowseFolder && !retrievedFolders.Contains(soulseekDir))
{ {
Console.WriteLine("Getting all files in folder..."); Console.WriteLine("Getting all files in folder...");
@ -548,10 +541,10 @@ static partial class Program
List<Track>? additionalImages = null; List<Track>? additionalImages = null;
if (Config.albumArtOnly || succeeded && Config.albumArtOption != AlbumArtOption.Default) if (Config.I.albumArtOnly || succeeded && Config.I.albumArtOption != AlbumArtOption.Default)
{ {
Console.WriteLine($"\nDownloading additional images:"); Console.WriteLine($"\nDownloading additional images:");
additionalImages = await DownloadImages(tle.list, Config.albumArtOption, tracks, organizer); additionalImages = await DownloadImages(tle.list, Config.I.albumArtOption, tracks, organizer);
tracks?.AddRange(additionalImages); tracks?.AddRange(additionalImages);
} }
@ -586,7 +579,7 @@ static partial class Program
tle.source.State = TrackState.Downloaded; tle.source.State = TrackState.Downloaded;
tle.source.DownloadPath = Utils.GreatestCommonDirectory(downloadedAudio.Select(t => t.DownloadPath)); tle.source.DownloadPath = Utils.GreatestCommonDirectory(downloadedAudio.Select(t => t.DownloadPath));
if (Config.removeTracksFromSource) if (Config.I.removeTracksFromSource)
{ {
await extractor.RemoveTrackFromSource(tle.source); await extractor.RemoveTrackFromSource(tle.source);
} }
@ -596,7 +589,7 @@ static partial class Program
static void OnAlbumFail(List<Track>? tracks) static void OnAlbumFail(List<Track>? tracks)
{ {
if (tracks == null || Config.IgnoreAlbumFail) if (tracks == null || Config.I.IgnoreAlbumFail)
return; return;
foreach (var track in tracks) foreach (var track in tracks)
@ -605,18 +598,18 @@ static partial class Program
{ {
try try
{ {
if (Config.DeleteAlbumOnFail) if (Config.I.DeleteAlbumOnFail)
{ {
File.Delete(track.DownloadPath); File.Delete(track.DownloadPath);
} }
else if (Config.failedAlbumPath.Length > 0) else if (Config.I.failedAlbumPath.Length > 0)
{ {
var newPath = Path.Join(Config.failedAlbumPath, Path.GetRelativePath(Config.parentDir, track.DownloadPath)); var newPath = Path.Join(Config.I.failedAlbumPath, Path.GetRelativePath(Config.I.parentDir, track.DownloadPath));
Directory.CreateDirectory(Path.GetDirectoryName(newPath)); Directory.CreateDirectory(Path.GetDirectoryName(newPath));
Utils.Move(track.DownloadPath, newPath); Utils.Move(track.DownloadPath, newPath);
} }
Utils.DeleteAncestorsIfEmpty(Path.GetDirectoryName(track.DownloadPath), Config.parentDir); Utils.DeleteAncestorsIfEmpty(Path.GetDirectoryName(track.DownloadPath), Config.I.parentDir);
} }
catch (Exception e) catch (Exception e)
{ {
@ -698,9 +691,9 @@ static partial class Program
while (albumArtLists.Count > 0) while (albumArtLists.Count > 0)
{ {
int index = 0; int index = 0;
bool wasInteractive = Config.interactiveMode; bool wasInteractive = Config.I.interactiveMode;
if (Config.interactiveMode) if (Config.I.interactiveMode)
{ {
index = await InteractiveModeAlbum(albumArtLists, false, null); index = await InteractiveModeAlbum(albumArtLists, false, null);
if (index == -1) break; if (index == -1) break;
@ -715,12 +708,17 @@ static partial class Program
return downloadedImages; return downloadedImages;
} }
if (!Config.interactiveMode && !wasInteractive) if (!Config.I.interactiveMode && !wasInteractive)
{ {
Console.WriteLine(); Console.WriteLine();
PrintAlbum(tracks); PrintAlbum(tracks);
} }
if (fileManager.remoteCommonDir == null)
{
fileManager.SetRemoteCommonDir(Utils.GreatestCommonDirectorySlsk(tracks.Select(t => t.FirstDownload.Filename)));
}
bool allSucceeded = true; bool allSucceeded = true;
var semaphore = new SemaphoreSlim(1); var semaphore = new SemaphoreSlim(1);
@ -743,14 +741,14 @@ 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(TrackListEntry? tle, Track track, SemaphoreSlim semaphore, FileManager organizer, CancellationTokenSource? cts, bool cancelOnFail, bool removeFromSource, bool organize)
{ {
if (track.State != TrackState.Initial) if (track.State != TrackState.Initial)
return; return;
await semaphore.WaitAsync(cts.Token); await semaphore.WaitAsync(cts.Token);
int tries = Config.unknownErrorRetries; int tries = Config.I.unknownErrorRetries;
string savedFilePath = ""; string savedFilePath = "";
SlFile? chosenFile = null; SlFile? chosenFile = null;
@ -809,7 +807,7 @@ static partial class Program
track.DownloadPath = savedFilePath; track.DownloadPath = savedFilePath;
} }
if (removeFromSource && Config.removeTracksFromSource) if (removeFromSource && Config.I.removeTracksFromSource)
{ {
try try
{ {
@ -827,9 +825,9 @@ static partial class Program
organizer?.OrganizeAudio(track, chosenFile); organizer?.OrganizeAudio(track, chosenFile);
} }
if (Config.onComplete.Length > 0) if (Config.I.onComplete.Length > 0)
{ {
OnComplete(Config.onComplete, track); OnComplete(Config.I.onComplete, track);
} }
semaphore.Release(); semaphore.Release();
@ -890,9 +888,11 @@ static partial class Program
case "s": case "s":
return -1; return -1;
case "q": case "q":
Config.interactiveMode = false; Config.I.interactiveMode = false;
return aidx; return aidx;
case "r": case "r":
if (!retrieveFolder)
break;
var folder = Utils.GreatestCommonDirectorySlsk(tracks.Select(t => t.FirstDownload.Filename)); var folder = Utils.GreatestCommonDirectorySlsk(tracks.Select(t => t.FirstDownload.Filename));
if (retrieveFolder && !retrievedFolders.Contains(username + '\\' + folder)) if (retrieveFolder && !retrievedFolders.Contains(username + '\\' + folder))
{ {
@ -939,7 +939,7 @@ static partial class Program
{ {
lock (val) lock (val)
{ {
if ((DateTime.Now - val.UpdateLastChangeTime()).TotalMilliseconds > Config.maxStaleTime) if ((DateTime.Now - val.UpdateLastChangeTime()).TotalMilliseconds > Config.I.maxStaleTime)
{ {
val.stalled = true; val.stalled = true;
val.UpdateText(); val.UpdateText();
@ -966,10 +966,10 @@ static partial class Program
&& !client.State.HasFlag(SoulseekClientStates.Connecting)) && !client.State.HasFlag(SoulseekClientStates.Connecting))
{ {
WriteLine($"\nDisconnected, logging in\n", ConsoleColor.DarkYellow, true); WriteLine($"\nDisconnected, logging in\n", ConsoleColor.DarkYellow, true);
try { await Login(Config.useRandomLogin); } try { await Login(Config.I.useRandomLogin); }
catch (Exception ex) catch (Exception ex)
{ {
string banMsg = Config.useRandomLogin ? "" : " (possibly a 30-minute ban caused by frequent searches)"; string banMsg = Config.I.useRandomLogin ? "" : " (possibly a 30-minute ban caused by frequent searches)";
WriteLine($"{ex.Message}{banMsg}", ConsoleColor.DarkYellow, true); WriteLine($"{ex.Message}{banMsg}", ConsoleColor.DarkYellow, true);
} }
} }
@ -989,14 +989,14 @@ static partial class Program
} }
} }
await Task.Delay(Config.updateDelay); await Task.Delay(Config.I.updateDelay);
} }
} }
static async Task Login(bool random = false, int tries = 3) static async Task Login(bool random = false, int tries = 3)
{ {
string user = Config.username, pass = Config.password; string user = Config.I.username, pass = Config.I.password;
if (random) if (random)
{ {
var r = new Random(); var r = new Random();
@ -1013,7 +1013,7 @@ static partial class Program
{ {
WriteLine($"Connecting {user}", debugOnly: true); WriteLine($"Connecting {user}", debugOnly: true);
await client.ConnectAsync(user, pass); await client.ConnectAsync(user, pass);
if (!Config.noModifyShareCount) if (!Config.I.noModifyShareCount)
{ {
WriteLine($"Setting share count", debugOnly: true); WriteLine($"Setting share count", debugOnly: true);
await client.SetSharedCountsAsync(20, 100); await client.SetSharedCountsAsync(20, 100);
@ -1074,7 +1074,7 @@ static partial class Program
.Replace("{failure-reason}", track.FailureReason.ToString()) .Replace("{failure-reason}", track.FailureReason.ToString())
.Replace("{path}", track.DownloadPath) .Replace("{path}", track.DownloadPath)
.Replace("{state}", track.State.ToString()) .Replace("{state}", track.State.ToString())
.Replace("{extractor}", Config.inputType.ToString()) .Replace("{extractor}", Config.I.inputType.ToString())
.Trim(); .Trim();
if (onComplete[0] == '"') if (onComplete[0] == '"')

View file

@ -15,19 +15,38 @@ using SlFile = Soulseek.File;
using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string, (Soulseek.SearchResponse, Soulseek.File)>; using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string, (Soulseek.SearchResponse, Soulseek.File)>;
using System.Diagnostics;
class TimerReporter
{
private Stopwatch stopwatch;
public TimerReporter()
{
stopwatch = new Stopwatch();
stopwatch.Start();
}
public void Report(string message = "")
{
Console.WriteLine($"Time elapsed: {stopwatch.ElapsedMilliseconds} ms. {message}");
stopwatch.Restart();
}
}
static class Search static class Search
{ {
public static RateLimitedSemaphore? searchSemaphore; public static RateLimitedSemaphore? searchSemaphore;
// very messy function that does everything // very messy function that does everything
public static async Task<(string, SlFile?)> SearchAndDownload(Track track, FileManager organizer, CancellationTokenSource cts) public static async Task<(string, SlFile?)> SearchAndDownload(Track track, FileManager organizer, CancellationTokenSource? cts = null)
{ {
if (Config.DoNotDownload) if (Config.I.DoNotDownload)
throw new Exception(); throw new Exception();
IEnumerable<(SlResponse response, SlFile file)>? orderedResults = null; IEnumerable<(SlResponse response, SlFile file)>? orderedResults = null;
var responseData = new ResponseData(); var responseData = new ResponseData();
var progress = Printing.GetProgressBar(Config.displayMode); var progress = Printing.GetProgressBar(Config.I.displayMode);
var results = new SlDictionary(); var results = new SlDictionary();
var fsResults = new SlDictionary(); var fsResults = new SlDictionary();
using var searchCts = new CancellationTokenSource(); using var searchCts = new CancellationTokenSource();
@ -62,7 +81,7 @@ static class Search
saveFilePath = organizer.GetSavePath(f.Filename); saveFilePath = organizer.GetSavePath(f.Filename);
fsUser = r.Username; fsUser = r.Username;
chosenFile = f; chosenFile = f;
downloadTask = Download.DownloadFile(r, f, saveFilePath, track, progress, cts, searchCts); downloadTask = Download.DownloadFile(r, f, saveFilePath, track, progress, cts?.Token, searchCts);
} }
} }
} }
@ -76,17 +95,17 @@ static class Search
foreach (var file in r.Files) foreach (var file in r.Files)
results.TryAdd(r.Username + '\\' + file.Filename, (r, file)); results.TryAdd(r.Username + '\\' + file.Filename, (r, file));
if (Config.fastSearch && userSuccessCount.GetValueOrDefault(r.Username, 0) > Config.downrankOn) if (Config.I.fastSearch && userSuccessCount.GetValueOrDefault(r.Username, 0) > Config.I.downrankOn)
{ {
var f = r.Files.First(); var f = r.Files.First();
if (r.HasFreeUploadSlot && r.UploadSpeed / 1024.0 / 1024.0 >= Config.fastSearchMinUpSpeed if (r.HasFreeUploadSlot && r.UploadSpeed / 1024.0 / 1024.0 >= Config.I.fastSearchMinUpSpeed
&& FileConditions.BracketCheck(track, InferTrack(f.Filename, track)) && Config.preferredCond.FileSatisfies(f, track, r)) && FileConditions.BracketCheck(track, InferTrack(f.Filename, track)) && Config.I.preferredCond.FileSatisfies(f, track, r))
{ {
fsResults.TryAdd(r.Username + '\\' + f.Filename, (r, f)); fsResults.TryAdd(r.Username + '\\' + f.Filename, (r, f));
if (Interlocked.Exchange(ref fsResultsStarted, 1) == 0) if (Interlocked.Exchange(ref fsResultsStarted, 1) == 0)
{ {
Task.Delay(Config.fastSearchDelay).ContinueWith(tt => fastSearchDownload()); Task.Delay(Config.I.fastSearchDelay).ContinueWith(tt => fastSearchDownload());
} }
} }
} }
@ -98,8 +117,8 @@ static class Search
return new SearchOptions( return new SearchOptions(
minimumResponseFileCount: 1, minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1, minimumPeerUploadSpeed: 1,
searchTimeout: Config.searchTimeout, searchTimeout: Config.I.searchTimeout,
removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms, removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
responseFilter: (response) => responseFilter: (response) =>
{ {
return response.UploadSpeed > 0 && necCond.BannedUsersSatisfies(response); return response.UploadSpeed > 0 && necCond.BannedUsersSatisfies(response);
@ -117,7 +136,7 @@ static class Search
searchEnded = true; searchEnded = true;
lock (fsDownloadLock) { } lock (fsDownloadLock) { }
if (downloading == 0 && results.IsEmpty && !Config.useYtdlp) if (downloading == 0 && results.IsEmpty && !Config.I.useYtdlp)
{ {
notFound = true; notFound = true;
} }
@ -151,7 +170,7 @@ static class Search
if (orderedResults == null) if (orderedResults == null)
orderedResults = OrderedResults(results, track, useInfer: true); orderedResults = OrderedResults(results, track, useInfer: true);
int trackTries = Config.maxRetriesPerTrack; int trackTries = Config.I.maxRetriesPerTrack;
async Task<bool> process(SlResponse response, SlFile file) async Task<bool> process(SlResponse response, SlFile file)
{ {
saveFilePath = organizer.GetSavePath(file.Filename); saveFilePath = organizer.GetSavePath(file.Filename);
@ -159,7 +178,7 @@ static class Search
try try
{ {
downloading = 1; downloading = 1;
await Download.DownloadFile(response, file, saveFilePath, track, progress, cts); await Download.DownloadFile(response, file, saveFilePath, track, progress, cts?.Token);
userSuccessCount.AddOrUpdate(response.Username, 1, (k, v) => v + 1); userSuccessCount.AddOrUpdate(response.Username, 1, (k, v) => v + 1);
return true; return true;
} }
@ -172,7 +191,7 @@ static class Search
if (!IsConnectedAndLoggedIn()) if (!IsConnectedAndLoggedIn())
throw; throw;
Printing.WriteLine("Error: " + e.Message, ConsoleColor.DarkYellow, true); Printing.WriteLine("Error: Download Error: " + e.Message, ConsoleColor.DarkYellow, debugOnly: true);
userSuccessCount.AddOrUpdate(response.Username, -1, (k, v) => v - 1); userSuccessCount.AddOrUpdate(response.Username, -1, (k, v) => v - 1);
if (--trackTries <= 0) if (--trackTries <= 0)
@ -194,7 +213,7 @@ static class Search
fr = orderedResults.Skip(1).FirstOrDefault(); fr = orderedResults.Skip(1).FirstOrDefault();
if (fr != default) if (fr != default)
{ {
if (userSuccessCount.GetValueOrDefault(fr.response.Username, 0) > Config.ignoreOn) if (userSuccessCount.GetValueOrDefault(fr.response.Username, 0) > Config.I.ignoreOn)
{ {
success = await process(fr.response, fr.file); success = await process(fr.response, fr.file);
} }
@ -202,7 +221,7 @@ static class Search
{ {
foreach (var (response, file) in orderedResults.Skip(2)) foreach (var (response, file) in orderedResults.Skip(2))
{ {
if (userSuccessCount.GetValueOrDefault(response.Username, 0) <= Config.ignoreOn) if (userSuccessCount.GetValueOrDefault(response.Username, 0) <= Config.I.ignoreOn)
continue; continue;
success = await process(response, file); success = await process(response, file);
if (success) break; if (success) break;
@ -212,7 +231,7 @@ static class Search
} }
} }
if (downloading == 0 && Config.useYtdlp) if (downloading == 0 && Config.I.useYtdlp)
{ {
notFound = false; notFound = false;
try try
@ -224,12 +243,12 @@ static class Search
{ {
foreach (var (length, id, title) in ytResults) foreach (var (length, id, title) in ytResults)
{ {
if (Config.necessaryCond.LengthToleranceSatisfies(length, track.Length)) if (Config.I.necessaryCond.LengthToleranceSatisfies(length, track.Length))
{ {
string saveFilePathNoExt = organizer.GetSavePathNoExt(title); string saveFilePathNoExt = organizer.GetSavePathNoExt(title);
downloading = 1; downloading = 1;
Printing.RefreshOrPrint(progress, 0, $"yt-dlp download: {track}", true); Printing.RefreshOrPrint(progress, 0, $"yt-dlp download: {track}", true);
saveFilePath = await Extractors.YouTube.YtdlpDownload(id, saveFilePathNoExt, Config.ytdlpArgument); saveFilePath = await Extractors.YouTube.YtdlpDownload(id, saveFilePathNoExt, Config.I.ytdlpArgument);
Printing.RefreshOrPrint(progress, 100, $"Succeded: yt-dlp completed download for {track}", true); Printing.RefreshOrPrint(progress, 100, $"Succeded: yt-dlp completed download for {track}", true);
break; break;
} }
@ -271,7 +290,7 @@ static class Search
new SearchOptions( new SearchOptions(
minimumResponseFileCount: 1, minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1, minimumPeerUploadSpeed: 1,
removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms, removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
searchTimeout: timeout, searchTimeout: timeout,
responseFilter: (response) => responseFilter: (response) =>
{ {
@ -354,10 +373,10 @@ static class Search
} }
int min, max; int min, max;
if (Config.minAlbumTrackCount > -1 || Config.maxAlbumTrackCount > -1) if (Config.I.minAlbumTrackCount > -1 || Config.I.maxAlbumTrackCount > -1)
{ {
min = Config.minAlbumTrackCount; min = Config.I.minAlbumTrackCount;
max = Config.maxAlbumTrackCount; max = Config.I.maxAlbumTrackCount;
} }
else else
{ {
@ -410,7 +429,7 @@ static class Search
new( new(
minimumResponseFileCount: 1, minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1, minimumPeerUploadSpeed: 1,
removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms, removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
searchTimeout: timeout, searchTimeout: timeout,
responseFilter: (response) => responseFilter: (response) =>
{ {
@ -442,7 +461,7 @@ static class Search
var equivalentFiles = EquivalentFiles(track, results.Select(x => x.Value)) var equivalentFiles = EquivalentFiles(track, results.Select(x => x.Value))
.Select(x => (x.Item1, OrderedResults(x.Item2, track, false, false, false))).ToList(); .Select(x => (x.Item1, OrderedResults(x.Item2, track, false, false, false))).ToList();
if (!Config.relax) if (!Config.I.relax)
{ {
equivalentFiles = equivalentFiles equivalentFiles = equivalentFiles
.Where(x => FileConditions.StrictString(x.Item1.Title, track.Title, ignoreCase: true) .Where(x => FileConditions.StrictString(x.Item1.Title, track.Title, ignoreCase: true)
@ -458,14 +477,14 @@ static class Search
kvp.Item1.Downloads = kvp.Item2.ToList(); kvp.Item1.Downloads = kvp.Item2.ToList();
return kvp.Item1; return kvp.Item1;
}).ToList(); }).ToList();
return tracks; return tracks;
} }
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)
{ {
int maxDiff = Config.aggregateLengthTol; int maxDiff = Config.I.aggregateLengthTol;
bool lengthsAreSimilar(int[] sorted1, int[] sorted2) bool lengthsAreSimilar(int[] sorted1, int[] sorted2)
{ {
@ -547,7 +566,7 @@ static class Search
} }
res = res.Select((x, i) => (x, i)) res = res.Select((x, i) => (x, i))
.Where(x => usernamesList[x.i].Count >= Config.minSharesAggregate) .Where(x => usernamesList[x.i].Count >= Config.I.minSharesAggregate)
.OrderByDescending(x => usernamesList[x.i].Count) .OrderByDescending(x => usernamesList[x.i].Count)
.Select(x => x.x) .Select(x => x.x)
.ToList(); .ToList();
@ -619,7 +638,7 @@ static class Search
IEnumerable<(SlResponse, SlFile)> fileResponses, int minShares = -1) IEnumerable<(SlResponse, SlFile)> fileResponses, int minShares = -1)
{ {
if (minShares == -1) if (minShares == -1)
minShares = Config.minSharesAggregate; minShares = Config.I.minSharesAggregate;
Track inferTrack((SearchResponse r, Soulseek.File f) x) Track inferTrack((SearchResponse r, Soulseek.File f) x)
{ {
@ -629,7 +648,7 @@ static class Search
} }
var groups = fileResponses var groups = fileResponses
.GroupBy(inferTrack, new TrackComparer(ignoreCase: true, Config.aggregateLengthTol)) .GroupBy(inferTrack, new TrackComparer(ignoreCase: true, Config.I.aggregateLengthTol))
.Select(x => (x, x.Select(y => y.Item1.Username).Distinct().Count())) .Select(x => (x, x.Select(y => y.Item1.Username).Distinct().Count()))
.Where(x => x.Item2 >= minShares) .Where(x => x.Item2 >= minShares)
.OrderByDescending(x => x.Item2) .OrderByDescending(x => x.Item2)
@ -691,22 +710,22 @@ static class Search
var random = new Random(); var random = new Random();
return results.Select(x => (response: x.Item1, file: x.Item2)) return results.Select(x => (response: x.Item1, file: x.Item2))
.Where(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.ignoreOn) .Where(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.I.ignoreOn)
.OrderByDescending(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.downrankOn) .OrderByDescending(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.I.downrankOn)
.ThenByDescending(x => Config.necessaryCond.FileSatisfies(x.file, track, x.response)) .ThenByDescending(x => Config.I.necessaryCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => Config.preferredCond.BannedUsersSatisfies(x.response)) .ThenByDescending(x => Config.I.preferredCond.BannedUsersSatisfies(x.response))
.ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || Config.preferredCond.AcceptNoLength) .ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || Config.I.preferredCond.AcceptNoLength)
.ThenByDescending(x => !useBracketCheck || FileConditions.BracketCheck(track, inferredTrack(x).Item1)) // downrank result if it contains '(' or '[' and the title does not (avoid remixes) .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.preferredCond.StrictTitleSatisfies(x.file.Filename, track.Title)) .ThenByDescending(x => Config.I.preferredCond.StrictTitleSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => !albumMode || Config.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album)) .ThenByDescending(x => !albumMode || Config.I.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
.ThenByDescending(x => Config.preferredCond.StrictArtistSatisfies(x.file.Filename, track.Title)) .ThenByDescending(x => Config.I.preferredCond.StrictArtistSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => Config.preferredCond.LengthToleranceSatisfies(x.file, track.Length)) .ThenByDescending(x => Config.I.preferredCond.LengthToleranceSatisfies(x.file, track.Length))
.ThenByDescending(x => Config.preferredCond.FormatSatisfies(x.file.Filename)) .ThenByDescending(x => Config.I.preferredCond.FormatSatisfies(x.file.Filename))
.ThenByDescending(x => albumMode || Config.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album)) .ThenByDescending(x => albumMode || Config.I.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
.ThenByDescending(x => Config.preferredCond.BitrateSatisfies(x.file)) .ThenByDescending(x => Config.I.preferredCond.BitrateSatisfies(x.file))
.ThenByDescending(x => Config.preferredCond.SampleRateSatisfies(x.file)) .ThenByDescending(x => Config.I.preferredCond.SampleRateSatisfies(x.file))
.ThenByDescending(x => Config.preferredCond.BitDepthSatisfies(x.file)) .ThenByDescending(x => Config.I.preferredCond.BitDepthSatisfies(x.file))
.ThenByDescending(x => Config.preferredCond.FileSatisfies(x.file, track, x.response)) .ThenByDescending(x => Config.I.preferredCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => x.response.HasFreeUploadSlot) .ThenByDescending(x => x.response.HasFreeUploadSlot)
.ThenByDescending(x => x.response.UploadSpeed / 1024 / 650) .ThenByDescending(x => x.response.UploadSpeed / 1024 / 650)
.ThenByDescending(x => albumMode || FileConditions.StrictString(x.file.Filename, track.Title)) .ThenByDescending(x => albumMode || FileConditions.StrictString(x.file.Filename, track.Title))
@ -730,7 +749,7 @@ static class Search
string search = GetSearchString(track); string search = GetSearchString(track);
var searchTasks = new List<Task>(); var searchTasks = new List<Task>();
var defaultSearchOpts = getSearchOptions(Config.searchTimeout, Config.necessaryCond, Config.preferredCond); var defaultSearchOpts = getSearchOptions(Config.I.searchTimeout, Config.I.necessaryCond, Config.I.preferredCond);
searchTasks.Add(DoSearch(search, defaultSearchOpts, responseHandler, ct, onSearch)); searchTasks.Add(DoSearch(search, defaultSearchOpts, responseHandler, ct, onSearch));
if (search.RemoveDiacriticsIfExist(out string noDiacrSearch) && !track.ArtistMaybeWrong) if (search.RemoveDiacriticsIfExist(out string noDiacrSearch) && !track.ArtistMaybeWrong)
@ -742,15 +761,15 @@ static class Search
if (results.IsEmpty && track.ArtistMaybeWrong && title) if (results.IsEmpty && track.ArtistMaybeWrong && title)
{ {
var cond = new FileConditions(Config.necessaryCond); var cond = new FileConditions(Config.I.necessaryCond);
var infTrack = InferTrack(track.Title, new Track()); var infTrack = InferTrack(track.Title, new Track());
cond.StrictTitle = infTrack.Title == track.Title; cond.StrictTitle = infTrack.Title == track.Title;
cond.StrictArtist = false; cond.StrictArtist = false;
var opts = getSearchOptions(Math.Min(Config.searchTimeout, 5000), cond, Config.preferredCond); var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{infTrack.Artist} {infTrack.Title}", opts, responseHandler, ct, onSearch)); searchTasks.Add(DoSearch($"{infTrack.Artist} {infTrack.Title}", opts, responseHandler, ct, onSearch));
} }
if (Config.desperateSearch) if (Config.I.desperateSearch)
{ {
await Task.WhenAll(searchTasks); await Task.WhenAll(searchTasks);
@ -758,23 +777,23 @@ static class Search
{ {
if (artist && album && title) if (artist && album && title)
{ {
var cond = new FileConditions(Config.necessaryCond) var cond = new FileConditions(Config.I.necessaryCond)
{ {
StrictTitle = true, StrictTitle = true,
StrictAlbum = true StrictAlbum = true
}; };
var opts = getSearchOptions(Math.Min(Config.searchTimeout, 5000), cond, Config.preferredCond); var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{track.Artist} {track.Album}", opts, responseHandler, ct, onSearch)); searchTasks.Add(DoSearch($"{track.Artist} {track.Album}", opts, responseHandler, ct, onSearch));
} }
if (artist && title && track.Length != -1 && Config.necessaryCond.LengthTolerance != -1) if (artist && title && track.Length != -1 && Config.I.necessaryCond.LengthTolerance != -1)
{ {
var cond = new FileConditions(Config.necessaryCond) var cond = new FileConditions(Config.I.necessaryCond)
{ {
LengthTolerance = -1, LengthTolerance = -1,
StrictTitle = true, StrictTitle = true,
StrictArtist = true StrictArtist = true
}; };
var opts = getSearchOptions(Math.Min(Config.searchTimeout, 5000), cond, Config.preferredCond); var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{track.Artist} {track.Title}", opts, responseHandler, ct, onSearch)); searchTasks.Add(DoSearch($"{track.Artist} {track.Title}", opts, responseHandler, ct, onSearch));
} }
} }
@ -787,36 +806,36 @@ static class Search
if (track.Album.Length > 3 && album) if (track.Album.Length > 3 && album)
{ {
var cond = new FileConditions(Config.necessaryCond) var cond = new FileConditions(Config.I.necessaryCond)
{ {
StrictAlbum = true, StrictAlbum = true,
StrictTitle = !track.ArtistMaybeWrong, StrictTitle = !track.ArtistMaybeWrong,
StrictArtist = !track.ArtistMaybeWrong, StrictArtist = !track.ArtistMaybeWrong,
LengthTolerance = -1 LengthTolerance = -1
}; };
var opts = getSearchOptions(Math.Min(Config.searchTimeout, 5000), cond, Config.preferredCond); var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{track.Album}", opts, responseHandler, ct, onSearch)); searchTasks.Add(DoSearch($"{track.Album}", opts, responseHandler, ct, onSearch));
} }
if (track2.Title.Length > 3 && artist) if (track2.Title.Length > 3 && artist)
{ {
var cond = new FileConditions(Config.necessaryCond) var cond = new FileConditions(Config.I.necessaryCond)
{ {
StrictTitle = !track.ArtistMaybeWrong, StrictTitle = !track.ArtistMaybeWrong,
StrictArtist = !track.ArtistMaybeWrong, StrictArtist = !track.ArtistMaybeWrong,
LengthTolerance = -1 LengthTolerance = -1
}; };
var opts = getSearchOptions(Math.Min(Config.searchTimeout, 5000), cond, Config.preferredCond); var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{track2.Title}", opts, responseHandler, ct, onSearch)); searchTasks.Add(DoSearch($"{track2.Title}", opts, responseHandler, ct, onSearch));
} }
if (track2.Artist.Length > 3 && title) if (track2.Artist.Length > 3 && title)
{ {
var cond = new FileConditions(Config.necessaryCond) var cond = new FileConditions(Config.I.necessaryCond)
{ {
StrictTitle = !track.ArtistMaybeWrong, StrictTitle = !track.ArtistMaybeWrong,
StrictArtist = !track.ArtistMaybeWrong, StrictArtist = !track.ArtistMaybeWrong,
LengthTolerance = -1 LengthTolerance = -1
}; };
var opts = getSearchOptions(Math.Min(Config.searchTimeout, 5000), cond, Config.preferredCond); var opts = getSearchOptions(Math.Min(Config.I.searchTimeout, 5000), cond, Config.I.preferredCond);
searchTasks.Add(DoSearch($"{track2.Artist}", opts, responseHandler, ct, onSearch)); searchTasks.Add(DoSearch($"{track2.Artist}", opts, responseHandler, ct, onSearch));
} }
} }
@ -851,15 +870,15 @@ static class Search
return new SearchOptions( return new SearchOptions(
minimumResponseFileCount: 1, minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1, minimumPeerUploadSpeed: 1,
searchTimeout: Config.searchTimeout, searchTimeout: Config.I.searchTimeout,
removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms, removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
responseFilter: (response) => responseFilter: (response) =>
{ {
return response.UploadSpeed > 0 && necCond.BannedUsersSatisfies(response); return response.UploadSpeed > 0 && necCond.BannedUsersSatisfies(response);
}, },
fileFilter: (file) => fileFilter: (file) =>
{ {
return Utils.IsMusicFile(file.Filename) && (necCond.FileSatisfies(file, track, null) || Config.PrintResultsFull); return Utils.IsMusicFile(file.Filename) && (necCond.FileSatisfies(file, track, null) || Config.I.PrintResultsFull);
}); });
} }
@ -876,7 +895,7 @@ static class Search
await RunSearches(track, results, getSearchOptions, responseHandler); await RunSearches(track, results, getSearchOptions, responseHandler);
if (Config.DoNotDownload && results.IsEmpty) if (Config.I.DoNotDownload && results.IsEmpty)
{ {
Printing.WriteLine($"No results", ConsoleColor.Yellow); Printing.WriteLine($"No results", ConsoleColor.Yellow);
} }
@ -888,8 +907,8 @@ static class Search
foreach (var (response, file) in orderedResults) foreach (var (response, file) in orderedResults)
{ {
Console.WriteLine(Printing.DisplayString(track, file, response, Console.WriteLine(Printing.DisplayString(track, file, response,
Config.PrintResultsFull ? Config.necessaryCond : null, Config.PrintResultsFull ? Config.preferredCond : null, Config.I.PrintResultsFull ? Config.I.necessaryCond : null, Config.I.PrintResultsFull ? Config.I.preferredCond : null,
fullpath: Config.PrintResultsFull, infoFirst: true, showSpeed: Config.PrintResultsFull)); fullpath: Config.I.PrintResultsFull, infoFirst: true, showSpeed: Config.I.PrintResultsFull));
count += 1; count += 1;
} }
Printing.WriteLine($"Total: {count}\n", ConsoleColor.Yellow); Printing.WriteLine($"Total: {count}\n", ConsoleColor.Yellow);
@ -924,7 +943,7 @@ static class Search
static string CleanSearchString(string str) static string CleanSearchString(string str)
{ {
string old; string old;
if (!Config.noRemoveSpecialChars) if (!Config.I.noRemoveSpecialChars)
{ {
old = str; old = str;
str = str.ReplaceSpecialChars(" ").Trim().RemoveConsecutiveWs(); str = str.ReplaceSpecialChars(" ").Trim().RemoveConsecutiveWs();

View file

@ -79,15 +79,13 @@ namespace Test
{ {
SetCurrentTest("TestAutoProfiles"); SetCurrentTest("TestAutoProfiles");
ResetProfiles(); ResetConfig();
Config.inputType = InputType.YouTube; Config.I.inputType = InputType.YouTube;
Config.interactiveMode = true; Config.I.interactiveMode = true;
Config.album = true; Config.I.aggregate = false;
Config.aggregate = false; Config.I.maxStaleTime = 50000;
Config.maxStaleTime = 500000;
string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf"); string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf");
Config.confPath = path;
string content = string content =
"max-stale-time = 5" + "max-stale-time = 5" +
@ -111,18 +109,19 @@ namespace Test
File.WriteAllText(path, content); File.WriteAllText(path, content);
Config.ParseArgsAndReadConfig(new string[] { }); Config.I.Load(new string[] { "-c", path });
//Config.PostProcessArgs(); var tle = new TrackListEntry(TrackType.Album);
Config.UpdateProfiles(tle);
Assert(Config.maxStaleTime == 10 && !Config.fastSearch && Config.necessaryCond.Formats[0] == "flac"); Assert(Config.I.maxStaleTime == 10 && !Config.I.fastSearch && Config.I.necessaryCond.Formats[0] == "flac");
ResetProfiles(); ResetConfig();
Config.inputType = InputType.CSV; Config.I.inputType = InputType.CSV;
Config.album = true; Config.I.album = true;
Config.interactiveMode = true; Config.I.interactiveMode = true;
Config.useYtdlp = false; Config.I.useYtdlp = false;
Config.maxStaleTime = 50000; Config.I.maxStaleTime = 50000;
content = content =
"\n[no-stale]" + "\n[no-stale]" +
"\nprofile-cond = interactive && download-mode == \"album\"" + "\nprofile-cond = interactive && download-mode == \"album\"" +
@ -133,16 +132,17 @@ namespace Test
File.WriteAllText(path, content); File.WriteAllText(path, content);
Config.ParseArgsAndReadConfig(new string[] { });
Config.I.Load(new string[] { "-c", path });
Config.UpdateProfiles(tle);
Assert(Config.I.maxStaleTime == 999999 && !Config.I.useYtdlp);
Assert(Config.maxStaleTime == 999999 && !Config.useYtdlp); ResetConfig();
Config.I.inputType = InputType.YouTube;
ResetProfiles(); Config.I.album = false;
Config.inputType = InputType.YouTube; Config.I.interactiveMode = true;
Config.album = false; Config.I.useYtdlp = false;
Config.interactiveMode = true; Config.I.maxStaleTime = 50000;
Config.useYtdlp = false;
Config.maxStaleTime = 50000;
content = content =
"\n[no-stale]" + "\n[no-stale]" +
"\nprofile-cond = interactive && download-mode == \"album\"" + "\nprofile-cond = interactive && download-mode == \"album\"" +
@ -152,10 +152,10 @@ namespace Test
"\nyt-dlp = true"; "\nyt-dlp = true";
File.WriteAllText(path, content); File.WriteAllText(path, content);
Config.I.Load(new string[] { "-c", path });
Config.UpdateProfiles(new TrackListEntry(TrackType.Normal));
Config.ParseArgsAndReadConfig(new string[] { }); Assert(Config.I.maxStaleTime == 50000 && Config.I.useYtdlp);
Assert(Config.maxStaleTime == 50000 && Config.useYtdlp);
if (File.Exists(path)) if (File.Exists(path))
File.Delete(path); File.Delete(path);
@ -167,13 +167,15 @@ namespace Test
{ {
SetCurrentTest("TestProfileConditions"); SetCurrentTest("TestProfileConditions");
Config.inputType = InputType.YouTube; Config.I.inputType = InputType.YouTube;
Config.interactiveMode = true; Config.I.interactiveMode = true;
Config.album = true; Config.I.album = true;
Config.aggregate = false; Config.I.aggregate = false;
var conds = new (bool, string)[] var conds = new (bool, string)[]
{ {
(true, "input-type == \"youtube\""),
(true, "download-mode == \"album\""),
(false, "aggregate"), (false, "aggregate"),
(true, "interactive"), (true, "interactive"),
(true, "album"), (true, "album"),
@ -190,7 +192,7 @@ namespace Test
foreach ((var b, var c) in conds) foreach ((var b, var c) in conds)
{ {
Console.WriteLine(c); Console.WriteLine(c);
Assert(b == Config.ProfileConditionSatisfied(c)); Assert(b == Config.I.ProfileConditionSatisfied(c));
} }
Passed(); Passed();
@ -247,29 +249,29 @@ namespace Test
var extractor = new Extractors.StringExtractor(); var extractor = new Extractors.StringExtractor();
Config.aggregate = false; Config.I.aggregate = false;
Config.album = false; Config.I.album = false;
Console.WriteLine("Testing songs: "); Console.WriteLine("Testing songs: ");
for (int i = 0; i < strings.Count; i++) for (int i = 0; i < strings.Count; i++)
{ {
Config.input = strings[i]; Config.I.input = strings[i];
Console.WriteLine(Config.input); Console.WriteLine(Config.I.input);
var res = await extractor.GetTracks(Config.input, 0, 0, false); var res = await extractor.GetTracks(Config.I.input, 0, 0, false);
var t = res[0].list[0][0]; var t = res[0].list[0][0];
Assert(Extractors.StringExtractor.InputMatches(Config.input)); Assert(Extractors.StringExtractor.InputMatches(Config.I.input));
Assert(t.ToKey() == tracks[i].ToKey()); Assert(t.ToKey() == tracks[i].ToKey());
} }
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("Testing albums"); Console.WriteLine("Testing albums");
Config.album = true; Config.I.album = true;
for (int i = 0; i < strings.Count; i++) for (int i = 0; i < strings.Count; i++)
{ {
Config.input = strings[i]; Config.I.input = strings[i];
Console.WriteLine(Config.input); Console.WriteLine(Config.I.input);
var t = (await extractor.GetTracks(Config.input, 0, 0, false))[0].source; var t = (await extractor.GetTracks(Config.I.input, 0, 0, false))[0].source;
Assert(Extractors.StringExtractor.InputMatches(Config.input)); Assert(Extractors.StringExtractor.InputMatches(Config.I.input));
Assert(t.ToKey() == albums[i].ToKey()); Assert(t.ToKey() == albums[i].ToKey());
} }
@ -280,11 +282,11 @@ namespace Test
{ {
SetCurrentTest("TestM3uEditor"); SetCurrentTest("TestM3uEditor");
Config.m3uOption = M3uOption.All; Config.I.m3uOption = M3uOption.All;
Config.skipMode = SkipMode.M3u; Config.I.skipMode = SkipMode.M3u;
Config.musicDir = ""; Config.I.musicDir = "";
Config.printOption = PrintOption.Tracks | PrintOption.Full; Config.I.printOption = PrintOption.Tracks | PrintOption.Full;
Config.skipExisting = true; Config.I.skipExisting = true;
string path = Path.Join(Directory.GetCurrentDirectory(), "test_m3u.m3u8"); string path = Path.Join(Directory.GetCurrentDirectory(), "test_m3u.m3u8");
@ -325,7 +327,7 @@ namespace Test
foreach (var t in toBeDownloadedInitial) foreach (var t in toBeDownloadedInitial)
trackLists.AddTrackToLast(t); trackLists.AddTrackToLast(t);
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption); Program.m3uEditor = new M3uEditor(path, trackLists, Config.I.m3uOption);
Program.outputDirSkipper = new M3uSkipper(Program.m3uEditor, false); Program.outputDirSkipper = new M3uSkipper(Program.m3uEditor, false);
@ -337,15 +339,15 @@ namespace Test
Assert(existing.SequenceEqualUpToPermutation(existingInitial)); Assert(existing.SequenceEqualUpToPermutation(existingInitial));
Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial)); Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
ProgramInvoke("PrintTracksTbd", new object[] { toBeDownloaded, existing, notFound, TrackType.Normal }); Printing.PrintTracksTbd(toBeDownloaded, existing, notFound, TrackType.Normal);
Program.m3uEditor.Update(); Program.m3uEditor.Update();
string output = File.ReadAllText(path); string output = File.ReadAllText(path);
string need = 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;" + "#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" +
"\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" + "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
"\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" + "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
"\npath/to/file1" + "\npath/to/file1" +
"\nfile1.5" + "\nfile1.5" +
"\npath/to/file2" + "\npath/to/file2" +
@ -364,20 +366,20 @@ namespace Test
"#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;" + "#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;" + ",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
"\n" + "\n" +
"\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" + "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
"\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" + "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
"\npath/to/file1" + "\npath/to/file1" +
"\n/other/new/file/path" + "\n/other/new/file/path" +
"\npath/to/file2" + "\npath/to/file2" +
"\nnew/file/path" + "\nnew/file/path" +
"\n# Failed: ArtistB - TitleB [NoSuitableFileFound]" + "\n#FAIL: ArtistB - TitleB [NoSuitableFileFound]" +
"\n"; "\n";
Assert(output == need); Assert(output == need);
Console.WriteLine(); Console.WriteLine();
Console.WriteLine(output); Console.WriteLine(output);
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption); Program.m3uEditor = new M3uEditor(path, trackLists, Config.I.m3uOption);
foreach (var t in trackLists.Flattened(false, false)) foreach (var t in trackLists.Flattened(false, false))
{ {
@ -406,8 +408,8 @@ namespace Test
trackLists.AddEntry(new TrackListEntry(t)); trackLists.AddEntry(new TrackListEntry(t));
File.WriteAllText(path, ""); File.WriteAllText(path, "");
Config.m3uOption = M3uOption.Index; Config.I.m3uOption = M3uOption.Index;
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption); Program.m3uEditor = new M3uEditor(path, trackLists, Config.I.m3uOption);
Program.m3uEditor.Update(); Program.m3uEditor.Update();
Assert(File.ReadAllText(path) == ""); Assert(File.ReadAllText(path) == "");
@ -420,7 +422,7 @@ namespace Test
Program.m3uEditor.Update(); Program.m3uEditor.Update();
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption); Program.m3uEditor = new M3uEditor(path, trackLists, Config.I.m3uOption);
foreach (var t in test) foreach (var t in test)
{ {
@ -470,12 +472,13 @@ namespace Test
} }
} }
public static void ResetProfiles() public static void ResetConfig()
{ {
var type = typeof(Config); var singletonType = typeof(Config);
var field = type.GetField("profiles", BindingFlags.NonPublic | BindingFlags.Static); var instanceField = singletonType.GetField("Instance", BindingFlags.Static | BindingFlags.NonPublic);
var value = (Dictionary<string, (List<string> args, string? cond)>)field.GetValue(null); var constructor = singletonType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
value.Clear(); var newInstance = constructor.Invoke(null);
instanceField.SetValue(null, newInstance);
} }
public static void Passed() public static void Passed()

View file

@ -89,7 +89,9 @@ public static class Utils
return path; return path;
} }
if (path.StartsWith('~')) path = path.Trim();
if (path[0] == '~' && (path.Length == 1 || path[1] == '\\' || path[1] == '/'))
{ {
string homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); string homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
path = Path.Join(homeDirectory, path.Substring(1).TrimStart('/').TrimStart('\\')); path = Path.Join(homeDirectory, path.Substring(1).TrimStart('/').TrimStart('\\'));