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