mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 06:22:41 +00:00
commit
This commit is contained in:
parent
923cd2cbf5
commit
149a27115a
19 changed files with 567 additions and 526 deletions
48
README.md
48
README.md
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
FileConditions? prevConds = null;
|
||||
FileConditions? prevPrefConds = null;
|
||||
|
||||
public static bool ParseArgsAndReadConfig(string[] args)
|
||||
private static Config Instance = new();
|
||||
|
||||
public static Config I { get { return Instance; } }
|
||||
|
||||
private Config() { }
|
||||
|
||||
private Config(Dictionary<string, (List<string> args, string? cond)> cfg, string[] args)
|
||||
{
|
||||
args = args.SelectMany(arg =>
|
||||
configProfiles = cfg;
|
||||
arguments = args;
|
||||
}
|
||||
|
||||
|
||||
public void Load(string[] args)
|
||||
{
|
||||
if (arg.Length > 3 && arg.StartsWith("--") && arg.Contains('='))
|
||||
arguments = args.SelectMany(arg =>
|
||||
{
|
||||
var parts = arg.Split('=', 2);
|
||||
return new[] { parts[0], parts[1] };
|
||||
if (arg.Length > 2 && arg[0] == '-')
|
||||
{
|
||||
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,8 +285,8 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -299,33 +294,70 @@ static class Config
|
|||
|
||||
public static void UpdateProfiles(TrackListEntry tle)
|
||||
{
|
||||
if (DoNotDownload)
|
||||
if (I.DoNotDownload)
|
||||
return;
|
||||
if (!HasAutoProfiles)
|
||||
if (!I.HasAutoProfiles)
|
||||
return;
|
||||
|
||||
var newProfiles = ApplyAutoProfiles(tle);
|
||||
bool needUpdate = false;
|
||||
var toApply = new List<(string name, List<string> args)>();
|
||||
|
||||
if (newProfiles.Count > 0)
|
||||
foreach ((var key, var val) in I.configProfiles)
|
||||
{
|
||||
//appliedProfiles.Clear();
|
||||
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;
|
||||
|
||||
// this means that auto profiles can't change --profile and --config
|
||||
var profile = I.profile;
|
||||
Instance = new Config(I.configProfiles, I.arguments);
|
||||
I.ApplyDefaultConfig();
|
||||
I.ApplyProfiles(profile);
|
||||
|
||||
foreach (var (name, args) in toApply)
|
||||
{
|
||||
Console.WriteLine($"Applying auto profile: {name}");
|
||||
I.ProcessArgs(args);
|
||||
I.appliedProfiles.Add(name);
|
||||
}
|
||||
|
||||
I.ProcessArgs(I.arguments);
|
||||
}
|
||||
|
||||
|
||||
void ApplyDefaultConfig()
|
||||
{
|
||||
if (configProfiles.ContainsKey("default"))
|
||||
{
|
||||
ProcessArgs(configProfiles["default"].args);
|
||||
appliedProfiles.Add("default");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void ApplyProfiles(string names)
|
||||
void ApplyProfiles(string names)
|
||||
{
|
||||
foreach (var name in names.Split(','))
|
||||
{
|
||||
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 +367,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 +388,7 @@ static class Config
|
|||
}
|
||||
|
||||
|
||||
public static bool ProfileConditionSatisfied(string cond, TrackListEntry? tle = null)
|
||||
public bool ProfileConditionSatisfied(string cond, TrackListEntry? tle = null)
|
||||
{
|
||||
var tokens = new Queue<string>(Regex.Split(cond, @"(\s+|\(|\)|&&|\|\||==|!=|!|\"".*?\"")").Where(t => !string.IsNullOrWhiteSpace(t)));
|
||||
|
||||
|
@ -456,10 +464,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,7 +483,7 @@ static class Config
|
|||
}
|
||||
|
||||
|
||||
public static void AddTemporaryConditions(FileConditionsPatch? cond, FileConditionsPatch? prefCond)
|
||||
public void AddTemporaryConditions(FileConditionsMod? cond, FileConditionsMod? prefCond)
|
||||
{
|
||||
if (cond != null)
|
||||
{
|
||||
|
@ -489,7 +497,8 @@ static class Config
|
|||
}
|
||||
}
|
||||
|
||||
public static void RestoreConditions()
|
||||
|
||||
public void RestoreConditions()
|
||||
{
|
||||
if (prevConds != null)
|
||||
necessaryCond = prevConds;
|
||||
|
@ -498,7 +507,7 @@ static class Config
|
|||
}
|
||||
|
||||
|
||||
public static FileConditionsPatch ParseConditions(string input)
|
||||
public static FileConditionsMod ParseConditions(string input)
|
||||
{
|
||||
static void UpdateMinMax(string value, string condition, ref int? min, ref int? max)
|
||||
{
|
||||
|
@ -514,7 +523,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 +592,7 @@ static class Config
|
|||
}
|
||||
|
||||
|
||||
static void ProcessArgs(IReadOnlyList<string> args)
|
||||
void ProcessArgs(IReadOnlyList<string> args)
|
||||
{
|
||||
void setFlag(ref bool flag, ref int i, bool trueVal = true)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@ namespace Extractors
|
|||
|
||||
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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
|
||||
using Data;
|
||||
|
||||
using SearchResponse = Soulseek.SearchResponse;
|
||||
|
@ -41,52 +40,38 @@ public class FileConditions
|
|||
BannedUsers = other.BannedUsers.ToArray();
|
||||
}
|
||||
|
||||
public FileConditions With(FileConditionsPatch patch)
|
||||
public FileConditions With(FileConditionsMod patch)
|
||||
{
|
||||
var cond = new FileConditions(this);
|
||||
|
||||
if (patch.LengthTolerance != null)
|
||||
cond.LengthTolerance = patch.LengthTolerance.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;
|
||||
|
||||
|
@ -298,7 +283,7 @@ public class FileConditions
|
|||
}
|
||||
|
||||
|
||||
public class FileConditionsPatch
|
||||
public class FileConditionsMod
|
||||
{
|
||||
public int? LengthTolerance = null;
|
||||
public int? MinBitrate = null;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = @"
|
||||
|
|
|
@ -32,6 +32,7 @@ public class M3uEditor
|
|||
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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,30 @@ 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);
|
||||
m3uEditor = new M3uEditor(trackLists, Config.I.m3uOption);
|
||||
|
||||
InitFileSkippers();
|
||||
|
||||
await MainLoop();
|
||||
|
||||
WriteLine("Mainloop done", debugOnly: true);
|
||||
}
|
||||
|
||||
|
@ -84,7 +77,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 +91,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 +117,19 @@ 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);
|
||||
if (Config.I.musicDir.Length == 0 || !Config.I.parentDir.StartsWith(Config.I.musicDir, StringComparison.OrdinalIgnoreCase))
|
||||
outputDirSkipper = FileSkipperRegistry.GetChecker(Config.I.skipMode, Config.I.parentDir, cond, m3uEditor);
|
||||
|
||||
if (Config.musicDir.Length > 0)
|
||||
if (Config.I.musicDir.Length > 0)
|
||||
{
|
||||
if (!Directory.Exists(Config.musicDir))
|
||||
if (!Directory.Exists(Config.I.musicDir))
|
||||
Console.WriteLine("Error: Music directory does not exist");
|
||||
else
|
||||
musicDirSkipper = FileSkipperRegistry.GetChecker(Config.skipModeMusicDir, Config.musicDir, cond, m3uEditor);
|
||||
musicDirSkipper = FileSkipperRegistry.GetChecker(Config.I.skipModeMusicDir, Config.I.musicDir, cond, m3uEditor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -160,22 +153,22 @@ static partial class Program
|
|||
|
||||
static void PreprocessTrack(Track track)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
@ -188,18 +181,18 @@ static partial class Program
|
|||
|
||||
static void PrepareListEntry(TrackListEntry tle)
|
||||
{
|
||||
Config.RestoreConditions();
|
||||
Config.I.RestoreConditions();
|
||||
|
||||
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);
|
||||
|
||||
|
@ -211,7 +204,7 @@ static partial class Program
|
|||
{
|
||||
for (int i = 0; i < trackLists.lists.Count; i++)
|
||||
{
|
||||
if (i > 0) Console.WriteLine();
|
||||
Console.WriteLine();
|
||||
|
||||
var tle = trackLists[i];
|
||||
|
||||
|
@ -220,7 +213,7 @@ static partial class Program
|
|||
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 +225,7 @@ static partial class Program
|
|||
}
|
||||
}
|
||||
|
||||
if (Config.skipExisting && !Config.PrintResults && tle.source.State != TrackState.NotFoundLastTime)
|
||||
if (Config.I.skipExisting && !Config.I.PrintResults && tle.source.State != TrackState.NotFoundLastTime)
|
||||
{
|
||||
if (tle.sourceCanBeSkipped && SetExisting(tle.source))
|
||||
existing.Add(tle.source);
|
||||
|
@ -244,7 +237,7 @@ static partial class Program
|
|||
}
|
||||
}
|
||||
|
||||
if (Config.PrintTracks)
|
||||
if (Config.I.PrintTracks)
|
||||
{
|
||||
if (tle.source.Type == TrackType.Normal)
|
||||
{
|
||||
|
@ -286,12 +279,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 +306,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 +316,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 +328,7 @@ static partial class Program
|
|||
}
|
||||
}
|
||||
|
||||
if (Config.PrintResults)
|
||||
if (Config.I.PrintResults)
|
||||
{
|
||||
await PrintResults(tle, existing, notFound);
|
||||
continue;
|
||||
|
@ -369,7 +362,7 @@ static partial class Program
|
|||
}
|
||||
}
|
||||
|
||||
if (!Config.DoNotDownload && (trackLists.lists.Count > 0 || trackLists.Flattened(false, false).Skip(1).Any()))
|
||||
if (!Config.I.DoNotDownload && (trackLists.lists.Count > 0 || trackLists.Flattened(false, false).Skip(1).Any()))
|
||||
{
|
||||
PrintComplete(trackLists);
|
||||
}
|
||||
|
@ -455,7 +448,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 +461,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 +474,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 +491,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 +541,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 +579,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 +589,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 +598,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 +691,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 +708,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 +741,14 @@ static partial class Program
|
|||
}
|
||||
|
||||
|
||||
static async Task DownloadTask(TrackListEntry? tle, Track track, SemaphoreSlim semaphore, FileManager organizer, CancellationTokenSource cts, bool cancelOnFail, bool removeFromSource, bool organize)
|
||||
static async Task DownloadTask(TrackListEntry? tle, Track track, SemaphoreSlim semaphore, FileManager organizer, CancellationTokenSource? cts, bool cancelOnFail, bool removeFromSource, bool organize)
|
||||
{
|
||||
if (track.State != TrackState.Initial)
|
||||
return;
|
||||
|
||||
await semaphore.WaitAsync(cts.Token);
|
||||
|
||||
int tries = Config.unknownErrorRetries;
|
||||
int tries = Config.I.unknownErrorRetries;
|
||||
string savedFilePath = "";
|
||||
SlFile? chosenFile = null;
|
||||
|
||||
|
@ -809,7 +807,7 @@ static partial class Program
|
|||
track.DownloadPath = savedFilePath;
|
||||
}
|
||||
|
||||
if (removeFromSource && Config.removeTracksFromSource)
|
||||
if (removeFromSource && Config.I.removeTracksFromSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -827,9 +825,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 +888,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 +939,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 +966,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 +989,14 @@ static partial class Program
|
|||
}
|
||||
}
|
||||
|
||||
await Task.Delay(Config.updateDelay);
|
||||
await Task.Delay(Config.I.updateDelay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static async Task Login(bool random = false, int tries = 3)
|
||||
{
|
||||
string user = Config.username, pass = Config.password;
|
||||
string user = Config.I.username, pass = Config.I.password;
|
||||
if (random)
|
||||
{
|
||||
var r = new Random();
|
||||
|
@ -1013,7 +1013,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 +1074,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] == '"')
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -89,7 +89,9 @@ public static class Utils
|
|||
return path;
|
||||
}
|
||||
|
||||
if (path.StartsWith('~'))
|
||||
path = path.Trim();
|
||||
|
||||
if (path[0] == '~' && (path.Length == 1 || path[1] == '\\' || path[1] == '/'))
|
||||
{
|
||||
string homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
path = Path.Join(homeDirectory, path.Substring(1).TrimStart('/').TrimStart('\\'));
|
||||
|
|
Loading…
Reference in a new issue