diff --git a/README.md b/README.md
index 3b5875a..0967f97 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
An automatic downloader for Soulseek built with Soulseek.NET. Accepts CSV files as well as Spotify and YouTube urls.
Supports playlist and album downloads; selects the best files according to user-configured file conditions and heuristics.
-See the usage [examples](#examples-1).
+See the [usage examples](#examples-1).
## Index
- [Options](#options)
@@ -68,14 +68,15 @@ Usage: sldl [OPTIONS]
--listen-port Port for incoming connections (default: 49998)
--on-complete Run a command whenever a file is downloaded.
- Available placeholders: {path} (local save path), {title},
+ Available placeholders: {path} (local path),{title},{row}
{artist},{album},{uri},{length},{failure-reason},{state}.
Prepend a state number to only run in specific cases:
1:, 2:, 3:, 4: for the Downloaded, Failed, Exists, and
NotFoundLastTime states respectively.
E.g: '1:' will only run the command if the file is
downloaded successfully. Prepend 's:' to use the system
- shell to execute the command.
+ shell to execute the command. Prepend 'a:' to run it only
+ whenever an album downloads or fails.
--print Print tracks or search results instead of downloading:
'tracks': Print all tracks to be downloaded
@@ -198,7 +199,7 @@ Usage: sldl [OPTIONS]
the directory fails to download. Set to 'delete' to delete
the files instead. Set to 'disable' keep it where it is.
Default: {configured output dir}/failed
- --album-parallel-search Run album searches in parallel
+ --album-parallel-search Run album searches in parallel, then download sequentially.
```
#### Aggregate Download Options
```
@@ -274,33 +275,22 @@ Name of the track, album, or artist to search for: Can either be any typical sea
(like what you would enter into the soulseek search bar), or a comma-separated list of
properties like 'title=Song Name, artist=Artist Name, length=215'.
-The following properties are accepted:
-```
-title
-artist
-album
-length (in seconds)
-artist-maybe-wrong
-album-track-count
-```
+The following properties are accepted: title, artist, album, length (in seconds),
+artist-maybe-wrong, album-track-count.
+
Example inputs and their interpretations:
- ```
-Input String | Artist | Title | Album | Length
----------------------------------------------------------------------------------
-'Foo Bar' (without any hyphens) | | Foo Bar | |
-'Foo - Bar' | Foo | Bar | |
-'Foo - Bar' (with --album enabled) | Foo | | Bar |
-'Artist - Title, length=42' | Artist | Title | | 42
-'artist=AR, title=T, album=AL' | AR | T | AL |
-```
+| Input String | Artist | Title | Album | Length |
+|-----------------------------------------|----------|----------|----------|--------|
+| 'Foo Bar' (without any hyphens) | | Foo Bar | | |
+| 'Foo - Bar' | Foo | Bar | | |
+| 'Foo - Bar' (with --album enabled) | Foo | | Bar | |
+| 'Artist - Title, length=42' | Artist | Title | | 42 |
+| 'artist=AR, title=T, album=AL' | AR | T | AL | |
### List
A path to a text file where each line has the following form:
-```
-"some input" "conditions" "preferred conditions"
-```
-e.g:
-```
+```bash
+# input conditions pref. conditions
"artist=Artist, album=Album" "format=mp3; br > 128" "br >= 320"
```
Where "some input" is any of the above input types. The quotes can be omitted if the field
@@ -389,7 +379,7 @@ a file that only satisfies strict-title (if enabled) will always be preferred ov
only satisfies the format condition. Run with --print "results-full" to reveal the sorting logic.
Conditions can also be supplied as a semicolon-delimited string with --cond and --pref, e.g
---cond "br >= 320; format = mp3,ogg; sr < 96000".
+`--cond "br >= 320; format = mp3,ogg; sr < 96000"`.
### Filtering irrelevant results
The options --strict-title, --strict-artist and --strict-album will filter any file that
@@ -418,13 +408,13 @@ Name format supports subdirectories as well as conditional expressions like {tag
tag1 is null, use tag2. String literals enclosed in parentheses are ignored in the null check.
### Examples:
- - "{artist} - {title}"
+ - `{artist} - {title}`
Always name it 'Artist - Title'. Because some files on Soulseek are untagged, the
following is generally preferred:
- - "{artist( - )title|filename}"
+ - `{artist( - )title|filename}`
If artist and title are not null, name it 'Artist - Title', otherwise use the original
filename.
- - "{albumartist(/)album(/)track(. )title|(missing-tags/)foldername(/)filename}"
+ - `{albumartist(/)album(/)track(. )title|(missing-tags/)foldername(/)filename}`
Sort files into artist/album folders if all tags are present, otherwise put them in
the 'missing-tags' folder.
@@ -466,7 +456,7 @@ pref-format = flac
fast-search = true
```
Lines starting with hashtags (#) will be ignored. Tildes in paths are expanded as the user
- directory.
+ directory. The path variable `{bindir}` stores the directory of the sldl binary.
### Configuration profiles:
Profiles are supported:
@@ -547,12 +537,12 @@ sldl "artist=MC MENTAL" -a -g -t
#### Advanced example: Automatic wishlist downloader
Create a file named `wishlist.txt`, and add some items as detailed in [Input types: List](#list):
-```bash
-"Artist - My Favorite Song"
-a:"Artist - Some Album, album-track-count=5" "format=flac"
+```
+"Artist - My Favorite Song" "format=flac"
+a:"Artist - Some Album, album-track-count=5"
```
Add a profile to your `sldl.conf`:
-```
+```bash
[wishlist]
input = ~/sldl/wishlist.txt
input-type = list
@@ -618,7 +608,7 @@ Example => Run `sldl` every Sunday at 1am, search for missing tracks from the sp
```
# min hour day month weekday command
-0 1 * * 0 sldl https://open.spotify.com/playlist/6sf1WR5grXGJ6dET -c /config -p /data --skip-existing --m3u-path /data/index.sldl"
+0 1 * * 0 sldl https://open.spotify.com/playlist/6sf1WR5grXGJ6dET -c /config -p /data --index-path /data/index.sldl
```
[crontab.guru](https://crontab.guru/) could be used to help with the scheduling expression.
diff --git a/publish.bat b/publish.bat
index c919e17..5d63b94 100644
--- a/publish.bat
+++ b/publish.bat
@@ -1,33 +1,22 @@
@echo off
setlocal
-set DOTNET_CLI_TELEMETRY_OPTOUT=1
+set FRAMEWORK=net6.0
if not exist slsk-batchdl\bin\zips mkdir slsk-batchdl\bin\zips
-
-REM win-x86
-dotnet publish -c Release -r win-x86 -p:PublishSingleFile=true -p:DefineConstants=WINDOWS --self-contained false
-if exist slsk-batchdl\bin\Release\net6.0\win-x86\publish\*.pdb del /F /Q slsk-batchdl\bin\Release\net6.0\win-x86\publish\*.pdb
-if exist slsk-batchdl\bin\zips\sldl_win-x86.zip del /F /Q slsk-batchdl\bin\zips\sldl_win-x86.zip
-powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::CreateFromDirectory('slsk-batchdl\bin\Release\net6.0\win-x86\publish', 'slsk-batchdl\bin\zips\sldl_win-x86.zip'); }"
-
-REM win-x86 self-contained
-dotnet publish -c Release -r win-x86 -p:PublishSingleFile=true -p:PublishTrimmed=true -p:DefineConstants=WINDOWS --self-contained true
-if exist slsk-batchdl\bin\Release\net6.0\win-x86\publish\*.pdb del /F /Q slsk-batchdl\bin\Release\net6.0\win-x86\publish\*.pdb
-if exist slsk-batchdl\bin\zips\sldl_win-x86_self-contained.zip del /F /Q slsk-batchdl\bin\zips\sldl_win-x86_self-contained.zip
-powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::CreateFromDirectory('slsk-batchdl\bin\Release\net6.0\win-x86\publish', 'slsk-batchdl\bin\zips\sldl_win-x86_self-contained.zip'); }"
-
-REM linux-x64
-dotnet publish -c Release -r linux-x64 -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true
-if exist slsk-batchdl\bin\Release\net6.0\linux-x64\publish\*.pdb del /F /Q slsk-batchdl\bin\Release\net6.0\linux-x64\publish\*.pdb
-if exist slsk-batchdl\bin\zips\sldl_linux-x64.zip del /F /Q slsk-batchdl\bin\zips\sldl_linux-x64.zip
-powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::CreateFromDirectory('slsk-batchdl\bin\Release\net6.0\linux-x64\publish', 'slsk-batchdl\bin\zips\sldl_linux-x64.zip'); }"
-
-REM linux-arm
-dotnet publish -c Release -r linux-arm -p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true
-if exist slsk-batchdl\bin\Release\net6.0\linux-arm\publish\*.pdb del /F /Q slsk-batchdl\bin\Release\net6.0\linux-arm\publish\*.pdb
-if exist slsk-batchdl\bin\zips\sldl_linux-arm.zip del /F /Q slsk-batchdl\bin\zips\sldl_linux-arm.zip
-powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::CreateFromDirectory('slsk-batchdl\bin\Release\net6.0\linux-arm\publish', 'slsk-batchdl\bin\zips\sldl_linux-arm.zip'); }"
+call :publish_and_zip win-x86 false sldl_win-x86.zip
+call :publish_and_zip win-x86 true sldl_win-x86_self-contained.zip
+call :publish_and_zip linux-x64 true sldl_linux-x64.zip
+call :publish_and_zip linux-arm true sldl_linux-arm.zip
endlocal
+exit /b
+
+:publish_and_zip
+dotnet publish -c Release -r %1 -p:PublishSingleFile=true -p:PublishTrimmed=%2 -p:DefineConstants=WINDOWS --self-contained=%2
+if exist slsk-batchdl\bin\Release\%FRAMEWORK%\%1\publish\*.pdb del /F /Q slsk-batchdl\bin\Release\%FRAMEWORK%\%1\publish\*.pdb
+if exist slsk-batchdl\bin\zips\%3 del /F /Q slsk-batchdl\bin\zips\%3
+powershell.exe -nologo -noprofile -command "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::CreateFromDirectory('slsk-batchdl\bin\Release\%FRAMEWORK%\%1\publish', 'slsk-batchdl\bin\zips\%3'); }"
+exit /b
+
diff --git a/slsk-batchdl/Config.cs b/slsk-batchdl/Config.cs
index 7224479..d2b0fff 100644
--- a/slsk-batchdl/Config.cs
+++ b/slsk-batchdl/Config.cs
@@ -9,7 +9,7 @@ public class Config
{
public FileConditions necessaryCond = new()
{
- Formats = new string[] { ".mp3", ".flac", ".ogg", ".m4a", ".opus", ".wav", ".aac", ".alac" },
+ Formats = new string[] { "mp3", "flac", "ogg", "m4a", "opus", "wav", "aac", "alac" },
};
public FileConditions preferredCond = new()
@@ -175,11 +175,6 @@ public class Config
ProcessArgs(arguments);
}
- public Config()
- {
-
- }
-
public Config Copy() // deep copies all fields except configProfiles and arguments
{
var copy = (Config)this.MemberwiseClone();
@@ -209,9 +204,12 @@ public class Config
if (idx != -1)
{
- confPath = Utils.ExpandUser(args[idx + 1]);
confPathChanged = true;
+ if (confPath == "none")
+ return;
+
+ confPath = Utils.ExpandUser(args[idx + 1]);
if(File.Exists(Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath)))
confPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath);
}
@@ -389,7 +387,7 @@ public class Config
foreach (var (name, args) in toApply)
{
- Console.WriteLine($"Applying auto profile: {name}");
+ tle.AddPrintLine($"Applying auto profile: {name}");
ProcessArgs(args);
appliedProfiles.Add(name);
}
@@ -421,7 +419,7 @@ public class Config
appliedProfiles.Add(name);
}
else
- Console.WriteLine($"Error: No profile '{name}' found in config");
+ Console.WriteLine($"Warning: No profile '{name}' found in config");
}
}
}
@@ -563,7 +561,7 @@ public class Config
}
- public static FileConditions ParseConditions(string input)
+ public static FileConditions ParseConditions(string input, Track? track = null)
{
static void UpdateMinMax(string value, string condition, ref int? min, ref int? max)
{
@@ -579,6 +577,15 @@ public class Config
min = max = int.Parse(value);
}
+ static void UpdateMinMax2(string value, string condition, ref int min, ref int max)
+ {
+ int? nullableMin = min;
+ int? nullableMax = max;
+ UpdateMinMax(value, condition, ref nullableMin, ref nullableMax);
+ min = nullableMin ?? min;
+ max = nullableMax ?? max;
+ }
+
var cond = new FileConditions();
var tr = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries;
@@ -639,6 +646,10 @@ public class Config
case "acceptmissingprops":
cond.AcceptMissingProps = bool.Parse(value);
break;
+ case "albumtrackcount":
+ if (track != null)
+ UpdateMinMax2(value, condition, ref track.MinAlbumTrackCount, ref track.MaxAlbumTrackCount);
+ break;
default:
throw new ArgumentException($"Unknown condition '{condition}'");
}
@@ -722,6 +733,10 @@ public class Config
case "--config":
confPath = args[++i];
break;
+ case "--nc":
+ case "--no-config":
+ confPath = "none";
+ break;
case "--smd":
case "--skip-music-dir":
skipMusicDir = args[++i];
diff --git a/slsk-batchdl/Extractors/Csv.cs b/slsk-batchdl/Extractors/Csv.cs
index 3433e69..1223b44 100644
--- a/slsk-batchdl/Extractors/Csv.cs
+++ b/slsk-batchdl/Extractors/Csv.cs
@@ -50,9 +50,9 @@ namespace Extractors
{
string[] lines = File.ReadAllLines(csvFilePath, System.Text.Encoding.UTF8);
- if (track.CsvRow > -1 && track.CsvRow < lines.Length)
+ if (track.CsvOrListRow > -1 && track.CsvOrListRow < lines.Length)
{
- lines[track.CsvRow] = new string(',', Math.Max(0, csvColumnCount - 1));
+ lines[track.CsvOrListRow] = new string(',', Math.Max(0, csvColumnCount - 1));
Utils.WriteAllLines(csvFilePath, lines, '\n');
}
}
@@ -135,7 +135,7 @@ namespace Extractors
csvColumnCount = values.Count;
var desc = "";
- var track = new Track() { CsvRow = index };
+ var track = new Track() { CsvOrListRow = index };
if (artistIndex >= 0) track.Artist = values[artistIndex];
if (trackIndex >= 0) track.Title = values[trackIndex];
diff --git a/slsk-batchdl/Extractors/List.cs b/slsk-batchdl/Extractors/List.cs
index ec4ffed..b1c7a00 100644
--- a/slsk-batchdl/Extractors/List.cs
+++ b/slsk-batchdl/Extractors/List.cs
@@ -66,16 +66,20 @@ namespace Extractors
foreach (var tle in tl.lists)
{
if (fields.Count >= 2)
- tle.extractorCond = Config.ParseConditions(fields[1]);
+ {
+ tle.extractorCond = Config.ParseConditions(fields[1], tle.source);
+ }
if (fields.Count >= 3)
+ {
tle.extractorPrefCond = Config.ParseConditions(fields[2]);
+ }
tle.defaultFolderName = foldername;
tle.enablesIndexByDefault = true;
}
if (tl.lists.Count == 1)
- tl[0].source.CsvRow = i;
+ tl[0].source.CsvOrListRow = i;
trackLists.lists.AddRange(tl.lists);
@@ -138,9 +142,9 @@ namespace Extractors
{
string[] lines = File.ReadAllLines(listFilePath, Encoding.UTF8);
- if (track.CsvRow > -1 && track.CsvRow < lines.Length)
+ if (track.CsvOrListRow > -1 && track.CsvOrListRow < lines.Length)
{
- lines[track.CsvRow] = "";
+ lines[track.CsvOrListRow] = "";
Utils.WriteAllLines(listFilePath, lines, '\n');
}
}
diff --git a/slsk-batchdl/Extractors/YouTube.cs b/slsk-batchdl/Extractors/YouTube.cs
index f10ed6a..25a5adb 100644
--- a/slsk-batchdl/Extractors/YouTube.cs
+++ b/slsk-batchdl/Extractors/YouTube.cs
@@ -648,8 +648,8 @@ namespace Extractors
public static async Task YtdlpDownload(string id, string savePathNoExt, string ytdlpArgument = "", bool printCommand = false)
{
- Process process = new Process();
- ProcessStartInfo startInfo = new ProcessStartInfo();
+ var process = new Process();
+ var startInfo = new ProcessStartInfo();
if (ytdlpArgument.Length == 0)
ytdlpArgument = "\"{id}\" -f bestaudio/best -ci -o \"{savepath-noext}.%(ext)s\" -x";
diff --git a/slsk-batchdl/FileManager.cs b/slsk-batchdl/FileManager.cs
index c9a7bc7..457c250 100644
--- a/slsk-batchdl/FileManager.cs
+++ b/slsk-batchdl/FileManager.cs
@@ -54,7 +54,7 @@ public class FileManager
this.defaultFolderName = defaultFolderName != null ? Utils.NormalizedPath(defaultFolderName) : null;
}
- public void OrganizeAlbum(List tracks, List? additionalImages, bool remainingOnly = true)
+ public void OrganizeAlbum(Track source, List tracks, List? additionalImages, bool remainingOnly = true)
{
foreach (var track in tracks.Where(t => !t.IsNotAudio))
{
@@ -64,6 +64,8 @@ public class FileManager
OrganizeAudio(track, track.FirstDownload);
}
+ source.DownloadPath = Utils.GreatestCommonDirectory(tracks.Where(t => !t.IsNotAudio).Select(t => t.DownloadPath));
+
bool onlyAdditionalImages = config.nameFormat.Length == 0;
var nonAudioToOrganize = onlyAdditionalImages ? additionalImages : tracks.Where(t => t.IsNotAudio);
diff --git a/slsk-batchdl/FileSkipper.cs b/slsk-batchdl/FileSkipper.cs
index 0f88c31..a5e5de6 100644
--- a/slsk-batchdl/FileSkipper.cs
+++ b/slsk-batchdl/FileSkipper.cs
@@ -6,22 +6,46 @@ namespace FileSkippers
{
public static class FileSkipperRegistry
{
- public static FileSkipper GetSkipper(SkipMode mode, string dir, FileConditions? conditions, M3uEditor indexEditor)
+ public static FileSkipper GetSkipper(SkipMode mode, string dir, bool useConditions)
{
- bool useConditions = conditions != null && !conditions.Equals(new FileConditions());
return mode switch
{
- 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),
+ SkipMode.Name => useConditions ? new NameConditionalSkipper(dir) : new NameSkipper(dir),
+ SkipMode.Tag => useConditions ? new TagConditionalSkipper(dir) : new TagSkipper(dir),
+ SkipMode.Index => useConditions ? new IndexConditionalSkipper() : new IndexSkipper(),
_ => throw new ArgumentException("Invalid SkipMode")
};
}
}
+ public struct FileSkipperContext
+ {
+ public FileConditions? conditions;
+ public M3uEditor? indexEditor;
+ public bool checkFileExists;
+
+ public static FileSkipperContext FromTrackListEntry(TrackListEntry tle)
+ {
+ FileConditions? cond = null;
+ if (tle.config.skipCheckPrefCond)
+ cond = tle.config.necessaryCond.With(tle.config.preferredCond);
+ else if (tle.config.skipCheckCond)
+ cond = tle.config.necessaryCond;
+
+ var context = new FileSkipperContext
+ {
+ checkFileExists = cond != null,
+ indexEditor = tle.indexEditor,
+ conditions = cond,
+ };
+
+ return context;
+ }
+ }
+
public abstract class FileSkipper
{
- public abstract bool TrackExists(Track track, out string? foundPath);
+ public abstract bool TrackExists(Track track, FileSkipperContext context, out string? foundPath);
public virtual void BuildIndex() { IndexIsBuilt = true; }
public bool IndexIsBuilt { get; protected set; } = false;
}
@@ -72,7 +96,7 @@ namespace FileSkippers
IndexIsBuilt = true;
}
- public override bool TrackExists(Track track, out string? foundPath)
+ public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{
foundPath = null;
@@ -101,12 +125,10 @@ namespace FileSkippers
readonly string[] ignore = new string[] { "_", "-", ".", "(", ")", "[", "]" };
readonly string dir;
readonly List<(string, string, SimpleFile)> index = new(); // (PreprocessedPath, PreprocessedName, file)
- FileConditions conditions;
- public NameConditionalSkipper(string dir, FileConditions conditions)
+ public NameConditionalSkipper(string dir)
{
this.dir = dir;
- this.conditions = conditions;
}
private string Preprocess(string s, bool removeSlash)
@@ -148,7 +170,7 @@ namespace FileSkippers
IndexIsBuilt = true;
}
- public override bool TrackExists(Track track, out string? foundPath)
+ public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{
foundPath = null;
@@ -160,7 +182,7 @@ namespace FileSkippers
foreach ((var ppath, var pname, var musicFile) in index)
{
- if (pname.ContainsWithBoundary(title) && ppath.ContainsWithBoundary(artist) && conditions.FileSatisfies(musicFile, track))
+ if (pname.ContainsWithBoundary(title) && ppath.ContainsWithBoundary(artist) && context.conditions.FileSatisfies(musicFile, track))
{
foundPath = musicFile.Path;
return true;
@@ -214,7 +236,7 @@ namespace FileSkippers
IndexIsBuilt = true;
}
- public override bool TrackExists(Track track, out string? foundPath)
+ public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{
foundPath = null;
@@ -241,12 +263,10 @@ namespace FileSkippers
{
readonly string dir;
readonly List<(string, string, SimpleFile)> index = new(); // (PreprocessedArtist, PreprocessedTitle, file)
- FileConditions conditions;
- public TagConditionalSkipper(string dir, FileConditions conditions)
+ public TagConditionalSkipper(string dir)
{
this.dir = dir;
- this.conditions = conditions;
}
private string Preprocess(string s)
@@ -281,7 +301,7 @@ namespace FileSkippers
IndexIsBuilt = true;
}
- public override bool TrackExists(Track track, out string? foundPath)
+ public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{
foundPath = null;
@@ -293,7 +313,7 @@ namespace FileSkippers
foreach ((var partist, var ptitle, var musicFile) in index)
{
- if (title == ptitle && partist.Contains(artist) && conditions.FileSatisfies(musicFile, track))
+ if (title == ptitle && partist.Contains(artist) && context.conditions.FileSatisfies(musicFile, track))
{
foundPath = musicFile.Path;
return true;
@@ -306,23 +326,18 @@ namespace FileSkippers
public class IndexSkipper : FileSkipper
{
- M3uEditor indexEditor;
- bool checkFileExists;
-
- public IndexSkipper(M3uEditor m3UEditor, bool checkFileExists)
+ public IndexSkipper()
{
- this.indexEditor = m3UEditor;
- this.checkFileExists = checkFileExists;
IndexIsBuilt = true;
}
- public override bool TrackExists(Track track, out string? foundPath)
+ public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{
foundPath = null;
- var t = indexEditor.PreviousRunResult(track);
+ var t = context.indexEditor.PreviousRunResult(track);
if (t != null && (t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists))
{
- if (checkFileExists)
+ if (context.checkFileExists)
{
if (t.DownloadPath.Length == 0)
return false;
@@ -348,20 +363,15 @@ namespace FileSkippers
public class IndexConditionalSkipper : FileSkipper
{
- M3uEditor indexEditor;
- FileConditions conditions;
-
- public IndexConditionalSkipper(M3uEditor m3UEditor, FileConditions conditions)
+ public IndexConditionalSkipper()
{
- this.indexEditor = m3UEditor;
- this.conditions = conditions;
IndexIsBuilt = true;
}
- public override bool TrackExists(Track track, out string? foundPath)
+ public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{
foundPath = null;
- var t = indexEditor.PreviousRunResult(track);
+ var t = context.indexEditor.PreviousRunResult(track);
if (t == null || t.DownloadPath.Length == 0)
return false;
@@ -375,7 +385,7 @@ namespace FileSkippers
try
{
musicFile = TagLib.File.Create(t.DownloadPath);
- if (conditions.FileSatisfies(musicFile, track, false))
+ if (context.conditions.FileSatisfies(musicFile, track, false))
{
foundPath = t.DownloadPath;
return true;
@@ -415,7 +425,7 @@ namespace FileSkippers
try { musicFile = TagLib.File.Create(path); }
catch { return false; }
- if (!conditions.FileSatisfies(musicFile, track))
+ if (!context.conditions.FileSatisfies(musicFile, track))
return false;
}
}
diff --git a/slsk-batchdl/Help.cs b/slsk-batchdl/Help.cs
index 9ae75db..0757d66 100644
--- a/slsk-batchdl/Help.cs
+++ b/slsk-batchdl/Help.cs
@@ -44,14 +44,15 @@ public static class Help
--listen-port Port for incoming connections (default: 49998)
--on-complete Run a command whenever a file is downloaded.
- Available placeholders: {path} (local save path), {title},
+ Available placeholders: {path} (local path),{title},{row}
{artist},{album},{uri},{length},{failure-reason},{state}.
Prepend a state number to only run in specific cases:
1:, 2:, 3:, 4: for the Downloaded, Failed, Exists, and
NotFoundLastTime states respectively.
E.g: '1:' will only run the command if the file is
downloaded successfully. Prepend 's:' to use the system
- shell to execute the command.
+ shell to execute the command. Prepend 'a:' to run it only
+ whenever an album downloads or fails.
--print Print tracks or search results instead of downloading:
'tracks': Print all tracks to be downloaded
@@ -168,7 +169,7 @@ public static class Help
the directory fails to download. Set to 'delete' to delete
the files instead. Set to the empty string """" to disable.
Default: {configured output dir}/failed
- --album-parallel-search Run album searches in parallel
+ --album-parallel-search Run album searches in parallel, then download sequentially.
Aggregate Download
-g, --aggregate Aggregate download mode: Find and download all distinct
@@ -249,13 +250,8 @@ public static class Help
(like what you would enter into the soulseek search bar), or a comma-separated list of
properties like 'title=Song Name, artist=Artist Name, length=215'.
- The following properties are accepted:
- title
- artist
- album
- length (in seconds)
- artist-maybe-wrong
- album-track-count
+ The following properties are accepted: title, artist, album, length (in seconds),
+ artist-maybe-wrong, album-track-count.
Example inputs and their interpretations:
Input String | Artist | Title | Album | Length
@@ -271,7 +267,7 @@ public static class Help
""some input"" ""conditions"" ""preferred conditions""
- e.g:
+ for example:
""artist=Artist, album=Album"" ""format=mp3; br > 128"" ""br >= 320""
@@ -447,7 +443,7 @@ public static class Help
fast-search = true
Lines starting with hashtags (#) will be ignored. Tildes in paths are expanded as the user
- directory.
+ directory. The path variable {bindir} stores the directory of the sldl binary.
Configuration profiles:
Profiles are supported:
diff --git a/slsk-batchdl/M3uEditor.cs b/slsk-batchdl/M3uEditor.cs
index 933b2c8..6edefd6 100644
--- a/slsk-batchdl/M3uEditor.cs
+++ b/slsk-batchdl/M3uEditor.cs
@@ -17,7 +17,7 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
private readonly object locker = new();
- public M3uEditor(TrackLists trackLists, M3uOption option, int offset = 0)
+ private M3uEditor(TrackLists trackLists, M3uOption option, int offset = 0)
{
this.trackLists = trackLists;
this.option = option;
@@ -25,12 +25,12 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
this.needFirstUpdate = option == M3uOption.All || option == M3uOption.Playlist;
}
- public M3uEditor(string path, TrackLists trackLists, M3uOption option) : this(trackLists, option)
+ public M3uEditor(string path, TrackLists trackLists, M3uOption option, bool loadPreviousResults) : this(trackLists, option)
{
- SetPathAndLoad(path);
+ SetPathAndLoad(path, loadPreviousResults);
}
- public void SetPathAndLoad(string path)
+ private void SetPathAndLoad(string path, bool loadPreviousResults)
{
if (string.IsNullOrEmpty(path))
return;
@@ -42,87 +42,104 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
parent = Utils.NormalizedPath(Path.GetDirectoryName(this.path));
lines = ReadAllLines().ToList();
- LoadPreviousResults();
+
+ if (loadPreviousResults)
+ LoadPreviousResults();
}
private void LoadPreviousResults()
{
- // Format:
- // #SLDL:;; ...
- // where = filepath,artist,album,title,length(int),tracktype(int),state(int),failurereason(int)
-
- if (lines.Count == 0 || !lines[0].StartsWith("#SLDL:"))
+ if (lines.Count == 0 || !lines.Any(x => x.Trim() != ""))
return;
- string sldlLine = lines[0];
- lines = lines.Skip(1).ToList();
+ bool useOldFormat = lines[0].StartsWith("#SLDL:");
- int k = "#SLDL:".Length;
+ var indexLines = useOldFormat ? new string[] { lines[0] } : lines.Skip(1);
var currentItem = new StringBuilder();
- bool inQuotes = false;
+
+ if (useOldFormat) lines = lines.Skip(1).ToList();
+ int offset = useOldFormat ? "#SLDL:".Length : 0;
- for (; k < sldlLine.Length && sldlLine[k] == ' '; k++);
-
- for (; k < sldlLine.Length; k++)
+ foreach (var sldlLine in indexLines)
{
- var track = new Track();
- int field = 0;
- for (int i = k; i < sldlLine.Length; i++)
- {
- char c = sldlLine[i];
+ if (string.IsNullOrWhiteSpace(sldlLine))
+ continue;
- if (c == '"' && (i == k || sldlLine[i - 1] != '\\'))
+ int k = offset;
+ bool inQuotes = false;
+
+ for (; k < sldlLine.Length && sldlLine[k] == ' '; k++);
+
+ for (; k < sldlLine.Length; k++)
+ {
+ var track = new Track();
+ int field = 0;
+ for (int i = k; i < sldlLine.Length; i++)
{
- if (inQuotes && i + 1 < sldlLine.Length && sldlLine[i + 1] == '"')
+ char c = sldlLine[i];
+
+ if (c == '"' && (i == k || sldlLine[i - 1] != '\\'))
{
- currentItem.Append('"');
- i++;
+ if (inQuotes && i + 1 < sldlLine.Length && sldlLine[i + 1] == '"')
+ {
+ currentItem.Append('"');
+ i++;
+ }
+ else
+ {
+ inQuotes = !inQuotes;
+ }
+ }
+ else if (field <= 6 && c == ',' && !inQuotes)
+ {
+ var x = currentItem.ToString();
+
+ if (field == 0)
+ {
+ if (x.StartsWith("./"))
+ x = Path.Join(parent, x[2..]);
+ track.DownloadPath = x;
+ }
+ else if (field == 1)
+ track.Artist = x;
+ else if (field == 2)
+ track.Album = x;
+ else if (field == 3)
+ track.Title = x;
+ else if (field == 4)
+ track.Length = int.Parse(x);
+ else if (field == 5)
+ track.Type = (TrackType)int.Parse(currentItem.ToString());
+ else if (field == 6)
+ track.State = (TrackState)int.Parse(x);
+
+ currentItem.Clear();
+ field++;
+ }
+ else if (field == 7 && c == ';' && useOldFormat)
+ {
+ track.FailureReason = (FailureReason)int.Parse(currentItem.ToString());
+ currentItem.Clear();
+ k = i;
+ break;
}
else
{
- inQuotes = !inQuotes;
+ currentItem.Append(c);
}
}
- else if (field <= 6 && c == ',' && !inQuotes)
- {
- var x = currentItem.ToString();
- if (field == 0)
- {
- if (x.StartsWith("./"))
- x = Path.Join(parent, x[2..]);
- track.DownloadPath = x;
- }
- else if (field == 1)
- track.Artist = x;
- else if (field == 2)
- track.Album = x;
- else if (field == 3)
- track.Title = x;
- else if (field == 4)
- track.Length = int.Parse(x);
- else if (field == 5)
- track.Type = (TrackType)int.Parse(currentItem.ToString());
- else if (field == 6)
- track.State = (TrackState)int.Parse(x);
-
- currentItem.Clear();
- field++;
- }
- else if (field == 7 && c == ';')
+ if (!useOldFormat)
{
track.FailureReason = (FailureReason)int.Parse(currentItem.ToString());
currentItem.Clear();
- k = i;
- break;
}
- else
- {
- currentItem.Append(c);
- }
- }
- previousRunData[track.ToKey()] = track;
+ previousRunData[track.ToKey()] = track;
+
+ if (!useOldFormat)
+ break;
+ }
}
}
@@ -261,10 +278,6 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
private void WriteSldlLine(Writer writer)
{
- // Format:
- // #SLDL:;; ...
- // where = filepath,artist,album,title,length(int),tracktype(int),state(int),failurereason(int)
-
void writeCsvLine(string[] items)
{
bool comma = false;
@@ -288,7 +301,8 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
}
}
- writer.Write("#SLDL:");
+ //writer.Write("#SLDL:");
+ writer.Write("filepath,artist,album,title,length,tracktype,state,failurereason\n");
foreach (var val in previousRunData.Values)
{
@@ -309,7 +323,8 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
};
writeCsvLine(items);
- writer.Write(';');
+ //writer.Write(';');
+ writer.Write('\n');
}
writer.Write('\n');
diff --git a/slsk-batchdl/Models/Track.cs b/slsk-batchdl/Models/Track.cs
index 0f525a2..38d2260 100644
--- a/slsk-batchdl/Models/Track.cs
+++ b/slsk-batchdl/Models/Track.cs
@@ -16,7 +16,7 @@ namespace Models
public bool IsNotAudio = false;
public string DownloadPath = "";
public string Other = "";
- public int CsvRow = -1;
+ public int CsvOrListRow = -1;
public TrackType Type = TrackType.Normal;
public FailureReason FailureReason = FailureReason.None;
public TrackState State = TrackState.Initial;
diff --git a/slsk-batchdl/Models/TrackListEntry.cs b/slsk-batchdl/Models/TrackListEntry.cs
index 4e064a4..68d90ee 100644
--- a/slsk-batchdl/Models/TrackListEntry.cs
+++ b/slsk-batchdl/Models/TrackListEntry.cs
@@ -24,6 +24,8 @@ namespace Models
public bool CanParallelSearch => source.Type == TrackType.Album || source.Type == TrackType.Aggregate;
+ private List? printLines = null;
+
public TrackListEntry(TrackType trackType)
{
list = new List>();
@@ -76,5 +78,20 @@ namespace Models
else
list[0].Add(track);
}
+
+ public void AddPrintLine(string line)
+ {
+ if (printLines == null)
+ printLines = new List();
+ printLines.Add(line);
+ }
+
+ public void PrintLines()
+ {
+ if (printLines == null) return;
+ foreach (var line in printLines)
+ Console.WriteLine(line);
+ printLines = null;
+ }
}
}
diff --git a/slsk-batchdl/Program.cs b/slsk-batchdl/Program.cs
index 1cb5bfe..351662e 100644
--- a/slsk-batchdl/Program.cs
+++ b/slsk-batchdl/Program.cs
@@ -55,7 +55,9 @@ static partial class Program
trackLists.UpgradeListTypes(config.aggregate, config.album);
trackLists.SetListEntryOptions();
- await MainLoop(config);
+ PrepareListEntries(config);
+
+ await MainLoop();
WriteLineIf("Mainloop done", config.debugInfo);
}
@@ -104,125 +106,36 @@ static partial class Program
}
- static void InitEditors(TrackListEntry tle, Config config)
+ static void PreprocessTracks(TrackListEntry tle)
{
- tle.playlistEditor = new M3uEditor(trackLists, config.writePlaylist ? M3uOption.Playlist : M3uOption.None, config.offset);
- tle.indexEditor = new M3uEditor(trackLists, config.writeIndex ? M3uOption.Index : M3uOption.None);
- }
-
- static void InitFileSkippers(TrackListEntry tle, Config config)
- {
- if (config.skipExisting)
+ static void preprocessTrack(Config config, Track track)
{
- FileConditions? cond = null;
-
- if (config.skipCheckPrefCond)
+ if (config.removeFt)
{
- cond = config.necessaryCond.With(config.preferredCond);
+ track.Title = track.Title.RemoveFt();
+ track.Artist = track.Artist.RemoveFt();
}
- else if (config.skipCheckCond)
+ if (config.removeBrackets)
{
- cond = config.necessaryCond;
+ track.Title = track.Title.RemoveSquareBrackets();
+ }
+ if (config.regexToReplace.Title.Length + config.regexToReplace.Artist.Length + config.regexToReplace.Album.Length > 0)
+ {
+ track.Title = Regex.Replace(track.Title, config.regexToReplace.Title, config.regexReplaceBy.Title);
+ track.Artist = Regex.Replace(track.Artist, config.regexToReplace.Artist, config.regexReplaceBy.Artist);
+ track.Album = Regex.Replace(track.Album, config.regexToReplace.Album, config.regexReplaceBy.Album);
+ }
+ if (config.artistMaybeWrong)
+ {
+ track.ArtistMaybeWrong = true;
}
- tle.outputDirSkipper = FileSkipperRegistry.GetSkipper(config.skipMode, config.parentDir, cond, tle.indexEditor);
-
- if (config.skipMusicDir.Length > 0)
- {
- if (!Directory.Exists(config.skipMusicDir))
- Console.WriteLine("Error: Music directory does not exist");
- else
- tle.musicDirSkipper = FileSkipperRegistry.GetSkipper(config.skipModeMusicDir, config.skipMusicDir, cond, tle.indexEditor);
- }
+ track.Artist = track.Artist.Trim();
+ track.Album = track.Album.Trim();
+ track.Title = track.Title.Trim();
}
- }
- static void InitConfigs(Config defaultConfig)
- {
- //if (trackLists.Count == 0)
- // return;
-
- //foreach (var tle in trackLists.lists)
- //{
- // tle.config = defaultConfig.Copy();
- // tle.config.UpdateProfiles(tle);
-
- // if (tle.extractorCond != null)
- // {
- // tle.config.necessaryCond = tle.config.necessaryCond.With(tle.extractorCond);
- // tle.extractorCond = null;
- // }
- // if (tle.extractorPrefCond != null)
- // {
- // tle.config.preferredCond = tle.config.preferredCond.With(tle.extractorPrefCond);
- // tle.extractorPrefCond = null;
- // }
-
- // initEditors(tle, tle.config);
- // initFileSkippers(tle, tle.config);
- //}
-
- //defaultConfig.UpdateProfiles(trackLists[0]);
- //trackLists[0].config = defaultConfig;
- //initEditors(trackLists[0], defaultConfig);
- //initFileSkippers(trackLists[0], defaultConfig);
-
- //var configs = new Dictionary() { { defaultConfig, trackLists[0] } };
-
- //// configs, skippers, and editors are assigned to every individual tle (since they may change based
- //// on auto-profiles). This loop re-uses existing configs/skippers/editors whenever autoprofiles
- //// don't change. Otherwise, a new file skipper would be created for every tle, and would require
- //// indexing every time, even if the directory to be indexed is unchanged.
- //foreach (var tle in trackLists.lists.Skip(1))
- //{
- // bool needUpdate = true;
-
- // foreach (var (config, exampleTle) in configs)
- // {
- // if (!config.NeedUpdateProfiles(tle))
- // {
- // tle.config = config;
-
- // if (exampleTle == null)
- // {
- // initEditors(tle, config);
- // initFileSkippers(tle, config);
- // configs[config] = tle;
- // }
- // else
- // {
- // tle.playlistEditor = exampleTle.playlistEditor;
- // tle.indexEditor = exampleTle.indexEditor;
- // tle.outputDirSkipper = exampleTle.outputDirSkipper;
- // tle.musicDirSkipper = exampleTle.musicDirSkipper;
- // }
-
- // needUpdate = false;
- // break;
- // }
- // }
-
- // bool hasExtractorConditions = tle.extractorCond != null || tle.extractorPrefCond != null;
-
- // if (!needUpdate)
- // continue;
-
- // var newConfig = defaultConfig.Copy();
- // newConfig.UpdateProfiles(tle);
- // configs[newConfig] = tle;
-
- // tle.config = newConfig;
-
- // // todo: only create new instances if a relevant config item has changed
- // initEditors(tle, newConfig);
- // initFileSkippers(tle, newConfig);
- //}
- }
-
-
- static void PreprocessTracks(Config config, TrackListEntry tle)
- {
- PreprocessTrack(config, tle.source);
+ preprocessTrack(tle.config, tle.source);
for (int k = 0; k < tle.list.Count; k++)
{
@@ -230,101 +143,127 @@ static partial class Program
{
for (int i = 0; i < ls.Count; i++)
{
- PreprocessTrack(config, ls[i]);
+ preprocessTrack(tle.config, ls[i]);
}
}
}
}
-
- static void PreprocessTrack(Config config, Track track)
+
+ static void PrepareListEntries(Config startConfig)
{
- if (config.removeFt)
- {
- track.Title = track.Title.RemoveFt();
- track.Artist = track.Artist.RemoveFt();
- }
- if (config.removeBrackets)
- {
- track.Title = track.Title.RemoveSquareBrackets();
- }
- if (config.regexToReplace.Title.Length + config.regexToReplace.Artist.Length + config.regexToReplace.Album.Length > 0)
- {
- track.Title = Regex.Replace(track.Title, config.regexToReplace.Title, config.regexReplaceBy.Title);
- track.Artist = Regex.Replace(track.Artist, config.regexToReplace.Artist, config.regexReplaceBy.Artist);
- track.Album = Regex.Replace(track.Album, config.regexToReplace.Album, config.regexReplaceBy.Album);
- }
- if (config.artistMaybeWrong)
- {
- track.ArtistMaybeWrong = true;
- }
+ var editors = new Dictionary<(string path, M3uOption option), M3uEditor>();
+ var skippers = new Dictionary<(string dir, SkipMode mode, bool checkCond), FileSkipper>();
- track.Artist = track.Artist.Trim();
- track.Album = track.Album.Trim();
- track.Title = track.Title.Trim();
+ foreach (var tle in trackLists.lists)
+ {
+ tle.config = startConfig.Copy();
+ tle.config.UpdateProfiles(tle);
+ startConfig = tle.config;
+
+ if (tle.extractorCond != null)
+ {
+ tle.config.necessaryCond.AddConditions(tle.extractorCond);
+ tle.extractorCond = null;
+ }
+ if (tle.extractorPrefCond != null)
+ {
+ tle.config.preferredCond.AddConditions(tle.extractorPrefCond);
+ tle.extractorPrefCond = null;
+ }
+
+ var indexOption = tle.config.writeIndex ? M3uOption.Index : M3uOption.None;
+ if (indexOption != M3uOption.None || (tle.config.skipExisting && tle.config.skipMode == SkipMode.Index) || tle.config.skipNotFound)
+ {
+ string indexPath;
+ if (tle.config.indexFilePath.Length > 0)
+ indexPath = tle.config.indexFilePath;
+ else
+ indexPath = Path.Join(tle.config.parentDir, tle.defaultFolderName, "_index.sldl");
+
+ if (editors.TryGetValue((indexPath, indexOption), out var indexEditor))
+ {
+ tle.indexEditor = indexEditor;
+ }
+ else
+ {
+ tle.indexEditor = new M3uEditor(indexPath, trackLists, indexOption, true);
+ editors.Add((indexPath, indexOption), tle.indexEditor);
+ }
+ }
+
+ var playlistOption = tle.config.writePlaylist ? M3uOption.Playlist : M3uOption.None;
+ if (playlistOption != M3uOption.None)
+ {
+ string m3uPath;
+ if (tle.config.m3uFilePath.Length > 0)
+ m3uPath = tle.config.m3uFilePath;
+ else
+ m3uPath = Path.Join(tle.config.parentDir, tle.defaultFolderName, "_playlist.m3u8");
+
+ if (editors.TryGetValue((m3uPath, playlistOption), out var playlistEditor))
+ {
+ tle.playlistEditor = playlistEditor;
+ }
+ else
+ {
+ tle.playlistEditor = new M3uEditor(m3uPath, trackLists, playlistOption, false);
+ editors.Add((m3uPath, playlistOption), tle.playlistEditor);
+ }
+ }
+
+ if (tle.config.skipExisting)
+ {
+ bool checkCond = tle.config.skipCheckCond || tle.config.skipCheckPrefCond;
+
+ if (skippers.TryGetValue((tle.config.parentDir, tle.config.skipMode, checkCond), out var outputDirSkipper))
+ {
+ tle.outputDirSkipper = outputDirSkipper;
+ }
+ else
+ {
+ tle.outputDirSkipper = FileSkipperRegistry.GetSkipper(tle.config.skipMode, tle.config.parentDir, checkCond);
+ skippers.Add((tle.config.parentDir, tle.config.skipMode, checkCond), tle.outputDirSkipper);
+ }
+
+ if (tle.config.skipMusicDir.Length > 0)
+ {
+ if (skippers.TryGetValue((tle.config.skipMusicDir, tle.config.skipModeMusicDir, checkCond), out var musicDirSkipper))
+ {
+ tle.musicDirSkipper = musicDirSkipper;
+ }
+ else
+ {
+ tle.musicDirSkipper = FileSkipperRegistry.GetSkipper(tle.config.skipModeMusicDir, tle.config.skipMusicDir, checkCond);
+ skippers.Add((tle.config.skipMusicDir, tle.config.skipModeMusicDir, checkCond), tle.musicDirSkipper);
+ }
+ }
+ }
+ }
}
- static void PrepareListEntry(Config prevConfig, TrackListEntry tle)
- {
- tle.config = prevConfig.Copy();
- tle.config.UpdateProfiles(tle);
-
- if (tle.extractorCond != null)
- {
- tle.config.necessaryCond = tle.config.necessaryCond.With(tle.extractorCond);
- tle.extractorCond = null;
- }
- if (tle.extractorPrefCond != null)
- {
- tle.config.preferredCond = tle.config.preferredCond.With(tle.extractorPrefCond);
- tle.extractorPrefCond = null;
- }
-
- InitEditors(tle, tle.config);
- InitFileSkippers(tle, tle.config);
-
- string m3uPath, indexPath;
-
- if (tle.config.m3uFilePath.Length > 0)
- m3uPath = tle.config.m3uFilePath;
- else
- m3uPath = Path.Join(tle.config.parentDir, tle.defaultFolderName, "_playlist.m3u8");
-
- if (tle.config.indexFilePath.Length > 0)
- indexPath = tle.config.indexFilePath;
- else
- indexPath = Path.Join(tle.config.parentDir, tle.defaultFolderName, "_index.sldl");
-
- if (tle.config.writePlaylist)
- tle.playlistEditor?.SetPathAndLoad(m3uPath);
- if (tle.config.writeIndex)
- tle.indexEditor?.SetPathAndLoad(indexPath);
-
- PreprocessTracks(tle.config, tle);
- }
-
-
- static async Task MainLoop(Config defaultConfig)
+ static async Task MainLoop()
{
if (trackLists.Count == 0) return;
- PrepareListEntry(defaultConfig, trackLists[0]);
- var firstConfig = trackLists.lists[0].config;
-
- bool enableParallelSearch = firstConfig.parallelAlbumSearch && !firstConfig.PrintResults && !firstConfig.PrintTracks && trackLists.lists.Any(x => x.CanParallelSearch);
+ var tle0 = trackLists.lists[0];
+ bool enableParallelSearch = tle0.config.parallelAlbumSearch && !tle0.config.PrintResults && !tle0.config.PrintTracks && trackLists.lists.Any(x => x.CanParallelSearch);
var parallelSearches = new List<(TrackListEntry tle, Task<(bool, ResponseData)> task)>();
- var parallelSearchSemaphore = new SemaphoreSlim(firstConfig.parallelAlbumSearchProcesses);
+ var parallelSearchSemaphore = new SemaphoreSlim(tle0.config.parallelAlbumSearchProcesses);
+
+ tle0.PrintLines();
for (int i = 0; i < trackLists.lists.Count; i++)
{
if (!enableParallelSearch) Console.WriteLine();
- if (i > 0) PrepareListEntry(trackLists[i-1].config, trackLists[i]);
-
var tle = trackLists[i];
var config = tle.config;
+ PreprocessTracks(tle);
+ if (!enableParallelSearch) tle.PrintLines();
+
var existing = new List();
var notFound = new List();
@@ -342,13 +281,13 @@ static partial class Program
if (config.skipExisting && !config.PrintResults && tle.source.State != TrackState.NotFoundLastTime)
{
- if (tle.sourceCanBeSkipped && SetExisting(tle, config, tle.source))
+ if (tle.sourceCanBeSkipped && SetExisting(tle, FileSkipperContext.FromTrackListEntry(tle), tle.source))
existing.Add(tle.source);
if (tle.source.State != TrackState.AlreadyExists && !tle.needSourceSearch)
{
foreach (var tracks in tle.list)
- existing.AddRange(DoSkipExisting(tle, config, tracks));
+ existing.AddRange(DoSkipExisting(tle, tracks));
}
}
@@ -393,7 +332,8 @@ static partial class Program
await parallelSearchSemaphore.WaitAsync();
progress = enableParallelSearch ? Printing.GetProgressBar(config) : null;
- Printing.RefreshOrPrint(progress, 0, $" {tle.source.Type} download: {tle.source.ToString(true)}, searching..", print: true);
+ var part = progress == null ? "" : " ";
+ Printing.RefreshOrPrint(progress, 0, $"{part}{tle.source.Type} download: {tle.source.ToString(true)}, searching..", print: true);
bool foundSomething = false;
var responseData = new ResponseData();
@@ -460,7 +400,7 @@ static partial class Program
if (config.skipExisting && tle.needSkipExistingAfterSearch)
{
foreach (var tracks in tle.list)
- existing.AddRange(DoSkipExisting(tle, config, tracks));
+ existing.AddRange(DoSkipExisting(tle, tracks));
}
if (tle.gotoNextAfterSearch)
@@ -557,7 +497,7 @@ static partial class Program
if (tle.config.skipExisting && tle.needSkipExistingAfterSearch)
{
foreach (var tracks in tle.list)
- DoSkipExisting(tle, tle.config, tracks);
+ DoSkipExisting(tle, tracks);
}
}
}
@@ -567,12 +507,13 @@ static partial class Program
}
- static List DoSkipExisting(TrackListEntry tle, Config config, List tracks)
+ static List DoSkipExisting(TrackListEntry tle, List tracks)
{
+ var context = FileSkipperContext.FromTrackListEntry(tle);
var existing = new List();
foreach (var track in tracks)
{
- if (SetExisting(tle, config, track))
+ if (SetExisting(tle, context, track))
{
existing.Add(track);
}
@@ -581,7 +522,7 @@ static partial class Program
}
- static bool SetExisting(TrackListEntry tle, Config config, Track track)
+ static bool SetExisting(TrackListEntry tle, FileSkipperContext context, Track track)
{
string? path = null;
@@ -590,7 +531,7 @@ static partial class Program
if (!tle.outputDirSkipper.IndexIsBuilt)
tle.outputDirSkipper.BuildIndex();
- tle.outputDirSkipper.TrackExists(track, out path);
+ tle.outputDirSkipper.TrackExists(track, context, out path);
}
if (path == null && tle.musicDirSkipper != null)
@@ -601,7 +542,7 @@ static partial class Program
tle.musicDirSkipper.BuildIndex();
}
- tle.musicDirSkipper.TrackExists(track, out path);
+ tle.musicDirSkipper.TrackExists(track, context, out path);
}
if (path != null)
@@ -753,11 +694,13 @@ static partial class Program
if (tracks != null && tle.source.DownloadPath.Length > 0)
{
- organizer.OrganizeAlbum(tracks, additionalImages);
+ organizer.OrganizeAlbum(tle.source, tracks, additionalImages);
}
tle.indexEditor?.Update();
tle.playlistEditor?.Update();
+
+ OnComplete(config, config.onComplete, tle.source, true);
}
@@ -1046,10 +989,7 @@ static partial class Program
}
}
- if (config.onComplete.Length > 0)
- {
- OnComplete(config, config.onComplete, track);
- }
+ OnComplete(config, config.onComplete, track, false);
semaphore.Release();
}
@@ -1300,48 +1240,63 @@ static partial class Program
}
- static void OnComplete(Config config, string onComplete, Track track)
+ static void OnComplete(Config config, string onComplete, Track track, bool isAlbumOnComplete)
{
if (onComplete.Length == 0)
return;
bool useShellExecute = false;
+ bool createNoWindow = false;
int count = 0;
- while (onComplete.Length > 2 && count++ < 2)
+ while (onComplete.Length > 2 && count++ < 4)
{
- if (onComplete[0] == 's' && onComplete[1] == ':')
+ if (onComplete[1] == ':')
{
- useShellExecute = true;
- }
- else if (onComplete[0].IsDigit() && onComplete[1] == ':')
- {
- if ((int)track.State != int.Parse(onComplete[0].ToString()))
- return;
+ if (onComplete[0] == 's')
+ {
+ useShellExecute = true;
+ }
+ else if (onComplete[0] == 'a')
+ {
+ if (!isAlbumOnComplete) return;
+ }
+ else if (onComplete[0] == 'w')
+ {
+ createNoWindow = true;
+ }
+ else if (onComplete[0].IsDigit())
+ {
+ if ((int)track.State != int.Parse(onComplete[0].ToString())) return;
+ }
+
+ onComplete = onComplete[2..];
}
else
{
break;
}
- onComplete = onComplete[2..];
}
var process = new Process();
var startInfo = new ProcessStartInfo();
- onComplete = onComplete.Replace("{title}", track.Title)
- .Replace("{artist}", track.Artist)
- .Replace("{album}", track.Album)
- .Replace("{uri}", track.URI)
- .Replace("{length}", track.Length.ToString())
- .Replace("{artist-maybe-wrong}", track.ArtistMaybeWrong.ToString())
- .Replace("{type}", track.Type.ToString())
- .Replace("{is-not-audio}", track.IsNotAudio.ToString())
- .Replace("{failure-reason}", track.FailureReason.ToString())
- .Replace("{path}", track.DownloadPath)
- .Replace("{state}", track.State.ToString())
- .Replace("{extractor}", config.inputType.ToString())
- .Trim();
+ onComplete = onComplete
+ .Replace("{title}", track.Title)
+ .Replace("{artist}", track.Artist)
+ .Replace("{album}", track.Album)
+ .Replace("{uri}", track.URI)
+ .Replace("{length}", track.Length.ToString())
+ .Replace("{row}", (track.CsvOrListRow == -1 ? -1 : track.CsvOrListRow + 1).ToString())
+ .Replace("{artist-maybe-wrong}", track.ArtistMaybeWrong.ToString())
+ .Replace("{type}", track.Type.ToString())
+ .Replace("{is-not-audio}", track.IsNotAudio.ToString())
+ .Replace("{failure-reason}", track.FailureReason.ToString())
+ .Replace("{path}", track.DownloadPath.TrimEnd('/').TrimEnd('\\'))
+ .Replace("{state}", track.State.ToString())
+ .Replace("{extractor}", config.inputType.ToString())
+ .Replace("{bindir}", AppDomain.CurrentDomain.BaseDirectory.TrimEnd('/').TrimEnd('\\'))
+ .Trim();
if (onComplete[0] == '"')
{
@@ -1367,6 +1322,8 @@ static partial class Program
{
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
+ if (!createNoWindow)
+ startInfo.CreateNoWindow = false;
}
startInfo.UseShellExecute = useShellExecute;
diff --git a/slsk-batchdl/Search.cs b/slsk-batchdl/Search.cs
index f5cce52..e670ccc 100644
--- a/slsk-batchdl/Search.cs
+++ b/slsk-batchdl/Search.cs
@@ -224,7 +224,7 @@ static class Search
{
string saveFilePathNoExt = organizer.GetSavePathNoExt(title);
downloading = 1;
- Printing.RefreshOrPrint(progress, 0, $"yt-dlp download: {track}, filename: {saveFilePathNoExt}", true);
+ Printing.RefreshOrPrint(progress, 0, $"yt-dlp download: {track}", true);
saveFilePath = await Extractors.YouTube.YtdlpDownload(id, saveFilePathNoExt, config.ytdlpArgument, printCommand: config.debugInfo);
Printing.RefreshOrPrint(progress, 100, $"Succeded: yt-dlp completed download for {track}", true);
break;
diff --git a/slsk-batchdl/Tests/Test.cs b/slsk-batchdl/Tests/Test.cs
index a5d3984..9db79b1 100644
--- a/slsk-batchdl/Tests/Test.cs
+++ b/slsk-batchdl/Tests/Test.cs
@@ -13,9 +13,9 @@ namespace Tests
public static async Task RunAllTests()
{
TestStringUtils();
- //TestAutoProfiles();
- //TestProfileConditions();
- //await TestStringExtractor();
+ TestAutoProfiles();
+ TestProfileConditions();
+ await TestStringExtractor();
//TestM3uEditor();
Console.WriteLine('\n' + new string('#', 50) + '\n' + "All tests passed.");
@@ -75,366 +75,357 @@ namespace Tests
Passed();
}
- //public static void TestAutoProfiles()
- //{
- // SetCurrentTest("TestAutoProfiles");
-
- // var config = new Config();
- // config.inputType = InputType.YouTube;
- // config.interactiveMode = true;
- // config.aggregate = false;
- // config.maxStaleTime = 50000;
-
- // string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf");
-
- // string content =
- // "max-stale-time = 5" +
- // "\nfast-search = true" +
- // "\nformat = flac" +
-
- // "\n[profile-true-1]" +
- // "\nprofile-cond = input-type == \"youtube\" && download-mode == \"album\"" +
- // "\nmax-stale-time = 10" +
-
- // "\n[profile-true-2]" +
- // "\nprofile-cond = !aggregate" +
- // "\nfast-search = false" +
-
- // "\n[profile-false-1]" +
- // "\nprofile-cond = input-type == \"string\"" +
- // "\nformat = mp3" +
-
- // "\n[profile-no-cond]" +
- // "\nformat = opus";
-
- // File.WriteAllText(path, content);
-
- // config.LoadAndParse(new string[] { "-c", path });
-
- // var tle = new TrackListEntry(TrackType.Album);
- // Config.UpdateProfiles(tle);
-
- // Assert(config.maxStaleTime == 10 && !config.fastSearch && config.necessaryCond.Formats[0] == "flac");
-
- // ResetConfig();
- // config.inputType = InputType.CSV;
- // config.album = true;
- // config.interactiveMode = true;
- // config.useYtdlp = false;
- // config.maxStaleTime = 50000;
- // content =
- // "\n[no-stale]" +
- // "\nprofile-cond = interactive && download-mode == \"album\"" +
- // "\nmax-stale-time = 999999" +
- // "\n[youtube]" +
- // "\nprofile-cond = input-type == \"youtube\"" +
- // "\nyt-dlp = true";
-
- // File.WriteAllText(path, content);
-
-
- // config.LoadAndParse(new string[] { "-c", path });
- // Config.UpdateProfiles(tle);
- // Assert(config.maxStaleTime == 999999 && !config.useYtdlp);
-
- // ResetConfig();
- // config.inputType = InputType.YouTube;
- // config.album = false;
- // config.interactiveMode = true;
- // config.useYtdlp = false;
- // config.maxStaleTime = 50000;
- // content =
- // "\n[no-stale]" +
- // "\nprofile-cond = interactive && download-mode == \"album\"" +
- // "\nmax-stale-time = 999999" +
- // "\n[youtube]" +
- // "\nprofile-cond = input-type == \"youtube\"" +
- // "\nyt-dlp = true";
-
- // File.WriteAllText(path, content);
- // config.LoadAndParse(new string[] { "-c", path });
- // Config.UpdateProfiles(new TrackListEntry(TrackType.Normal));
-
- // Assert(config.maxStaleTime == 50000 && config.useYtdlp);
-
- // if (File.Exists(path))
- // File.Delete(path);
-
- // Passed();
- //}
-
- //public static void TestProfileConditions()
- //{
- // SetCurrentTest("TestProfileConditions");
-
- // config.inputType = InputType.YouTube;
- // config.interactiveMode = true;
- // config.album = true;
- // config.aggregate = false;
-
- // var conds = new (bool, string)[]
- // {
- // (true, "input-type == \"youtube\""),
- // (true, "download-mode == \"album\""),
- // (false, "aggregate"),
- // (true, "interactive"),
- // (true, "album"),
- // (false, "!interactive"),
- // (true, "album && input-type == \"youtube\""),
- // (false, "album && input-type != \"youtube\""),
- // (false, "(interactive && aggregate)"),
- // (true, "album && (interactive || aggregate)"),
- // (true, "input-type == \"spotify\" || aggregate || input-type == \"csv\" || interactive && album"),
- // (true, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || interactive ) )"),
- // (false, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || !interactive ) )"),
- // };
-
- // foreach ((var b, var c) in conds)
- // {
- // Console.WriteLine(c);
- // Assert(b == config.ProfileConditionSatisfied(c));
- // }
-
- // Passed();
- //}
-
- //public static async Task TestStringExtractor()
- //{
- // SetCurrentTest("TestStringExtractor");
-
- // var strings = new List()
- // {
- // "Some Title",
- // "Some, Title",
- // "artist = Some artist, title = some title",
- // "Artist - Title, length = 42",
- // "title=Some, Title, artist=Some, Artist, album = Some, Album, length= 42",
- // "Some, Artist = a - Some, Title = b, album = Some, Album, length = 42",
-
- // "Foo Bar",
- // "Foo - Bar",
- // "Artist - Title, length=42",
- // "title=Title, artist=Artist, length=42",
- // };
-
- // var tracks = new List()
- // {
- // new Track() { Title="Some Title" },
- // new Track() { Title="Some, Title" },
- // new Track() { Title = "some title", Artist = "Some artist" },
- // new Track() { Title = "Title", Artist = "Artist", Length = 42 },
- // new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42 },
- // new Track() { Title="Some, Title = b", Artist = "Some, Artist = a", Album = "Some, Album", Length = 42 },
-
- // new Track() { Title = "Foo Bar" },
- // new Track() { Title = "Bar", Artist = "Foo" },
- // new Track() { Title = "Title", Artist = "Artist", Length = 42 },
- // new Track() { Title = "Title", Artist = "Artist", Length = 42 },
- // };
-
- // var albums = new List()
- // {
- // new Track() { Album="Some Title", Type = TrackType.Album },
- // new Track() { Album="Some, Title", Type = TrackType.Album },
- // new Track() { Title = "some title", Artist = "Some artist", Type = TrackType.Album },
- // new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
- // new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42, Type = TrackType.Album },
- // new Track() { Artist = "Some, Artist = a", Album = "Some, Album", Length = 42, Type = TrackType.Album },
-
- // new Track() { Album = "Foo Bar", Type = TrackType.Album },
- // new Track() { Album = "Bar", Artist = "Foo", Type = TrackType.Album },
- // new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
- // new Track() { Title = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
- // };
-
- // var extractor = new Extractors.StringExtractor();
-
- // config.aggregate = false;
- // config.album = false;
-
- // Console.WriteLine("Testing songs: ");
- // for (int i = 0; i < strings.Count; i++)
- // {
- // config.input = strings[i];
- // Console.WriteLine(config.input);
- // var res = await extractor.GetTracks(config.input, 0, 0, false);
- // var t = res[0].list[0][0];
- // Assert(Extractors.StringExtractor.InputMatches(config.input));
- // Assert(t.ToKey() == tracks[i].ToKey());
- // }
-
- // Console.WriteLine();
- // Console.WriteLine("Testing albums");
- // config.album = true;
- // for (int i = 0; i < strings.Count; i++)
- // {
- // config.input = strings[i];
- // Console.WriteLine(config.input);
- // var t = (await extractor.GetTracks(config.input, 0, 0, false))[0].source;
- // Assert(Extractors.StringExtractor.InputMatches(config.input));
- // Assert(t.ToKey() == albums[i].ToKey());
- // }
-
- // Passed();
- //}
-
- //public static void TestM3uEditor()
- //{
- // SetCurrentTest("TestM3uEditor");
-
- // config.skipMode = SkipMode.Index;
- // config.skipMusicDir = "";
- // config.printOption = PrintOption.Tracks | PrintOption.Full;
- // config.skipExisting = true;
-
- // string path = Path.Join(Directory.GetCurrentDirectory(), "test_m3u.m3u8");
-
- // if (File.Exists(path))
- // File.Delete(path);
-
- // File.WriteAllText(path, $"#SLDL:" +
- // $"{Path.Join(Directory.GetCurrentDirectory(), "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;");
-
- // var notFoundInitial = new List()
- // {
- // new() { Artist = "Artist; ,3", Title = "Title3 ;a" },
- // new() { Artist = "Artist,,, ;4", Title = "Title4", State = TrackState.Failed, FailureReason = FailureReason.NoSuitableFileFound }
- // };
- // var existingInitial = new List()
- // {
- // new() { Artist = "Artist, 1", Title = "Title, , 1", DownloadPath = "path/to/file1", State = TrackState.Downloaded },
- // new() { Artist = "Artist, 1.5", Title = "Title, , 1.5", DownloadPath = Path.Join(Directory.GetCurrentDirectory(), "file1.5"), State = TrackState.Downloaded },
- // new() { Artist = "Artist, 2", Title = "Title2", DownloadPath = "path/to/file2", State = TrackState.AlreadyExists }
- // };
- // var toBeDownloadedInitial = new List()
- // {
- // new() { Artist = "ArtistA", Album = "Albumm", Title = "TitleA" },
- // new() { Artist = "ArtistB", Album = "Albumm", Title = "TitleB" }
- // };
-
- // var trackLists = new TrackLists();
- // trackLists.AddEntry(new TrackListEntry(TrackType.Normal));
- // foreach (var t in notFoundInitial)
- // trackLists.AddTrackToLast(t);
- // foreach (var t in existingInitial)
- // trackLists.AddTrackToLast(t);
- // foreach (var t in toBeDownloadedInitial)
- // trackLists.AddTrackToLast(t);
-
- // Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All);
-
- // 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] });
- // var toBeDownloaded = trackLists[0].list[0].Where(t => t.State == TrackState.Initial).ToList();
-
- // Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial));
- // Assert(existing.SequenceEqualUpToPermutation(existingInitial));
- // Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
-
- // Printing.PrintTracksTbd(toBeDownloaded, existing, notFound, TrackType.Normal);
-
- // 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;" +
- // "\n" +
- // "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
- // "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
- // "\npath/to/file1" +
- // "\nfile1.5" +
- // "\npath/to/file2" +
- // "\n";
- // Assert(output == need);
-
- // toBeDownloaded[0].State = TrackState.Downloaded;
- // toBeDownloaded[0].DownloadPath = "new/file/path";
- // toBeDownloaded[1].State = TrackState.Failed;
- // toBeDownloaded[1].FailureReason = FailureReason.NoSuitableFileFound;
- // existing[1].DownloadPath = "/other/new/file/path";
-
- // 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;" +
- // ",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
- // "\n" +
- // "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
- // "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
- // "\npath/to/file1" +
- // "\n/other/new/file/path" +
- // "\npath/to/file2" +
- // "\nnew/file/path" +
- // "\n#FAIL: ArtistB - TitleB [NoSuitableFileFound]" +
- // "\n";
- // Assert(output == need);
-
- // Console.WriteLine();
- // Console.WriteLine(output);
-
- // Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All);
-
- // foreach (var t in trackLists.Flattened(false, false))
- // {
- // Program.indexEditor.TryGetPreviousRunResult(t, out var prev);
- // Assert(prev != null);
- // Assert(prev.ToKey() == t.ToKey());
- // Assert(prev.DownloadPath == t.DownloadPath);
- // Assert(prev.State == t.State || prev.State == TrackState.NotFoundLastTime);
- // Assert(prev.FailureReason == t.FailureReason);
- // }
-
- // Program.indexEditor.Update();
- // output = File.ReadAllText(path);
- // Assert(output == need);
-
-
- // var test = new List
- // {
- // new() { Artist = "ArtistA", Album = "AlbumA", Type = TrackType.Album },
- // new() { Artist = "ArtistB", Album = "AlbumB", Type = TrackType.Album },
- // new() { Artist = "ArtistC", Album = "AlbumC", Type = TrackType.Album },
- // };
-
- // trackLists = new TrackLists();
- // foreach (var t in test)
- // trackLists.AddEntry(new TrackListEntry(t));
-
- // File.WriteAllText(path, "");
- // Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index);
- // Program.indexEditor.Update();
-
- // Assert(File.ReadAllText(path) == "");
-
- // test[0].State = TrackState.Downloaded;
- // test[0].DownloadPath = "download/path";
- // test[1].State = TrackState.Failed;
- // test[1].FailureReason = FailureReason.NoSuitableFileFound;
- // test[2].State = TrackState.AlreadyExists;
-
- // Program.indexEditor.Update();
-
- // Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index);
-
- // foreach (var t in test)
- // {
- // Program.indexEditor.TryGetPreviousRunResult(t, out var tt);
- // Assert(tt != null);
- // Assert(tt.ToKey() == t.ToKey());
- // t.DownloadPath = "this should not change tt.DownloadPath";
- // Assert(t.DownloadPath != tt.DownloadPath);
- // }
-
- // File.Delete(path);
-
- // Passed();
- //}
+ public static void TestAutoProfiles()
+ {
+ SetCurrentTest("TestAutoProfiles");
+
+ string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf");
+
+ string content =
+ "max-stale-time = 5" +
+ "\nfast-search = true" +
+ "\nformat = flac" +
+
+ "\n[profile-true-1]" +
+ "\nprofile-cond = input-type == \"youtube\" && download-mode == \"album\"" +
+ "\nmax-stale-time = 10" +
+
+ "\n[profile-true-2]" +
+ "\nprofile-cond = !aggregate" +
+ "\nfast-search = false" +
+
+ "\n[profile-false-1]" +
+ "\nprofile-cond = input-type == \"string\"" +
+ "\nformat = mp3" +
+
+ "\n[profile-no-cond]" +
+ "\nformat = opus";
+ File.WriteAllText(path, content);
+ var config = new Config(new string[] { "-c", path });
+ config.inputType = InputType.YouTube;
+ config.interactiveMode = true;
+ config.aggregate = false;
+ config.maxStaleTime = 50000;
+ var tle = new TrackListEntry(TrackType.Album);
+ config.UpdateProfiles(tle);
+ Assert(config.maxStaleTime == 10 && !config.fastSearch && config.necessaryCond.Formats[0] == "flac");
+
+ content =
+ "\n[no-stale]" +
+ "\nprofile-cond = interactive && download-mode == \"album\"" +
+ "\nmax-stale-time = 999999" +
+ "\n[youtube]" +
+ "\nprofile-cond = input-type == \"youtube\"" +
+ "\nyt-dlp = true";
+ File.WriteAllText(path, content);
+ config = new Config(new string[] { "-c", path });
+ config.inputType = InputType.CSV;
+ config.album = true;
+ config.interactiveMode = true;
+ config.useYtdlp = false;
+ config.maxStaleTime = 50000;
+ config.UpdateProfiles(tle);
+ Assert(config.maxStaleTime == 999999 && !config.useYtdlp);
+
+ content =
+ "\n[no-stale]" +
+ "\nprofile-cond = interactive && download-mode == \"album\"" +
+ "\nmax-stale-time = 999999" +
+ "\n[youtube]" +
+ "\nprofile-cond = input-type == \"youtube\"" +
+ "\nyt-dlp = true";
+ File.WriteAllText(path, content);
+ config = new Config(new string[] { "-c", path });
+ config.inputType = InputType.YouTube;
+ config.album = false;
+ config.interactiveMode = true;
+ config.useYtdlp = false;
+ config.maxStaleTime = 50000;
+ config.UpdateProfiles(new TrackListEntry(TrackType.Normal));
+ Assert(config.maxStaleTime == 50000 && config.useYtdlp);
+
+ if (File.Exists(path)) File.Delete(path);
+
+ Passed();
+ }
+
+ public static void TestProfileConditions()
+ {
+ SetCurrentTest("TestProfileConditions");
+
+ var config = new Config(new string[] { });
+ config.inputType = InputType.YouTube;
+ config.interactiveMode = true;
+ config.album = true;
+ config.aggregate = false;
+
+ var conds = new (bool, string)[]
+ {
+ (true, "input-type == \"youtube\""),
+ (true, "download-mode == \"album\""),
+ (false, "aggregate"),
+ (true, "interactive"),
+ (true, "album"),
+ (false, "!interactive"),
+ (true, "album && input-type == \"youtube\""),
+ (false, "album && input-type != \"youtube\""),
+ (false, "(interactive && aggregate)"),
+ (true, "album && (interactive || aggregate)"),
+ (true, "input-type == \"spotify\" || aggregate || input-type == \"csv\" || interactive && album"),
+ (true, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || interactive ) )"),
+ (false, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || !interactive ) )"),
+ };
+
+ foreach ((var b, var c) in conds)
+ {
+ Console.WriteLine(c);
+ Assert(b == config.ProfileConditionSatisfied(c));
+ }
+
+ Passed();
+ }
+
+ public static async Task TestStringExtractor()
+ {
+ SetCurrentTest("TestStringExtractor");
+
+ var strings = new List()
+ {
+ "Some Title",
+ "Some, Title",
+ "artist = Some artist, title = some title",
+ "Artist - Title, length = 42",
+ "title=Some, Title, artist=Some, Artist, album = Some, Album, length= 42",
+ "Some, Artist = a - Some, Title = b, album = Some, Album, length = 42",
+
+ "Foo Bar",
+ "Foo - Bar",
+ "Artist - Title, length=42",
+ "title=Title, artist=Artist, length=42",
+ };
+
+ var tracks = new List()
+ {
+ new Track() { Title="Some Title" },
+ new Track() { Title="Some, Title" },
+ new Track() { Title = "some title", Artist = "Some artist" },
+ new Track() { Title = "Title", Artist = "Artist", Length = 42 },
+ new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42 },
+ new Track() { Title="Some, Title = b", Artist = "Some, Artist = a", Album = "Some, Album", Length = 42 },
+
+ new Track() { Title = "Foo Bar" },
+ new Track() { Title = "Bar", Artist = "Foo" },
+ new Track() { Title = "Title", Artist = "Artist", Length = 42 },
+ new Track() { Title = "Title", Artist = "Artist", Length = 42 },
+ };
+
+ var albums = new List()
+ {
+ new Track() { Album="Some Title", Type = TrackType.Album },
+ new Track() { Album="Some, Title", Type = TrackType.Album },
+ new Track() { Title = "some title", Artist = "Some artist", Type = TrackType.Album },
+ new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
+ new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42, Type = TrackType.Album },
+ new Track() { Artist = "Some, Artist = a", Album = "Some, Album", Length = 42, Type = TrackType.Album },
+
+ new Track() { Album = "Foo Bar", Type = TrackType.Album },
+ new Track() { Album = "Bar", Artist = "Foo", Type = TrackType.Album },
+ new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
+ new Track() { Title = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
+ };
+
+ var extractor = new Extractors.StringExtractor();
+
+ var config = new Config(new string[] { });
+ config.aggregate = false;
+ config.album = false;
+
+ Console.WriteLine("Testing songs: ");
+ for (int i = 0; i < strings.Count; i++)
+ {
+ config.input = strings[i];
+ Console.WriteLine(config.input);
+ var res = await extractor.GetTracks(config.input, 0, 0, false, config);
+ var t = res[0].list[0][0];
+ Assert(Extractors.StringExtractor.InputMatches(config.input));
+ Assert(t.ToKey() == tracks[i].ToKey());
+ }
+
+ Console.WriteLine();
+ Console.WriteLine("Testing albums");
+ config.album = true;
+ for (int i = 0; i < strings.Count; i++)
+ {
+ config.input = strings[i];
+ Console.WriteLine(config.input);
+ var t = (await extractor.GetTracks(config.input, 0, 0, false, config))[0].source;
+ Assert(Extractors.StringExtractor.InputMatches(config.input));
+ Assert(t.ToKey() == albums[i].ToKey());
+ }
+
+ Passed();
+ }
+
+ public static void TestM3uEditor()
+ {
+ throw new NotImplementedException();
+ //SetCurrentTest("TestM3uEditor");
+
+ //var config = new Config(new string[] { });
+ //config.skipMode = SkipMode.Index;
+ //config.skipMusicDir = "";
+ //config.printOption = PrintOption.Tracks | PrintOption.Full;
+ //config.skipExisting = true;
+
+ //string path = Path.Join(Directory.GetCurrentDirectory(), "test_m3u.m3u8");
+
+ //if (File.Exists(path))
+ // File.Delete(path);
+
+ //File.WriteAllText(path, $"#SLDL:" +
+ // $"{Path.Join(Directory.GetCurrentDirectory(), "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;");
+
+ //var notFoundInitial = new List()
+ //{
+ // new() { Artist = "Artist; ,3", Title = "Title3 ;a" },
+ // new() { Artist = "Artist,,, ;4", Title = "Title4", State = TrackState.Failed, FailureReason = FailureReason.NoSuitableFileFound }
+ //};
+ //var existingInitial = new List()
+ //{
+ // new() { Artist = "Artist, 1", Title = "Title, , 1", DownloadPath = "path/to/file1", State = TrackState.Downloaded },
+ // new() { Artist = "Artist, 1.5", Title = "Title, , 1.5", DownloadPath = Path.Join(Directory.GetCurrentDirectory(), "file1.5"), State = TrackState.Downloaded },
+ // new() { Artist = "Artist, 2", Title = "Title2", DownloadPath = "path/to/file2", State = TrackState.AlreadyExists }
+ //};
+ //var toBeDownloadedInitial = new List()
+ //{
+ // new() { Artist = "ArtistA", Album = "Albumm", Title = "TitleA" },
+ // new() { Artist = "ArtistB", Album = "Albumm", Title = "TitleB" }
+ //};
+
+ //var trackLists = new TrackLists();
+ //trackLists.AddEntry(new TrackListEntry(TrackType.Normal));
+ //foreach (var t in notFoundInitial)
+ // trackLists.AddTrackToLast(t);
+ //foreach (var t in existingInitial)
+ // trackLists.AddTrackToLast(t);
+ //foreach (var t in toBeDownloadedInitial)
+ // trackLists.AddTrackToLast(t);
+
+ //var editor = new M3uEditor(path, trackLists, M3uOption.All);
+ //trackLists[0].indexEditor = editor;
+
+ //trackLists[0].outputDirSkipper = new IndexSkipper();
+
+ //var notFound = (List)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
+ //var existing = (List)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0] });
+ //var toBeDownloaded = trackLists[0].list[0].Where(t => t.State == TrackState.Initial).ToList();
+
+ //Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial));
+ //Assert(existing.SequenceEqualUpToPermutation(existingInitial));
+ //Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
+
+ //Printing.PrintTracksTbd(toBeDownloaded, existing, notFound, TrackType.Normal, config);
+
+ //editor.Update();
+ //string output = File.ReadAllText(path);
+ //string need =
+ // "#SLDL:./file1.5,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;,,,,-1,0,0,0;" +
+ // "\n" +
+ // "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
+ // "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
+ // "\npath/to/file1" +
+ // "\nfile1.5" +
+ // "\npath/to/file2" +
+ // "\n";
+ //Assert(output == need);
+
+ //toBeDownloaded[0].State = TrackState.Downloaded;
+ //toBeDownloaded[0].DownloadPath = "new/file/path";
+ //toBeDownloaded[1].State = TrackState.Failed;
+ //toBeDownloaded[1].FailureReason = FailureReason.NoSuitableFileFound;
+ //existing[1].DownloadPath = "/other/new/file/path";
+
+ //editor.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;" +
+ // ",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
+ // "\n" +
+ // "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
+ // "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
+ // "\npath/to/file1" +
+ // "\n/other/new/file/path" +
+ // "\npath/to/file2" +
+ // "\nnew/file/path" +
+ // "\n#FAIL: ArtistB - TitleB [NoSuitableFileFound]" +
+ // "\n";
+ //Assert(output == need);
+
+ //Console.WriteLine();
+ //Console.WriteLine(output);
+
+ //editor = new M3uEditor(path, trackLists, M3uOption.All);
+
+ //foreach (var t in trackLists.Flattened(false, false))
+ //{
+ // editor.TryGetPreviousRunResult(t, out var prev);
+ // Assert(prev != null);
+ // Assert(prev.ToKey() == t.ToKey());
+ // Assert(prev.DownloadPath == t.DownloadPath);
+ // Assert(prev.State == t.State || prev.State == TrackState.NotFoundLastTime);
+ // Assert(prev.FailureReason == t.FailureReason);
+ //}
+
+ //editor.Update();
+ //output = File.ReadAllText(path);
+ //Assert(output == need);
+
+
+ //var test = new List
+ //{
+ // new() { Artist = "ArtistA", Album = "AlbumA", Type = TrackType.Album },
+ // new() { Artist = "ArtistB", Album = "AlbumB", Type = TrackType.Album },
+ // new() { Artist = "ArtistC", Album = "AlbumC", Type = TrackType.Album },
+ //};
+
+ //trackLists = new TrackLists();
+ //foreach (var t in test)
+ // trackLists.AddEntry(new TrackListEntry(t));
+
+ //File.WriteAllText(path, "");
+ //editor = new M3uEditor(path, trackLists, M3uOption.Index);
+ //editor.Update();
+
+ //Assert(File.ReadAllText(path) == "");
+
+ //test[0].State = TrackState.Downloaded;
+ //test[0].DownloadPath = "download/path";
+ //test[1].State = TrackState.Failed;
+ //test[1].FailureReason = FailureReason.NoSuitableFileFound;
+ //test[2].State = TrackState.AlreadyExists;
+
+ //editor.Update();
+
+ //editor = new M3uEditor(path, trackLists, M3uOption.Index);
+
+ //foreach (var t in test)
+ //{
+ // editor.TryGetPreviousRunResult(t, out var tt);
+ // Assert(tt != null);
+ // Assert(tt.ToKey() == t.ToKey());
+ // t.DownloadPath = "this should not change tt.DownloadPath";
+ // Assert(t.DownloadPath != tt.DownloadPath);
+ //}
+
+ //File.Delete(path);
+
+ //Passed();
+ }
}
static class Helpers
@@ -470,15 +461,6 @@ namespace Tests
}
}
- public static void ResetConfig()
- {
- var singletonType = typeof(Config);
- var instanceField = singletonType.GetField("Instance", BindingFlags.Static | BindingFlags.NonPublic);
- var constructor = singletonType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
- var newInstance = constructor.Invoke(null);
- instanceField.SetValue(null, newInstance);
- }
-
public static void Passed()
{
Console.WriteLine($"{currentTest} passed");
diff --git a/slsk-batchdl/Utilities/Printing.cs b/slsk-batchdl/Utilities/Printing.cs
index 28c72ff..c1603ec 100644
--- a/slsk-batchdl/Utilities/Printing.cs
+++ b/slsk-batchdl/Utilities/Printing.cs
@@ -187,7 +187,7 @@ public static class Printing
public static void PrintComplete(TrackLists trackLists)
{
- var ls = trackLists.Flattened(true, true);
+ var ls = trackLists.Flattened(true, false);
int successes = 0, fails = 0;
foreach (var x in ls)
{
diff --git a/slsk-batchdl/Utilities/Utils.cs b/slsk-batchdl/Utilities/Utils.cs
index 5c423e0..5f20329 100644
--- a/slsk-batchdl/Utilities/Utils.cs
+++ b/slsk-batchdl/Utilities/Utils.cs
@@ -103,17 +103,20 @@ public static class Utils
public static string ExpandUser(string path)
{
- if (string.IsNullOrEmpty(path))
- {
+ if (string.IsNullOrWhiteSpace(path))
return path;
- }
- path = path.Trim();
+ path = NormalizedPath(path);
- if (path.Length > 0 && path[0] == '~' && (path.Length == 1 || path[1] == '\\' || path[1] == '/'))
+ if (path[0] == '~' && (path.Length == 1 || path[1] == '/'))
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
- path = Path.Join(home, path[1..].TrimStart('/').TrimStart('\\'));
+ path = Path.Join(home, path[1..].TrimStart('/'));
+ }
+ else if (path.StartsWith("{bindir}") && (path.Length == 8 || path[8] == '/'))
+ {
+ string bindir = AppDomain.CurrentDomain.BaseDirectory;
+ path = Path.Join(bindir, path[8..].TrimStart('/'));
}
return path;