1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 14:32:40 +00:00
This commit is contained in:
fiso64 2024-09-02 09:28:38 +02:00
parent 923cd2cbf5
commit c0b9a859ee
20 changed files with 677 additions and 593 deletions

View file

@ -11,6 +11,7 @@ See the [examples](#examples-1).
- [Spotify](#spotify)
- [Bandcamp](#bandcamp)
- [Search string](#search-string)
- [List](#list)
- [Download modes](#download-modes)
- [Normal](#normal)
- [Album](#album)
@ -51,7 +52,7 @@ Usage: sldl <input> [OPTIONS]
--profile <names> Configuration profile(s) to use. See --help "config".
--concurrent-downloads <num> Max concurrent downloads (default: 2)
--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
files, required for skip-not-found or skip-existing=m3u
'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.
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
improve search results. Every row that does not have a title column text will be treated as an
album download.
improve search result ranking. Every row that does not have a title column text will be treated
as an album download.
### YouTube
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
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
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.
@ -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
properties like 'title=Song Name, artist=Artist Name, length=215'.
The following properties are allowed:
The following properties are accepted:
```
title
artist
@ -452,6 +449,7 @@ disc Disc number
filename Soulseek filename without extension
foldername Soulseek folder name
extractor Name of the extractor used (CSV/Spotify/YouTube/etc)
default-folder Default sldl folder name (usually the playlist name)
```
## Skip existing
@ -547,20 +545,20 @@ sldl spotify-likes
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>
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>
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>
@ -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:
```
sldl "artist=MC MENTAL" --aggregate --album
sldl "artist=MC MENTAL" --aggregate --album --interactive
```
<hr style="height:0px; visibility:hidden;" />
#### 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
echo title=My Favorite Song, artist=Artist >> wishlist.txt
echo https://spotify/album/url >> wishlist.txt
echo "title=My Favorite Song, artist=Artist" >> 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.
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.
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.
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.
Finally, set up a cron job (or a scheduled task on windows) to periodically run sldl with the following option:
```
sldl --profile wishlist
```
## 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.

View file

@ -5,11 +5,11 @@ using System.Text;
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" },
LengthTolerance = 3,
@ -21,180 +21,174 @@ static class Config
AcceptNoLength = false,
};
public static string parentDir = Directory.GetCurrentDirectory();
public static string input = "";
public static string m3uFilePath = "";
public static string musicDir = "";
public static string spotifyId = "";
public static string spotifySecret = "";
public static string spotifyToken = "";
public static string spotifyRefresh = "";
public static string ytKey = "";
public static string username = "";
public static string password = "";
public static string artistCol = "";
public static string albumCol = "";
public static string trackCol = "";
public static string ytIdCol = "";
public static string descCol = "";
public static string trackCountCol = "";
public static string lengthCol = "";
public static string timeUnit = "s";
public static string nameFormat = "";
public static string invalidReplaceStr = " ";
public static string ytdlpArgument = "";
public static string onComplete = "";
public static string confPath = "";
public static string profile = "";
public static string failedAlbumPath = "";
public static bool aggregate = false;
public static bool album = false;
public static bool albumArtOnly = false;
public static bool interactiveMode = false;
public static bool setAlbumMinTrackCount = true;
public static bool setAlbumMaxTrackCount = false;
public static bool skipNotFound = false;
public static bool desperateSearch = false;
public static bool noRemoveSpecialChars = false;
public static bool artistMaybeWrong = false;
public static bool fastSearch = false;
public static bool ytParse = false;
public static bool removeFt = false;
public static bool removeBrackets = false;
public static bool reverse = false;
public static bool useYtdlp = false;
public static bool skipExisting = false;
public static bool removeTracksFromSource = false;
public static bool getDeleted = false;
public static bool deletedOnly = false;
public static bool removeSingleCharacterSearchTerms = false;
public static bool relax = false;
public static bool debugInfo = false;
public static bool noModifyShareCount = false;
public static bool useRandomLogin = false;
public static bool noBrowseFolder = false;
public static bool skipExistingPrefCond = false;
public static int downrankOn = -1;
public static int ignoreOn = -2;
public static int minAlbumTrackCount = -1;
public static int maxAlbumTrackCount = -1;
public static int fastSearchDelay = 300;
public static int minSharesAggregate = 2;
public static int maxTracks = int.MaxValue;
public static int offset = 0;
public static int maxStaleTime = 50000;
public static int updateDelay = 100;
public static int searchTimeout = 6000;
public static int concurrentProcesses = 2;
public static int unknownErrorRetries = 2;
public static int maxRetriesPerTrack = 30;
public static int listenPort = 49998;
public static int searchesPerTime = 34;
public static int searchRenewTime = 220;
public static int aggregateLengthTol = 3;
public static double fastSearchMinUpSpeed = 1.0;
public static Track regexToReplace = new();
public static Track regexReplaceBy = new();
public static AlbumArtOption albumArtOption = AlbumArtOption.Default;
public static M3uOption m3uOption = M3uOption.Index;
public static DisplayMode displayMode = DisplayMode.Single;
public static InputType inputType = InputType.None;
public static SkipMode skipMode = SkipMode.M3u;
public static SkipMode skipModeMusicDir = SkipMode.Name;
public static PrintOption printOption = PrintOption.None;
public string parentDir = Directory.GetCurrentDirectory();
public string input = "";
public string m3uFilePath = "";
public string musicDir = "";
public string spotifyId = "";
public string spotifySecret = "";
public string spotifyToken = "";
public string spotifyRefresh = "";
public string ytKey = "";
public string username = "";
public string password = "";
public string artistCol = "";
public string albumCol = "";
public string trackCol = "";
public string ytIdCol = "";
public string descCol = "";
public string trackCountCol = "";
public string lengthCol = "";
public string timeUnit = "s";
public string nameFormat = "";
public string invalidReplaceStr = " ";
public string ytdlpArgument = "";
public string onComplete = "";
public string confPath = "";
public string profile = "";
public string failedAlbumPath = "";
public bool aggregate = false;
public bool album = false;
public bool albumArtOnly = false;
public bool interactiveMode = false;
public bool setAlbumMinTrackCount = true;
public bool setAlbumMaxTrackCount = false;
public bool skipNotFound = false;
public bool desperateSearch = false;
public bool noRemoveSpecialChars = false;
public bool artistMaybeWrong = false;
public bool fastSearch = false;
public bool ytParse = false;
public bool removeFt = false;
public bool removeBrackets = false;
public bool reverse = false;
public bool useYtdlp = false;
public bool skipExisting = false;
public bool removeTracksFromSource = false;
public bool getDeleted = false;
public bool deletedOnly = false;
public bool removeSingleCharacterSearchTerms = false;
public bool relax = false;
public bool debugInfo = false;
public bool noModifyShareCount = false;
public bool useRandomLogin = false;
public bool noBrowseFolder = false;
public bool skipExistingPrefCond = false;
public int downrankOn = -1;
public int ignoreOn = -2;
public int minAlbumTrackCount = -1;
public int maxAlbumTrackCount = -1;
public int fastSearchDelay = 300;
public int minSharesAggregate = 2;
public int maxTracks = int.MaxValue;
public int offset = 0;
public int maxStaleTime = 50000;
public int updateDelay = 100;
public int searchTimeout = 6000;
public int concurrentProcesses = 2;
public int unknownErrorRetries = 2;
public int maxRetriesPerTrack = 30;
public int listenPort = 49998;
public int searchesPerTime = 34;
public int searchRenewTime = 220;
public int aggregateLengthTol = 3;
public double fastSearchMinUpSpeed = 1.0;
public Track regexToReplace = new();
public Track regexReplaceBy = new();
public AlbumArtOption albumArtOption = AlbumArtOption.Default;
public M3uOption m3uOption = M3uOption.Index;
public DisplayMode displayMode = DisplayMode.Single;
public InputType inputType = InputType.None;
public SkipMode skipMode = SkipMode.M3u;
public SkipMode skipModeMusicDir = SkipMode.Name;
public PrintOption printOption = PrintOption.None;
public static bool HasAutoProfiles { get; private set; } = false;
public static bool DoNotDownload => (printOption & (PrintOption.Results | PrintOption.Tracks)) != 0;
public static bool PrintTracks => (printOption & PrintOption.Tracks) != 0;
public static bool PrintResults => (printOption & PrintOption.Results) != 0;
public static bool PrintTracksFull => (printOption & PrintOption.Tracks) != 0 && (printOption & PrintOption.Full) != 0;
public static bool PrintResultsFull => (printOption & PrintOption.Results) != 0 && (printOption & PrintOption.Full) != 0;
public static bool DeleteAlbumOnFail => failedAlbumPath == "delete";
public static bool IgnoreAlbumFail => failedAlbumPath == "disable";
public bool HasAutoProfiles { get; private set; } = false;
public bool DoNotDownload => (printOption & (PrintOption.Results | PrintOption.Tracks)) != 0;
public bool PrintTracks => (printOption & PrintOption.Tracks) != 0;
public bool PrintResults => (printOption & PrintOption.Results) != 0;
public bool PrintTracksFull => (printOption & PrintOption.Tracks) != 0 && (printOption & PrintOption.Full) != 0;
public bool PrintResultsFull => (printOption & PrintOption.Results) != 0 && (printOption & PrintOption.Full) != 0;
public bool DeleteAlbumOnFail => failedAlbumPath == "delete";
public bool IgnoreAlbumFail => failedAlbumPath == "disable";
static readonly Dictionary<string, (List<string> args, string? cond)> profiles = new();
static readonly HashSet<string> appliedProfiles = new();
static bool hasConfiguredM3uMode = false;
static bool confPathChanged = false;
static string[] arguments;
static FileConditions? prevConds = null;
static FileConditions? prevPrefConds = null;
readonly Dictionary<string, (List<string> args, string? cond)> configProfiles = new();
readonly HashSet<string> appliedProfiles = new();
bool hasConfiguredM3uMode = false;
bool confPathChanged = false;
string[] arguments;
FileConditionsMod? undoTempConds = null;
FileConditionsMod? undoTempPrefConds = 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)
{
args = args.SelectMany(arg =>
configProfiles = cfg;
arguments = args;
}
public void Load(string[] args)
{
arguments = args.SelectMany(arg =>
{
if (arg.Length > 3 && arg.StartsWith("--") && arg.Contains('='))
if (arg.Length > 2 && arg[0] == '-')
{
var parts = arg.Split('=', 2);
return new[] { parts[0], parts[1] };
if (arg[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 };
}).ToArray();
SetConfigPath(args);
SetConfigPath(arguments);
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);
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(' '))
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];
profile = arguments[profileIndex + 1];
if (profile == "help")
{
ListProfiles();
return false;
Environment.Exit(0);
}
}
if (profiles.ContainsKey("default"))
{
ProcessArgs(profiles["default"].args);
appliedProfiles.Add("default");
}
if (HasAutoProfiles)
{
ProcessArgs(args);
ApplyAutoProfiles();
}
ApplyProfiles(profile);
ProcessArgs(args);
return true;
ProcessArgs(arguments);
}
static void SetConfigPath(string[] args)
void SetConfigPath(string[] args)
{
int idx = Array.LastIndexOf(args, "-c");
int idx2 = Array.LastIndexOf(args, "--config");
idx = idx > idx2 ? idx : idx2;
int idx = Array.FindLastIndex(args, x => x == "-c" || x == "--config");
if (idx != -1)
{
confPath = Utils.ExpandUser(args[idx + 1]);
}
if (confPath.Length > 0)
{
confPathChanged = true;
if(File.Exists(Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath)))
confPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath);
}
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.ApplicationData), "sldl", "sldl.conf"),
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sldl.conf"),
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "slsk-batchdl.conf"),
};
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)
concurrentProcesses = 1;
@ -230,6 +223,8 @@ static class Config
m3uOption = M3uOption.None;
else if (!hasConfiguredM3uMode && inputType == InputType.String)
m3uOption = M3uOption.None;
else if (!hasConfiguredM3uMode && !Program.trackLists.Flattened(true, true).Skip(1).Any())
m3uOption = M3uOption.None;
if (albumArtOnly && albumArtOption == AlbumArtOption.Default)
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 curProfile = "default";
@ -274,13 +269,13 @@ static class Config
if (val[0] == '"' && val[^1] == '"')
val = val[1..^1];
if (!profiles.ContainsKey(curProfile))
profiles[curProfile] = (new List<string>(), null);
if (!configProfiles.ContainsKey(curProfile))
configProfiles[curProfile] = (new List<string>(), null);
if (key == "profile-cond" && curProfile != "default")
{
var a = profiles[curProfile].args;
profiles[curProfile] = (a, val);
var a = configProfiles[curProfile].args;
configProfiles[curProfile] = (a, val);
HasAutoProfiles = true;
}
else
@ -290,42 +285,82 @@ static class Config
else
key = "--" + key;
profiles[curProfile].args.Add(key);
profiles[curProfile].args.Add(val);
configProfiles[curProfile].args.Add(key);
configProfiles[curProfile].args.Add(val);
}
}
}
public static void UpdateProfiles(TrackListEntry tle)
public static bool UpdateProfiles(TrackListEntry tle)
{
if (DoNotDownload)
return;
if (!HasAutoProfiles)
return;
if (I.DoNotDownload)
return false;
if (!I.HasAutoProfiles)
return false;
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();
appliedProfiles.UnionWith(newProfiles);
ApplyProfiles(profile);
ProcessArgs(arguments);
PostProcessArgs();
if (key == "default" || val.cond == null)
continue;
bool condSatisfied = I.ProfileConditionSatisfied(val.cond, tle);
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 false;
// 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);
I.PostProcessArgs();
return true;
}
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(','))
{
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);
}
else
@ -335,31 +370,7 @@ static class Config
}
static HashSet<string> ApplyAutoProfiles(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)
object GetVarValue(string var, TrackListEntry? tle = null)
{
static string toKebab(string input)
{
@ -380,7 +391,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)));
@ -456,10 +467,10 @@ static class Config
}
static void ListProfiles()
void ListProfiles()
{
Console.WriteLine("Available profiles:");
foreach ((var key, var val) in profiles)
foreach ((var key, var val) in configProfiles)
{
if (key == "default")
continue;
@ -475,30 +486,25 @@ static class Config
}
public static void AddTemporaryConditions(FileConditionsPatch? cond, FileConditionsPatch? prefCond)
public void AddTemporaryConditions(FileConditionsMod? cond, FileConditionsMod? prefCond)
{
if (cond != null)
{
prevConds = necessaryCond;
necessaryCond = necessaryCond.With(cond);
}
undoTempConds = necessaryCond.ApplyMod(cond);
if (prefCond != null)
{
prevPrefConds = preferredCond;
preferredCond = preferredCond.With(prefCond);
}
undoTempPrefConds = preferredCond.ApplyMod(prefCond);
}
public static void RestoreConditions()
public void RestoreConditions()
{
if (prevConds != null)
necessaryCond = prevConds;
if (prevPrefConds != null)
preferredCond = prevPrefConds;
if (undoTempConds != null)
necessaryCond.ApplyMod(undoTempConds);
if (undoTempPrefConds != null)
preferredCond.ApplyMod(undoTempPrefConds);
}
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)
{
@ -514,7 +520,7 @@ static class Config
min = max = int.Parse(value);
}
var cond = new FileConditionsPatch();
var cond = new FileConditionsMod();
var tr = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries;
string[] conditions = input.Split(';', tr);
@ -583,7 +589,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)
{
@ -1068,15 +1074,14 @@ static class Config
case "--accept-no-length":
setFlag(ref necessaryCond.AcceptNoLength, ref i);
break;
case "--c":
case "--cond":
case "--conditions":
necessaryCond.With(ParseConditions(args[++i]));
necessaryCond.ApplyMod(ParseConditions(args[++i]));
break;
case "--pc":
case "--pref":
case "--preferred-conditions":
preferredCond.With(ParseConditions(args[++i]));
preferredCond.ApplyMod(ParseConditions(args[++i]));
break;
case "--nmsc":
case "--no-modify-share-count":

View file

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

View file

@ -1,6 +1,7 @@
using Soulseek;
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using System.Diagnostics;
using Data;
using Enums;
@ -17,9 +18,9 @@ using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string,
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();
await Program.WaitForLogin();
@ -42,8 +43,12 @@ static class Download
try
{
using var downloadCts = ct != null ?
CancellationTokenSource.CreateLinkedTokenSource((CancellationToken)ct) :
new CancellationTokenSource();
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);
int maxRetries = 3;
@ -55,7 +60,7 @@ static class Download
await client.DownloadAsync(response.Username, file.Filename,
() => Task.FromResult((Stream)outputStream),
file.Size, startOffset: outputStream.Position,
options: transferOptions, cancellationToken: cts.Token);
options: transferOptions, cancellationToken: downloadCts.Token);
break;
}
@ -156,7 +161,12 @@ public class DownloadWrapper
else if (transfer != null)
{
if (queued)
state = "Queued";
{
if ((transfer.State & TransferStates.Remotely) != 0)
state = "Queued (R)";
else
state = "Queued (L)";
}
else if ((transfer.State & TransferStates.Initializing) != 0)
state = "Initialize";
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 };
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']");
int n = trackTable.SelectNodes(".//tr").Count;
if (Config.setAlbumMinTrackCount)
if (Config.I.setAlbumMinTrackCount)
track.MinAlbumTrackCount = n;
if (Config.setAlbumMaxTrackCount)
if (Config.I.setAlbumMaxTrackCount)
track.MaxAlbumTrackCount = n;
}
}

