mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 06:22:41 +00:00
commit
This commit is contained in:
parent
85b94de641
commit
c639daabce
17 changed files with 781 additions and 801 deletions
64
README.md
64
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.
|
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.
|
||||||
|
|
39
publish.bat
39
publish.bat
|
@ -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
|
call :publish_and_zip win-x86 false sldl_win-x86.zip
|
||||||
dotnet publish -c Release -r win-x86 -p:PublishSingleFile=true -p:DefineConstants=WINDOWS --self-contained false
|
call :publish_and_zip win-x86 true sldl_win-x86_self-contained.zip
|
||||||
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
|
call :publish_and_zip linux-x64 true sldl_linux-x64.zip
|
||||||
if exist slsk-batchdl\bin\zips\sldl_win-x86.zip del /F /Q slsk-batchdl\bin\zips\sldl_win-x86.zip
|
call :publish_and_zip linux-arm true 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\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'); }"
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,23 +42,30 @@ 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();
|
||||||
|
|
||||||
|
if (loadPreviousResults)
|
||||||
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();
|
||||||
|
|
||||||
|
if (useOldFormat) lines = lines.Skip(1).ToList();
|
||||||
|
int offset = useOldFormat ? "#SLDL:".Length : 0;
|
||||||
|
|
||||||
|
foreach (var sldlLine in indexLines)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sldlLine))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int k = offset;
|
||||||
bool inQuotes = false;
|
bool inQuotes = false;
|
||||||
|
|
||||||
for (; k < sldlLine.Length && sldlLine[k] == ' '; k++);
|
for (; k < sldlLine.Length && sldlLine[k] == ' '; k++);
|
||||||
|
@ -109,7 +116,7 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
|
||||||
currentItem.Clear();
|
currentItem.Clear();
|
||||||
field++;
|
field++;
|
||||||
}
|
}
|
||||||
else if (field == 7 && c == ';')
|
else if (field == 7 && c == ';' && useOldFormat)
|
||||||
{
|
{
|
||||||
track.FailureReason = (FailureReason)int.Parse(currentItem.ToString());
|
track.FailureReason = (FailureReason)int.Parse(currentItem.ToString());
|
||||||
currentItem.Clear();
|
currentItem.Clear();
|
||||||
|
@ -122,7 +129,17 @@ public class M3uEditor // todo: separate into M3uEditor and IndexEditor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!useOldFormat)
|
||||||
|
{
|
||||||
|
track.FailureReason = (FailureReason)int.Parse(currentItem.ToString());
|
||||||
|
currentItem.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,140 +106,9 @@ 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.skipCheckPrefCond)
|
|
||||||
{
|
|
||||||
cond = config.necessaryCond.With(config.preferredCond);
|
|
||||||
}
|
|
||||||
else if (config.skipCheckCond)
|
|
||||||
{
|
|
||||||
cond = config.necessaryCond;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<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++)
|
|
||||||
{
|
|
||||||
foreach (var ls in tle.list)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < ls.Count; i++)
|
|
||||||
{
|
|
||||||
PreprocessTrack(config, ls[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void PreprocessTrack(Config config, Track track)
|
|
||||||
{
|
{
|
||||||
if (config.removeFt)
|
if (config.removeFt)
|
||||||
{
|
{
|
||||||
|
@ -264,67 +135,135 @@ static partial class Program
|
||||||
track.Title = track.Title.Trim();
|
track.Title = track.Title.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preprocessTrack(tle.config, tle.source);
|
||||||
|
|
||||||
static void PrepareListEntry(Config prevConfig, TrackListEntry tle)
|
for (int k = 0; k < tle.list.Count; k++)
|
||||||
{
|
{
|
||||||
tle.config = prevConfig.Copy();
|
foreach (var ls in tle.list)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < ls.Count; i++)
|
||||||
|
{
|
||||||
|
preprocessTrack(tle.config, ls[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void PrepareListEntries(Config startConfig)
|
||||||
|
{
|
||||||
|
var editors = new Dictionary<(string path, M3uOption option), M3uEditor>();
|
||||||
|
var skippers = new Dictionary<(string dir, SkipMode mode, bool checkCond), FileSkipper>();
|
||||||
|
|
||||||
|
foreach (var tle in trackLists.lists)
|
||||||
|
{
|
||||||
|
tle.config = startConfig.Copy();
|
||||||
tle.config.UpdateProfiles(tle);
|
tle.config.UpdateProfiles(tle);
|
||||||
|
startConfig = tle.config;
|
||||||
|
|
||||||
if (tle.extractorCond != null)
|
if (tle.extractorCond != null)
|
||||||
{
|
{
|
||||||
tle.config.necessaryCond = tle.config.necessaryCond.With(tle.extractorCond);
|
tle.config.necessaryCond.AddConditions(tle.extractorCond);
|
||||||
tle.extractorCond = null;
|
tle.extractorCond = null;
|
||||||
}
|
}
|
||||||
if (tle.extractorPrefCond != null)
|
if (tle.extractorPrefCond != null)
|
||||||
{
|
{
|
||||||
tle.config.preferredCond = tle.config.preferredCond.With(tle.extractorPrefCond);
|
tle.config.preferredCond.AddConditions(tle.extractorPrefCond);
|
||||||
tle.extractorPrefCond = null;
|
tle.extractorPrefCond = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
InitEditors(tle, tle.config);
|
var indexOption = tle.config.writeIndex ? M3uOption.Index : M3uOption.None;
|
||||||
InitFileSkippers(tle, tle.config);
|
if (indexOption != M3uOption.None || (tle.config.skipExisting && tle.config.skipMode == SkipMode.Index) || tle.config.skipNotFound)
|
||||||
|
{
|
||||||
string m3uPath, indexPath;
|
string 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)
|
if (tle.config.indexFilePath.Length > 0)
|
||||||
indexPath = tle.config.indexFilePath;
|
indexPath = tle.config.indexFilePath;
|
||||||
else
|
else
|
||||||
indexPath = Path.Join(tle.config.parentDir, tle.defaultFolderName, "_index.sldl");
|
indexPath = Path.Join(tle.config.parentDir, tle.defaultFolderName, "_index.sldl");
|
||||||
|
|
||||||
if (tle.config.writePlaylist)
|
if (editors.TryGetValue((indexPath, indexOption), out var indexEditor))
|
||||||
tle.playlistEditor?.SetPathAndLoad(m3uPath);
|
{
|
||||||
if (tle.config.writeIndex)
|
tle.indexEditor = indexEditor;
|
||||||
tle.indexEditor?.SetPathAndLoad(indexPath);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tle.indexEditor = new M3uEditor(indexPath, trackLists, indexOption, true);
|
||||||
|
editors.Add((indexPath, indexOption), tle.indexEditor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PreprocessTracks(tle.config, tle);
|
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 async Task MainLoop(Config defaultConfig)
|
static async Task MainLoop()
|
||||||
{
|
{
|
||||||
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,47 +1240,62 @@ 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] == ':')
|
||||||
|
{
|
||||||
|
if (onComplete[0] == 's')
|
||||||
{
|
{
|
||||||
useShellExecute = true;
|
useShellExecute = true;
|
||||||
}
|
}
|
||||||
else if (onComplete[0].IsDigit() && onComplete[1] == ':')
|
else if (onComplete[0] == 'a')
|
||||||
{
|
{
|
||||||
if ((int)track.State != int.Parse(onComplete[0].ToString()))
|
if (!isAlbumOnComplete) return;
|
||||||
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("{title}", track.Title)
|
||||||
.Replace("{artist}", track.Artist)
|
.Replace("{artist}", track.Artist)
|
||||||
.Replace("{album}", track.Album)
|
.Replace("{album}", track.Album)
|
||||||
.Replace("{uri}", track.URI)
|
.Replace("{uri}", track.URI)
|
||||||
.Replace("{length}", track.Length.ToString())
|
.Replace("{length}", track.Length.ToString())
|
||||||
|
.Replace("{row}", (track.CsvOrListRow == -1 ? -1 : track.CsvOrListRow + 1).ToString())
|
||||||
.Replace("{artist-maybe-wrong}", track.ArtistMaybeWrong.ToString())
|
.Replace("{artist-maybe-wrong}", track.ArtistMaybeWrong.ToString())
|
||||||
.Replace("{type}", track.Type.ToString())
|
.Replace("{type}", track.Type.ToString())
|
||||||
.Replace("{is-not-audio}", track.IsNotAudio.ToString())
|
.Replace("{is-not-audio}", track.IsNotAudio.ToString())
|
||||||
.Replace("{failure-reason}", track.FailureReason.ToString())
|
.Replace("{failure-reason}", track.FailureReason.ToString())
|
||||||
.Replace("{path}", track.DownloadPath)
|
.Replace("{path}", track.DownloadPath.TrimEnd('/').TrimEnd('\\'))
|
||||||
.Replace("{state}", track.State.ToString())
|
.Replace("{state}", track.State.ToString())
|
||||||
.Replace("{extractor}", config.inputType.ToString())
|
.Replace("{extractor}", config.inputType.ToString())
|
||||||
|
.Replace("{bindir}", AppDomain.CurrentDomain.BaseDirectory.TrimEnd('/').TrimEnd('\\'))
|
||||||
.Trim();
|
.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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,213 +75,203 @@ 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;
|
|
||||||
// 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" +
|
||||||
|
|
||||||
// string content =
|
"\n[profile-true-1]" +
|
||||||
// "max-stale-time = 5" +
|
"\nprofile-cond = input-type == \"youtube\" && download-mode == \"album\"" +
|
||||||
// "\nfast-search = true" +
|
"\nmax-stale-time = 10" +
|
||||||
// "\nformat = flac" +
|
|
||||||
|
|
||||||
// "\n[profile-true-1]" +
|
"\n[profile-true-2]" +
|
||||||
// "\nprofile-cond = input-type == \"youtube\" && download-mode == \"album\"" +
|
"\nprofile-cond = !aggregate" +
|
||||||
// "\nmax-stale-time = 10" +
|
"\nfast-search = false" +
|
||||||
|
|
||||||
// "\n[profile-true-2]" +
|
"\n[profile-false-1]" +
|
||||||
// "\nprofile-cond = !aggregate" +
|
"\nprofile-cond = input-type == \"string\"" +
|
||||||
// "\nfast-search = false" +
|
"\nformat = mp3" +
|
||||||
|
|
||||||
// "\n[profile-false-1]" +
|
"\n[profile-no-cond]" +
|
||||||
// "\nprofile-cond = input-type == \"string\"" +
|
"\nformat = opus";
|
||||||
// "\nformat = mp3" +
|
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");
|
||||||
|
|
||||||
// "\n[profile-no-cond]" +
|
content =
|
||||||
// "\nformat = opus";
|
"\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);
|
||||||
|
|
||||||
// File.WriteAllText(path, content);
|
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);
|
||||||
|
|
||||||
// config.LoadAndParse(new string[] { "-c", path });
|
if (File.Exists(path)) File.Delete(path);
|
||||||
|
|
||||||
// var tle = new TrackListEntry(TrackType.Album);
|
Passed();
|
||||||
// Config.UpdateProfiles(tle);
|
}
|
||||||
|
|
||||||
// Assert(config.maxStaleTime == 10 && !config.fastSearch && config.necessaryCond.Formats[0] == "flac");
|
public static void TestProfileConditions()
|
||||||
|
{
|
||||||
|
SetCurrentTest("TestProfileConditions");
|
||||||
|
|
||||||
// ResetConfig();
|
var config = new Config(new string[] { });
|
||||||
// config.inputType = InputType.CSV;
|
config.inputType = InputType.YouTube;
|
||||||
// config.album = true;
|
config.interactiveMode = true;
|
||||||
// config.interactiveMode = true;
|
config.album = true;
|
||||||
// config.useYtdlp = false;
|
config.aggregate = 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);
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
// config.LoadAndParse(new string[] { "-c", path });
|
Passed();
|
||||||
// Config.UpdateProfiles(tle);
|
}
|
||||||
// Assert(config.maxStaleTime == 999999 && !config.useYtdlp);
|
|
||||||
|
|
||||||
// ResetConfig();
|
public static async Task TestStringExtractor()
|
||||||
// config.inputType = InputType.YouTube;
|
{
|
||||||
// config.album = false;
|
SetCurrentTest("TestStringExtractor");
|
||||||
// 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);
|
var strings = new List<string>()
|
||||||
// config.LoadAndParse(new string[] { "-c", path });
|
{
|
||||||
// Config.UpdateProfiles(new TrackListEntry(TrackType.Normal));
|
"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",
|
||||||
|
|
||||||
// Assert(config.maxStaleTime == 50000 && config.useYtdlp);
|
"Foo Bar",
|
||||||
|
"Foo - Bar",
|
||||||
|
"Artist - Title, length=42",
|
||||||
|
"title=Title, artist=Artist, length=42",
|
||||||
|
};
|
||||||
|
|
||||||
// if (File.Exists(path))
|
var tracks = new List<Track>()
|
||||||
// File.Delete(path);
|
{
|
||||||
|
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 },
|
||||||
|
|
||||||
// Passed();
|
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 },
|
||||||
|
};
|
||||||
|
|
||||||
//public static void TestProfileConditions()
|
var albums = new List<Track>()
|
||||||
//{
|
{
|
||||||
// SetCurrentTest("TestProfileConditions");
|
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 },
|
||||||
|
|
||||||
// config.inputType = InputType.YouTube;
|
new Track() { Album = "Foo Bar", Type = TrackType.Album },
|
||||||
// config.interactiveMode = true;
|
new Track() { Album = "Bar", Artist = "Foo", Type = TrackType.Album },
|
||||||
// config.album = true;
|
new Track() { Album = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
|
||||||
// config.aggregate = false;
|
new Track() { Title = "Title", Artist = "Artist", Length = 42, Type = TrackType.Album },
|
||||||
|
};
|
||||||
|
|
||||||
// var conds = new (bool, string)[]
|
var extractor = new Extractors.StringExtractor();
|
||||||
// {
|
|
||||||
// (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)
|
var config = new Config(new string[] { });
|
||||||
// {
|
config.aggregate = false;
|
||||||
// Console.WriteLine(c);
|
config.album = false;
|
||||||
// Assert(b == config.ProfileConditionSatisfied(c));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Passed();
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
//public static async Task TestStringExtractor()
|
Console.WriteLine();
|
||||||
//{
|
Console.WriteLine("Testing albums");
|
||||||
// SetCurrentTest("TestStringExtractor");
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
// var strings = new List<string>()
|
Passed();
|
||||||
// {
|
}
|
||||||
// "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",
|
public static void TestM3uEditor()
|
||||||
// "Foo - Bar",
|
{
|
||||||
// "Artist - Title, length=42",
|
throw new NotImplementedException();
|
||||||
// "title=Title, artist=Artist, length=42",
|
|
||||||
// };
|
|
||||||
|
|
||||||
// var tracks = new List<Track>()
|
|
||||||
// {
|
|
||||||
// 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<Track>()
|
|
||||||
// {
|
|
||||||
// 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");
|
//SetCurrentTest("TestM3uEditor");
|
||||||
|
|
||||||
|
//var config = new Config(new string[] { });
|
||||||
//config.skipMode = SkipMode.Index;
|
//config.skipMode = SkipMode.Index;
|
||||||
//config.skipMusicDir = "";
|
//config.skipMusicDir = "";
|
||||||
//config.printOption = PrintOption.Tracks | PrintOption.Full;
|
//config.printOption = PrintOption.Tracks | PrintOption.Full;
|
||||||
|
@ -326,9 +316,10 @@ namespace Tests
|
||||||
//foreach (var t in toBeDownloadedInitial)
|
//foreach (var t in toBeDownloadedInitial)
|
||||||
// trackLists.AddTrackToLast(t);
|
// trackLists.AddTrackToLast(t);
|
||||||
|
|
||||||
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All);
|
//var editor = new M3uEditor(path, trackLists, M3uOption.All);
|
||||||
|
//trackLists[0].indexEditor = editor;
|
||||||
|
|
||||||
// Program.outputDirSkipper = new IndexSkipper(Program.indexEditor, false);
|
//trackLists[0].outputDirSkipper = new IndexSkipper();
|
||||||
|
|
||||||
//var notFound = (List<Track>)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
|
//var notFound = (List<Track>)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
|
||||||
//var existing = (List<Track>)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0] });
|
//var existing = (List<Track>)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0] });
|
||||||
|
@ -338,9 +329,9 @@ namespace Tests
|
||||||
//Assert(existing.SequenceEqualUpToPermutation(existingInitial));
|
//Assert(existing.SequenceEqualUpToPermutation(existingInitial));
|
||||||
//Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
|
//Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
|
||||||
|
|
||||||
// Printing.PrintTracksTbd(toBeDownloaded, existing, notFound, TrackType.Normal);
|
//Printing.PrintTracksTbd(toBeDownloaded, existing, notFound, TrackType.Normal, config);
|
||||||
|
|
||||||
// Program.indexEditor.Update();
|
//editor.Update();
|
||||||
//string output = File.ReadAllText(path);
|
//string output = File.ReadAllText(path);
|
||||||
//string need =
|
//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;" +
|
// "#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;" +
|
||||||
|
@ -359,7 +350,7 @@ namespace Tests
|
||||||
//toBeDownloaded[1].FailureReason = FailureReason.NoSuitableFileFound;
|
//toBeDownloaded[1].FailureReason = FailureReason.NoSuitableFileFound;
|
||||||
//existing[1].DownloadPath = "/other/new/file/path";
|
//existing[1].DownloadPath = "/other/new/file/path";
|
||||||
|
|
||||||
// Program.indexEditor.Update();
|
//editor.Update();
|
||||||
//output = File.ReadAllText(path);
|
//output = File.ReadAllText(path);
|
||||||
//need =
|
//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;" +
|
// "#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;" +
|
||||||
|
@ -378,11 +369,11 @@ namespace Tests
|
||||||
//Console.WriteLine();
|
//Console.WriteLine();
|
||||||
//Console.WriteLine(output);
|
//Console.WriteLine(output);
|
||||||
|
|
||||||
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.All);
|
//editor = new M3uEditor(path, trackLists, M3uOption.All);
|
||||||
|
|
||||||
//foreach (var t in trackLists.Flattened(false, false))
|
//foreach (var t in trackLists.Flattened(false, false))
|
||||||
//{
|
//{
|
||||||
// Program.indexEditor.TryGetPreviousRunResult(t, out var prev);
|
// editor.TryGetPreviousRunResult(t, out var prev);
|
||||||
// Assert(prev != null);
|
// Assert(prev != null);
|
||||||
// Assert(prev.ToKey() == t.ToKey());
|
// Assert(prev.ToKey() == t.ToKey());
|
||||||
// Assert(prev.DownloadPath == t.DownloadPath);
|
// Assert(prev.DownloadPath == t.DownloadPath);
|
||||||
|
@ -390,7 +381,7 @@ namespace Tests
|
||||||
// Assert(prev.FailureReason == t.FailureReason);
|
// Assert(prev.FailureReason == t.FailureReason);
|
||||||
//}
|
//}
|
||||||
|
|
||||||
// Program.indexEditor.Update();
|
//editor.Update();
|
||||||
//output = File.ReadAllText(path);
|
//output = File.ReadAllText(path);
|
||||||
//Assert(output == need);
|
//Assert(output == need);
|
||||||
|
|
||||||
|
@ -407,8 +398,8 @@ namespace Tests
|
||||||
// trackLists.AddEntry(new TrackListEntry(t));
|
// trackLists.AddEntry(new TrackListEntry(t));
|
||||||
|
|
||||||
//File.WriteAllText(path, "");
|
//File.WriteAllText(path, "");
|
||||||
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index);
|
//editor = new M3uEditor(path, trackLists, M3uOption.Index);
|
||||||
// Program.indexEditor.Update();
|
//editor.Update();
|
||||||
|
|
||||||
//Assert(File.ReadAllText(path) == "");
|
//Assert(File.ReadAllText(path) == "");
|
||||||
|
|
||||||
|
@ -418,13 +409,13 @@ namespace Tests
|
||||||
//test[1].FailureReason = FailureReason.NoSuitableFileFound;
|
//test[1].FailureReason = FailureReason.NoSuitableFileFound;
|
||||||
//test[2].State = TrackState.AlreadyExists;
|
//test[2].State = TrackState.AlreadyExists;
|
||||||
|
|
||||||
// Program.indexEditor.Update();
|
//editor.Update();
|
||||||
|
|
||||||
// Program.indexEditor = new M3uEditor(path, trackLists, M3uOption.Index);
|
//editor = new M3uEditor(path, trackLists, M3uOption.Index);
|
||||||
|
|
||||||
//foreach (var t in test)
|
//foreach (var t in test)
|
||||||
//{
|
//{
|
||||||
// Program.indexEditor.TryGetPreviousRunResult(t, out var tt);
|
// editor.TryGetPreviousRunResult(t, out var tt);
|
||||||
// Assert(tt != null);
|
// Assert(tt != null);
|
||||||
// Assert(tt.ToKey() == t.ToKey());
|
// Assert(tt.ToKey() == t.ToKey());
|
||||||
// t.DownloadPath = "this should not change tt.DownloadPath";
|
// t.DownloadPath = "this should not change tt.DownloadPath";
|
||||||
|
@ -434,7 +425,7 @@ namespace Tests
|
||||||
//File.Delete(path);
|
//File.Delete(path);
|
||||||
|
|
||||||
//Passed();
|
//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");
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue