1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 06:22:41 +00:00
This commit is contained in:
fiso64 2024-12-20 23:56:44 +01:00
parent 85b94de641
commit c639daabce
17 changed files with 781 additions and 801 deletions

View file

@ -3,7 +3,7 @@
An automatic downloader for Soulseek built with Soulseek.NET. Accepts CSV files as well as Spotify and YouTube urls. 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. 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 ## Index
- [Options](#options) - [Options](#options)
@ -68,14 +68,15 @@ Usage: sldl <input> [OPTIONS]
--listen-port <port> Port for incoming connections (default: 49998) --listen-port <port> Port for incoming connections (default: 49998)
--on-complete <command> Run a command whenever a file is downloaded. --on-complete <command> 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}. {artist},{album},{uri},{length},{failure-reason},{state}.
Prepend a state number to only run in specific cases: Prepend a state number to only run in specific cases:
1:, 2:, 3:, 4: for the Downloaded, Failed, Exists, and 1:, 2:, 3:, 4: for the Downloaded, Failed, Exists, and
NotFoundLastTime states respectively. NotFoundLastTime states respectively.
E.g: '1:<cmd>' will only run the command if the file is E.g: '1:<cmd>' will only run the command if the file is
downloaded successfully. Prepend 's:' to use the system 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 <option> Print tracks or search results instead of downloading: --print <option> Print tracks or search results instead of downloading:
'tracks': Print all tracks to be downloaded 'tracks': Print all tracks to be downloaded
@ -198,7 +199,7 @@ Usage: sldl <input> [OPTIONS]
the directory fails to download. Set to 'delete' to delete the directory fails to download. Set to 'delete' to delete
the files instead. Set to 'disable' keep it where it is. the files instead. Set to 'disable' keep it where it is.
Default: {configured output dir}/failed 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 #### 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 (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'. properties like 'title=Song Name, artist=Artist Name, length=215'.
The following properties are accepted: The following properties are accepted: title, artist, album, length (in seconds),
``` artist-maybe-wrong, album-track-count.
title
artist
album
length (in seconds)
artist-maybe-wrong
album-track-count
```
Example inputs and their interpretations: Example inputs and their interpretations:
``` | Input String | Artist | Title | Album | Length |
Input String | Artist | Title | Album | Length |-----------------------------------------|----------|----------|----------|--------|
--------------------------------------------------------------------------------- | 'Foo Bar' (without any hyphens) | | Foo Bar | | |
'Foo Bar' (without any hyphens) | | Foo Bar | | | 'Foo - Bar' | Foo | Bar | | |
'Foo - Bar' | Foo | Bar | | | 'Foo - Bar' (with --album enabled) | Foo | | Bar | |
'Foo - Bar' (with --album enabled) | Foo | | Bar | | 'Artist - Title, length=42' | Artist | Title | | 42 |
'Artist - Title, length=42' | Artist | Title | | 42 | 'artist=AR, title=T, album=AL' | AR | T | AL | |
'artist=AR, title=T, album=AL' | AR | T | AL |
```
### List ### List
A path to a text file where each line has the following form: A path to a text file where each line has the following form:
``` ```bash
"some input" "conditions" "preferred conditions" # input conditions pref. conditions
```
e.g:
```
"artist=Artist, album=Album" "format=mp3; br > 128" "br >= 320" "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 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. 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 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 ### Filtering irrelevant results
The options --strict-title, --strict-artist and --strict-album will filter any file that 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. tag1 is null, use tag2. String literals enclosed in parentheses are ignored in the null check.
### Examples: ### Examples:
- "{artist} - {title}" - `{artist} - {title}`
Always name it 'Artist - Title'. Because some files on Soulseek are untagged, the Always name it 'Artist - Title'. Because some files on Soulseek are untagged, the
following is generally preferred: 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 If artist and title are not null, name it 'Artist - Title', otherwise use the original
filename. 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 Sort files into artist/album folders if all tags are present, otherwise put them in
the 'missing-tags' folder. the 'missing-tags' folder.
@ -466,7 +456,7 @@ pref-format = flac
fast-search = true fast-search = true
``` ```
Lines starting with hashtags (#) will be ignored. Tildes in paths are expanded as the user 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: ### Configuration profiles:
Profiles are supported: Profiles are supported:
@ -547,12 +537,12 @@ sldl "artist=MC MENTAL" -a -g -t
#### Advanced example: Automatic wishlist downloader #### Advanced example: Automatic wishlist downloader
Create a file named `wishlist.txt`, and add some items as detailed in [Input types: List](#list): Create a file named `wishlist.txt`, and add some items as detailed in [Input types: List](#list):
```bash ```
"Artist - My Favorite Song" "Artist - My Favorite Song" "format=flac"
a:"Artist - Some Album, album-track-count=5" "format=flac" a:"Artist - Some Album, album-track-count=5"
``` ```
Add a profile to your `sldl.conf`: Add a profile to your `sldl.conf`:
``` ```bash
[wishlist] [wishlist]
input = ~/sldl/wishlist.txt input = ~/sldl/wishlist.txt
input-type = list 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 # 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. [crontab.guru](https://crontab.guru/) could be used to help with the scheduling expression.

View file

@ -1,33 +1,22 @@
@echo off @echo off
setlocal setlocal
set DOTNET_CLI_TELEMETRY_OPTOUT=1 set FRAMEWORK=net6.0
if not exist slsk-batchdl\bin\zips mkdir slsk-batchdl\bin\zips 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 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

View file

@ -9,7 +9,7 @@ public class Config
{ {
public FileConditions necessaryCond = new() 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() public FileConditions preferredCond = new()
@ -175,11 +175,6 @@ public class Config
ProcessArgs(arguments); ProcessArgs(arguments);
} }
public Config()
{
}
public Config Copy() // deep copies all fields except configProfiles and arguments public Config Copy() // deep copies all fields except configProfiles and arguments
{ {
var copy = (Config)this.MemberwiseClone(); var copy = (Config)this.MemberwiseClone();
@ -209,9 +204,12 @@ public class Config
if (idx != -1) if (idx != -1)
{ {
confPath = Utils.ExpandUser(args[idx + 1]);
confPathChanged = true; confPathChanged = true;
if (confPath == "none")
return;
confPath = Utils.ExpandUser(args[idx + 1]);
if(File.Exists(Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath))) if(File.Exists(Path.Join(AppDomain.CurrentDomain.BaseDirectory, confPath)))
confPath = 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) foreach (var (name, args) in toApply)
{ {
Console.WriteLine($"Applying auto profile: {name}"); tle.AddPrintLine($"Applying auto profile: {name}");
ProcessArgs(args); ProcessArgs(args);
appliedProfiles.Add(name); appliedProfiles.Add(name);
} }
@ -421,7 +419,7 @@ public class Config
appliedProfiles.Add(name); appliedProfiles.Add(name);
} }
else 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) 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); 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 cond = new FileConditions();
var tr = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries; var tr = StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries;
@ -639,6 +646,10 @@ public class Config
case "acceptmissingprops": case "acceptmissingprops":
cond.AcceptMissingProps = bool.Parse(value); cond.AcceptMissingProps = bool.Parse(value);
break; break;
case "albumtrackcount":
if (track != null)
UpdateMinMax2(value, condition, ref track.MinAlbumTrackCount, ref track.MaxAlbumTrackCount);
break;
default: default:
throw new ArgumentException($"Unknown condition '{condition}'"); throw new ArgumentException($"Unknown condition '{condition}'");
} }
@ -722,6 +733,10 @@ public class Config
case "--config": case "--config":
confPath = args[++i]; confPath = args[++i];
break; break;
case "--nc":
case "--no-config":
confPath = "none";
break;
case "--smd": case "--smd":
case "--skip-music-dir": case "--skip-music-dir":
skipMusicDir = args[++i]; skipMusicDir = args[++i];

