mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 14:32:40 +00:00
allow separate index & playlist
--write-playlist, --index-path, --playlist-path make the index independent from the playlist. The index is always created by default. skip-existing is now on by default.
This commit is contained in:
parent
caa5bfc58d
commit
a66f2aad03
17 changed files with 368 additions and 372 deletions
120
README.md
120
README.md
|
@ -1,7 +1,9 @@
|
|||
# slsk-batchdl
|
||||
# sldl
|
||||
|
||||
An automatic downloader for Soulseek built with Soulseek.NET. Accepts CSV files as well as Spotify and YouTube urls.
|
||||
See the [examples](#examples-1).
|
||||
Supports playlist and album downloads; selects the best files according to user-configured file conditions and some heuristics.
|
||||
|
||||
See the usage [examples](#examples-1).
|
||||
|
||||
## Index
|
||||
- [Options](#options)
|
||||
|
@ -20,7 +22,6 @@ See the [examples](#examples-1).
|
|||
- [Searching](#searching)
|
||||
- [File conditions](#file-conditions)
|
||||
- [Name format](#name-format)
|
||||
- [Skip-existing](#skip-existing)
|
||||
- [Configuration](#configuration)
|
||||
- [Examples](#examples-1)
|
||||
- [Notes](#notes)
|
||||
|
@ -31,16 +32,17 @@ See the [examples](#examples-1).
|
|||
|
||||
```
|
||||
Usage: sldl <input> [OPTIONS]
|
||||
|
||||
Required Arguments
|
||||
```
|
||||
#### Required Arguments
|
||||
```
|
||||
<input> A url, search string, or path to a local CSV file.
|
||||
Run --help "input" to view the accepted inputs.
|
||||
Can also be passed with -i, --input <input>
|
||||
--user <username> Soulseek username
|
||||
--pass <password> Soulseek password
|
||||
```
|
||||
#### General Options
|
||||
```
|
||||
General Options
|
||||
-p, --path <path> Download directory
|
||||
--input-type <type> [csv|youtube|spotify|bandcamp|string|list]
|
||||
--name-format <format> Name format for downloaded tracks. See --help name-format
|
||||
|
@ -51,22 +53,18 @@ Usage: sldl <input> [OPTIONS]
|
|||
-c, --config <path> Set config file location. Set to 'none' to ignore config
|
||||
--profile <names> Configuration profile(s) to use. See --help ""config"".
|
||||
--concurrent-downloads <num> Max concurrent downloads (default: 2)
|
||||
--m3u <option> Create an m3u playlist file in the output directory
|
||||
'none' (default for string inputs): Do not create
|
||||
'index'(default): Write a single line for sldl to index
|
||||
downloaded files. Required for skip-existing=m3u.
|
||||
'all': Create a playable m3u playlist file and sldl index.
|
||||
--m3u-path <path> Override default m3u path
|
||||
--write-playlist Create an m3u playlist file in the output directory
|
||||
--playlist-path <path> Override default path for m3u playlist file
|
||||
|
||||
-s, --skip-existing Skip if a track matching file conditions is found in the
|
||||
output folder or your music library (if provided)
|
||||
--skip-mode <mode> [name|tag|m3u|name-cond|tag-cond|m3u-cond]. See --help
|
||||
skip-existing.
|
||||
--music-dir <path> Specify to also skip downloading tracks found in a music
|
||||
library. Use with --skip-existing
|
||||
--no-skip-existing Do not skip downloaded tracks
|
||||
--no-write-index Do not create a file indexing all downloaded tracks
|
||||
--index-path <path> Override default path for sldl index
|
||||
--skip-check-cond Check file conditions when skipping existing files
|
||||
--skip-check-pref-cond Check preferred conditions when skipping existing files
|
||||
--skip-music-dir <path> Also skip downloading tracks found in a music library by
|
||||
comparing filenames. Not 100% reliable.
|
||||
--skip-not-found Skip searching for tracks that weren't found on Soulseek
|
||||
during the last run. Fails are read from the m3u file.
|
||||
--skip-existing-pref-cond Use preferred instead of necessary conds for skip-existing
|
||||
during the last run.
|
||||
|
||||
--listen-port <port> Port for incoming connections (default: 49998)
|
||||
--on-complete <command> Run a command whenever a file is downloaded.
|
||||
|
@ -87,8 +85,8 @@ Usage: sldl <input> [OPTIONS]
|
|||
--no-progress Disable progress bars/percentages, only simple printing
|
||||
--debug Print extra debug info
|
||||
```
|
||||
#### Search Options
|
||||
```
|
||||
Searching
|
||||
--fast-search Begin downloading as soon as a file satisfying the preferred
|
||||
conditions is found. Only for normal download mode.
|
||||
--remove-ft Remove 'feat.' and everything after before searching
|
||||
|
@ -113,8 +111,8 @@ Usage: sldl <input> [OPTIONS]
|
|||
--yt-dlp-argument <str> The command line arguments when running yt-dlp. Default:
|
||||
"{id}" -f bestaudio/best -cix -o "{savepath}.%(ext)s"
|
||||
Available vars are: {id}, {savedir}, {savepath} (w/o ext).
|
||||
Note that with -x, yt-dlp will download webms in case
|
||||
ffmpeg is unavailable.
|
||||
Note that -x causes yt-dlp to download webms in case ffmpeg
|
||||
is unavailable.
|
||||
|
||||
--search-timeout <ms> Max search time in ms (default: 6000)
|
||||
--max-stale-time <ms> Max download time without progress in ms (default: 50000)
|
||||
|
@ -123,23 +121,23 @@ Usage: sldl <input> [OPTIONS]
|
|||
--searches-renew-time <sec> Controls how often available searches are replenished.
|
||||
See --help "search". (default: 220)
|
||||
```
|
||||
#### Spotify Options
|
||||
```
|
||||
Spotify
|
||||
--spotify-id <id> Spotify client ID
|
||||
--spotify-secret <secret> Spotify client secret
|
||||
--spotify-token <token> Spotify access token
|
||||
--spotify-refresh <token> Spotify refresh token
|
||||
--remove-from-source Remove downloaded tracks from source playlist
|
||||
```
|
||||
#### YouTube Options
|
||||
```
|
||||
YouTube
|
||||
--youtube-key <key> Youtube data API key
|
||||
--get-deleted Attempt to retrieve titles of deleted videos from wayback
|
||||
machine. Requires yt-dlp.
|
||||
--deleted-only Only retrieve & download deleted music.
|
||||
```
|
||||
#### CSV File Options
|
||||
```
|
||||
CSV Files
|
||||
--artist-col Artist column name
|
||||
--title-col Track title column name
|
||||
--album-col Album column name
|
||||
|
@ -154,8 +152,8 @@ Usage: sldl <input> [OPTIONS]
|
|||
names; attempt to parse them into title and artist names.
|
||||
--remove-from-source Remove downloaded tracks from source CSV file
|
||||
```
|
||||
#### File Condition Options
|
||||
```
|
||||
File Conditions
|
||||
--format <formats> Accepted file format(s), comma-separated, without periods
|
||||
--length-tol <sec> Length tolerance in seconds
|
||||
--min-bitrate <rate> Minimum file bitrate
|
||||
|
@ -183,8 +181,8 @@ Usage: sldl <input> [OPTIONS]
|
|||
default; if --min-bitrate is set, ignores any files with
|
||||
unknown bitrate.
|
||||
```
|
||||
#### Album Download Options
|
||||
```
|
||||
Album Download
|
||||
-a, --album Album download mode: Download a folder
|
||||
-t, --interactive Interactive mode, allows to select the folder and images
|
||||
--album-track-count <num> Specify the exact number of tracks in the album. Add a + or
|
||||
|
@ -201,8 +199,8 @@ Usage: sldl <input> [OPTIONS]
|
|||
the files instead. Set to 'disable' keep it where it is.
|
||||
Default: {configured output dir}/failed
|
||||
```
|
||||
#### Aggregate Download Options
|
||||
```
|
||||
Aggregate Download
|
||||
-g, --aggregate Aggregate download mode: Find and download all distinct
|
||||
songs associated with the provided artist, album, or title.
|
||||
--aggregate-length-tol <tol> Max length tolerance in seconds to consider two tracks or
|
||||
|
@ -212,19 +210,12 @@ Usage: sldl <input> [OPTIONS]
|
|||
--relax-filtering Slightly relax file filtering in aggregate mode to include
|
||||
more results
|
||||
```
|
||||
```
|
||||
Help
|
||||
-h, --help [option] [all|input|download-modes|search|name-format|
|
||||
file-conditions|skip-existing|config]
|
||||
```
|
||||
```
|
||||
Notes
|
||||
Acronyms of two- and --three-word-flags are also accepted, e.g. --twf. If the option
|
||||
contains the word 'max' then the m should be uppercase. 'bitrate', 'sameplerate' and
|
||||
'bitdepth' should be all treated as two separate words, e.g --Mbr for --max-bitrate.
|
||||
### Notes
|
||||
Acronyms of two- and --three-word-flags are also accepted, e.g. --twf. If the option
|
||||
contains the word 'max' then the m should be uppercase. 'bitrate', 'sameplerate' and
|
||||
'bitdepth' should be all treated as two separate words, e.g --Mbr for --max-bitrate.
|
||||
|
||||
Flags can be explicitly disabled by setting them to false, e.g '--interactive false'
|
||||
```
|
||||
Flags can be explicitly disabled by setting them to false, e.g '--interactive false'
|
||||
|
||||
## Input types
|
||||
|
||||
|
@ -319,7 +310,7 @@ configured conditions and can also be omitted. List input must be manually activ
|
|||
## Download modes
|
||||
|
||||
### Normal
|
||||
The program will download a single file for every input entry.
|
||||
The default. Downloads a single file for every input entry.
|
||||
|
||||
### Album
|
||||
sldl will search for the album and download an entire folder including non-audio
|
||||
|
@ -454,36 +445,6 @@ extractor Name of the extractor used (CSV/Spotify/YouTube/
|
|||
default-folder Default sldl folder name (usually the playlist name)
|
||||
```
|
||||
|
||||
## Skip-existing
|
||||
|
||||
sldl can skip downloads that exist in the output directory or a specified directory configured
|
||||
with --music-dir.
|
||||
The following modes are available for --skip-mode:
|
||||
|
||||
### m3u
|
||||
Default when checking in the output directory.
|
||||
Checks whether the output m3u file contains the track in the '#SLDL' line. Does not check if
|
||||
the audio file exists or satisfies the file conditions (use m3u-cond for that). m3u and
|
||||
m3u-cond are the only modes that can skip album downloads.
|
||||
|
||||
### name
|
||||
Default when checking in the music directory.
|
||||
Compares filenames to the track title and artist name to determine if a track already exists.
|
||||
Specifically, a track will be skipped if there exists a file whose name contains the title
|
||||
and whose full path contains the artist name.
|
||||
|
||||
### tag
|
||||
Compares file tags to the track title and artist name. A track is skipped if there is a file
|
||||
whose artist tag contains the track artist and whose title tag equals the track title
|
||||
(ignoring case and ws). Slower than name mode as it needs to read all file tags.
|
||||
|
||||
### m3u-cond, name-cond, tag-cond
|
||||
Same as the above modes but also checks whether the found file satisfies the configured
|
||||
conditions. Uses necessary conditions by default, run with --skip-existing-pref-cond to use
|
||||
preferred conditions instead. Equivalent to the above modes if no necessary conditions have
|
||||
been specified (except m3u-cond, which always checks if the file exists).
|
||||
May be slower and use a lot of memory for large libraries.
|
||||
|
||||
## Configuration
|
||||
### Config Location:
|
||||
sldl will look for a file named sldl.conf in the following locations:
|
||||
|
@ -559,7 +520,7 @@ sldl "Some Album" --album --interactive
|
|||
|
||||
Download the album of every song in a spotify playlist:
|
||||
```
|
||||
sldl https://spotify/playlist/id --album --skip-existing
|
||||
sldl https://spotify/playlist/id --album
|
||||
```
|
||||
|
||||
<br>
|
||||
|
@ -572,7 +533,7 @@ sldl https://www.youtube.com/playlist/id --get-deleted --yt-dlp
|
|||
|
||||
Print all songs by an artist which are not in your library:
|
||||
```
|
||||
sldl "artist=MC MENTAL" --aggregate --skip-existing --music-dir "path/to/music" --print tracks-full
|
||||
sldl "artist=MC MENTAL" --aggregate --skip-music-dir "path/to/music" --print results-full
|
||||
```
|
||||
<br>
|
||||
|
||||
|
@ -584,20 +545,17 @@ sldl "artist=MC MENTAL" --aggregate --album --interactive
|
|||
#### Advanced example: Automatic wishlist downloader
|
||||
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 "album=Album, album-track-count=5" "format=mp3" >> wishlist.txt
|
||||
echo "Artist - My Favorite Song" >> wishlist.txt
|
||||
echo "a:Artist - Some Album, album-track-count=5" "format=flac" >> wishlist.txt
|
||||
```
|
||||
Add a profile to your `sldl.conf`:
|
||||
```
|
||||
[wishlist]
|
||||
input = wishlist.txt
|
||||
input-type = list
|
||||
skip-existing = true
|
||||
skip-mode = m3u
|
||||
m3u = index
|
||||
m3u-path = wishlist-archive.sldl
|
||||
index-path = wishlist-index.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.
|
||||
This will create a global archive file `wishlist-index.sldl` which will be scanned every time sldl is run to skip wishlist items that have already been downloaded. If you want to continue searching until a version satisfying the preferred conditions is downloaded, also add `skip-check-pref-cond = true` (note that this requires the files to remain in the same spot after being downloaded).
|
||||
Finally, set up a cron job (or a scheduled task on windows) to periodically run sldl with the following option:
|
||||
```
|
||||
sldl --profile wishlist
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
using AngleSharp.Css;
|
||||
using Enums;
|
||||
using Models;
|
||||
using System.Text;
|
||||
|
@ -24,7 +25,8 @@ public class Config
|
|||
public string parentDir = Directory.GetCurrentDirectory();
|
||||
public string input = "";
|
||||
public string m3uFilePath = "";
|
||||
public string musicDir = "";
|
||||
public string indexFilePath = "";
|
||||
public string skipMusicDir = "";
|
||||
public string spotifyId = "";
|
||||
public string spotifySecret = "";
|
||||
public string spotifyToken = "";
|
||||
|
@ -63,7 +65,6 @@ public class Config
|
|||
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;
|
||||
|
@ -73,8 +74,12 @@ public class Config
|
|||
public bool noModifyShareCount = false;
|
||||
public bool useRandomLogin = false;
|
||||
public bool noBrowseFolder = false;
|
||||
public bool skipExistingPrefCond = false;
|
||||
public bool skipCheckCond = false;
|
||||
public bool skipCheckPrefCond = false;
|
||||
public bool noProgress = false;
|
||||
public bool writePlaylist = false;
|
||||
public bool skipExisting = true;
|
||||
public bool writeIndex = true;
|
||||
public int downrankOn = -1;
|
||||
public int ignoreOn = -2;
|
||||
public int minAlbumTrackCount = -1;
|
||||
|
@ -97,9 +102,8 @@ public class Config
|
|||
public Track regexToReplace = new();
|
||||
public Track regexReplaceBy = new();
|
||||
public AlbumArtOption albumArtOption = AlbumArtOption.Default;
|
||||
public M3uOption m3uOption = M3uOption.Index;
|
||||
public InputType inputType = InputType.None;
|
||||
public SkipMode skipMode = SkipMode.M3u;
|
||||
public SkipMode skipMode = SkipMode.Index;
|
||||
public SkipMode skipModeMusicDir = SkipMode.Name;
|
||||
public PrintOption printOption = PrintOption.None;
|
||||
|
||||
|
@ -114,11 +118,11 @@ public class Config
|
|||
|
||||
readonly Dictionary<string, (List<string> args, string? cond)> configProfiles = new();
|
||||
readonly HashSet<string> appliedProfiles = new();
|
||||
bool hasConfiguredM3uMode = false;
|
||||
bool hasConfiguredIndex = false;
|
||||
bool confPathChanged = false;
|
||||
string[] arguments;
|
||||
FileConditionsMod? undoTempConds = null;
|
||||
FileConditionsMod? undoTempPrefConds = null;
|
||||
FileConditions? undoTempConds = null;
|
||||
FileConditions? undoTempPrefConds = null;
|
||||
|
||||
private static Config Instance = new();
|
||||
|
||||
|
@ -228,11 +232,13 @@ public class Config
|
|||
ignoreOn = Math.Min(ignoreOn, downrankOn);
|
||||
|
||||
if (DoNotDownload)
|
||||
m3uOption = M3uOption.None;
|
||||
else if (!hasConfiguredM3uMode && inputType == InputType.String)
|
||||
m3uOption = M3uOption.None;
|
||||
else if (!hasConfiguredM3uMode && Program.trackLists != null && !Program.trackLists.Flattened(true, true).Skip(1).Any())
|
||||
m3uOption = M3uOption.None;
|
||||
{
|
||||
writeIndex = false;
|
||||
}
|
||||
else if (!hasConfiguredIndex && Program.trackLists != null && !Program.trackLists.lists.Any(x => x.enablesIndexByDefault))
|
||||
{
|
||||
writeIndex = false;
|
||||
}
|
||||
|
||||
if (albumArtOnly && albumArtOption == AlbumArtOption.Default)
|
||||
albumArtOption = AlbumArtOption.Largest;
|
||||
|
@ -241,7 +247,7 @@ public class Config
|
|||
|
||||
parentDir = Utils.ExpandUser(parentDir);
|
||||
m3uFilePath = Utils.ExpandUser(m3uFilePath);
|
||||
musicDir = Utils.ExpandUser(musicDir);
|
||||
skipMusicDir = Utils.ExpandUser(skipMusicDir);
|
||||
failedAlbumPath = Utils.ExpandUser(failedAlbumPath);
|
||||
|
||||
if (failedAlbumPath.Length == 0)
|
||||
|
@ -494,25 +500,25 @@ public class Config
|
|||
}
|
||||
|
||||
|
||||
public void AddTemporaryConditions(FileConditionsMod? cond, FileConditionsMod? prefCond)
|
||||
public void AddTemporaryConditions(FileConditions? cond, FileConditions? prefCond)
|
||||
{
|
||||
if (cond != null)
|
||||
undoTempConds = necessaryCond.ApplyMod(cond);
|
||||
undoTempConds = necessaryCond.AddConditions(cond);
|
||||
if (prefCond != null)
|
||||
undoTempPrefConds = preferredCond.ApplyMod(prefCond);
|
||||
undoTempPrefConds = preferredCond.AddConditions(prefCond);
|
||||
}
|
||||
|
||||
|
||||
public void RestoreConditions()
|
||||
{
|
||||
if (undoTempConds != null)
|
||||
necessaryCond.ApplyMod(undoTempConds);
|
||||
necessaryCond.AddConditions(undoTempConds);
|
||||
if (undoTempPrefConds != null)
|
||||
preferredCond.ApplyMod(undoTempPrefConds);
|
||||
preferredCond.AddConditions(undoTempPrefConds);
|
||||
}
|
||||
|
||||
|
||||
public static FileConditionsMod ParseConditions(string input)
|
||||
public static FileConditions ParseConditions(string input)
|
||||
{
|
||||
static void UpdateMinMax(string value, string condition, ref int? min, ref int? max)
|
||||
{
|
||||
|
@ -528,7 +534,7 @@ public class Config
|
|||
min = max = int.Parse(value);
|
||||
}
|
||||
|
||||
var cond = new FileConditionsMod();
|
||||
var cond = new FileConditions();
|
||||
|
||||
var tr = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries;
|
||||
string[] conditions = input.Split(';', tr);
|
||||
|
@ -617,6 +623,25 @@ public class Config
|
|||
flag = trueVal;
|
||||
}
|
||||
|
||||
void setNullableFlag(ref bool? flag, ref int i, bool trueVal = true)
|
||||
{
|
||||
if (i >= args.Count - 1 || args[i + 1].StartsWith('-'))
|
||||
flag = trueVal;
|
||||
else if (args[i + 1] == "false")
|
||||
{
|
||||
flag = !trueVal;
|
||||
i++;
|
||||
}
|
||||
else if (args[i + 1] == "true")
|
||||
{
|
||||
flag = trueVal;
|
||||
i++;
|
||||
}
|
||||
else
|
||||
flag = trueVal;
|
||||
}
|
||||
|
||||
|
||||
bool inputSet = false;
|
||||
|
||||
for (int i = 0; i < args.Count; i++)
|
||||
|
@ -645,16 +670,16 @@ public class Config
|
|||
break;
|
||||
case "-p":
|
||||
case "--path":
|
||||
case "--parent":
|
||||
parentDir = args[++i];
|
||||
break;
|
||||
case "-c":
|
||||
case "--config":
|
||||
confPath = args[++i];
|
||||
break;
|
||||
case "-m":
|
||||
case "--md":
|
||||
case "--music-dir":
|
||||
musicDir = args[++i];
|
||||
case "--smd":
|
||||
case "--skip-music-dir":
|
||||
skipMusicDir = args[++i];
|
||||
break;
|
||||
case "-g":
|
||||
case "--aggregate":
|
||||
|
@ -791,10 +816,9 @@ public class Config
|
|||
case "--yt-dlp":
|
||||
setFlag(ref useYtdlp, ref i);
|
||||
break;
|
||||
case "-s":
|
||||
case "--se":
|
||||
case "--skip-existing":
|
||||
setFlag(ref skipExisting, ref i);
|
||||
case "--nse":
|
||||
case "--no-skip-existing":
|
||||
setFlag(ref skipExisting, ref i, false);
|
||||
break;
|
||||
case "--snf":
|
||||
case "--skip-not-found":
|
||||
|
@ -858,21 +882,24 @@ public class Config
|
|||
case "--reverse":
|
||||
setFlag(ref reverse, ref i);
|
||||
break;
|
||||
case "--m3u":
|
||||
case "--m3u8":
|
||||
hasConfiguredM3uMode = true;
|
||||
m3uOption = args[++i].ToLower().Trim() switch
|
||||
{
|
||||
"none" => M3uOption.None,
|
||||
"index" => M3uOption.Index,
|
||||
"all" => M3uOption.All,
|
||||
_ => throw new ArgumentException($"Invalid m3u option '{args[i]}'"),
|
||||
};
|
||||
case "--wp":
|
||||
case "--write-playlist":
|
||||
setFlag(ref writePlaylist, ref i);
|
||||
break;
|
||||
case "--m3up":
|
||||
case "--m3u-path":
|
||||
case "--pp":
|
||||
case "--playlist-path":
|
||||
m3uFilePath = args[++i];
|
||||
break;
|
||||
case "--nwi":
|
||||
case "--no-write-index":
|
||||
hasConfiguredIndex = true;
|
||||
setFlag(ref writeIndex, ref i, false);
|
||||
break;
|
||||
case "--ip":
|
||||
case "--index-path":
|
||||
hasConfiguredIndex = true;
|
||||
indexFilePath = args[++i];
|
||||
break;
|
||||
case "--lp":
|
||||
case "--port":
|
||||
case "--listen-port":
|
||||
|
@ -1011,19 +1038,19 @@ public class Config
|
|||
case "--pst":
|
||||
case "--pstt":
|
||||
case "--pref-strict-title":
|
||||
setFlag(ref preferredCond.StrictTitle, ref i);
|
||||
setNullableFlag(ref preferredCond.StrictTitle, ref i);
|
||||
break;
|
||||
case "--psa":
|
||||
case "--pref-strict-artist":
|
||||
setFlag(ref preferredCond.StrictArtist, ref i);
|
||||
setNullableFlag(ref preferredCond.StrictArtist, ref i);
|
||||
break;
|
||||
case "--psal":
|
||||
case "--pref-strict-album":
|
||||
setFlag(ref preferredCond.StrictAlbum, ref i);
|
||||
setNullableFlag(ref preferredCond.StrictAlbum, ref i);
|
||||
break;
|
||||
case "--panl":
|
||||
case "--pref-accept-no-length":
|
||||
setFlag(ref preferredCond.AcceptNoLength, ref i);
|
||||
setNullableFlag(ref preferredCond.AcceptNoLength, ref i);
|
||||
break;
|
||||
case "--pbu":
|
||||
case "--pref-banned-users":
|
||||
|
@ -1065,15 +1092,15 @@ public class Config
|
|||
break;
|
||||
case "--stt":
|
||||
case "--strict-title":
|
||||
setFlag(ref necessaryCond.StrictTitle, ref i);
|
||||
setNullableFlag(ref necessaryCond.StrictTitle, ref i);
|
||||
break;
|
||||
case "--sa":
|
||||
case "--strict-artist":
|
||||
setFlag(ref necessaryCond.StrictArtist, ref i);
|
||||
setNullableFlag(ref necessaryCond.StrictArtist, ref i);
|
||||
break;
|
||||
case "--sal":
|
||||
case "--strict-album":
|
||||
setFlag(ref necessaryCond.StrictAlbum, ref i);
|
||||
setNullableFlag(ref necessaryCond.StrictAlbum, ref i);
|
||||
break;
|
||||
case "--bu":
|
||||
case "--banned-users":
|
||||
|
@ -1081,16 +1108,16 @@ public class Config
|
|||
break;
|
||||
case "--anl":
|
||||
case "--accept-no-length":
|
||||
setFlag(ref necessaryCond.AcceptNoLength, ref i);
|
||||
setNullableFlag(ref necessaryCond.AcceptNoLength, ref i);
|
||||
break;
|
||||
case "--cond":
|
||||
case "--conditions":
|
||||
necessaryCond.ApplyMod(ParseConditions(args[++i]));
|
||||
necessaryCond.AddConditions(ParseConditions(args[++i]));
|
||||
break;
|
||||
case "--pc":
|
||||
case "--pref":
|
||||
case "--preferred-conditions":
|
||||
preferredCond.ApplyMod(ParseConditions(args[++i]));
|
||||
preferredCond.AddConditions(ParseConditions(args[++i]));
|
||||
break;
|
||||
case "--nmsc":
|
||||
case "--no-modify-share-count":
|
||||
|
@ -1104,17 +1131,14 @@ public class Config
|
|||
case "--no-progress":
|
||||
setFlag(ref noProgress, ref i);
|
||||
break;
|
||||
case "--sm":
|
||||
case "--skip-mode":
|
||||
case "--smod":
|
||||
case "--skip-mode-output-dir":
|
||||
skipMode = args[++i].ToLower().Trim() switch
|
||||
{
|
||||
"name" => SkipMode.Name,
|
||||
"name-cond" => SkipMode.NameCond,
|
||||
"tag" => SkipMode.Tag,
|
||||
"tag-cond" => SkipMode.TagCond,
|
||||
"m3u" => SkipMode.M3u,
|
||||
"m3u-cond" => SkipMode.M3uCond,
|
||||
_ => throw new ArgumentException($"Invalid skip mode '{args[i]}'"),
|
||||
"index" => SkipMode.Index,
|
||||
_ => throw new ArgumentException($"Invalid output dir skip mode '{args[i]}'"),
|
||||
};
|
||||
break;
|
||||
case "--smmd":
|
||||
|
@ -1122,9 +1146,7 @@ public class Config
|
|||
skipModeMusicDir = args[++i].ToLower().Trim() switch
|
||||
{
|
||||
"name" => SkipMode.Name,
|
||||
"name-cond" => SkipMode.NameCond,
|
||||
"tag" => SkipMode.Tag,
|
||||
"tag-cond" => SkipMode.TagCond,
|
||||
_ => throw new ArgumentException($"Invalid music dir skip mode '{args[i]}'"),
|
||||
};
|
||||
break;
|
||||
|
@ -1154,8 +1176,8 @@ public class Config
|
|||
case "--sc":
|
||||
case "--strict":
|
||||
case "--strict-conditions":
|
||||
setFlag(ref preferredCond.AcceptMissingProps, ref i, false);
|
||||
setFlag(ref necessaryCond.AcceptMissingProps, ref i, false);
|
||||
setNullableFlag(ref preferredCond.AcceptMissingProps, ref i, false);
|
||||
setNullableFlag(ref necessaryCond.AcceptMissingProps, ref i, false);
|
||||
break;
|
||||
case "--yda":
|
||||
case "--yt-dlp-argument":
|
||||
|
@ -1188,9 +1210,13 @@ public class Config
|
|||
case "--no-browse-folder":
|
||||
setFlag(ref noBrowseFolder, ref i);
|
||||
break;
|
||||
case "--sepc":
|
||||
case "--skip-existing-pref-cond":
|
||||
setFlag(ref skipExistingPrefCond, ref i);
|
||||
case "--scc":
|
||||
case "--skip-check-cond":
|
||||
setFlag(ref skipCheckCond, ref i);
|
||||
break;
|
||||
case "--scpc":
|
||||
case "--skip-check-pref-cond":
|
||||
setFlag(ref skipCheckPrefCond, ref i);
|
||||
break;
|
||||
case "--alt":
|
||||
case "--aggregate-length-tol":
|
||||
|
|
|
@ -22,12 +22,9 @@ namespace Enums
|
|||
public enum SkipMode
|
||||
{
|
||||
Name = 0,
|
||||
NameCond = 1,
|
||||
Tag = 2,
|
||||
TagCond = 3,
|
||||
// non file-based skip modes are >= 4
|
||||
M3u = 4,
|
||||
M3uCond = 5,
|
||||
Index = 4,
|
||||
}
|
||||
|
||||
public enum InputType
|
||||
|
@ -53,6 +50,7 @@ namespace Enums
|
|||
{
|
||||
None,
|
||||
Index,
|
||||
Playlist,
|
||||
All,
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ namespace Extractors
|
|||
};
|
||||
var tle = new TrackListEntry(track);
|
||||
tle.defaultFolderName = track.Artist;
|
||||
tle.enablesIndexByDefault = true;
|
||||
trackLists.AddEntry(tle);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ namespace Extractors
|
|||
foreach (var tle in trackLists.lists)
|
||||
{
|
||||
tle.defaultFolderName = csvName;
|
||||
tle.enablesIndexByDefault = true;
|
||||
}
|
||||
|
||||
return trackLists;
|
||||
|
|
|
@ -68,6 +68,7 @@ namespace Extractors
|
|||
tle.additionalPrefConds = Config.ParseConditions(fields[2]);
|
||||
|
||||
tle.defaultFolderName = foldername;
|
||||
tle.enablesIndexByDefault = true;
|
||||
}
|
||||
|
||||
if (tl.lists.Count == 1)
|
||||
|
|
|
@ -43,6 +43,7 @@ namespace Extractors
|
|||
var tracks = await spotifyClient.GetLikes(max, off);
|
||||
tle = new TrackListEntry(TrackType.Normal);
|
||||
tle.defaultFolderName = "Spotify Likes";
|
||||
tle.enablesIndexByDefault = true;
|
||||
tle.list.Add(tracks);
|
||||
}
|
||||
else if (input.Contains("/album/"))
|
||||
|
@ -69,19 +70,19 @@ namespace Extractors
|
|||
var tracks = new List<Track>();
|
||||
tle = new TrackListEntry(TrackType.Normal);
|
||||
|
||||
string? playlistName = null;
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Loading Spotify playlist");
|
||||
(var playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(input, max, off);
|
||||
tle.defaultFolderName = playlistName;
|
||||
(playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(input, max, off);
|
||||
}
|
||||
catch (SpotifyAPI.Web.APIException)
|
||||
{
|
||||
if (!needLogin && !spotifyClient.UsedDefaultCredentials)
|
||||
{
|
||||
await spotifyClient.Authorize(true, Config.I.removeTracksFromSource);
|
||||
(var playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(input, max, off);
|
||||
tle.defaultFolderName = playlistName;
|
||||
(playlistName, playlistUri, tracks) = await spotifyClient.GetPlaylist(input, max, off);
|
||||
}
|
||||
else if (!needLogin)
|
||||
{
|
||||
|
@ -91,6 +92,8 @@ namespace Extractors
|
|||
else throw;
|
||||
}
|
||||
|
||||
tle.defaultFolderName = playlistName;
|
||||
tle.enablesIndexByDefault = true;
|
||||
tle.list.Add(tracks);
|
||||
}
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@ namespace Extractors
|
|||
|
||||
var tle = new TrackListEntry(TrackType.Normal);
|
||||
|
||||
tle.enablesIndexByDefault = true;
|
||||
tle.defaultFolderName = name;
|
||||
tle.list.Add(tracks);
|
||||
|
||||
|
|
|
@ -6,17 +6,15 @@ namespace FileSkippers
|
|||
{
|
||||
public static class FileSkipperRegistry
|
||||
{
|
||||
public static FileSkipper GetSkipper(SkipMode mode, string dir, FileConditions conditions, M3uEditor m3uEditor)
|
||||
public static FileSkipper GetSkipper(SkipMode mode, string dir, FileConditions? conditions, M3uEditor indexEditor)
|
||||
{
|
||||
bool noConditions = conditions.Equals(new FileConditions());
|
||||
bool useConditions = conditions != null && !conditions.Equals(new FileConditions());
|
||||
return mode switch
|
||||
{
|
||||
SkipMode.Name => new NameSkipper(dir),
|
||||
SkipMode.NameCond => noConditions ? new NameSkipper(dir) : new NameConditionalSkipper(dir, conditions),
|
||||
SkipMode.Tag => new TagSkipper(dir),
|
||||
SkipMode.TagCond => noConditions ? new TagSkipper(dir) : new TagConditionalSkipper(dir, conditions),
|
||||
SkipMode.M3u => new M3uSkipper(m3uEditor, false),
|
||||
SkipMode.M3uCond => noConditions ? new M3uSkipper(m3uEditor, true) : new M3uConditionalSkipper(m3uEditor, conditions),
|
||||
SkipMode.Name => useConditions ? new NameConditionalSkipper(dir, conditions) : new NameSkipper(dir),
|
||||
SkipMode.Tag => useConditions ? new TagConditionalSkipper(dir, conditions) : new TagSkipper(dir),
|
||||
SkipMode.Index => useConditions ? new IndexConditionalSkipper(indexEditor, conditions) : new IndexSkipper(indexEditor, conditions != null),
|
||||
_ => throw new ArgumentException("Invalid SkipMode")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -306,14 +304,14 @@ namespace FileSkippers
|
|||
}
|
||||
}
|
||||
|
||||
public class M3uSkipper : FileSkipper
|
||||
public class IndexSkipper : FileSkipper
|
||||
{
|
||||
M3uEditor m3uEditor;
|
||||
M3uEditor indexEditor;
|
||||
bool checkFileExists;
|
||||
|
||||
public M3uSkipper(M3uEditor m3UEditor, bool checkFileExists)
|
||||
public IndexSkipper(M3uEditor m3UEditor, bool checkFileExists)
|
||||
{
|
||||
this.m3uEditor = m3UEditor;
|
||||
this.indexEditor = m3UEditor;
|
||||
this.checkFileExists = checkFileExists;
|
||||
IndexIsBuilt = true;
|
||||
}
|
||||
|
@ -321,7 +319,7 @@ namespace FileSkippers
|
|||
public override bool TrackExists(Track track, out string? foundPath)
|
||||
{
|
||||
foundPath = null;
|
||||
var t = m3uEditor.PreviousRunResult(track);
|
||||
var t = indexEditor.PreviousRunResult(track);
|
||||
if (t != null && (t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists))
|
||||
{
|
||||
if (checkFileExists)
|
||||
|
@ -348,14 +346,14 @@ namespace FileSkippers
|
|||
}
|
||||
}
|
||||
|
||||
public class M3uConditionalSkipper : FileSkipper
|
||||
public class IndexConditionalSkipper : FileSkipper
|
||||
{
|
||||
M3uEditor m3uEditor;
|
||||
M3uEditor indexEditor;
|
||||
FileConditions conditions;
|
||||
|
||||
public M3uConditionalSkipper(M3uEditor m3UEditor, FileConditions conditions)
|
||||
public IndexConditionalSkipper(M3uEditor m3UEditor, FileConditions conditions)
|
||||
{
|
||||
this.m3uEditor = m3UEditor;
|
||||
this.indexEditor = m3UEditor;
|
||||
this.conditions = conditions;
|
||||
IndexIsBuilt = true;
|
||||
}
|
||||
|
@ -363,7 +361,7 @@ namespace FileSkippers
|
|||
public override bool TrackExists(Track track, out string? foundPath)
|
||||
{
|
||||
foundPath = null;
|
||||
var t = m3uEditor.PreviousRunResult(track);
|
||||
var t = indexEditor.PreviousRunResult(track);
|
||||
|
||||
if (t == null || t.DownloadPath.Length == 0)
|
||||
return false;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
// --invalid-replace-str, --cond, --pref
|
||||
// --fast-search-delay, --fast-search-min-up-speed
|
||||
// --min-album-track-count, --max-album-track-count, --extract-max-track-count, --extract-min-track-count
|
||||
// --skip-mode-music-dir, --skip-mode-output-dir
|
||||
|
||||
public static class Help
|
||||
{
|
||||
|
@ -28,22 +29,18 @@ public static class Help
|
|||
-c, --config <path> Set config file location. Set to 'none' to ignore config
|
||||
--profile <names> Configuration profile(s) to use. See --help ""config"".
|
||||
--concurrent-downloads <num> Max concurrent downloads (default: 2)
|
||||
--m3u <option> Create an m3u playlist file in the output directory
|
||||
'none' (default for string inputs): Do not create
|
||||
'index'(default): Write a single line for sldl to index
|
||||
downloaded files. Required for skip-existing=m3u.
|
||||
'all': Create a playable m3u playlist file and sldl index.
|
||||
--m3u-path <path> Override default m3u path
|
||||
--write-playlist Create an m3u playlist file in the output directory
|
||||
--playlist-path <path> Override default path for m3u playlist file
|
||||
|
||||
-s, --skip-existing Skip if a track matching file conditions is found in the
|
||||
output folder or your music library (if provided)
|
||||
--skip-mode <mode> [name|tag|m3u|name-cond|tag-cond|m3u-cond]. See --help
|
||||
skip-existing.
|
||||
--music-dir <path> Specify to also skip downloading tracks found in a music
|
||||
library. Use with --skip-existing
|
||||
--no-skip-existing Do not skip downloaded tracks
|
||||
--no-write-index Do not create a file indexing all downloaded tracks
|
||||
--index-path <path> Override default path for sldl index
|
||||
--skip-check-cond Check file conditions when skipping existing files
|
||||
--skip-check-pref-cond Check preferred conditions when skipping existing files
|
||||
--skip-music-dir <path> Also skip downloading tracks found in a music library by
|
||||
comparing filenames. Not 100% reliable.
|
||||
--skip-not-found Skip searching for tracks that weren't found on Soulseek
|
||||
during the last run. Fails are read from the m3u file.
|
||||
--skip-existing-pref-cond Use preferred instead of necessary conds for skip-existing
|
||||
during the last run.
|
||||
|
||||
--listen-port <port> Port for incoming connections (default: 49998)
|
||||
--on-complete <command> Run a command whenever a file is downloaded.
|
||||
|
@ -89,8 +86,8 @@ public static class Help
|
|||
--yt-dlp-argument <str> The command line arguments when running yt-dlp. Default:
|
||||
""{id}"" -f bestaudio/best -cix -o ""{savepath}.%(ext)s""
|
||||
Available vars are: {id}, {savedir}, {savepath} (w/o ext).
|
||||
Note that with -x, yt-dlp will download webms in case
|
||||
ffmpeg is unavailable.
|
||||
Note that -x causes yt-dlp to download webms in case ffmpeg
|
||||
is unavailable.
|
||||
|
||||
--search-timeout <ms> Max search time in ms (default: 6000)
|
||||
--max-stale-time <ms> Max download time without progress in ms (default: 50000)
|
||||
|
@ -184,7 +181,7 @@ public static class Help
|
|||
|
||||
Help
|
||||
-h, --help [option] [all|input|download-modes|search|name-format|
|
||||
file-conditions|skip-existing|config]
|
||||
file-conditions|config]
|
||||
|
||||
Notes
|
||||
Acronyms of two- and --three-word-flags are also accepted, e.g. --twf. If the option
|
||||
|
@ -287,7 +284,7 @@ public static class Help
|
|||
Download modes
|
||||
|
||||
Normal
|
||||
The program will download a single file for every input entry.
|
||||
The default. Downloads a single file for every input entry.
|
||||
|
||||
Album
|
||||
sldl will search for the album and download an entire folder including non-audio files.
|
||||
|
@ -427,38 +424,6 @@ public static class Help
|
|||
default-folder Default sldl folder name (usually the playlist name)
|
||||
";
|
||||
|
||||
const string skipExistingHelp = @"
|
||||
Skip-existing
|
||||
|
||||
sldl can skip downloads that exist in the output directory or a specified directory configured
|
||||
with --music-dir.
|
||||
The following modes are available for --skip-mode:
|
||||
|
||||
m3u
|
||||
Default when checking in the output directory.
|
||||
Checks whether the output m3u file contains the track in the '#SLDL' line. Does not check if
|
||||
the audio file exists or satisfies the file conditions (use m3u-cond for that). m3u and
|
||||
m3u-cond are the only modes that can skip album downloads.
|
||||
|
||||
name
|
||||
Default when checking in the music directory.
|
||||
Compares filenames to the track title and artist name to determine if a track already exists.
|
||||
Specifically, a track will be skipped if there exists a file whose name contains the title
|
||||
and whose full path contains the artist name.
|
||||
|
||||
tag
|
||||
Compares file tags to the track title and artist name. A track is skipped if there is a file
|
||||
whose artist tag contains the track artist and whose title tag equals the track title
|
||||
(ignoring case and ws). Slower than name mode as it needs to read all file tags.
|
||||
|
||||
m3u-cond, name-cond, tag-cond
|
||||
Same as the above modes but also checks whether the found file satisfies the configured
|
||||
conditions. Uses necessary conditions by default, run with --skip-existing-pref-cond to use
|
||||
preferred conditions instead. Equivalent to the above modes if no necessary conditions have
|
||||
been specified (except m3u-cond, which always checks if the file exists).
|
||||
May be slower and use a lot of memory for large libraries.
|
||||
";
|
||||
|
||||
const string configHelp = @"
|
||||
Configuration
|
||||
Config Location:
|
||||
|
@ -519,7 +484,6 @@ public static class Help
|
|||
{ "search", searchHelp },
|
||||
{ "file-conditions", fileConditionsHelp },
|
||||
{ "name-format", nameFormatHelp },
|
||||
{ "skip-existing", skipExistingHelp },
|
||||
{ "config", configHelp },
|
||||
};
|
||||
|
||||
|
|
|
@ -1,23 +1,28 @@
|
|||
using Models;
|
||||
using Enums;
|
||||
using System.Text;
|
||||
using System.Diagnostics;
|
||||
|
||||
|
||||
public class M3uEditor
|
||||
public class M3uEditor // todo: separate into M3uEditor and IndexEditor
|
||||
{
|
||||
public string path { get; private set; }
|
||||
public M3uOption option = M3uOption.Index;
|
||||
string parent;
|
||||
List<string> lines;
|
||||
bool needFirstUpdate = false;
|
||||
int offset = 0;
|
||||
readonly TrackLists trackLists;
|
||||
readonly Dictionary<string, Track> previousRunData = new(); // { track.ToKey(), track }
|
||||
|
||||
public M3uEditor(TrackLists trackLists, M3uOption option)
|
||||
private readonly object locker = new();
|
||||
|
||||
public M3uEditor(TrackLists trackLists, M3uOption option, int offset = 0)
|
||||
{
|
||||
this.trackLists = trackLists;
|
||||
this.option = option;
|
||||
this.needFirstUpdate = option == M3uOption.All;
|
||||
this.offset = offset;
|
||||
this.needFirstUpdate = option == M3uOption.All || option == M3uOption.Playlist;
|
||||
}
|
||||
|
||||
public M3uEditor(string path, TrackLists trackLists, M3uOption option) : this(trackLists, option)
|
||||
|
@ -123,10 +128,10 @@ public class M3uEditor
|
|||
if (option == M3uOption.None)
|
||||
return;
|
||||
|
||||
lock (trackLists)
|
||||
lock (trackLists) lock (locker)
|
||||
{
|
||||
bool needUpdate = false;
|
||||
int index = 1;
|
||||
int index = 1 + offset;
|
||||
|
||||
bool updateLine(string newLine)
|
||||
{
|
||||
|
@ -146,8 +151,11 @@ public class M3uEditor
|
|||
|| Utils.NormalizedPath(indexTrack.DownloadPath) != Utils.NormalizedPath(track.DownloadPath);
|
||||
}
|
||||
|
||||
void updateTrackIfNeeded(Track track)
|
||||
void updateIndexTrackIfNeeded(Track track)
|
||||
{
|
||||
if (option == M3uOption.Playlist)
|
||||
return;
|
||||
|
||||
var key = track.ToKey();
|
||||
|
||||
previousRunData.TryGetValue(key, out Track? indexTrack);
|
||||
|
@ -174,7 +182,7 @@ public class M3uEditor
|
|||
{
|
||||
if (tle.source.State != TrackState.Initial)
|
||||
{
|
||||
updateTrackIfNeeded(tle.source);
|
||||
updateIndexTrackIfNeeded(tle.source);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,12 +192,19 @@ public class M3uEditor
|
|||
{
|
||||
var track = tle.list[k][j];
|
||||
|
||||
if (track.IsNotAudio || track.State == TrackState.Initial)
|
||||
if (track.IsNotAudio)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (track.State == TrackState.Initial)
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
updateTrackIfNeeded(track);
|
||||
updateIndexTrackIfNeeded(track);
|
||||
|
||||
if (option == M3uOption.All)
|
||||
if (option == M3uOption.All || option == M3uOption.Playlist)
|
||||
{
|
||||
needUpdate |= updateLine(TrackToLine(track));
|
||||
index++;
|
||||
|
@ -206,14 +221,31 @@ public class M3uEditor
|
|||
}
|
||||
}
|
||||
|
||||
class Writer // temporary fix because streamwriter sometimes writes garbled text (for unknown reasons)
|
||||
{
|
||||
private StringBuilder sb = new();
|
||||
public void Write(string s) => sb.Append(s);
|
||||
public void Write(char c) => sb.Append(c);
|
||||
public override string ToString() => sb.ToString();
|
||||
}
|
||||
|
||||
private void WriteAllLines()
|
||||
{
|
||||
if (!Directory.Exists(parent))
|
||||
Directory.CreateDirectory(parent);
|
||||
|
||||
using var fileStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);
|
||||
using var writer = new StreamWriter(fileStream);
|
||||
//using var fileStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write/*, FileShare.ReadWrite*/);
|
||||
//using var writer = new StreamWriter(fileStream, encoding: Encoding.UTF8);
|
||||
//using var writer = TextWriter.Synchronized(new StreamWriter(fileStream, encoding: Encoding.UTF8));
|
||||
var writer = new Writer();
|
||||
|
||||
if (option != M3uOption.Playlist)
|
||||
{
|
||||
WriteSldlLine(writer);
|
||||
}
|
||||
|
||||
if (option != M3uOption.Index)
|
||||
{
|
||||
foreach (var line in lines)
|
||||
{
|
||||
writer.Write(line);
|
||||
|
@ -221,7 +253,10 @@ public class M3uEditor
|
|||
}
|
||||
}
|
||||
|
||||
private void WriteSldlLine(StreamWriter writer)
|
||||
File.WriteAllText(path, writer.ToString());
|
||||
}
|
||||
|
||||
private void WriteSldlLine(Writer writer)
|
||||
{
|
||||
// Format:
|
||||
// #SLDL:<trackinfo>;<trackinfo>; ...
|
||||
|
@ -327,7 +362,7 @@ public class M3uEditor
|
|||
if (!File.Exists(path))
|
||||
return "";
|
||||
using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var streamReader = new StreamReader(fileStream);
|
||||
using var streamReader = new StreamReader(fileStream, encoding: Encoding.UTF8);
|
||||
return streamReader.ReadToEnd();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,21 +6,20 @@ namespace Models
|
|||
{
|
||||
public class FileConditions
|
||||
{
|
||||
public int LengthTolerance = -1;
|
||||
public int MinBitrate = -1;
|
||||
public int MaxBitrate = -1;
|
||||
public int MinSampleRate = -1;
|
||||
public int MaxSampleRate = -1;
|
||||
public int MinBitDepth = -1;
|
||||
public int MaxBitDepth = -1;
|
||||
public bool StrictTitle = false;
|
||||
public bool StrictArtist = false;
|
||||
public bool StrictAlbum = false;
|
||||
public string[] Formats = Array.Empty<string>();
|
||||
public string[] BannedUsers = Array.Empty<string>();
|
||||
public bool StrictStringDiacrRemove = true;
|
||||
public bool AcceptNoLength = true;
|
||||
public bool AcceptMissingProps = true;
|
||||
public int? LengthTolerance;
|
||||
public int? MinBitrate;
|
||||
public int? MaxBitrate;
|
||||
public int? MinSampleRate;
|
||||
public int? MaxSampleRate;
|
||||
public int? MinBitDepth;
|
||||
public int? MaxBitDepth;
|
||||
public bool? StrictTitle;
|
||||
public bool? StrictArtist;
|
||||
public bool? StrictAlbum;
|
||||
public string[]? Formats;
|
||||
public string[]? BannedUsers;
|
||||
public bool? AcceptNoLength;
|
||||
public bool? AcceptMissingProps;
|
||||
|
||||
public FileConditions() { }
|
||||
|
||||
|
@ -38,14 +37,20 @@ namespace Models
|
|||
MinBitDepth = other.MinBitDepth;
|
||||
MaxBitDepth = other.MaxBitDepth;
|
||||
AcceptMissingProps = other.AcceptMissingProps;
|
||||
StrictStringDiacrRemove = other.StrictStringDiacrRemove;
|
||||
Formats = other.Formats.ToArray();
|
||||
BannedUsers = other.BannedUsers.ToArray();
|
||||
Formats = other.Formats?.ToArray();
|
||||
BannedUsers = other.BannedUsers?.ToArray();
|
||||
}
|
||||
|
||||
public FileConditionsMod ApplyMod(FileConditionsMod mod)
|
||||
public FileConditions With(FileConditions other)
|
||||
{
|
||||
var undoMod = new FileConditionsMod();
|
||||
var res = new FileConditions(this);
|
||||
res.AddConditions(other);
|
||||
return res;
|
||||
}
|
||||
|
||||
public FileConditions AddConditions(FileConditions mod)
|
||||
{
|
||||
var undoMod = new FileConditions();
|
||||
|
||||
if (mod.LengthTolerance != null)
|
||||
{
|
||||
|
@ -107,11 +112,6 @@ namespace Models
|
|||
undoMod.BannedUsers = BannedUsers;
|
||||
BannedUsers = mod.BannedUsers;
|
||||
}
|
||||
if (mod.StrictStringDiacrRemove != null)
|
||||
{
|
||||
undoMod.StrictStringDiacrRemove = StrictStringDiacrRemove;
|
||||
StrictStringDiacrRemove = mod.StrictStringDiacrRemove.Value;
|
||||
}
|
||||
if (mod.AcceptNoLength != null)
|
||||
{
|
||||
undoMod.AcceptNoLength = AcceptNoLength;
|
||||
|
@ -126,38 +126,37 @@ namespace Models
|
|||
return undoMod;
|
||||
}
|
||||
|
||||
|
||||
public override bool Equals(object obj)
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is FileConditions other)
|
||||
{
|
||||
return LengthTolerance == other.LengthTolerance &&
|
||||
MinBitrate == other.MinBitrate &&
|
||||
MaxBitrate == other.MaxBitrate &&
|
||||
MinSampleRate == other.MinSampleRate &&
|
||||
MaxSampleRate == other.MaxSampleRate &&
|
||||
MinBitDepth == other.MinBitDepth &&
|
||||
MaxBitDepth == other.MaxBitDepth &&
|
||||
StrictTitle == other.StrictTitle &&
|
||||
StrictArtist == other.StrictArtist &&
|
||||
StrictAlbum == other.StrictAlbum &&
|
||||
StrictStringDiacrRemove == other.StrictStringDiacrRemove &&
|
||||
AcceptNoLength == other.AcceptNoLength &&
|
||||
AcceptMissingProps == other.AcceptMissingProps &&
|
||||
Formats.SequenceEqual(other.Formats) &&
|
||||
BannedUsers.SequenceEqual(other.BannedUsers);
|
||||
}
|
||||
if (obj == null || GetType() != obj.GetType())
|
||||
return false;
|
||||
|
||||
var other = (FileConditions)obj;
|
||||
|
||||
return LengthTolerance == other.LengthTolerance
|
||||
&& MinBitrate == other.MinBitrate
|
||||
&& MaxBitrate == other.MaxBitrate
|
||||
&& MinSampleRate == other.MinSampleRate
|
||||
&& MaxSampleRate == other.MaxSampleRate
|
||||
&& MinBitDepth == other.MinBitDepth
|
||||
&& MaxBitDepth == other.MaxBitDepth
|
||||
&& StrictTitle == other.StrictTitle
|
||||
&& StrictArtist == other.StrictArtist
|
||||
&& StrictAlbum == other.StrictAlbum
|
||||
&& AcceptNoLength == other.AcceptNoLength
|
||||
&& AcceptMissingProps == other.AcceptMissingProps
|
||||
&& ((Formats == null && other.Formats == null) || (Formats != null && other.Formats != null && Formats.SequenceEqual(other.Formats)))
|
||||
&& ((BannedUsers == null && other.BannedUsers == null) || (BannedUsers != null && other.BannedUsers != null && BannedUsers.SequenceEqual(other.BannedUsers)));
|
||||
}
|
||||
|
||||
public void UnsetClientSpecificFields()
|
||||
{
|
||||
MinBitrate = -1;
|
||||
MaxBitrate = -1;
|
||||
MinSampleRate = -1;
|
||||
MaxSampleRate = -1;
|
||||
MinBitDepth = -1;
|
||||
MaxBitDepth = -1;
|
||||
MinBitrate = null;
|
||||
MaxBitrate = null;
|
||||
MinSampleRate = null;
|
||||
MaxSampleRate = null;
|
||||
MinBitDepth = null;
|
||||
MaxBitDepth = null;
|
||||
}
|
||||
|
||||
public bool FileSatisfies(Soulseek.File file, Track track, SearchResponse? response)
|
||||
|
@ -186,27 +185,27 @@ namespace Models
|
|||
|
||||
public bool StrictTitleSatisfies(string fname, string tname, bool noPath = true)
|
||||
{
|
||||
if (!StrictTitle || tname.Length == 0)
|
||||
if (StrictTitle == null || !StrictTitle.Value || tname.Length == 0)
|
||||
return true;
|
||||
|
||||
fname = noPath ? Utils.GetFileNameWithoutExtSlsk(fname) : fname;
|
||||
return StrictString(fname, tname, StrictStringDiacrRemove, ignoreCase: true);
|
||||
return StrictString(fname, tname, diacrRemove: true, ignoreCase: true);
|
||||
}
|
||||
|
||||
public bool StrictArtistSatisfies(string fname, string aname)
|
||||
{
|
||||
if (!StrictArtist || aname.Length == 0)
|
||||
if (StrictArtist == null || !StrictArtist.Value || aname.Length == 0)
|
||||
return true;
|
||||
|
||||
return StrictString(fname, aname, StrictStringDiacrRemove, ignoreCase: true, boundarySkipWs: false);
|
||||
return StrictString(fname, aname, diacrRemove: true, ignoreCase: true, boundarySkipWs: false);
|
||||
}
|
||||
|
||||
public bool StrictAlbumSatisfies(string fname, string alname)
|
||||
{
|
||||
if (!StrictAlbum || alname.Length == 0)
|
||||
if (StrictAlbum == null || !StrictAlbum.Value || alname.Length == 0)
|
||||
return true;
|
||||
|
||||
return StrictString(Utils.GetDirectoryNameSlsk(fname), alname, StrictStringDiacrRemove, ignoreCase: true, boundarySkipWs: true);
|
||||
return StrictString(Utils.GetDirectoryNameSlsk(fname), alname, diacrRemove: true, ignoreCase: true, boundarySkipWs: true);
|
||||
}
|
||||
|
||||
public static string StrictStringPreprocess(string str, bool diacrRemove = true)
|
||||
|
@ -246,7 +245,7 @@ namespace Models
|
|||
|
||||
public bool FormatSatisfies(string fname)
|
||||
{
|
||||
if (Formats.Length == 0)
|
||||
if (Formats == null || Formats.Length == 0)
|
||||
return true;
|
||||
|
||||
string ext = Path.GetExtension(fname).TrimStart('.').ToLower();
|
||||
|
@ -258,10 +257,10 @@ namespace Models
|
|||
public bool LengthToleranceSatisfies(SimpleFile file, int wantedLength) => LengthToleranceSatisfies(file.Length, wantedLength);
|
||||
public bool LengthToleranceSatisfies(int? length, int wantedLength)
|
||||
{
|
||||
if (LengthTolerance < 0 || wantedLength < 0)
|
||||
if (LengthTolerance == null || LengthTolerance < 0 || wantedLength < 0)
|
||||
return true;
|
||||
if (length == null || length < 0)
|
||||
return AcceptNoLength && AcceptMissingProps;
|
||||
return AcceptNoLength == null || AcceptNoLength.Value;
|
||||
return Math.Abs((int)length - wantedLength) <= LengthTolerance;
|
||||
}
|
||||
|
||||
|
@ -289,20 +288,20 @@ namespace Models
|
|||
return BoundCheck(bitdepth, MinBitDepth, MaxBitDepth);
|
||||
}
|
||||
|
||||
public bool BoundCheck(int? num, int min, int max)
|
||||
public bool BoundCheck(int? num, int? min, int? max)
|
||||
{
|
||||
if (max < 0 && min < 0)
|
||||
if (max == null && min == null)
|
||||
return true;
|
||||
if (num == null || num < 0)
|
||||
return AcceptMissingProps;
|
||||
if (num < min || max != -1 && num > max)
|
||||
if (num == null)
|
||||
return AcceptMissingProps == null || AcceptMissingProps.Value;
|
||||
if ((min != null && num < min) || (max != null && num > max))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool BannedUsersSatisfies(SearchResponse? response)
|
||||
{
|
||||
return response == null || !BannedUsers.Any(x => x == response.Username);
|
||||
return response == null || BannedUsers == null || !BannedUsers.Any(x => x == response.Username);
|
||||
}
|
||||
|
||||
public string GetNotSatisfiedName(Soulseek.File file, Track track, SearchResponse? response)
|
||||
|
@ -330,24 +329,4 @@ namespace Models
|
|||
return "Satisfied";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class FileConditionsMod
|
||||
{
|
||||
public int? LengthTolerance = null;
|
||||
public int? MinBitrate = null;
|
||||
public int? MaxBitrate = null;
|
||||
public int? MinSampleRate = null;
|
||||
public int? MaxSampleRate = null;
|
||||
public int? MinBitDepth = null;
|
||||
public int? MaxBitDepth = null;
|
||||
public bool? StrictTitle = null;
|
||||
public bool? StrictArtist = null;
|
||||
public bool? StrictAlbum = null;
|
||||
public string[]? Formats = null;
|
||||
public string[]? BannedUsers = null;
|
||||
public bool? StrictStringDiacrRemove = null;
|
||||
public bool? AcceptNoLength = null;
|
||||
public bool? AcceptMissingProps = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,10 @@ namespace Models
|
|||
public bool sourceCanBeSkipped = false;
|
||||
public bool needSkipExistingAfterSearch = false;
|
||||
public bool gotoNextAfterSearch = false;
|
||||
public bool enablesIndexByDefault = false;
|
||||
public string? defaultFolderName = null;
|
||||
public FileConditionsMod? additionalConds = null;
|
||||
public FileConditionsMod? additionalPrefConds = null;
|
||||
public FileConditions? additionalConds = null;
|
||||
public FileConditions? additionalPrefConds = null;
|
||||
|
||||
public TrackListEntry(TrackType trackType)
|
||||
{
|
||||
|
|
|
@ -21,11 +21,12 @@ static partial class Program
|
|||
public static bool skipUpdate = false;
|
||||
public static bool initialized = false;
|
||||
public static IExtractor? extractor;
|
||||
public static FileSkipper? outputDirSkipper;
|
||||
public static FileSkipper? musicDirSkipper;
|
||||
public static SoulseekClient? client;
|
||||
public static TrackLists? trackLists;
|
||||
public static M3uEditor? m3uEditor;
|
||||
public static M3uEditor? playlistEditor;
|
||||
public static M3uEditor? indexEditor;
|
||||
public static FileSkipper? outputDirSkipper = null;
|
||||
public static FileSkipper? musicDirSkipper = null;
|
||||
public static readonly ConcurrentDictionary<Track, SearchInfo> searches = new();
|
||||
public static readonly ConcurrentDictionary<string, DownloadWrapper> downloads = new();
|
||||
public static readonly ConcurrentDictionary<string, int> userSuccessCount = new();
|
||||
|
@ -53,7 +54,8 @@ static partial class Program
|
|||
trackLists.UpgradeListTypes(Config.I.aggregate, Config.I.album);
|
||||
trackLists.SetListEntryOptions();
|
||||
|
||||
m3uEditor = new M3uEditor(trackLists, Config.I.m3uOption);
|
||||
playlistEditor = new M3uEditor(trackLists, Config.I.writePlaylist ? M3uOption.Playlist : M3uOption.None, Config.I.offset);
|
||||
indexEditor = new M3uEditor(trackLists, Config.I.writeIndex ? M3uOption.Index : M3uOption.None);
|
||||
|
||||
await MainLoop();
|
||||
|
||||
|
@ -108,16 +110,25 @@ static partial class Program
|
|||
{
|
||||
if (Config.I.skipExisting)
|
||||
{
|
||||
var cond = Config.I.skipExistingPrefCond ? Config.I.preferredCond : Config.I.necessaryCond;
|
||||
FileConditions? cond = null;
|
||||
|
||||
outputDirSkipper = FileSkipperRegistry.GetSkipper(Config.I.skipMode, Config.I.parentDir, cond, m3uEditor);
|
||||
|
||||
if (Config.I.musicDir.Length > 0)
|
||||
if (Config.I.skipCheckPrefCond)
|
||||
{
|
||||
if (!Directory.Exists(Config.I.musicDir))
|
||||
cond = Config.I.necessaryCond.With(Config.I.preferredCond);
|
||||
}
|
||||
else if (Config.I.skipCheckCond)
|
||||
{
|
||||
cond = Config.I.necessaryCond;
|
||||
}
|
||||
|
||||
outputDirSkipper = FileSkipperRegistry.GetSkipper(Config.I.skipMode, Config.I.parentDir, cond, indexEditor);
|
||||
|
||||
if (Config.I.skipMusicDir.Length > 0)
|
||||
{
|
||||
if (!Directory.Exists(Config.I.skipMusicDir))
|
||||
Console.WriteLine("Error: Music directory does not exist");
|
||||
else
|
||||
musicDirSkipper = FileSkipperRegistry.GetSkipper(Config.I.skipModeMusicDir, Config.I.musicDir, cond, m3uEditor);
|
||||
musicDirSkipper = FileSkipperRegistry.GetSkipper(Config.I.skipModeMusicDir, Config.I.skipMusicDir, cond, indexEditor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -176,15 +187,30 @@ static partial class Program
|
|||
|
||||
Config.I.AddTemporaryConditions(tle.additionalConds, tle.additionalPrefConds);
|
||||
|
||||
string m3uPath;
|
||||
string m3uPath, indexPath;
|
||||
|
||||
if (Config.I.m3uFilePath.Length > 0)
|
||||
m3uPath = Config.I.m3uFilePath;
|
||||
else
|
||||
m3uPath = Path.Join(Config.I.parentDir, tle.defaultFolderName, "sldl.m3u8");
|
||||
m3uPath = Path.Join(Config.I.parentDir, tle.defaultFolderName, "_playlist.m3u8");
|
||||
|
||||
m3uEditor.option = Config.I.m3uOption;
|
||||
m3uEditor.SetPathAndLoad(m3uPath); // does nothing if the path is the same
|
||||
if (Config.I.indexFilePath.Length > 0)
|
||||
indexPath = Config.I.indexFilePath;
|
||||
else
|
||||
indexPath = Path.Join(Config.I.parentDir, tle.defaultFolderName, "_index.sldl");
|
||||
|
||||
indexEditor.option = Config.I.writeIndex ? M3uOption.Index : M3uOption.None;
|
||||
indexEditor.SetPathAndLoad(indexPath); // does nothing if the path is unchanged
|
||||
|
||||
if (Config.I.writePlaylist)
|
||||
{
|
||||
playlistEditor.option = M3uOption.Playlist;
|
||||
playlistEditor.SetPathAndLoad(m3uPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
playlistEditor.option = M3uOption.None;
|
||||
}
|
||||
|
||||
if (changed || isFirstEntry)
|
||||
{
|
||||
|
@ -305,7 +331,7 @@ static partial class Program
|
|||
{
|
||||
tle.source.State = TrackState.Failed;
|
||||
tle.source.FailureReason = FailureReason.NoSuitableFileFound;
|
||||
m3uEditor.Update();
|
||||
indexEditor.Update();
|
||||
}
|
||||
|
||||
continue;
|
||||
|
@ -329,7 +355,8 @@ static partial class Program
|
|||
continue;
|
||||
}
|
||||
|
||||
m3uEditor.Update();
|
||||
indexEditor.Update();
|
||||
playlistEditor.Update();
|
||||
|
||||
if (tle.source.Type != TrackType.Album)
|
||||
{
|
||||
|
@ -427,7 +454,7 @@ static partial class Program
|
|||
|
||||
static bool SetNotFoundLastTime(Track track)
|
||||
{
|
||||
if (m3uEditor.TryGetPreviousRunResult(track, out var prevTrack))
|
||||
if (indexEditor.TryGetPreviousRunResult(track, out var prevTrack))
|
||||
{
|
||||
if (prevTrack.FailureReason == FailureReason.NoSuitableFileFound || prevTrack.State == TrackState.NotFoundLastTime)
|
||||
{
|
||||
|
@ -451,7 +478,8 @@ static partial class Program
|
|||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
await DownloadTask(tle, track, semaphore, organizer, cts, false, true, true);
|
||||
m3uEditor.Update();
|
||||
indexEditor.Update();
|
||||
playlistEditor.Update();
|
||||
});
|
||||
|
||||
await Task.WhenAll(downloadTasks);
|
||||
|
@ -492,7 +520,7 @@ static partial class Program
|
|||
PrintAlbum(tracks);
|
||||
}
|
||||
|
||||
var semaphore = new SemaphoreSlim(Config.I.concurrentProcesses == -2 ? 1 : 999); // Needs to be uncapped due to a bug that causes album downloads to fail after some time
|
||||
var semaphore = new SemaphoreSlim(999); // Needs to be uncapped due to a bug that causes album downloads to fail after some time
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
|
@ -548,7 +576,8 @@ static partial class Program
|
|||
organizer.OrganizeAlbum(tracks, additionalImages);
|
||||
}
|
||||
|
||||
m3uEditor.Update();
|
||||
indexEditor.Update();
|
||||
playlistEditor.Update();
|
||||
}
|
||||
|
||||
|
||||
|
@ -821,9 +850,12 @@ static partial class Program
|
|||
}
|
||||
|
||||
if (track.State == TrackState.Downloaded && organize)
|
||||
{
|
||||
lock (trackLists)
|
||||
{
|
||||
organizer?.OrganizeAudio(track, chosenFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.I.onComplete.Length > 0)
|
||||
{
|
||||
|
|
|
@ -704,7 +704,7 @@ static class Search
|
|||
.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 => (x.file.Length != null && x.file.Length > 0) || Config.I.preferredCond.AcceptNoLength == null || Config.I.preferredCond.AcceptNoLength.Value)
|
||||
.ThenByDescending(x => !useBracketCheck || FileConditions.BracketCheck(track, inferredTrack(x).Item1)) // downrank result if it contains '(' or '[' and the title does not (avoid remixes)
|
||||
.ThenByDescending(x => Config.I.preferredCond.StrictTitleSatisfies(x.file.Filename, track.Title))
|
||||
.ThenByDescending(x => !albumMode || Config.I.preferredCond.StrictAlbumSatisfies(x.file.Filename, track.Album))
|
||||
|
|
|
@ -282,9 +282,8 @@ namespace Tests
|
|||
{
|
||||
SetCurrentTest("TestM3uEditor");
|
||||
|
||||
Config.I.m3uOption = M3uOption.All;
|
||||
Config.I.skipMode = SkipMode.M3u;
|
||||
Config.I.musicDir = "";
|
||||
Config.I.skipMode = SkipMode.Index;
|
||||
Config.I.skipMusicDir = "";
|
||||
Config.I.printOption = PrintOption.Tracks | PrintOption.Full;
|
||||
Config.I.skipExisting = true;
|
||||
|
||||
|
@ -327,9 +326,9 @@ namespace Tests
|
|||
foreach (var t in toBeDownloadedInitial)
|
||||
trackLists.AddTrackToLast(t);
|
||||
|
||||
Program.m3uEditor = new M3uEditor(path, trackLists, Config.I.m3uOption);
|
||||
Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All);
|
||||
|
||||
Program.outputDirSkipper = new M3uSkipper(Program.m3uEditor, false);
|
||||
Program.outputDirSkipper = new IndexSkipper(Program.indexEditor, false);
|
||||
|
||||
var notFound = (List<Track>)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
|
||||
var existing = (List<Track>)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0] });
|
||||
|
@ -341,7 +340,7 @@ namespace Tests
|
|||
|
||||
Printing.PrintTracksTbd(toBeDownloaded, existing, notFound, TrackType.Normal);
|
||||
|
||||
Program.m3uEditor.Update();
|
||||
Program.indexEditor.Update();
|
||||
string output = File.ReadAllText(path);
|
||||
string need =
|
||||
"#SLDL:./file1.5,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;,,,,-1,0,0,0;" +
|
||||
|
@ -360,7 +359,7 @@ namespace Tests
|
|||
toBeDownloaded[1].FailureReason = FailureReason.NoSuitableFileFound;
|
||||
existing[1].DownloadPath = "/other/new/file/path";
|
||||
|
||||
Program.m3uEditor.Update();
|
||||
Program.indexEditor.Update();
|
||||
output = File.ReadAllText(path);
|
||||
need =
|
||||
"#SLDL:/other/new/file/path,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
|
||||
|
@ -379,11 +378,11 @@ namespace Tests
|
|||
Console.WriteLine();
|
||||
Console.WriteLine(output);
|
||||
|
||||
Program.m3uEditor = new M3uEditor(path, trackLists, Config.I.m3uOption);
|
||||
Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All);
|
||||
|
||||
foreach (var t in trackLists.Flattened(false, false))
|
||||
{
|
||||
Program.m3uEditor.TryGetPreviousRunResult(t, out var prev);
|
||||
Program.indexEditor.TryGetPreviousRunResult(t, out var prev);
|
||||
Assert(prev != null);
|
||||
Assert(prev.ToKey() == t.ToKey());
|
||||
Assert(prev.DownloadPath == t.DownloadPath);
|
||||
|
@ -391,7 +390,7 @@ namespace Tests
|
|||
Assert(prev.FailureReason == t.FailureReason);
|
||||
}
|
||||
|
||||
Program.m3uEditor.Update();
|
||||
Program.indexEditor.Update();
|
||||
output = File.ReadAllText(path);
|
||||
Assert(output == need);
|
||||
|
||||
|
@ -408,9 +407,8 @@ namespace Tests
|
|||
trackLists.AddEntry(new TrackListEntry(t));
|
||||
|
||||
File.WriteAllText(path, "");
|
||||
Config.I.m3uOption = M3uOption.Index;
|
||||
Program.m3uEditor = new M3uEditor(path, trackLists, Config.I.m3uOption);
|
||||
Program.m3uEditor.Update();
|
||||
Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index);
|
||||
Program.indexEditor.Update();
|
||||
|
||||
Assert(File.ReadAllText(path) == "");
|
||||
|
||||
|
@ -420,13 +418,13 @@ namespace Tests
|
|||
test[1].FailureReason = FailureReason.NoSuitableFileFound;
|
||||
test[2].State = TrackState.AlreadyExists;
|
||||
|
||||
Program.m3uEditor.Update();
|
||||
Program.indexEditor.Update();
|
||||
|
||||
Program.m3uEditor = new M3uEditor(path, trackLists, Config.I.m3uOption);
|
||||
Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index);
|
||||
|
||||
foreach (var t in test)
|
||||
{
|
||||
Program.m3uEditor.TryGetPreviousRunResult(t, out var tt);
|
||||
Program.indexEditor.TryGetPreviousRunResult(t, out var tt);
|
||||
Assert(tt != null);
|
||||
Assert(tt.ToKey() == t.ToKey());
|
||||
t.DownloadPath = "this should not change tt.DownloadPath";
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>sldl</AssemblyName>
|
||||
<VersionPrefix>2.3</VersionPrefix>
|
||||
<VersionPrefix>2.3.1</VersionPrefix>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
|
Loading…
Reference in a new issue