View file

@ -19,12 +19,12 @@ namespace Extractors
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse)
{
if (!File.Exists(input))
throw new FileNotFoundException("CSV file not found");
throw new FileNotFoundException($"CSV file '{input}' not found");
csvFilePath = input;
var tracks = await ParseCsvIntoTrackInfo(input, Config.artistCol, Config.trackCol, Config.lengthCol,
Config.albumCol, Config.descCol, Config.ytIdCol, Config.trackCountCol, Config.timeUnit, Config.ytParse);
var tracks = await ParseCsvIntoTrackInfo(input, Config.I.artistCol, Config.I.trackCol, Config.I.lengthCol,
Config.I.albumCol, Config.I.descCol, Config.I.ytIdCol, Config.I.trackCountCol, Config.I.timeUnit, Config.I.ytParse);
if (reverse)
tracks.Reverse();

View file

@ -22,7 +22,7 @@ namespace Extractors
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse)
{
if (!File.Exists(input))
throw new FileNotFoundException("List file not found");
throw new FileNotFoundException($"List file '{input}' not found");
listFilePath = input;
@ -49,12 +49,22 @@ namespace Extractors
if (added >= maxTracks)
break;
bool savedVal = Config.I.album;
if (line.StartsWith("a:"))
{
line = line[2..];
Config.I.album = true;
}
var fields = ParseLine(line);
var (_, ex) = ExtractorRegistry.GetMatchingExtractor(fields[0]);
var tl = await ex.GetTracks(fields[0], int.MaxValue, 0, false);
Config.I.album = savedVal;
foreach (var tle in tl.lists)
{
if (fields.Count >= 2)

View file

@ -25,22 +25,24 @@ namespace Extractors
int max = reverse ? int.MaxValue : maxTracks;
int off = reverse ? 0 : offset;
bool needLogin = input == "spotify-likes" || Config.removeTracksFromSource;
var tle = new TrackListEntry(TrackType.Normal);
bool needLogin = input == "spotify-likes" || Config.I.removeTracksFromSource;
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.");
Environment.Exit(1);
}
spotifyClient = new Spotify(Config.spotifyId, Config.spotifySecret, Config.spotifyToken, Config.spotifyRefresh);
await spotifyClient.Authorize(needLogin, Config.removeTracksFromSource);
spotifyClient = new Spotify(Config.I.spotifyId, Config.I.spotifySecret, Config.I.spotifyToken, Config.I.spotifyRefresh);
await spotifyClient.Authorize(needLogin, Config.I.removeTracksFromSource);
TrackListEntry? tle = null;
if (input == "spotify-likes")
{
Console.WriteLine("Loading Spotify likes..");
var tracks = await spotifyClient.GetLikes(max, off);
tle = new TrackListEntry(TrackType.Normal);
tle.defaultFolderName = "Spotify Likes";
tle.list.Add(tracks);
}
@ -48,12 +50,13 @@ namespace Extractors
{
Console.WriteLine("Loading Spotify album..");
(var source, var tracks) = await spotifyClient.GetAlbum(input);
tle = new TrackListEntry(TrackType.Album);
tle.source = source;
if (Config.setAlbumMinTrackCount)
if (Config.I.setAlbumMinTrackCount)
source.MinAlbumTrackCount = tracks.Count;
if (Config.setAlbumMaxTrackCount)
if (Config.I.setAlbumMaxTrackCount)
source.MaxAlbumTrackCount = tracks.Count;
}
else if (input.Contains("/artist/"))
@ -65,6 +68,7 @@ namespace Extractors
else
{
var tracks = new List<Track>();
tle = new TrackListEntry(TrackType.Normal);
try
{
@ -76,7 +80,7 @@ namespace Extractors
{
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);
tle.defaultFolderName = playlistName;
}
@ -219,7 +223,7 @@ namespace Extractors
}
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)