View file

@ -50,9 +50,9 @@ namespace Extractors
{ {
string[] lines = File.ReadAllLines(csvFilePath, System.Text.Encoding.UTF8); 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'); Utils.WriteAllLines(csvFilePath, lines, '\n');
} }
} }
@ -135,7 +135,7 @@ namespace Extractors
csvColumnCount = values.Count; csvColumnCount = values.Count;
var desc = ""; var desc = "";
var track = new Track() { CsvRow = index }; var track = new Track() { CsvOrListRow = index };
if (artistIndex >= 0) track.Artist = values[artistIndex]; if (artistIndex >= 0) track.Artist = values[artistIndex];
if (trackIndex >= 0) track.Title = values[trackIndex]; if (trackIndex >= 0) track.Title = values[trackIndex];

View file

@ -66,16 +66,20 @@ namespace Extractors
foreach (var tle in tl.lists) foreach (var tle in tl.lists)
{ {
if (fields.Count >= 2) if (fields.Count >= 2)
tle.extractorCond = Config.ParseConditions(fields[1]); {
tle.extractorCond = Config.ParseConditions(fields[1], tle.source);
}
if (fields.Count >= 3) if (fields.Count >= 3)
{
tle.extractorPrefCond = Config.ParseConditions(fields[2]); tle.extractorPrefCond = Config.ParseConditions(fields[2]);
}
tle.defaultFolderName = foldername; tle.defaultFolderName = foldername;
tle.enablesIndexByDefault = true; tle.enablesIndexByDefault = true;
} }
if (tl.lists.Count == 1) if (tl.lists.Count == 1)
tl[0].source.CsvRow = i; tl[0].source.CsvOrListRow = i;
trackLists.lists.AddRange(tl.lists); trackLists.lists.AddRange(tl.lists);
@ -138,9 +142,9 @@ namespace Extractors
{ {
string[] lines = File.ReadAllLines(listFilePath, Encoding.UTF8); 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'); Utils.WriteAllLines(listFilePath, lines, '\n');
} }
} }

View file

