diff --git a/README.md b/README.md
index 969b54b..ffd4ffd 100644
--- a/README.md
+++ b/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 [OPTIONS]
-
- Required Arguments
+```
+#### Required Arguments
+```
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
--user Soulseek username
--pass Soulseek password
```
+#### General Options
```
- General Options
-p, --path Download directory
--input-type [csv|youtube|spotify|bandcamp|string|list]
--name-format Name format for downloaded tracks. See --help name-format
@@ -51,22 +53,18 @@ Usage: sldl [OPTIONS]
-c, --config Set config file location. Set to 'none' to ignore config
--profile Configuration profile(s) to use. See --help ""config"".
--concurrent-downloads Max concurrent downloads (default: 2)
- --m3u 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 Override default m3u path
-
- -s, --skip-existing Skip if a track matching file conditions is found in the
- output folder or your music library (if provided)
- --skip-mode [name|tag|m3u|name-cond|tag-cond|m3u-cond]. See --help
- skip-existing.
- --music-dir Specify to also skip downloading tracks found in a music
- library. Use with --skip-existing
+ --write-playlist Create an m3u playlist file in the output directory
+ --playlist-path Override default path for m3u playlist file
+
+ --no-skip-existing Do not skip downloaded tracks
+ --no-write-index Do not create a file indexing all downloaded tracks
+ --index-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 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 for incoming connections (default: 49998)
--on-complete Run a command whenever a file is downloaded.
@@ -87,8 +85,8 @@ Usage: sldl [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 [OPTIONS]
--yt-dlp-argument 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 Max search time in ms (default: 6000)
--max-stale-time Max download time without progress in ms (default: 50000)
@@ -123,23 +121,23 @@ Usage: sldl [OPTIONS]
--searches-renew-time Controls how often available searches are replenished.
See --help "search". (default: 220)
```
+#### Spotify Options
```
- Spotify
--spotify-id Spotify client ID
--spotify-secret Spotify client secret
--spotify-token Spotify access token
--spotify-refresh Spotify refresh token
--remove-from-source Remove downloaded tracks from source playlist
```
+#### YouTube Options
```
- YouTube
--youtube-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 [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 Accepted file format(s), comma-separated, without periods
--length-tol Length tolerance in seconds
--min-bitrate Minimum file bitrate
@@ -183,8 +181,8 @@ Usage: sldl [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 Specify the exact number of tracks in the album. Add a + or
@@ -201,8 +199,8 @@ Usage: sldl [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 Max length tolerance in seconds to consider two tracks or
@@ -212,19 +210,12 @@ Usage: sldl [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
```
@@ -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
```
@@ -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
+input-type = list
+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
diff --git a/slsk-batchdl/Config.cs b/slsk-batchdl/Config.cs
index c35cf0c..c22660c 100644
--- a/slsk-batchdl/Config.cs
+++ b/slsk-batchdl/Config.cs
@@ -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 args, string? cond)> configProfiles = new();
readonly HashSet 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":
diff --git a/slsk-batchdl/Enums.cs b/slsk-batchdl/Enums.cs
index 8aa02cc..ec722a9 100644
--- a/slsk-batchdl/Enums.cs
+++ b/slsk-batchdl/Enums.cs
@@ -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,
}
diff --git a/slsk-batchdl/Extractors/Bandcamp.cs b/slsk-batchdl/Extractors/Bandcamp.cs
index add6709..863a9ed 100644
--- a/slsk-batchdl/Extractors/Bandcamp.cs
+++ b/slsk-batchdl/Extractors/Bandcamp.cs
@@ -58,6 +58,7 @@ namespace Extractors
};
var tle = new TrackListEntry(track);
tle.defaultFolderName = track.Artist;
+ tle.enablesIndexByDefault = true;
trackLists.AddEntry(tle);
}
}
diff --git a/slsk-batchdl/Extractors/Csv.cs b/slsk-batchdl/Extractors/Csv.cs
index 4bc8472..307e5d6 100644
--- a/slsk-batchdl/Extractors/Csv.cs
+++ b/slsk-batchdl/Extractors/Csv.cs
@@ -34,6 +34,7 @@ namespace Extractors
foreach (var tle in trackLists.lists)
{
tle.defaultFolderName = csvName;
+ tle.enablesIndexByDefault = true;
}
return trackLists;
diff --git a/slsk-batchdl/Extractors/List.cs b/slsk-batchdl/Extractors/List.cs
index 3e0436b..c67643e 100644
--- a/slsk-batchdl/Extractors/List.cs
+++ b/slsk-batchdl/Extractors/List.cs
@@ -68,6 +68,7 @@ namespace Extractors
tle.additionalPrefConds = Config.ParseConditions(fields[2]);
tle.defaultFolderName = foldername;
+ tle.enablesIndexByDefault = true;
}
if (tl.lists.Count == 1)
diff --git a/slsk-batchdl/Extractors/Spotify.cs b/slsk-batchdl/Extractors/Spotify.cs
index 9311a93..ea85f62 100644
--- a/slsk-batchdl/Extractors/Spotify.cs
+++ b/slsk-batchdl/Extractors/Spotify.cs
@@ -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();
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);
}
diff --git a/slsk-batchdl/Extractors/YouTube.cs b/slsk-batchdl/Extractors/YouTube.cs
index 8a6012d..131ad3b 100644
--- a/slsk-batchdl/Extractors/YouTube.cs
+++ b/slsk-batchdl/Extractors/YouTube.cs
@@ -64,6 +64,7 @@ namespace Extractors
var tle = new TrackListEntry(TrackType.Normal);
+ tle.enablesIndexByDefault = true;
tle.defaultFolderName = name;
tle.list.Add(tracks);
diff --git a/slsk-batchdl/FileSkipper.cs b/slsk-batchdl/FileSkipper.cs
index f21ff42..0f88c31 100644
--- a/slsk-batchdl/FileSkipper.cs
+++ b/slsk-batchdl/FileSkipper.cs
@@ -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;
diff --git a/slsk-batchdl/Help.cs b/slsk-batchdl/Help.cs
index 759f601..06db8e1 100644
--- a/slsk-batchdl/Help.cs
+++ b/slsk-batchdl/Help.cs
@@ -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 Set config file location. Set to 'none' to ignore config
--profile Configuration profile(s) to use. See --help ""config"".
--concurrent-downloads Max concurrent downloads (default: 2)
- --m3u 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 Override default m3u path
-
- -s, --skip-existing Skip if a track matching file conditions is found in the
- output folder or your music library (if provided)
- --skip-mode [name|tag|m3u|name-cond|tag-cond|m3u-cond]. See --help
- skip-existing.
- --music-dir Specify to also skip downloading tracks found in a music
- library. Use with --skip-existing
+ --write-playlist Create an m3u playlist file in the output directory
+ --playlist-path Override default path for m3u playlist file
+
+ --no-skip-existing Do not skip downloaded tracks
+ --no-write-index Do not create a file indexing all downloaded tracks
+ --index-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 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 for incoming connections (default: 49998)
--on-complete Run a command whenever a file is downloaded.
@@ -89,8 +86,8 @@ public static class Help
--yt-dlp-argument 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 Max search time in ms (default: 6000)
--max-stale-time 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 },
};
diff --git a/slsk-batchdl/M3uEditor.cs b/slsk-batchdl/M3uEditor.cs
index bdacdd7..886a40b 100644
--- a/slsk-batchdl/M3uEditor.cs
+++ b/slsk-batchdl/M3uEditor.cs
@@ -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 lines;
bool needFirstUpdate = false;
+ int offset = 0;
readonly TrackLists trackLists;
readonly Dictionary 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,22 +221,42 @@ 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);
- WriteSldlLine(writer);
- foreach (var line in lines)
+ //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)
{
- writer.Write(line);
- writer.Write('\n');
+ WriteSldlLine(writer);
}
+
+ if (option != M3uOption.Index)
+ {
+ foreach (var line in lines)
+ {
+ writer.Write(line);
+ writer.Write('\n');
+ }
+ }
+
+ File.WriteAllText(path, writer.ToString());
}
- private void WriteSldlLine(StreamWriter writer)
+ private void WriteSldlLine(Writer writer)
{
// Format:
// #SLDL:;; ...
@@ -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();
}
diff --git a/slsk-batchdl/Models/FileConditions.cs b/slsk-batchdl/Models/FileConditions.cs
index 12b109a..cc1fe47 100644
--- a/slsk-batchdl/Models/FileConditions.cs
+++ b/slsk-batchdl/Models/FileConditions.cs
@@ -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();
- public string[] BannedUsers = Array.Empty();
- 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);
- }
- return false;
+ 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;
- }
}
diff --git a/slsk-batchdl/Models/TrackListEntry.cs b/slsk-batchdl/Models/TrackListEntry.cs
index 42714bd..6db4dfe 100644
--- a/slsk-batchdl/Models/TrackListEntry.cs
+++ b/slsk-batchdl/Models/TrackListEntry.cs
@@ -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)
{
diff --git a/slsk-batchdl/Program.cs b/slsk-batchdl/Program.cs
index 0120099..8df0931 100644
--- a/slsk-batchdl/Program.cs
+++ b/slsk-batchdl/Program.cs
@@ -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 searches = new();
public static readonly ConcurrentDictionary downloads = new();
public static readonly ConcurrentDictionary 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();
}
@@ -822,7 +851,10 @@ static partial class Program
if (track.State == TrackState.Downloaded && organize)
{
- organizer?.OrganizeAudio(track, chosenFile);
+ lock (trackLists)
+ {
+ organizer?.OrganizeAudio(track, chosenFile);
+ }
}
if (Config.I.onComplete.Length > 0)
diff --git a/slsk-batchdl/Search.cs b/slsk-batchdl/Search.cs
index 9a2dde0..3e1eaa1 100644
--- a/slsk-batchdl/Search.cs
+++ b/slsk-batchdl/Search.cs
@@ -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))
diff --git a/slsk-batchdl/Tests/Test.cs b/slsk-batchdl/Tests/Test.cs
index cf2fa5b..01ddf10 100644
--- a/slsk-batchdl/Tests/Test.cs
+++ b/slsk-batchdl/Tests/Test.cs
@@ -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)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
var existing = (List)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";
diff --git a/slsk-batchdl/slsk-batchdl.csproj b/slsk-batchdl/slsk-batchdl.csproj
index 10c6f13..d9b6ca4 100644
--- a/slsk-batchdl/slsk-batchdl.csproj
+++ b/slsk-batchdl/slsk-batchdl.csproj
@@ -6,7 +6,7 @@
enable
enable
sldl
- 2.3
+ 2.3.1