View file

@ -14,10 +14,10 @@ namespace Extractors
public async Task<TrackLists> GetTracks(string input, int maxTracks, int offset, bool reverse)
{
var trackLists = new TrackLists();
var music = ParseTrackArg(input, Config.album);
var music = ParseTrackArg(input, Config.I.album);
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;
tle = new TrackListEntry(music);

View file

@ -26,19 +26,19 @@ namespace Extractors
var trackLists = new TrackLists();
int max = reverse ? int.MaxValue : maxTracks;
int off = reverse ? 0 : offset;
YouTube.apiKey = Config.ytKey;
YouTube.apiKey = Config.I.ytKey;
string name;
List<Track>? deleted = null;
List<Track> tracks = new();
if (Config.getDeleted)
if (Config.I.getDeleted)
{
Console.WriteLine("Getting deleted videos..");
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)
{

View file

@ -1,5 +1,4 @@
using System.Text.RegularExpressions;

using Data;
using SearchResponse = Soulseek.SearchResponse;
@ -41,58 +40,90 @@ public class FileConditions
BannedUsers = other.BannedUsers.ToArray();
}
public FileConditions With(FileConditionsPatch patch)
public FileConditionsMod ApplyMod(FileConditionsMod mod)
{
var cond = new FileConditions(this);
var undoMod = new FileConditionsMod();
if (patch.LengthTolerance != null)
cond.LengthTolerance = patch.LengthTolerance.Value;
if (mod.LengthTolerance != null)
{
undoMod.LengthTolerance = LengthTolerance;
LengthTolerance = mod.LengthTolerance.Value;
}
if (mod.MinBitrate != null)
{
undoMod.MinBitrate = MinBitrate;
MinBitrate = mod.MinBitrate.Value;
}
if (mod.MaxBitrate != null)
{
undoMod.MaxBitrate = MaxBitrate;
MaxBitrate = mod.MaxBitrate.Value;
}
if (mod.MinSampleRate != null)
{
undoMod.MinSampleRate = MinSampleRate;
MinSampleRate = mod.MinSampleRate.Value;
}
if (mod.MaxSampleRate != null)
{
undoMod.MaxSampleRate = MaxSampleRate;
MaxSampleRate = mod.MaxSampleRate.Value;
}
if (mod.MinBitDepth != null)
{
undoMod.MinBitDepth = MinBitDepth;
MinBitDepth = mod.MinBitDepth.Value;
}
if (mod.MaxBitDepth != null)
{
undoMod.MaxBitDepth = MaxBitDepth;
MaxBitDepth = mod.MaxBitDepth.Value;
}
if (mod.StrictTitle != null)
{
undoMod.StrictTitle = StrictTitle;
StrictTitle = mod.StrictTitle.Value;
}
if (mod.StrictArtist != null)
{
undoMod.StrictArtist = StrictArtist;
StrictArtist = mod.StrictArtist.Value;
}
if (mod.StrictAlbum != null)
{
undoMod.StrictAlbum = StrictAlbum;
StrictAlbum = mod.StrictAlbum.Value;
}
if (mod.Formats != null)
{
undoMod.Formats = Formats;
Formats = mod.Formats;
}
if (mod.BannedUsers != null)
{
undoMod.BannedUsers = BannedUsers;
BannedUsers = mod.BannedUsers;
}
if (mod.StrictStringDiacrRemove != null)
{
undoMod.StrictStringDiacrRemove = StrictStringDiacrRemove;
StrictStringDiacrRemove = mod.StrictStringDiacrRemove.Value;
}
if (mod.AcceptNoLength != null)
{
undoMod.AcceptNoLength = AcceptNoLength;
AcceptNoLength = mod.AcceptNoLength.Value;
}
if (mod.AcceptMissingProps != null)
{
undoMod.AcceptMissingProps = AcceptMissingProps;
AcceptMissingProps = mod.AcceptMissingProps.Value;
}
if (patch.MinBitrate != null)
cond.MinBitrate = patch.MinBitrate.Value;
if (patch.MaxBitrate != null)
cond.MaxBitrate = patch.MaxBitrate.Value;
if (patch.MinSampleRate != null)
cond.MinSampleRate = patch.MinSampleRate.Value;
if (patch.MaxSampleRate != null)
cond.MaxSampleRate = patch.MaxSampleRate.Value;
if (patch.MinBitDepth != null)
cond.MinBitDepth = patch.MinBitDepth.Value;
if (patch.MaxBitDepth != null)
cond.MaxBitDepth = patch.MaxBitDepth.Value;
if (patch.StrictTitle != null)
cond.StrictTitle = patch.StrictTitle.Value;
if (patch.StrictArtist != null)
cond.StrictArtist = patch.StrictArtist.Value;
if (patch.StrictAlbum != null)
cond.StrictAlbum = patch.StrictAlbum.Value;
if (patch.Formats != null)
cond.Formats = patch.Formats;
if (patch.BannedUsers != null)
cond.BannedUsers = patch.BannedUsers;
if (patch.StrictStringDiacrRemove != null)
cond.StrictStringDiacrRemove = patch.StrictStringDiacrRemove.Value;
if (patch.AcceptNoLength != null)
cond.AcceptNoLength = patch.AcceptNoLength.Value;
if (patch.AcceptMissingProps != null)
cond.AcceptMissingProps = patch.AcceptMissingProps.Value;
return cond;
return undoMod;
}
public override bool Equals(object obj)
{
if (obj is FileConditions other)
@ -298,7 +329,7 @@ public class FileConditions
}
public class FileConditionsPatch
public class FileConditionsMod
{
public int? LengthTolerance = null;
public int? MinBitrate = null;

View file

@ -7,14 +7,13 @@ using System.Text.RegularExpressions;
using Data;
using Enums;
using System.ComponentModel;
public class FileManager
{
readonly TrackListEntry tle;
readonly HashSet<Track> organized = new();
string? remoteCommonDir;
public string? remoteCommonDir { get; private set; }
public FileManager(TrackListEntry tle)
{
@ -28,19 +27,16 @@ public class FileManager
public string GetSavePathNoExt(string sourceFname)
{
string parent = Config.parentDir;
string parent = Config.I.parentDir;
string name = Utils.GetFileNameWithoutExtSlsk(sourceFname);
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 relpath = Path.GetRelativePath(remoteCommonDir, Utils.NormalizedPath(sourceFname));
parent = Path.Join(parent, dirname, Path.GetDirectoryName(relpath));
@ -64,7 +60,7 @@ public class FileManager
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);
@ -88,14 +84,14 @@ public class FileManager
if (track.DownloadPath.Length == 0 || !Utils.IsMusicFile(track.DownloadPath))
return;
if (Config.nameFormat.Length == 0)
if (Config.I.nameFormat.Length == 0)
{
organized.Add(track);
return;
}
string pathPart = SubstituteValues(Config.nameFormat, track, file);
string newFilePath = Path.Join(Config.parentDir, pathPart + Path.GetExtension(track.DownloadPath));
string pathPart = SubstituteValues(Config.I.nameFormat, track, file);
string newFilePath = Path.Join(Config.I.parentDir, pathPart + Path.GetExtension(track.DownloadPath));
try
{
@ -140,7 +136,7 @@ public class FileManager
{
Directory.CreateDirectory(Path.GetDirectoryName(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 =>
{
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
{
TryGetVarValue(match.Value, file, slfile, track, out string res);
@ -205,7 +201,7 @@ public class FileManager
char dirsep = Path.DirectorySeparatorChar;
newName = newName.Replace('/', dirsep).Replace('\\', dirsep);
var x = newName.Split(dirsep, StringSplitOptions.RemoveEmptyEntries);
newName = string.Join(dirsep, x.Select(x => x.ReplaceInvalidChars(Config.invalidReplaceStr).Trim(' ', '.')));
newName = string.Join(dirsep, x.Select(x => x.ReplaceInvalidChars(Config.I.invalidReplaceStr).Trim(' ', '.')));
return newName;
}
@ -257,12 +253,14 @@ public class FileManager
return true;
}
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:
res = x; return false;
}
res = res.ReplaceInvalidChars(Config.invalidReplaceStr);
res = res.ReplaceInvalidChars(Config.I.invalidReplaceStr);
return true;
}
}

View file

@ -7,7 +7,7 @@ namespace FileSkippers
{
public static class FileSkipperRegistry
{
public static FileSkipper GetChecker(SkipMode mode, string dir, FileConditions conditions, M3uEditor m3uEditor)
public static FileSkipper GetSkipper(SkipMode mode, string dir, FileConditions conditions, M3uEditor m3uEditor)
{
bool noConditions = conditions.Equals(new FileConditions());
return mode switch

View file

@ -29,7 +29,7 @@ public static class Help
--profile <names> Configuration profile(s) to use. See --help ""config"".
--concurrent-downloads <num> Max concurrent downloads (default: 2)
--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
files, required for skip-not-found or skip-existing=m3u
'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.
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
improve search results. Every row that does not have a title column text will be treated as an
album download.
improve search result ranking. Every row that does not have a title column text will be treated
as an album download.
YouTube
A playlist url: Download songs from a youtube playlist.
@ -214,10 +214,6 @@ public static class Help
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.
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
A playlist/album url or 'spotify-likes': Download a spotify playlist, album, or your
liked songs. --spotify-id and --spotify-secret are required in addition when downloading
@ -255,7 +251,7 @@ public static class Help
(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'.
The following properties are allowed:
The following properties are accepted:
title
artist
album
@ -423,6 +419,7 @@ public static class Help
filename Soulseek filename without extension
foldername Soulseek folder name
extractor Name of the extractor used (CSV/Spotify/YouTube/etc)
default-folder Default sldl folder name (usually the playlist name)
";
const string skipExistingHelp = @"

View file

@ -6,11 +6,11 @@ using System.Text;
public class M3uEditor
{
public string path { get; private set; }
public M3uOption option = M3uOption.Index;
string parent;
List<string> lines;
bool needFirstUpdate = false;
readonly TrackLists trackLists;
readonly M3uOption option = M3uOption.Index;
readonly Dictionary<string, Track> previousRunData = new(); // { track.ToKey(), track }
public M3uEditor(TrackLists trackLists, M3uOption option)
@ -27,11 +27,12 @@ public class M3uEditor
public void SetPathAndLoad(string path)
{
if (this.path == path)
if (Utils.NormalizedPath(this.path) == Utils.NormalizedPath(path))
return;
this.path = Path.GetFullPath(path);
parent = Utils.NormalizedPath(Path.GetDirectoryName(this.path));
lines = ReadAllLines().ToList();
LoadPreviousResults();
}
@ -81,7 +82,7 @@ public class M3uEditor
if (field == 0)
{
if (x.StartsWith("./"))
x = System.IO.Path.Join(parent, x[2..]);
x = Path.Join(parent, x[2..]);
track.DownloadPath = x;
}
else if (field == 1)

View file

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

View file

@ -15,17 +15,13 @@ using static Printing;
using Directory = System.IO.Directory;
using File = System.IO.File;
using ProgressBar = Konsole.ProgressBar;
using SearchResponse = Soulseek.SearchResponse;
using SlFile = Soulseek.File;
using SlResponse = Soulseek.SearchResponse;
static partial class Program
{
public static bool skipUpdate = false;
public static bool initialized = false;
public static Extractors.IExtractor? extractor;
public static IExtractor? extractor;
public static FileSkipper? outputDirSkipper;
public static FileSkipper? musicDirSkipper;
public static SoulseekClient? client;
@ -48,33 +44,28 @@ static partial class Program
return;
}
bool doContinue = Config.ParseArgsAndReadConfig(args);
Config.I.Load(args);
if (!doContinue)
return;
if (Config.input.Length == 0)
if (Config.I.input.Length == 0)
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);
trackLists.UpgradeListTypes(Config.aggregate, Config.album);
Config.I.PostProcessArgs();
trackLists.UpgradeListTypes(Config.I.aggregate, Config.I.album);
trackLists.SetListEntryOptions();
Config.PostProcessArgs();
m3uEditor = new M3uEditor(trackLists, Config.m3uOption);
InitFileSkippers();
m3uEditor = new M3uEditor(trackLists, Config.I.m3uOption);
await MainLoop();
WriteLine("Mainloop done", debugOnly: true);
}
@ -84,7 +75,7 @@ static partial class Program
if (initialized)
return;
bool needLogin = !Config.PrintTracks;
bool needLogin = !Config.I.PrintTracks;
if (needLogin)
{
var connectionOptions = new ConnectionOptions(configureSocket: (socket) =>
@ -98,17 +89,17 @@ static partial class Program
var clientOptions = new SoulseekClientOptions(
transferConnectionOptions: connectionOptions,
serverConnectionOptions: connectionOptions,
listenPort: Config.listenPort
listenPort: Config.I.listenPort
);
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");
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;
@ -124,19 +115,18 @@ static partial class Program
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))
outputDirSkipper = FileSkipperRegistry.GetChecker(Config.skipMode, Config.parentDir, cond, m3uEditor);
outputDirSkipper = FileSkipperRegistry.GetSkipper(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");
else
musicDirSkipper = FileSkipperRegistry.GetChecker(Config.skipModeMusicDir, Config.musicDir, cond, m3uEditor);
musicDirSkipper = FileSkipperRegistry.GetSkipper(Config.I.skipModeMusicDir, Config.I.musicDir, cond, m3uEditor);
}
}
}
@ -144,9 +134,10 @@ static partial class Program
static void PreprocessTracks(TrackListEntry tle)
{
PreprocessTrack(tle.source);
for (int k = 0; k < tle.list.Count; k++)
{
PreprocessTrack(tle.source);
foreach (var ls in tle.list)
{
for (int i = 0; i < ls.Count; i++)
@ -160,22 +151,22 @@ static partial class Program
static void PreprocessTrack(Track track)
{
if (Config.removeFt)
if (Config.I.removeFt)
{
track.Title = track.Title.RemoveFt();
track.Artist = track.Artist.RemoveFt();
}
if (Config.removeBrackets)
if (Config.I.removeBrackets)
{
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.Artist = Regex.Replace(track.Artist, Config.regexToReplace.Artist, Config.regexReplaceBy.Artist);
track.Album = Regex.Replace(track.Album, Config.regexToReplace.Album, Config.regexReplaceBy.Album);
track.Title = Regex.Replace(track.Title, Config.I.regexToReplace.Title, Config.I.regexReplaceBy.Title);
track.Artist = Regex.Replace(track.Artist, Config.I.regexToReplace.Artist, Config.I.regexReplaceBy.Artist);
track.Album = Regex.Replace(track.Album, Config.I.regexToReplace.Album, Config.I.regexReplaceBy.Album);
}
if (Config.artistMaybeWrong)
if (Config.I.artistMaybeWrong)
{
track.ArtistMaybeWrong = true;
}
@ -186,22 +177,28 @@ static partial class Program
}
static void PrepareListEntry(TrackListEntry tle)
static void PrepareListEntry(TrackListEntry tle, bool isFirstEntry)
{
Config.RestoreConditions();
Config.I.RestoreConditions();
Config.UpdateProfiles(tle);
bool changed = Config.UpdateProfiles(tle);
Config.AddTemporaryConditions(tle.additionalConds, tle.additionalPrefConds);
Config.I.AddTemporaryConditions(tle.additionalConds, tle.additionalPrefConds);
string m3uPath;
if (Config.m3uFilePath.Length > 0)
m3uPath = Config.m3uFilePath;
if (Config.I.m3uFilePath.Length > 0)
m3uPath = Config.I.m3uFilePath;
else
m3uPath = Path.Join(Config.parentDir, tle.defaultFolderName, "sldl.m3u");
m3uPath = Path.Join(Config.I.parentDir, tle.defaultFolderName, "sldl.m3u8");
m3uEditor.SetPathAndLoad(m3uPath);
m3uEditor.option = Config.I.m3uOption;
m3uEditor.SetPathAndLoad(m3uPath); // does nothing if the path is the same
if (changed || isFirstEntry)
{
InitFileSkippers(); // todo: only do this when a relevant config item changes
}
PreprocessTracks(tle);
}
@ -211,16 +208,16 @@ static partial class Program
{
for (int i = 0; i < trackLists.lists.Count; i++)
{
if (i > 0) Console.WriteLine();
Console.WriteLine();
var tle = trackLists[i];
PrepareListEntry(tle);
PrepareListEntry(tle, isFirstEntry: i == 0);
var existing = 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))
notFound.Add(tle.source);
@ -232,7 +229,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))
existing.Add(tle.source);
@ -244,7 +241,7 @@ static partial class Program
}
}
if (Config.PrintTracks)
if (Config.I.PrintTracks)
{
if (tle.source.Type == TrackType.Normal)
{
@ -286,12 +283,12 @@ static partial class Program
if (tle.source.Type == TrackType.Album)
{
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)
{
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)
{
@ -313,7 +310,7 @@ static partial class Program
var lockedFiles = responseData.lockedFilesCount > 0 ? $" (Found {responseData.lockedFilesCount} locked files)" : "";
Console.WriteLine($"No results.{lockedFiles}");
if (!Config.PrintResults)
if (!Config.I.PrintResults)
{
tle.source.State = TrackState.Failed;
tle.source.FailureReason = FailureReason.NoSuitableFileFound;
@ -323,7 +320,7 @@ static partial class Program
continue;
}
if (Config.skipExisting && tle.needSkipExistingAfterSearch)
if (Config.I.skipExisting && tle.needSkipExistingAfterSearch)
{
foreach (var tracks in tle.list)
existing.AddRange(DoSkipExisting(tracks));
@ -335,7 +332,7 @@ static partial class Program
}
}
if (Config.PrintResults)
if (Config.I.PrintResults)
{
await PrintResults(tle, existing, notFound);
continue;
@ -369,7 +366,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);
}
@ -455,7 +452,7 @@ static partial class Program
{
var tracks = tle.list[0];
var semaphore = new SemaphoreSlim(Config.concurrentProcesses);
var semaphore = new SemaphoreSlim(Config.I.concurrentProcesses);
var organizer = new FileManager(tle);
@ -468,7 +465,7 @@ static partial class Program
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);
}
@ -481,14 +478,14 @@ static partial class Program
bool succeeded = false;
string? soulseekDir = null;
while (tle.list.Count > 0 && !Config.albumArtOnly)
while (tle.list.Count > 0 && !Config.I.albumArtOnly)
{
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;
}
@ -498,20 +495,20 @@ static partial class Program
organizer.SetRemoteCommonDir(soulseekDir);
if (!Config.interactiveMode && !wasInteractive)
if (!Config.I.interactiveMode && !wasInteractive)
{
Console.WriteLine();
PrintAlbum(tracks);
}
var semaphore = new SemaphoreSlim(Config.concurrentProcesses);
var semaphore = new SemaphoreSlim(Config.I.concurrentProcesses);
using var cts = new CancellationTokenSource();
try
{
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...");
@ -548,10 +545,10 @@ static partial class Program
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:");
additionalImages = await DownloadImages(tle.list, Config.albumArtOption, tracks, organizer);
additionalImages = await DownloadImages(tle.list, Config.I.albumArtOption, tracks, organizer);
tracks?.AddRange(additionalImages);
}
@ -586,7 +583,7 @@ static partial class Program
tle.source.State = TrackState.Downloaded;
tle.source.DownloadPath = Utils.GreatestCommonDirectory(downloadedAudio.Select(t => t.DownloadPath));
if (Config.removeTracksFromSource)
if (Config.I.removeTracksFromSource)
{
await extractor.RemoveTrackFromSource(tle.source);
}
@ -596,7 +593,7 @@ static partial class Program
static void OnAlbumFail(List<Track>? tracks)
{
if (tracks == null || Config.IgnoreAlbumFail)
if (tracks == null || Config.I.IgnoreAlbumFail)
return;
foreach (var track in tracks)
@ -605,18 +602,18 @@ static partial class Program
{
try
{
if (Config.DeleteAlbumOnFail)
if (Config.I.DeleteAlbumOnFail)
{
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));
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)
{
@ -698,9 +695,9 @@ static partial class Program
while (albumArtLists.Count > 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);
if (index == -1) break;
@ -715,12 +712,17 @@ static partial class Program
return downloadedImages;
}
if (!Config.interactiveMode && !wasInteractive)
if (!Config.I.interactiveMode && !wasInteractive)
{
Console.WriteLine();
PrintAlbum(tracks);
}
if (fileManager.remoteCommonDir == null)
{
fileManager.SetRemoteCommonDir(Utils.GreatestCommonDirectorySlsk(tracks.Select(t => t.FirstDownload.Filename)));
}
bool allSucceeded = true;
var semaphore = new SemaphoreSlim(1);
@ -743,14 +745,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)
return;
await semaphore.WaitAsync(cts.Token);
int tries = Config.unknownErrorRetries;
int tries = Config.I.unknownErrorRetries;
string savedFilePath = "";
SlFile? chosenFile = null;
@ -809,7 +811,7 @@ static partial class Program
track.DownloadPath = savedFilePath;
}
if (removeFromSource && Config.removeTracksFromSource)
if (removeFromSource && Config.I.removeTracksFromSource)
{
try
{
@ -827,9 +829,9 @@ static partial class Program
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();
@ -890,9 +892,11 @@ static partial class Program
case "s":
return -1;
case "q":
Config.interactiveMode = false;
Config.I.interactiveMode = false;
return aidx;
case "r":
if (!retrieveFolder)
break;
var folder = Utils.GreatestCommonDirectorySlsk(tracks.Select(t => t.FirstDownload.Filename));
if (retrieveFolder && !retrievedFolders.Contains(username + '\\' + folder))
{
@ -939,7 +943,7 @@ static partial class Program
{
lock (val)
{
if ((DateTime.Now - val.UpdateLastChangeTime()).TotalMilliseconds > Config.maxStaleTime)
if ((DateTime.Now - val.UpdateLastChangeTime()).TotalMilliseconds > Config.I.maxStaleTime)
{
val.stalled = true;
val.UpdateText();
@ -966,10 +970,10 @@ static partial class Program
&& !client.State.HasFlag(SoulseekClientStates.Connecting))
{
WriteLine($"\nDisconnected, logging in\n", ConsoleColor.DarkYellow, true);
try { await Login(Config.useRandomLogin); }
try { await Login(Config.I.useRandomLogin); }
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);
}
}
@ -989,14 +993,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)
{
string user = Config.username, pass = Config.password;
string user = Config.I.username, pass = Config.I.password;
if (random)
{
var r = new Random();
@ -1013,7 +1017,7 @@ static partial class Program
{
WriteLine($"Connecting {user}", debugOnly: true);
await client.ConnectAsync(user, pass);
if (!Config.noModifyShareCount)
if (!Config.I.noModifyShareCount)
{
WriteLine($"Setting share count", debugOnly: true);
await client.SetSharedCountsAsync(20, 100);
@ -1074,7 +1078,7 @@ static partial class Program
.Replace("{failure-reason}", track.FailureReason.ToString())
.Replace("{path}", track.DownloadPath)
.Replace("{state}", track.State.ToString())
.Replace("{extractor}", Config.inputType.ToString())
.Replace("{extractor}", Config.I.inputType.ToString())
.Trim();
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 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
{
public static RateLimitedSemaphore? searchSemaphore;
// 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();
IEnumerable<(SlResponse response, SlFile file)>? orderedResults = null;
var responseData = new ResponseData();
var progress = Printing.GetProgressBar(Config.displayMode);
var progress = Printing.GetProgressBar(Config.I.displayMode);
var results = new SlDictionary();
var fsResults = new SlDictionary();
using var searchCts = new CancellationTokenSource();
@ -62,7 +81,7 @@ static class Search
saveFilePath = organizer.GetSavePath(f.Filename);
fsUser = r.Username;
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)
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();
if (r.HasFreeUploadSlot && r.UploadSpeed / 1024.0 / 1024.0 >= Config.fastSearchMinUpSpeed
&& FileConditions.BracketCheck(track, InferTrack(f.Filename, track)) && Config.preferredCond.FileSatisfies(f, track, r))
if (r.HasFreeUploadSlot && r.UploadSpeed / 1024.0 / 1024.0 >= Config.I.fastSearchMinUpSpeed
&& FileConditions.BracketCheck(track, InferTrack(f.Filename, track)) && Config.I.preferredCond.FileSatisfies(f, track, r))
{
fsResults.TryAdd(r.Username + '\\' + f.Filename, (r, f));
if (Interlocked.Exchange(ref fsResultsStarted, 1) == 0)
{
Task.Delay(Config.fastSearchDelay).ContinueWith(tt => fastSearchDownload());
Task.Delay(Config.I.fastSearchDelay).ContinueWith(tt => fastSearchDownload());
}
}
}
@ -98,8 +117,8 @@ static class Search
return new SearchOptions(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
searchTimeout: Config.searchTimeout,
removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms,
searchTimeout: Config.I.searchTimeout,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
responseFilter: (response) =>
{
return response.UploadSpeed > 0 && necCond.BannedUsersSatisfies(response);
@ -117,7 +136,7 @@ static class Search
searchEnded = true;
lock (fsDownloadLock) { }
if (downloading == 0 && results.IsEmpty && !Config.useYtdlp)
if (downloading == 0 && results.IsEmpty && !Config.I.useYtdlp)
{
notFound = true;
}
@ -151,7 +170,7 @@ static class Search
if (orderedResults == null)
orderedResults = OrderedResults(results, track, useInfer: true);
int trackTries = Config.maxRetriesPerTrack;
int trackTries = Config.I.maxRetriesPerTrack;
async Task<bool> process(SlResponse response, SlFile file)
{
saveFilePath = organizer.GetSavePath(file.Filename);
@ -159,7 +178,7 @@ static class Search
try
{
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);
return true;
}
@ -172,7 +191,7 @@ static class Search
if (!IsConnectedAndLoggedIn())
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);
if (--trackTries <= 0)
@ -194,7 +213,7 @@ static class Search
fr = orderedResults.Skip(1).FirstOrDefault();
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);
}
@ -202,7 +221,7 @@ static class Search
{
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;
success = await process(response, file);
if (success) break;
@ -212,7 +231,7 @@ static class Search
}
}
if (downloading == 0 && Config.useYtdlp)
if (downloading == 0 && Config.I.useYtdlp)
{
notFound = false;
try
@ -224,12 +243,12 @@ static class Search
{
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);
downloading = 1;
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);
break;
}
@ -271,7 +290,7 @@ static class Search
new SearchOptions(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
searchTimeout: timeout,
responseFilter: (response) =>
{
@ -354,10 +373,10 @@ static class Search
}
int min, max;
if (Config.minAlbumTrackCount > -1 || Config.maxAlbumTrackCount > -1)
if (Config.I.minAlbumTrackCount > -1 || Config.I.maxAlbumTrackCount > -1)
{
min = Config.minAlbumTrackCount;
max = Config.maxAlbumTrackCount;
min = Config.I.minAlbumTrackCount;
max = Config.I.maxAlbumTrackCount;
}
else
{
@ -410,7 +429,7 @@ static class Search
new(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
searchTimeout: timeout,
responseFilter: (response) =>
{
@ -442,7 +461,7 @@ static class Search
var equivalentFiles = EquivalentFiles(track, results.Select(x => x.Value))
.Select(x => (x.Item1, OrderedResults(x.Item2, track, false, false, false))).ToList();
if (!Config.relax)
if (!Config.I.relax)
{
equivalentFiles = equivalentFiles
.Where(x => FileConditions.StrictString(x.Item1.Title, track.Title, ignoreCase: true)
@ -465,7 +484,7 @@ static class Search
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)
{
@ -547,7 +566,7 @@ static class Search
}
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)
.Select(x => x.x)
.ToList();
@ -619,7 +638,7 @@ static class Search
IEnumerable<(SlResponse, SlFile)> fileResponses, int minShares = -1)
{
if (minShares == -1)
minShares = Config.minSharesAggregate;
minShares = Config.I.minSharesAggregate;
Track inferTrack((SearchResponse r, Soulseek.File f) x)
{
@ -629,7 +648,7 @@ static class Search
}
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()))
.Where(x => x.Item2 >= minShares)
.OrderByDescending(x => x.Item2)
@ -691,22 +710,22 @@ static class Search
var random = new Random();
return results.Select(x => (response: x.Item1, file: x.Item2))
.Where(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.ignoreOn)
.OrderByDescending(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.downrankOn)
.ThenByDescending(x => Config.necessaryCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => Config.preferredCond.BannedUsersSatisfies(x.response))
.ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || Config.preferredCond.AcceptNoLength)
.Where(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.I.ignoreOn)
.OrderByDescending(x => userSuccessCount.GetValueOrDefault(x.response.Username, 0) > Config.I.downrankOn)
.ThenByDescending(x => Config.I.necessaryCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => Config.I.preferredCond.BannedUsersSatisfies(x.response))
.ThenByDescending(x => (x.file.Length != null && x.file.Length > 0) || Config.I.preferredCond.AcceptNoLength)
.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 => !albumMode || Config.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
.ThenByDescending(x => Config.preferredCond.StrictArtistSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => Config.preferredCond.LengthToleranceSatisfies(x.file, track.Length))
.ThenByDescending(x => Config.preferredCond.FormatSatisfies(x.file.Filename))
.ThenByDescending(x => albumMode || Config.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
.ThenByDescending(x => Config.preferredCond.BitrateSatisfies(x.file))
.ThenByDescending(x => Config.preferredCond.SampleRateSatisfies(x.file))
.ThenByDescending(x => Config.preferredCond.BitDepthSatisfies(x.file))
.ThenByDescending(x => Config.preferredCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => Config.I.preferredCond.StrictTitleSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => !albumMode || Config.I.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
.ThenByDescending(x => Config.I.preferredCond.StrictArtistSatisfies(x.file.Filename, track.Title))
.ThenByDescending(x => Config.I.preferredCond.LengthToleranceSatisfies(x.file, track.Length))
.ThenByDescending(x => Config.I.preferredCond.FormatSatisfies(x.file.Filename))
.ThenByDescending(x => albumMode || Config.I.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
.ThenByDescending(x => Config.I.preferredCond.BitrateSatisfies(x.file))
.ThenByDescending(x => Config.I.preferredCond.SampleRateSatisfies(x.file))
.ThenByDescending(x => Config.I.preferredCond.BitDepthSatisfies(x.file))
.ThenByDescending(x => Config.I.preferredCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => x.response.HasFreeUploadSlot)
.ThenByDescending(x => x.response.UploadSpeed / 1024 / 650)
.ThenByDescending(x => albumMode || FileConditions.StrictString(x.file.Filename, track.Title))
@ -730,7 +749,7 @@ static class Search
string search = GetSearchString(track);
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));
if (search.RemoveDiacriticsIfExist(out string noDiacrSearch) && !track.ArtistMaybeWrong)
@ -742,15 +761,15 @@ static class Search
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());
cond.StrictTitle = infTrack.Title == track.Title;
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));
}
if (Config.desperateSearch)
if (Config.I.desperateSearch)
{
await Task.WhenAll(searchTasks);
@ -758,23 +777,23 @@ static class Search
{
if (artist && album && title)
{
var cond = new FileConditions(Config.necessaryCond)
var cond = new FileConditions(Config.I.necessaryCond)
{
StrictTitle = 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));
}
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,
StrictTitle = 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));
}
}
@ -787,36 +806,36 @@ static class Search
if (track.Album.Length > 3 && album)
{
var cond = new FileConditions(Config.necessaryCond)
var cond = new FileConditions(Config.I.necessaryCond)
{
StrictAlbum = true,
StrictTitle = !track.ArtistMaybeWrong,
StrictArtist = !track.ArtistMaybeWrong,
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));
}
if (track2.Title.Length > 3 && artist)
{
var cond = new FileConditions(Config.necessaryCond)
var cond = new FileConditions(Config.I.necessaryCond)
{
StrictTitle = !track.ArtistMaybeWrong,
StrictArtist = !track.ArtistMaybeWrong,
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));
}
if (track2.Artist.Length > 3 && title)
{
var cond = new FileConditions(Config.necessaryCond)
var cond = new FileConditions(Config.I.necessaryCond)
{
StrictTitle = !track.ArtistMaybeWrong,
StrictArtist = !track.ArtistMaybeWrong,
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));
}
}
@ -851,15 +870,15 @@ static class Search
return new SearchOptions(
minimumResponseFileCount: 1,
minimumPeerUploadSpeed: 1,
searchTimeout: Config.searchTimeout,
removeSingleCharacterSearchTerms: Config.removeSingleCharacterSearchTerms,
searchTimeout: Config.I.searchTimeout,
removeSingleCharacterSearchTerms: Config.I.removeSingleCharacterSearchTerms,
responseFilter: (response) =>
{
return response.UploadSpeed > 0 && necCond.BannedUsersSatisfies(response);
},
fileFilter: (file) =>
{
return Utils.IsMusicFile(file.Filename) && (necCond.FileSatisfies(file, track, null) || Config.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);
if (Config.DoNotDownload && results.IsEmpty)
if (Config.I.DoNotDownload && results.IsEmpty)
{
Printing.WriteLine($"No results", ConsoleColor.Yellow);
}
@ -888,8 +907,8 @@ static class Search
foreach (var (response, file) in orderedResults)
{
Console.WriteLine(Printing.DisplayString(track, file, response,
Config.PrintResultsFull ? Config.necessaryCond : null, Config.PrintResultsFull ? Config.preferredCond : null,
fullpath: Config.PrintResultsFull, infoFirst: true, showSpeed: Config.PrintResultsFull));
Config.I.PrintResultsFull ? Config.I.necessaryCond : null, Config.I.PrintResultsFull ? Config.I.preferredCond : null,
fullpath: Config.I.PrintResultsFull, infoFirst: true, showSpeed: Config.I.PrintResultsFull));
count += 1;
}
Printing.WriteLine($"Total: {count}\n", ConsoleColor.Yellow);
@ -924,7 +943,7 @@ static class Search
static string CleanSearchString(string str)
{
string old;
if (!Config.noRemoveSpecialChars)
if (!Config.I.noRemoveSpecialChars)
{
old = str;
str = str.ReplaceSpecialChars(" ").Trim().RemoveConsecutiveWs();

View file

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

View file

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