@ -648,8 +648,8 @@ namespace Extractors
public static async Task<string> YtdlpDownload(string id, string savePathNoExt, string ytdlpArgument = "", bool printCommand = false) public static async Task<string> YtdlpDownload(string id, string savePathNoExt, string ytdlpArgument = "", bool printCommand = false)
{ {
Process process = new Process(); var process = new Process();
ProcessStartInfo startInfo = new ProcessStartInfo(); var startInfo = new ProcessStartInfo();
if (ytdlpArgument.Length == 0) if (ytdlpArgument.Length == 0)
ytdlpArgument = "\"{id}\" -f bestaudio/best -ci -o \"{savepath-noext}.%(ext)s\" -x"; ytdlpArgument = "\"{id}\" -f bestaudio/best -ci -o \"{savepath-noext}.%(ext)s\" -x";

View file

@ -54,7 +54,7 @@ public class FileManager
this.defaultFolderName = defaultFolderName != null ? Utils.NormalizedPath(defaultFolderName) : null; this.defaultFolderName = defaultFolderName != null ? Utils.NormalizedPath(defaultFolderName) : null;
} }
public void OrganizeAlbum(List<Track> tracks, List<Track>? additionalImages, bool remainingOnly = true) public void OrganizeAlbum(Track source, List<Track> tracks, List<Track>? additionalImages, bool remainingOnly = true)
{ {
foreach (var track in tracks.Where(t => !t.IsNotAudio)) foreach (var track in tracks.Where(t => !t.IsNotAudio))
{ {
@ -64,6 +64,8 @@ public class FileManager
OrganizeAudio(track, track.FirstDownload); OrganizeAudio(track, track.FirstDownload);
} }
source.DownloadPath = Utils.GreatestCommonDirectory(tracks.Where(t => !t.IsNotAudio).Select(t => t.DownloadPath));
bool onlyAdditionalImages = config.nameFormat.Length == 0; bool onlyAdditionalImages = config.nameFormat.Length == 0;
var nonAudioToOrganize = onlyAdditionalImages ? additionalImages : tracks.Where(t => t.IsNotAudio); var nonAudioToOrganize = onlyAdditionalImages ? additionalImages : tracks.Where(t => t.IsNotAudio);

View file

@ -6,22 +6,46 @@ namespace FileSkippers
{ {
public static class FileSkipperRegistry 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 return mode switch
{ {
SkipMode.Name => useConditions ? new NameConditionalSkipper(dir, conditions) : new NameSkipper(dir), SkipMode.Name => useConditions ? new NameConditionalSkipper(dir) : new NameSkipper(dir),
SkipMode.Tag => useConditions ? new TagConditionalSkipper(dir, conditions) : new TagSkipper(dir), SkipMode.Tag => useConditions ? new TagConditionalSkipper(dir) : new TagSkipper(dir),
SkipMode.Index => useConditions ? new IndexConditionalSkipper(indexEditor, conditions) : new IndexSkipper(indexEditor, conditions != null), SkipMode.Index => useConditions ? new IndexConditionalSkipper() : new IndexSkipper(),
_ => throw new ArgumentException("Invalid SkipMode") _ => 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 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 virtual void BuildIndex() { IndexIsBuilt = true; }
public bool IndexIsBuilt { get; protected set; } = false; public bool IndexIsBuilt { get; protected set; } = false;
} }
@ -72,7 +96,7 @@ namespace FileSkippers
IndexIsBuilt = true; IndexIsBuilt = true;
} }
public override bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{ {
foundPath = null; foundPath = null;
@ -101,12 +125,10 @@ namespace FileSkippers
readonly string[] ignore = new string[] { "_", "-", ".", "(", ")", "[", "]" }; readonly string[] ignore = new string[] { "_", "-", ".", "(", ")", "[", "]" };
readonly string dir; readonly string dir;
readonly List<(string, string, SimpleFile)> index = new(); // (PreprocessedPath, PreprocessedName, file) 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.dir = dir;
this.conditions = conditions;
} }
private string Preprocess(string s, bool removeSlash) private string Preprocess(string s, bool removeSlash)
@ -148,7 +170,7 @@ namespace FileSkippers
IndexIsBuilt = true; IndexIsBuilt = true;
} }
public override bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{ {
foundPath = null; foundPath = null;
@ -160,7 +182,7 @@ namespace FileSkippers
foreach ((var ppath, var pname, var musicFile) in index) 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; foundPath = musicFile.Path;
return true; return true;
@ -214,7 +236,7 @@ namespace FileSkippers
IndexIsBuilt = true; IndexIsBuilt = true;
} }
public override bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{ {
foundPath = null; foundPath = null;
@ -241,12 +263,10 @@ namespace FileSkippers
{ {
readonly string dir; readonly string dir;
readonly List<(string, string, SimpleFile)> index = new(); // (PreprocessedArtist, PreprocessedTitle, file) 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.dir = dir;
this.conditions = conditions;
} }
private string Preprocess(string s) private string Preprocess(string s)
@ -281,7 +301,7 @@ namespace FileSkippers
IndexIsBuilt = true; IndexIsBuilt = true;
} }
public override bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{ {
foundPath = null; foundPath = null;
@ -293,7 +313,7 @@ namespace FileSkippers
foreach ((var partist, var ptitle, var musicFile) in index) 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; foundPath = musicFile.Path;
return true; return true;
@ -306,23 +326,18 @@ namespace FileSkippers
public class IndexSkipper : FileSkipper public class IndexSkipper : FileSkipper
{ {
M3uEditor indexEditor; public IndexSkipper()
bool checkFileExists;
public IndexSkipper(M3uEditor m3UEditor, bool checkFileExists)
{ {
this.indexEditor = m3UEditor;
this.checkFileExists = checkFileExists;
IndexIsBuilt = true; IndexIsBuilt = true;
} }
public override bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{ {
foundPath = null; 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 (t != null && (t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists))
{ {
if (checkFileExists) if (context.checkFileExists)
{ {
if (t.DownloadPath.Length == 0) if (t.DownloadPath.Length == 0)
return false; return false;
@ -348,20 +363,15 @@ namespace FileSkippers
public class IndexConditionalSkipper : FileSkipper public class IndexConditionalSkipper : FileSkipper
{ {
M3uEditor indexEditor; public IndexConditionalSkipper()
FileConditions conditions;
public IndexConditionalSkipper(M3uEditor m3UEditor, FileConditions conditions)
{ {
this.indexEditor = m3UEditor;
this.conditions = conditions;
IndexIsBuilt = true; IndexIsBuilt = true;
} }
public override bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, FileSkipperContext context, out string? foundPath)
{ {
foundPath = null; foundPath = null;
var t = indexEditor.PreviousRunResult(track); var t = context.indexEditor.PreviousRunResult(track);
if (t == null || t.DownloadPath.Length == 0) if (t == null || t.DownloadPath.Length == 0)
return false; return false;
@ -375,7 +385,7 @@ namespace FileSkippers
try try
{ {
musicFile = TagLib.File.Create(t.DownloadPath); musicFile = TagLib.File.Create(t.DownloadPath);
if (conditions.FileSatisfies(musicFile, track, false)) if (context.conditions.FileSatisfies(musicFile, track, false))
{ {
foundPath = t.DownloadPath; foundPath = t.DownloadPath;
return true; return true;
@ -415,7 +425,7 @@ namespace FileSkippers
try { musicFile = TagLib.File.Create(path); } try { musicFile = TagLib.File.Create(path); }
catch { return false; } catch { return false; }
if (!conditions.FileSatisfies(musicFile, track)) if (!context.conditions.FileSatisfies(musicFile, track))
return false; return false;
} }
} }

View file

@ -44,14 +44,15 @@ public static class Help
--listen-port <port> Port for incoming connections (default: 49998) --listen-port <port> Port for incoming connections (default: 49998)
--on-complete <command> Run a command whenever a file is downloaded. --on-complete <command> 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}. {artist},{album},{uri},{length},{failure-reason},{state}.
Prepend a state number to only run in specific cases: Prepend a state number to only run in specific cases:
1:, 2:, 3:, 4: for the Downloaded, Failed, Exists, and 1:, 2:, 3:, 4: for the Downloaded, Failed, Exists, and
NotFoundLastTime states respectively. NotFoundLastTime states respectively.
E.g: '1:<cmd>' will only run the command if the file is E.g: '1:<cmd>' will only run the command if the file is
downloaded successfully. Prepend 's:' to use the system 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 <option> Print tracks or search results instead of downloading: --print <option> Print tracks or search results instead of downloading:
'tracks': Print all tracks to be downloaded '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 directory fails to download. Set to 'delete' to delete
the files instead. Set to the empty string """" to disable. the files instead. Set to the empty string """" to disable.
Default: {configured output dir}/failed 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 Aggregate Download
-g, --aggregate Aggregate download mode: Find and download all distinct -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 (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'. properties like 'title=Song Name, artist=Artist Name, length=215'.
The following properties are accepted: The following properties are accepted: title, artist, album, length (in seconds),
title artist-maybe-wrong, album-track-count.
artist
album
length (in seconds)
artist-maybe-wrong
album-track-count
Example inputs and their interpretations: Example inputs and their interpretations:
Input String | Artist | Title | Album | Length Input String | Artist | Title | Album | Length
@ -271,7 +267,7 @@ public static class Help
""some input"" ""conditions"" ""preferred conditions"" ""some input"" ""conditions"" ""preferred conditions""
e.g: for example:
""artist=Artist, album=Album"" ""format=mp3; br > 128"" ""br >= 320"" ""artist=Artist, album=Album"" ""format=mp3; br > 128"" ""br >= 320""
@ -447,7 +443,7 @@ public static class Help
fast-search = true fast-search = true
Lines starting with hashtags (#) will be ignored. Tildes in paths are expanded as the user 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: Configuration profiles:
Profiles are supported: Profiles are supported:

View file

@ -17,7 +17,7 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
private readonly object locker = new(); 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.trackLists = trackLists;
this.option = option; this.option = option;
@ -25,12 +25,12 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
this.needFirstUpdate = option == M3uOption.All || option == M3uOption.Playlist; 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)) if (string.IsNullOrEmpty(path))
return; return;
@ -42,87 +42,104 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
parent = Utils.NormalizedPath(Path.GetDirectoryName(this.path)); parent = Utils.NormalizedPath(Path.GetDirectoryName(this.path));
lines = ReadAllLines().ToList(); lines = ReadAllLines().ToList();
LoadPreviousResults();
if (loadPreviousResults)
LoadPreviousResults();
} }
private void LoadPreviousResults() private void LoadPreviousResults()
{ {
// Format: if (lines.Count == 0 || !lines.Any(x => x.Trim() != ""))
// #SLDL:<trackinfo>;<trackinfo>; ...
// where <trackinfo> = filepath,artist,album,title,length(int),tracktype(int),state(int),failurereason(int)
if (lines.Count == 0 || !lines[0].StartsWith("#SLDL:"))
return; return;
string sldlLine = lines[0]; bool useOldFormat = lines[0].StartsWith("#SLDL:");
lines = lines.Skip(1).ToList();
int k = "#SLDL:".Length; var indexLines = useOldFormat ? new string[] { lines[0] } : lines.Skip(1);
var currentItem = new StringBuilder(); 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++); foreach (var sldlLine in indexLines)
for (; k < sldlLine.Length; k++)
{ {
var track = new Track(); if (string.IsNullOrWhiteSpace(sldlLine))
int field = 0; continue;
for (int i = k; i < sldlLine.Length; i++)
{
char c = sldlLine[i];
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('"'); if (inQuotes && i + 1 < sldlLine.Length && sldlLine[i + 1] == '"')
i++; {
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 else
{ {
inQuotes = !inQuotes; currentItem.Append(c);
} }
} }
else if (field <= 6 && c == ',' && !inQuotes)
{
var x = currentItem.ToString();
if (field == 0) if (!useOldFormat)
{
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 == ';')
{ {
track.FailureReason = (FailureReason)int.Parse(currentItem.ToString()); track.FailureReason = (FailureReason)int.Parse(currentItem.ToString());
currentItem.Clear(); 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) private void WriteSldlLine(Writer writer)
{ {
// Format:
// #SLDL:<trackinfo>;<trackinfo>; ...
// where <trackinfo> = filepath,artist,album,title,length(int),tracktype(int),state(int),failurereason(int)
void writeCsvLine(string[] items) void writeCsvLine(string[] items)
{ {
bool comma = false; 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) foreach (var val in previousRunData.Values)
{ {
@ -309,7 +323,8 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
}; };
writeCsvLine(items); writeCsvLine(items);
writer.Write(';'); //writer.Write(';');
writer.Write('\n');
} }
writer.Write('\n'); writer.Write('\n');

View file

@ -16,7 +16,7 @@ namespace Models
public bool IsNotAudio = false; public bool IsNotAudio = false;
public string DownloadPath = ""; public string DownloadPath = "";
public string Other = ""; public string Other = "";
public int CsvRow = -1; public int CsvOrListRow = -1;
public TrackType Type = TrackType.Normal; public TrackType Type = TrackType.Normal;
public FailureReason FailureReason = FailureReason.None; public FailureReason FailureReason = FailureReason.None;
public TrackState State = TrackState.Initial; public TrackState State = TrackState.Initial;

View file

@ -24,6 +24,8 @@ namespace Models
public bool CanParallelSearch => source.Type == TrackType.Album || source.Type == TrackType.Aggregate; public bool CanParallelSearch => source.Type == TrackType.Album || source.Type == TrackType.Aggregate;
private List<string>? printLines = null;
public TrackListEntry(TrackType trackType) public TrackListEntry(TrackType trackType)
{ {
list = new List<List<Track>>(); list = new List<List<Track>>();
@ -76,5 +78,20 @@ namespace Models
else else
list[0].Add(track); list[0].Add(track);
} }
public void AddPrintLine(string line)
{
if (printLines == null)
printLines = new List<string>();
printLines.Add(line);
}
public void PrintLines()
{
if (printLines == null) return;
foreach (var line in printLines)
Console.WriteLine(line);
printLines = null;
}
} }
} }

View file

@ -55,7 +55,9 @@ static partial class Program
trackLists.UpgradeListTypes(config.aggregate, config.album); trackLists.UpgradeListTypes(config.aggregate, config.album);
trackLists.SetListEntryOptions(); trackLists.SetListEntryOptions();
await MainLoop(config); PrepareListEntries(config);
await MainLoop();
WriteLineIf("Mainloop done", config.debugInfo); 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); static void preprocessTrack(Config config, Track track)
tle.indexEditor = new M3uEditor(trackLists, config.writeIndex ? M3uOption.Index : M3uOption.None);
}
static void InitFileSkippers(TrackListEntry tle, Config config)
{
if (config.skipExisting)
{ {
FileConditions? cond = null; if (config.removeFt)
if (config.skipCheckPrefCond)
{ {
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); track.Artist = track.Artist.Trim();
track.Album = track.Album.Trim();
if (config.skipMusicDir.Length > 0) track.Title = track.Title.Trim();
{
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);
}
} }
}
static void InitConfigs(Config defaultConfig) preprocessTrack(tle.config, tle.source);
{
//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<Config, TrackListEntry?>() { { 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);
for (int k = 0; k < tle.list.Count; k++) 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++) 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) var editors = new Dictionary<(string path, M3uOption option), M3uEditor>();
{ var skippers = new Dictionary<(string dir, SkipMode mode, bool checkCond), FileSkipper>();
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;
}
track.Artist = track.Artist.Trim(); foreach (var tle in trackLists.lists)
track.Album = track.Album.Trim(); {
track.Title = track.Title.Trim(); 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) static async Task MainLoop()
{
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)
{ {
if (trackLists.Count == 0) return; if (trackLists.Count == 0) return;
PrepareListEntry(defaultConfig, trackLists[0]); var tle0 = trackLists.lists[0];
var firstConfig = trackLists.lists[0].config; bool enableParallelSearch = tle0.config.parallelAlbumSearch && !tle0.config.PrintResults && !tle0.config.PrintTracks && trackLists.lists.Any(x => x.CanParallelSearch);
bool enableParallelSearch = firstConfig.parallelAlbumSearch && !firstConfig.PrintResults && !firstConfig.PrintTracks && trackLists.lists.Any(x => x.CanParallelSearch);
var parallelSearches = new List<(TrackListEntry tle, Task<(bool, ResponseData)> task)>(); 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++) for (int i = 0; i < trackLists.lists.Count; i++)
{ {
if (!enableParallelSearch) Console.WriteLine(); if (!enableParallelSearch) Console.WriteLine();
if (i > 0) PrepareListEntry(trackLists[i-1].config, trackLists[i]);
var tle = trackLists[i]; var tle = trackLists[i];
var config = tle.config; var config = tle.config;
PreprocessTracks(tle);
if (!enableParallelSearch) tle.PrintLines();
var existing = new List<Track>(); var existing = new List<Track>();
var notFound = new List<Track>(); var notFound = new List<Track>();
@ -342,13 +281,13 @@ static partial class Program
if (config.skipExisting && !config.PrintResults && tle.source.State != TrackState.NotFoundLastTime) 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); existing.Add(tle.source);
if (tle.source.State != TrackState.AlreadyExists && !tle.needSourceSearch) if (tle.source.State != TrackState.AlreadyExists && !tle.needSourceSearch)
{ {
foreach (var tracks in tle.list) 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(); await parallelSearchSemaphore.WaitAsync();
progress = enableParallelSearch ? Printing.GetProgressBar(config) : null; 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; bool foundSomething = false;
var responseData = new ResponseData(); var responseData = new ResponseData();
@ -460,7 +400,7 @@ static partial class Program
if (config.skipExisting && tle.needSkipExistingAfterSearch) if (config.skipExisting && tle.needSkipExistingAfterSearch)
{ {
foreach (var tracks in tle.list) foreach (var tracks in tle.list)
existing.AddRange(DoSkipExisting(tle, config, tracks)); existing.AddRange(DoSkipExisting(tle, tracks));
} }
if (tle.gotoNextAfterSearch) if (tle.gotoNextAfterSearch)
@ -557,7 +497,7 @@ static partial class Program
if (tle.config.skipExisting && tle.needSkipExistingAfterSearch) if (tle.config.skipExisting && tle.needSkipExistingAfterSearch)
{ {
foreach (var tracks in tle.list) foreach (var tracks in tle.list)
DoSkipExisting(tle, tle.config, tracks); DoSkipExisting(tle, tracks);
} }
} }
} }
@ -567,12 +507,13 @@ static partial class Program
} }
static List<Track> DoSkipExisting(TrackListEntry tle, Config config, List<Track> tracks) static List<Track> DoSkipExisting(TrackListEntry tle, List<Track> tracks)
{ {
var context = FileSkipperContext.FromTrackListEntry(tle);
var existing = new List<Track>(); var existing = new List<Track>();
foreach (var track in tracks) foreach (var track in tracks)
{ {
if (SetExisting(tle, config, track)) if (SetExisting(tle, context, track))
{ {
existing.Add(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; string? path = null;
@ -590,7 +531,7 @@ static partial class Program
if (!tle.outputDirSkipper.IndexIsBuilt) if (!tle.outputDirSkipper.IndexIsBuilt)
tle.outputDirSkipper.BuildIndex(); tle.outputDirSkipper.BuildIndex();
tle.outputDirSkipper.TrackExists(track, out path); tle.outputDirSkipper.TrackExists(track, context, out path);
} }
if (path == null && tle.musicDirSkipper != null) if (path == null && tle.musicDirSkipper != null)
@ -601,7 +542,7 @@ static partial class Program
tle.musicDirSkipper.BuildIndex(); tle.musicDirSkipper.BuildIndex();
} }
tle.musicDirSkipper.TrackExists(track, out path); tle.musicDirSkipper.TrackExists(track, context, out path);
} }
if (path != null) if (path != null)
@ -753,11 +694,13 @@ static partial class Program
if (tracks != null && tle.source.DownloadPath.Length > 0) if (tracks != null && tle.source.DownloadPath.Length > 0)
{ {
organizer.OrganizeAlbum(tracks, additionalImages); organizer.OrganizeAlbum(tle.source, tracks, additionalImages);
} }
tle.indexEditor?.Update(); tle.indexEditor?.Update();
tle.playlistEditor?.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, false);
{
OnComplete(config, config.onComplete, track);
}
semaphore.Release(); 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) if (onComplete.Length == 0)
return; return;
bool useShellExecute = false; bool useShellExecute = false;
bool createNoWindow = false;
int count = 0; 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; if (onComplete[0] == 's')
} {
else if (onComplete[0].IsDigit() && onComplete[1] == ':') useShellExecute = true;
{ }
if ((int)track.State != int.Parse(onComplete[0].ToString())) else if (onComplete[0] == 'a')
return; {
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 else
{ {
break; break;
} }
onComplete = onComplete[2..];
} }
var process = new Process(); var process = new Process();
var startInfo = new ProcessStartInfo(); var startInfo = new ProcessStartInfo();
onComplete = onComplete.Replace("{title}", track.Title) onComplete = onComplete
.Replace("{artist}", track.Artist) .Replace("{title}", track.Title)
.Replace("{album}", track.Album) .Replace("{artist}", track.Artist)
.Replace("{uri}", track.URI) .Replace("{album}", track.Album)
.Replace("{length}", track.Length.ToString()) .Replace("{uri}", track.URI)
.Replace("{artist-maybe-wrong}", track.ArtistMaybeWrong.ToString()) .Replace("{length}", track.Length.ToString())
.Replace("{type}", track.Type.ToString()) .Replace("{row}", (track.CsvOrListRow == -1 ? -1 : track.CsvOrListRow + 1).ToString())
.Replace("{is-not-audio}", track.IsNotAudio.ToString()) .Replace("{artist-maybe-wrong}", track.ArtistMaybeWrong.ToString())
.Replace("{failure-reason}", track.FailureReason.ToString()) .Replace("{type}", track.Type.ToString())
.Replace("{path}", track.DownloadPath) .Replace("{is-not-audio}", track.IsNotAudio.ToString())
.Replace("{state}", track.State.ToString()) .Replace("{failure-reason}", track.FailureReason.ToString())
.Replace("{extractor}", config.inputType.ToString()) .Replace("{path}", track.DownloadPath.TrimEnd('/').TrimEnd('\\'))
.Trim(); .Replace("{state}", track.State.ToString())
.Replace("{extractor}", config.inputType.ToString())
.Replace("{bindir}", AppDomain.CurrentDomain.BaseDirectory.TrimEnd('/').TrimEnd('\\'))
.Trim();
if (onComplete[0] == '"') if (onComplete[0] == '"')
{ {
@ -1367,6 +1322,8 @@ static partial class Program
{ {
startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true; startInfo.RedirectStandardError = true;
if (!createNoWindow)
startInfo.CreateNoWindow = false;
} }
startInfo.UseShellExecute = useShellExecute; startInfo.UseShellExecute = useShellExecute;

View file

@ -224,7 +224,7 @@ static class Search
{ {
string saveFilePathNoExt = organizer.GetSavePathNoExt(title); string saveFilePathNoExt = organizer.GetSavePathNoExt(title);
downloading = 1; 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); saveFilePath = await Extractors.YouTube.YtdlpDownload(id, saveFilePathNoExt, config.ytdlpArgument, printCommand: config.debugInfo);
Printing.RefreshOrPrint(progress, 100, $"Succeded: yt-dlp completed download for {track}", true); Printing.RefreshOrPrint(progress, 100, $"Succeded: yt-dlp completed download for {track}", true);
break; break;

View file

@ -13,9 +13,9 @@ namespace Tests
public static async Task RunAllTests() public static async Task RunAllTests()
{ {
TestStringUtils(); TestStringUtils();
//TestAutoProfiles(); TestAutoProfiles();
//TestProfileConditions(); TestProfileConditions();
//await TestStringExtractor(); await TestStringExtractor();
//TestM3uEditor(); //TestM3uEditor();
Console.WriteLine('\n' + new string('#', 50) + '\n' + "All tests passed."); Console.WriteLine('\n' + new string('#', 50) + '\n' + "All tests passed.");
@ -75,366 +75,357 @@ namespace Tests
Passed(); Passed();
} }
//public static void TestAutoProfiles() public static void TestAutoProfiles()
//{ {
// SetCurrentTest("TestAutoProfiles"); SetCurrentTest("TestAutoProfiles");
// var config = new Config(); string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf");
// config.inputType = InputType.YouTube;
// config.interactiveMode = true; string content =
// config.aggregate = false; "max-stale-time = 5" +
// config.maxStaleTime = 50000; "\nfast-search = true" +
"\nformat = flac" +
// string path = Path.Join(Directory.GetCurrentDirectory(), "test_conf.conf");
"\n[profile-true-1]" +
// string content = "\nprofile-cond = input-type == \"youtube\" && download-mode == \"album\"" +
// "max-stale-time = 5" + "\nmax-stale-time = 10" +
// "\nfast-search = true" +
// "\nformat = flac" + "\n[profile-true-2]" +
"\nprofile-cond = !aggregate" +
// "\n[profile-true-1]" + "\nfast-search = false" +
// "\nprofile-cond = input-type == \"youtube\" && download-mode == \"album\"" +
// "\nmax-stale-time = 10" + "\n[profile-false-1]" +
"\nprofile-cond = input-type == \"string\"" +
// "\n[profile-true-2]" + "\nformat = mp3" +
// "\nprofile-cond = !aggregate" +
// "\nfast-search = false" + "\n[profile-no-cond]" +
"\nformat = opus";
// "\n[profile-false-1]" + File.WriteAllText(path, content);
// "\nprofile-cond = input-type == \"string\"" + var config = new Config(new string[] { "-c", path });
// "\nformat = mp3" + config.inputType = InputType.YouTube;
config.interactiveMode = true;
// "\n[profile-no-cond]" + config.aggregate = false;
// "\nformat = opus"; config.maxStaleTime = 50000;
var tle = new TrackListEntry(TrackType.Album);
// File.WriteAllText(path, content); config.UpdateProfiles(tle);
Assert(config.maxStaleTime == 10 && !config.fastSearch && config.necessaryCond.Formats[0] == "flac");
// config.LoadAndParse(new string[] { "-c", path });
content =
// var tle = new TrackListEntry(TrackType.Album); "\n[no-stale]" +
// Config.UpdateProfiles(tle); "\nprofile-cond = interactive && download-mode == \"album\"" +
"\nmax-stale-time = 999999" +
// Assert(config.maxStaleTime == 10 && !config.fastSearch && config.necessaryCond.Formats[0] == "flac"); "\n[youtube]" +
"\nprofile-cond = input-type == \"youtube\"" +
// ResetConfig(); "\nyt-dlp = true";
// config.inputType = InputType.CSV; File.WriteAllText(path, content);
// config.album = true; config = new Config(new string[] { "-c", path });
// config.interactiveMode = true; config.inputType = InputType.CSV;
// config.useYtdlp = false; config.album = true;
// config.maxStaleTime = 50000; config.interactiveMode = true;
// content = config.useYtdlp = false;
// "\n[no-stale]" + config.maxStaleTime = 50000;
// "\nprofile-cond = interactive && download-mode == \"album\"" + config.UpdateProfiles(tle);
// "\nmax-stale-time = 999999" + Assert(config.maxStaleTime == 999999 && !config.useYtdlp);
// "\n[youtube]" +
// "\nprofile-cond = input-type == \"youtube\"" + content =
// "\nyt-dlp = true"; "\n[no-stale]" +
"\nprofile-cond = interactive && download-mode == \"album\"" +
// File.WriteAllText(path, content); "\nmax-stale-time = 999999" +
"\n[youtube]" +
"\nprofile-cond = input-type == \"youtube\"" +
// config.LoadAndParse(new string[] { "-c", path }); "\nyt-dlp = true";
// Config.UpdateProfiles(tle); File.WriteAllText(path, content);
// Assert(config.maxStaleTime == 999999 && !config.useYtdlp); config = new Config(new string[] { "-c", path });
config.inputType = InputType.YouTube;
// ResetConfig(); config.album = false;
// config.inputType = InputType.YouTube; config.interactiveMode = true;
// config.album = false; config.useYtdlp = false;
// config.interactiveMode = true; config.maxStaleTime = 50000;
// config.useYtdlp = false; config.UpdateProfiles(new TrackListEntry(TrackType.Normal));
// config.maxStaleTime = 50000; Assert(config.maxStaleTime == 50000 && config.useYtdlp);
// content =
// "\n[no-stale]" + if (File.Exists(path)) File.Delete(path);
// "\nprofile-cond = interactive && download-mode == \"album\"" +
// "\nmax-stale-time = 999999" + Passed();
// "\n[youtube]" + }
// "\nprofile-cond = input-type == \"youtube\"" +
// "\nyt-dlp = true"; public static void TestProfileConditions()
{
// File.WriteAllText(path, content); SetCurrentTest("TestProfileConditions");
// config.LoadAndParse(new string[] { "-c", path });
// Config.UpdateProfiles(new TrackListEntry(TrackType.Normal)); var config = new Config(new string[] { });
config.inputType = InputType.YouTube;
// Assert(config.maxStaleTime == 50000 && config.useYtdlp); config.interactiveMode = true;
config.album = true;
// if (File.Exists(path)) config.aggregate = false;
// File.Delete(path);
var conds = new (bool, string)[]
// Passed(); {
//} (true, "input-type == \"youtube\""),
(true, "download-mode == \"album\""),
//public static void TestProfileConditions() (false, "aggregate"),
//{ (true, "interactive"),
// SetCurrentTest("TestProfileConditions"); (true, "album"),
(false, "!interactive"),
// config.inputType = InputType.YouTube; (true, "album && input-type == \"youtube\""),
// config.interactiveMode = true; (false, "album && input-type != \"youtube\""),
// config.album = true; (false, "(interactive && aggregate)"),
// config.aggregate = false; (true, "album && (interactive || aggregate)"),
(true, "input-type == \"spotify\" || aggregate || input-type == \"csv\" || interactive && album"),
// var conds = new (bool, string)[] (true, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || interactive ) )"),
// { (false, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || !interactive ) )"),
// (true, "input-type == \"youtube\""), };
// (true, "download-mode == \"album\""),
// (false, "aggregate"), foreach ((var b, var c) in conds)
// (true, "interactive"), {
// (true, "album"), Console.WriteLine(c);
// (false, "!interactive"), Assert(b == config.ProfileConditionSatisfied(c));
// (true, "album && input-type == \"youtube\""), }
// (false, "album && input-type != \"youtube\""),
// (false, "(interactive && aggregate)"), Passed();
// (true, "album && (interactive || aggregate)"), }
// (true, "input-type == \"spotify\" || aggregate || input-type == \"csv\" || interactive && album"),
// (true, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || interactive ) )"), public static async Task TestStringExtractor()
// (false, " input-type!=\"youtube\"||(album&&!interactive ||(aggregate || !interactive ) )"), {
// }; SetCurrentTest("TestStringExtractor");
// foreach ((var b, var c) in conds) var strings = new List<string>()
// { {
// Console.WriteLine(c); "Some Title",
// Assert(b == config.ProfileConditionSatisfied(c)); "Some, Title",
// } "artist = Some artist, title = some title",
"Artist - Title, length = 42",
// Passed(); "title=Some, Title, artist=Some, Artist, album = Some, Album, length= 42",
//} "Some, Artist = a - Some, Title = b, album = Some, Album, length = 42",
//public static async Task TestStringExtractor() "Foo Bar",
//{ "Foo - Bar",
// SetCurrentTest("TestStringExtractor"); "Artist - Title, length=42",
"title=Title, artist=Artist, length=42",
// var strings = new List<string>() };
// {
// "Some Title", var tracks = new List<Track>()
// "Some, Title", {
// "artist = Some artist, title = some title", new Track() { Title="Some Title" },
// "Artist - Title, length = 42", new Track() { Title="Some, Title" },
// "title=Some, Title, artist=Some, Artist, album = Some, Album, length= 42", new Track() { Title = "some title", Artist = "Some artist" },
// "Some, Artist = a - Some, Title = b, album = Some, Album, length = 42", new Track() { Title = "Title", Artist = "Artist", Length = 42 },
new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42 },
// "Foo Bar", new Track() { Title="Some, Title = b", Artist = "Some, Artist = a", Album = "Some, Album", Length = 42 },
// "Foo - Bar",
// "Artist - Title, length=42", new Track() { Title = "Foo Bar" },
// "title=Title, artist=Artist, length=42", new Track() { Title = "Bar", Artist = "Foo" },
// }; new Track() { Title = "Title", Artist = "Artist", Length = 42 },
new Track() { Title = "Title", Artist = "Artist", Length = 42 },
// var tracks = new List<Track>() };
// {
// new Track() { Title="Some Title" }, var albums = new List<Track>()
// new Track() { Title="Some, Title" }, {
// new Track() { Title = "some title", Artist = "Some artist" }, new Track() { Album="Some Title", Type = TrackType.Album },
// new Track() { Title = "Title", Artist = "Artist", Length = 42 }, new Track() { Album="Some, Title", Type = TrackType.Album },
// new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42 }, new Track() { Title = "some title", Artist = "Some artist", Type = TrackType.Album },
// new Track() { Title="Some, Title = b", Artist = "Some, Artist = a", Album = "Some, Album", Length = 42 }, 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() { Title = "Foo Bar" }, new Track() { Artist = "Some, Artist = a", Album = "Some, Album", Length = 42, Type = TrackType.Album },
// new Track() { Title = "Bar", Artist = "Foo" },
// new Track() { Title = "Title", Artist = "Artist", Length = 42 }, new Track() { Album = "Foo Bar", Type = TrackType.Album },
// new Track() { Title = "Title", Artist = "Artist", Length = 42 }, 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 albums = new List<Track>() };
// {
// new Track() { Album="Some Title", Type = TrackType.Album }, var extractor = new Extractors.StringExtractor();
// new Track() { Album="Some, Title", Type = TrackType.Album },
// new Track() { Title = "some title", Artist = "Some artist", Type = TrackType.Album }, var config = new Config(new string[] { });
// new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album }, config.aggregate = false;
// new Track() { Title="Some, Title", Artist = "Some, Artist", Album = "Some, Album", Length = 42, Type = TrackType.Album }, config.album = false;
// new Track() { Artist = "Some, Artist = a", Album = "Some, Album", Length = 42, Type = TrackType.Album },
Console.WriteLine("Testing songs: ");
// new Track() { Album = "Foo Bar", Type = TrackType.Album }, for (int i = 0; i < strings.Count; i++)
// new Track() { Album = "Bar", Artist = "Foo", Type = TrackType.Album }, {
// new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album }, config.input = strings[i];
// new Track() { Title = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album }, Console.WriteLine(config.input);
// }; var res = await extractor.GetTracks(config.input, 0, 0, false, config);
var t = res[0].list[0][0];
// var extractor = new Extractors.StringExtractor(); Assert(Extractors.StringExtractor.InputMatches(config.input));
Assert(t.ToKey() == tracks[i].ToKey());
// config.aggregate = false; }
// config.album = false;
Console.WriteLine();
// Console.WriteLine("Testing songs: "); Console.WriteLine("Testing albums");
// for (int i = 0; i < strings.Count; i++) config.album = true;
// { for (int i = 0; i < strings.Count; i++)
// config.input = strings[i]; {
// Console.WriteLine(config.input); config.input = strings[i];
// var res = await extractor.GetTracks(config.input, 0, 0, false); Console.WriteLine(config.input);
// var t = res[0].list[0][0]; var t = (await extractor.GetTracks(config.input, 0, 0, false, config))[0].source;
// Assert(Extractors.StringExtractor.InputMatches(config.input)); Assert(Extractors.StringExtractor.InputMatches(config.input));
// Assert(t.ToKey() == tracks[i].ToKey()); Assert(t.ToKey() == albums[i].ToKey());
// } }
// Console.WriteLine(); Passed();
// Console.WriteLine("Testing albums"); }
// config.album = true;
// for (int i = 0; i < strings.Count; i++) public static void TestM3uEditor()
// { {
// config.input = strings[i]; throw new NotImplementedException();
// Console.WriteLine(config.input); //SetCurrentTest("TestM3uEditor");
// var t = (await extractor.GetTracks(config.input, 0, 0, false))[0].source;
// Assert(Extractors.StringExtractor.InputMatches(config.input)); //var config = new Config(new string[] { });
// Assert(t.ToKey() == albums[i].ToKey()); //config.skipMode = SkipMode.Index;
// } //config.skipMusicDir = "";
//config.printOption = PrintOption.Tracks | PrintOption.Full;
// Passed(); //config.skipExisting = true;
//}
//string path = Path.Join(Directory.GetCurrentDirectory(), "test_m3u.m3u8");
//public static void TestM3uEditor()
//{ //if (File.Exists(path))
// SetCurrentTest("TestM3uEditor"); // File.Delete(path);
// config.skipMode = SkipMode.Index; //File.WriteAllText(path, $"#SLDL:" +
// config.skipMusicDir = ""; // $"{Path.Join(Directory.GetCurrentDirectory(), "file1.5")},\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;" +
// config.printOption = PrintOption.Tracks | PrintOption.Full; // $"path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;" +
// config.skipExisting = true; // $"path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;" +
// $",\"Artist; ,3\",,Title3 ;a,-1,0,4,0;" +
// string path = Path.Join(Directory.GetCurrentDirectory(), "test_m3u.m3u8"); // $",\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
// $",,,,-1,0,0,0;");
// if (File.Exists(path))
// File.Delete(path); //var notFoundInitial = new List<Track>()
//{
// File.WriteAllText(path, $"#SLDL:" + // new() { Artist = "Artist; ,3", Title = "Title3 ;a" },
// $"{Path.Join(Directory.GetCurrentDirectory(), "file1.5")},\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;" + // new() { Artist = "Artist,,, ;4", Title = "Title4", State = TrackState.Failed, FailureReason = FailureReason.NoSuitableFileFound }
// $"path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;" + //};
// $"path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;" + //var existingInitial = new List<Track>()
// $",\"Artist; ,3\",,Title3 ;a,-1,0,4,0;" + //{
// $",\"Artist,,, ;4\",,Title4,-1,0,4,3;" + // new() { Artist = "Artist, 1", Title = "Title, , 1", DownloadPath = "path/to/file1", State = TrackState.Downloaded },
// $",,,,-1,0,0,0;"); // 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 notFoundInitial = new List<Track>() //};
// { //var toBeDownloadedInitial = new List<Track>()
// new() { Artist = "Artist; ,3", Title = "Title3 ;a" }, //{
// new() { Artist = "Artist,,, ;4", Title = "Title4", State = TrackState.Failed, FailureReason = FailureReason.NoSuitableFileFound } // new() { Artist = "ArtistA", Album = "Albumm", Title = "TitleA" },
// }; // new() { Artist = "ArtistB", Album = "Albumm", Title = "TitleB" }
// var existingInitial = new List<Track>() //};
// {
// new() { Artist = "Artist, 1", Title = "Title, , 1", DownloadPath = "path/to/file1", State = TrackState.Downloaded }, //var trackLists = new TrackLists();
// new() { Artist = "Artist, 1.5", Title = "Title, , 1.5", DownloadPath = Path.Join(Directory.GetCurrentDirectory(), "file1.5"), State = TrackState.Downloaded }, //trackLists.AddEntry(new TrackListEntry(TrackType.Normal));
// new() { Artist = "Artist, 2", Title = "Title2", DownloadPath = "path/to/file2", State = TrackState.AlreadyExists } //foreach (var t in notFoundInitial)
// }; // trackLists.AddTrackToLast(t);
// var toBeDownloadedInitial = new List<Track>() //foreach (var t in existingInitial)
// { // trackLists.AddTrackToLast(t);
// new() { Artist = "ArtistA", Album = "Albumm", Title = "TitleA" }, //foreach (var t in toBeDownloadedInitial)
// new() { Artist = "ArtistB", Album = "Albumm", Title = "TitleB" } // trackLists.AddTrackToLast(t);
// };
//var editor = new M3uEditor(path, trackLists, M3uOption.All);
// var trackLists = new TrackLists(); //trackLists[0].indexEditor = editor;
// trackLists.AddEntry(new TrackListEntry(TrackType.Normal));
// foreach (var t in notFoundInitial) //trackLists[0].outputDirSkipper = new IndexSkipper();
// trackLists.AddTrackToLast(t);
// foreach (var t in existingInitial) //var notFound = (List<Track>)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
// trackLists.AddTrackToLast(t); //var existing = (List<Track>)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0] });
// foreach (var t in toBeDownloadedInitial) //var toBeDownloaded = trackLists[0].list[0].Where(t => t.State == TrackState.Initial).ToList();
// trackLists.AddTrackToLast(t);
//Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial));
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All); //Assert(existing.SequenceEqualUpToPermutation(existingInitial));
//Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
// Program.outputDirSkipper = new IndexSkipper(Program.indexEditor, false);
//Printing.PrintTracksTbd(toBeDownloaded, existing, notFound, TrackType.Normal, config);
// var notFound = (List<Track>)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
// var existing = (List<Track>)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0] }); //editor.Update();
// var toBeDownloaded = trackLists[0].list[0].Where(t => t.State == TrackState.Initial).ToList(); //string output = File.ReadAllText(path);
//string need =
// Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial)); // "#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;" +
// Assert(existing.SequenceEqualUpToPermutation(existingInitial)); // "\n" +
// Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial)); // "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
// "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
// Printing.PrintTracksTbd(toBeDownloaded, existing, notFound, TrackType.Normal); // "\npath/to/file1" +
// "\nfile1.5" +
// Program.indexEditor.Update(); // "\npath/to/file2" +
// string output = File.ReadAllText(path); // "\n";
// string need = //Assert(output == 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" + //toBeDownloaded[0].State = TrackState.Downloaded;
// "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" + //toBeDownloaded[0].DownloadPath = "new/file/path";
// "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" + //toBeDownloaded[1].State = TrackState.Failed;
// "\npath/to/file1" + //toBeDownloaded[1].FailureReason = FailureReason.NoSuitableFileFound;
// "\nfile1.5" + //existing[1].DownloadPath = "/other/new/file/path";
// "\npath/to/file2" +
// "\n"; //editor.Update();
// Assert(output == need); //output = File.ReadAllText(path);
//need =
// toBeDownloaded[0].State = TrackState.Downloaded; // "#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;" +
// toBeDownloaded[0].DownloadPath = "new/file/path"; // ",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
// toBeDownloaded[1].State = TrackState.Failed; // "\n" +
// toBeDownloaded[1].FailureReason = FailureReason.NoSuitableFileFound; // "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
// existing[1].DownloadPath = "/other/new/file/path"; // "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
// "\npath/to/file1" +
// Program.indexEditor.Update(); // "\n/other/new/file/path" +
// output = File.ReadAllText(path); // "\npath/to/file2" +
// need = // "\nnew/file/path" +
// "#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;" + // "\n#FAIL: ArtistB - TitleB [NoSuitableFileFound]" +
// ",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" + // "\n";
// "\n" + //Assert(output == need);
// "\n#FAIL: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
// "\n#FAIL: Artist,,, ;4 - Title4 [NoSuitableFileFound]" + //Console.WriteLine();
// "\npath/to/file1" + //Console.WriteLine(output);
// "\n/other/new/file/path" +
// "\npath/to/file2" + //editor = new M3uEditor(path, trackLists, M3uOption.All);
// "\nnew/file/path" +
// "\n#FAIL: ArtistB - TitleB [NoSuitableFileFound]" + //foreach (var t in trackLists.Flattened(false, false))
// "\n"; //{
// Assert(output == need); // editor.TryGetPreviousRunResult(t, out var prev);
// Assert(prev != null);
// Console.WriteLine(); // Assert(prev.ToKey() == t.ToKey());
// Console.WriteLine(output); // Assert(prev.DownloadPath == t.DownloadPath);
// Assert(prev.State == t.State || prev.State == TrackState.NotFoundLastTime);
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All); // Assert(prev.FailureReason == t.FailureReason);
//}
// foreach (var t in trackLists.Flattened(false, false))
// { //editor.Update();
// Program.indexEditor.TryGetPreviousRunResult(t, out var prev); //output = File.ReadAllText(path);
// Assert(prev != null); //Assert(output == need);
// Assert(prev.ToKey() == t.ToKey());
// Assert(prev.DownloadPath == t.DownloadPath);
// Assert(prev.State == t.State || prev.State == TrackState.NotFoundLastTime); //var test = new List<Track>
// Assert(prev.FailureReason == t.FailureReason); //{
// } // new() { Artist = "ArtistA", Album = "AlbumA", Type = TrackType.Album },
// new() { Artist = "ArtistB", Album = "AlbumB", Type = TrackType.Album },
// Program.indexEditor.Update(); // new() { Artist = "ArtistC", Album = "AlbumC", Type = TrackType.Album },
// output = File.ReadAllText(path); //};
// Assert(output == need);
//trackLists = new TrackLists();
//foreach (var t in test)
// var test = new List<Track> // trackLists.AddEntry(new TrackListEntry(t));
// {
// new() { Artist = "ArtistA", Album = "AlbumA", Type = TrackType.Album }, //File.WriteAllText(path, "");
// new() { Artist = "ArtistB", Album = "AlbumB", Type = TrackType.Album }, //editor = new M3uEditor(path, trackLists, M3uOption.Index);
// new() { Artist = "ArtistC", Album = "AlbumC", Type = TrackType.Album }, //editor.Update();
// };
//Assert(File.ReadAllText(path) == "");
// trackLists = new TrackLists();
// foreach (var t in test) //test[0].State = TrackState.Downloaded;
// trackLists.AddEntry(new TrackListEntry(t)); //test[0].DownloadPath = "download/path";
//test[1].State = TrackState.Failed;
// File.WriteAllText(path, ""); //test[1].FailureReason = FailureReason.NoSuitableFileFound;
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index); //test[2].State = TrackState.AlreadyExists;
// Program.indexEditor.Update();
//editor.Update();
// Assert(File.ReadAllText(path) == "");
//editor = new M3uEditor(path, trackLists, M3uOption.Index);
// test[0].State = TrackState.Downloaded;
// test[0].DownloadPath = "download/path"; //foreach (var t in test)
// test[1].State = TrackState.Failed; //{
// test[1].FailureReason = FailureReason.NoSuitableFileFound; // editor.TryGetPreviousRunResult(t, out var tt);
// test[2].State = TrackState.AlreadyExists; // Assert(tt != null);
// Assert(tt.ToKey() == t.ToKey());
// Program.indexEditor.Update(); // t.DownloadPath = "this should not change tt.DownloadPath";
// Assert(t.DownloadPath != tt.DownloadPath);
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index); //}
// foreach (var t in test) //File.Delete(path);
// {
// Program.indexEditor.TryGetPreviousRunResult(t, out var tt); //Passed();
// 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 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() public static void Passed()
{ {
Console.WriteLine($"{currentTest} passed"); Console.WriteLine($"{currentTest} passed");

View file

@ -187,7 +187,7 @@ public static class Printing
public static void PrintComplete(TrackLists trackLists) public static void PrintComplete(TrackLists trackLists)
{ {
var ls = trackLists.Flattened(true, true); var ls = trackLists.Flattened(true, false);
int successes = 0, fails = 0; int successes = 0, fails = 0;
foreach (var x in ls) foreach (var x in ls)
{ {

View file

@ -103,17 +103,20 @@ public static class Utils
public static string ExpandUser(string path) public static string ExpandUser(string path)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrWhiteSpace(path))
{
return 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); 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; return path;