1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 06:22:41 +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:
fiso64 2024-10-10 21:41:55 +02:00
parent caa5bfc58d
commit a66f2aad03
17 changed files with 368 additions and 372 deletions

124
README.md
View file

@ -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
-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
--write-playlist Create an m3u playlist file in the output directory
--playlist-path <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 <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
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

View file

@ -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":

View file

@ -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,
}

View file

@ -58,6 +58,7 @@ namespace Extractors
};
var tle = new TrackListEntry(track);
tle.defaultFolderName = track.Artist;
tle.enablesIndexByDefault = true;
trackLists.AddEntry(tle);
}
}

View file

@ -34,6 +34,7 @@ namespace Extractors
foreach (var tle in trackLists.lists)
{
tle.defaultFolderName = csvName;
tle.enablesIndexByDefault = true;
}
return trackLists;

View file

@ -68,6 +68,7 @@ namespace Extractors
tle.additionalPrefConds = Config.ParseConditions(fields[2]);
tle.defaultFolderName = foldername;
tle.enablesIndexByDefault = true;
}
if (tl.lists.Count == 1)

View file

@ -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);
}

View file

@ -64,6 +64,7 @@ namespace Extractors
var tle = new TrackListEntry(TrackType.Normal);
tle.enablesIndexByDefault = true;
tle.defaultFolderName = name;
tle.list.Add(tracks);

View file

@ -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;

View file

@ -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
-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
--write-playlist Create an m3u playlist file in the output directory
--playlist-path <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 <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 },
};

View file

@ -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,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:<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();
}

View file

@ -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);
}
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;
}
}

View file

@ -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)
{

View file

@ -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();
}
@ -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)

View file

@ -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))

View file

@ -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";

View file

@ -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'">