mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 14:32:40 +00:00
commit
This commit is contained in:
parent
71160047d8
commit
6e7b8d5d67
15 changed files with 704 additions and 438 deletions
25
README.md
25
README.md
|
@ -63,6 +63,7 @@ Usage: sldl <input> [OPTIONS]
|
||||||
library. Use with --skip-existing
|
library. Use with --skip-existing
|
||||||
--skip-not-found Skip searching for tracks that weren't found on Soulseek
|
--skip-not-found Skip searching for tracks that weren't found on Soulseek
|
||||||
during the last run. Fails are read from the m3u file.
|
during the last run. Fails are read from the m3u file.
|
||||||
|
--skip-existing-pref-cond Use preferred instead of necessary conds for skip-existing
|
||||||
|
|
||||||
--display-mode <option> Changes how searches and downloads are displayed:
|
--display-mode <option> Changes how searches and downloads are displayed:
|
||||||
'single' (default): Show transfer state and percentage
|
'single' (default): Show transfer state and percentage
|
||||||
|
@ -402,23 +403,25 @@ salbum Source album name
|
||||||
year Track year or date
|
year Track year or date
|
||||||
track Track number
|
track Track number
|
||||||
disc Disc number
|
disc Disc number
|
||||||
filename Soulseek filename without extension
|
foldername Soulseek folder name (only available for album downloads)
|
||||||
foldername Default sldl folder name
|
default-foldername Default sldl folder name
|
||||||
extractor Name of the extractor used (CSV/Spotify/YouTube/etc)
|
extractor Name of the extractor used (CSV/Spotify/YouTube/etc)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Skip existing
|
## Skip existing
|
||||||
|
|
||||||
sldl can skip files that exist in the download directory or a specified directory configured with
|
sldl can skip downloads that exist in the output directory or a specified directory configured
|
||||||
--music-dir.
|
with --music-dir.
|
||||||
The following modes are available for --skip-mode:
|
The following modes are available for --skip-mode:
|
||||||
|
|
||||||
### m3u
|
### m3u
|
||||||
Default when checking in the output directory.
|
Default when checking in the output directory.
|
||||||
Checks whether the output m3u file contains the track in the '#SLDL' line. Does not check if
|
Checks whether the output m3u file contains the track in the '#SLDL' line. Does not check if
|
||||||
the audio file exists or satisfies the file conditions (use m3u-cond for that).
|
the audio file exists or satisfies the file conditions (use m3u-cond for that). m3u and
|
||||||
|
m3u-cond are the only modes that can skip album downloads.
|
||||||
|
|
||||||
### name
|
### name
|
||||||
|
Default when checking in the music directory.
|
||||||
Compares filenames to the track title and artist name to determine if a track already exists.
|
Compares filenames to the track title and artist name to determine if a track already exists.
|
||||||
Specifically, a track will be skipped if there exists a file whose name contains the title
|
Specifically, a track will be skipped if there exists a file whose name contains the title
|
||||||
and whose full path contains the artist name.
|
and whose full path contains the artist name.
|
||||||
|
@ -429,11 +432,11 @@ Default when checking in the output directory.
|
||||||
(ignoring case and ws). Slower than name mode as it needs to read all file tags.
|
(ignoring case and ws). Slower than name mode as it needs to read all file tags.
|
||||||
|
|
||||||
### m3u-cond, name-cond, tag-cond
|
### m3u-cond, name-cond, tag-cond
|
||||||
Default for checking in --music-dir: name-cond.
|
Same as the above modes but also checks whether the found file satisfies the configured
|
||||||
Same as the above modes but also checks whether the found file satisfies necessary conditions.
|
conditions. Uses necessary conditions by default, run with --skip-existing-pref-cond to use
|
||||||
Equivalent to the above modes if no necessary conditions have been specified (except m3u-cond
|
preferred conditions instead. Equivalent to the above modes if no necessary conditions have
|
||||||
which always checks if the file exists). May be slower and use a lot of memory for large
|
been specified (except m3u-cond, which always checks if the file exists).
|
||||||
libraries.
|
May be slower and use a lot of memory for large libraries.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
### Config Location:
|
### Config Location:
|
||||||
|
|
|
@ -74,13 +74,14 @@ static class Config
|
||||||
public static bool noModifyShareCount = false;
|
public static bool noModifyShareCount = false;
|
||||||
public static bool useRandomLogin = false;
|
public static bool useRandomLogin = false;
|
||||||
public static bool noBrowseFolder = false;
|
public static bool noBrowseFolder = false;
|
||||||
|
public static bool skipExistingPrefCond = false;
|
||||||
public static int downrankOn = -1;
|
public static int downrankOn = -1;
|
||||||
public static int ignoreOn = -2;
|
public static int ignoreOn = -2;
|
||||||
public static int minAlbumTrackCount = -1;
|
public static int minAlbumTrackCount = -1;
|
||||||
public static int maxAlbumTrackCount = -1;
|
public static int maxAlbumTrackCount = -1;
|
||||||
public static int fastSearchDelay = 300;
|
public static int fastSearchDelay = 300;
|
||||||
public static int maxTracks = int.MaxValue;
|
|
||||||
public static int minUsersAggregate = 2;
|
public static int minUsersAggregate = 2;
|
||||||
|
public static int maxTracks = int.MaxValue;
|
||||||
public static int offset = 0;
|
public static int offset = 0;
|
||||||
public static int maxStaleTime = 50000;
|
public static int maxStaleTime = 50000;
|
||||||
public static int updateDelay = 100;
|
public static int updateDelay = 100;
|
||||||
|
@ -98,8 +99,8 @@ static class Config
|
||||||
public static M3uOption m3uOption = M3uOption.Index;
|
public static M3uOption m3uOption = M3uOption.Index;
|
||||||
public static DisplayMode displayMode = DisplayMode.Single;
|
public static DisplayMode displayMode = DisplayMode.Single;
|
||||||
public static InputType inputType = InputType.None;
|
public static InputType inputType = InputType.None;
|
||||||
public static SkipMode skipMode = SkipMode.M3uCond;
|
public static SkipMode skipMode = SkipMode.M3u;
|
||||||
public static SkipMode skipModeMusicDir = SkipMode.NameCond;
|
public static SkipMode skipModeMusicDir = SkipMode.Name;
|
||||||
public static PrintOption printOption = PrintOption.None;
|
public static PrintOption printOption = PrintOption.None;
|
||||||
|
|
||||||
static readonly Dictionary<string, (List<string> args, string? cond)> profiles = new();
|
static readonly Dictionary<string, (List<string> args, string? cond)> profiles = new();
|
||||||
|
@ -226,7 +227,7 @@ static class Config
|
||||||
m3uOption = M3uOption.None;
|
m3uOption = M3uOption.None;
|
||||||
else if (!hasConfiguredM3uMode && inputType == InputType.String)
|
else if (!hasConfiguredM3uMode && inputType == InputType.String)
|
||||||
m3uOption = M3uOption.None;
|
m3uOption = M3uOption.None;
|
||||||
else if (!hasConfiguredM3uMode && !aggregate && Program.trackLists != null && Program.trackLists.Flattened(true, false, true).All(t => t.IsAlbum))
|
else if (!hasConfiguredM3uMode && Program.trackLists != null && !aggregate &&!Program.trackLists.Flattened(true, false, true).Skip(1).Any())
|
||||||
m3uOption = M3uOption.None;
|
m3uOption = M3uOption.None;
|
||||||
|
|
||||||
parentFolder = Utils.ExpandUser(parentFolder);
|
parentFolder = Utils.ExpandUser(parentFolder);
|
||||||
|
@ -238,7 +239,7 @@ static class Config
|
||||||
if (folderName == ".")
|
if (folderName == ".")
|
||||||
folderName = "";
|
folderName = "";
|
||||||
|
|
||||||
folderName = folderName.Replace("\\", "/");
|
folderName = folderName.Replace('\\', '/');
|
||||||
folderName = string.Join('/', folderName.Split('/').Select(x => x.ReplaceInvalidChars(invalidReplaceStr).Trim()));
|
folderName = string.Join('/', folderName.Split('/').Select(x => x.ReplaceInvalidChars(invalidReplaceStr).Trim()));
|
||||||
folderName = folderName.Replace('/', Path.DirectorySeparatorChar);
|
folderName = folderName.Replace('/', Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
@ -370,7 +371,7 @@ static class Config
|
||||||
return var switch
|
return var switch
|
||||||
{
|
{
|
||||||
"input-type" => inputType.ToString().ToLower(),
|
"input-type" => inputType.ToString().ToLower(),
|
||||||
"download-mode" => tle != null ? toKebab(tle.type.ToString())
|
"download-mode" => tle != null ? toKebab(tle.source.Type.ToString())
|
||||||
: album && aggregate ? "album-aggregate" : album ? "album" : aggregate ? "aggregate" : "normal",
|
: album && aggregate ? "album-aggregate" : album ? "album" : aggregate ? "aggregate" : "normal",
|
||||||
"interactive" => interactiveMode,
|
"interactive" => interactiveMode,
|
||||||
"album" => album,
|
"album" => album,
|
||||||
|
@ -1148,6 +1149,10 @@ static class Config
|
||||||
case "--no-browse-folder":
|
case "--no-browse-folder":
|
||||||
setFlag(ref noBrowseFolder, ref i);
|
setFlag(ref noBrowseFolder, ref i);
|
||||||
break;
|
break;
|
||||||
|
case "--sepc":
|
||||||
|
case "--skip-existing-pref-cond":
|
||||||
|
setFlag(ref skipExistingPrefCond, ref i);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException($"Unknown argument: {args[i]}");
|
throw new ArgumentException($"Unknown argument: {args[i]}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,17 +12,20 @@ namespace Data
|
||||||
public string URI = "";
|
public string URI = "";
|
||||||
public int Length = -1;
|
public int Length = -1;
|
||||||
public bool ArtistMaybeWrong = false;
|
public bool ArtistMaybeWrong = false;
|
||||||
public bool IsAlbum = false;
|
|
||||||
public int MinAlbumTrackCount = -1;
|
public int MinAlbumTrackCount = -1;
|
||||||
public int MaxAlbumTrackCount = -1;
|
public int MaxAlbumTrackCount = -1;
|
||||||
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 CsvRow = -1;
|
||||||
|
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;
|
||||||
public SlDictionary? Downloads = null;
|
public SlDictionary? Downloads = null;
|
||||||
|
|
||||||
|
public bool OutputsDirectory => Type != TrackType.Normal;
|
||||||
|
public Soulseek.File? FirstDownload => Downloads?.FirstOrDefault().Value.Item2;
|
||||||
|
|
||||||
public Track() { }
|
public Track() { }
|
||||||
|
|
||||||
public Track(Track other)
|
public Track(Track other)
|
||||||
|
@ -34,7 +37,7 @@ namespace Data
|
||||||
URI = other.URI;
|
URI = other.URI;
|
||||||
ArtistMaybeWrong = other.ArtistMaybeWrong;
|
ArtistMaybeWrong = other.ArtistMaybeWrong;
|
||||||
Downloads = other.Downloads;
|
Downloads = other.Downloads;
|
||||||
IsAlbum = other.IsAlbum;
|
Type = other.Type;
|
||||||
IsNotAudio = other.IsNotAudio;
|
IsNotAudio = other.IsNotAudio;
|
||||||
State = other.State;
|
State = other.State;
|
||||||
FailureReason = other.FailureReason;
|
FailureReason = other.FailureReason;
|
||||||
|
@ -47,7 +50,7 @@ namespace Data
|
||||||
|
|
||||||
public string ToKey()
|
public string ToKey()
|
||||||
{
|
{
|
||||||
return $"{Artist};{Album};{Title};{Length}";
|
return $"{Artist};{Album};{Title};{Length};{(int)Type}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
@ -61,7 +64,7 @@ namespace Data
|
||||||
return $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}";
|
return $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}";
|
||||||
|
|
||||||
string str = Artist;
|
string str = Artist;
|
||||||
if (!IsAlbum && Title.Length == 0 && Downloads != null && !Downloads.IsEmpty)
|
if (Type == TrackType.Normal && Title.Length == 0 && Downloads != null && !Downloads.IsEmpty)
|
||||||
{
|
{
|
||||||
str = $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}";
|
str = $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}";
|
||||||
}
|
}
|
||||||
|
@ -69,7 +72,7 @@ namespace Data
|
||||||
{
|
{
|
||||||
if (str.Length > 0)
|
if (str.Length > 0)
|
||||||
str += " - ";
|
str += " - ";
|
||||||
if (IsAlbum)
|
if (Type == TrackType.Album)
|
||||||
str += Album;
|
str += Album;
|
||||||
else if (Title.Length > 0)
|
else if (Title.Length > 0)
|
||||||
str += Title;
|
str += Title;
|
||||||
|
@ -77,7 +80,7 @@ namespace Data
|
||||||
{
|
{
|
||||||
if (Length > 0)
|
if (Length > 0)
|
||||||
str += $" ({Length}s)";
|
str += $" ({Length}s)";
|
||||||
if (IsAlbum)
|
if (Type == TrackType.Album)
|
||||||
str += " (album)";
|
str += " (album)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,28 +96,56 @@ namespace Data
|
||||||
public class TrackListEntry
|
public class TrackListEntry
|
||||||
{
|
{
|
||||||
public List<List<Track>> list;
|
public List<List<Track>> list;
|
||||||
public ListType type;
|
|
||||||
public Track source;
|
public Track source;
|
||||||
public bool needSearch;
|
public bool needSourceSearch = false;
|
||||||
public bool placeInSubdir;
|
public bool sourceCanBeSkipped = false;
|
||||||
|
public bool needSkipExistingAfterSearch = false;
|
||||||
|
public bool gotoNextAfterSearch = false;
|
||||||
|
public bool placeInSubdir = false;
|
||||||
|
public string? subdirOverride;
|
||||||
|
|
||||||
public TrackListEntry(List<List<Track>> list, ListType type, Track source)
|
public TrackListEntry()
|
||||||
{
|
{
|
||||||
this.list = list;
|
list = new List<List<Track>>();
|
||||||
this.type = type;
|
source = new Track();
|
||||||
this.source = source;
|
|
||||||
|
|
||||||
needSearch = type != ListType.Normal;
|
|
||||||
placeInSubdir = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TrackListEntry(List<List<Track>> list, ListType type, Track source, bool needSearch, bool placeInSubdir)
|
public TrackListEntry(Track source)
|
||||||
|
{
|
||||||
|
list = new List<List<Track>>();
|
||||||
|
this.source = source;
|
||||||
|
|
||||||
|
needSourceSearch = source.Type != TrackType.Normal;
|
||||||
|
needSkipExistingAfterSearch = source.Type == TrackType.Aggregate;
|
||||||
|
gotoNextAfterSearch = source.Type == TrackType.AlbumAggregate;
|
||||||
|
sourceCanBeSkipped = source.Type != TrackType.Normal
|
||||||
|
&& source.Type != TrackType.Aggregate
|
||||||
|
&& source.Type != TrackType.AlbumAggregate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackListEntry(List<List<Track>> list, Track source)
|
||||||
{
|
{
|
||||||
this.list = list;
|
this.list = list;
|
||||||
this.type = type;
|
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.needSearch = needSearch;
|
|
||||||
|
needSourceSearch = source.Type != TrackType.Normal;
|
||||||
|
needSkipExistingAfterSearch = source.Type == TrackType.Aggregate;
|
||||||
|
gotoNextAfterSearch = source.Type == TrackType.AlbumAggregate;
|
||||||
|
sourceCanBeSkipped = source.Type != TrackType.Normal
|
||||||
|
&& source.Type != TrackType.Aggregate
|
||||||
|
&& source.Type != TrackType.AlbumAggregate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TrackListEntry(List<List<Track>> list, Track source, bool needSearch, bool placeInSubdir,
|
||||||
|
bool canBeSkipped, bool needSkipExistingAfterSearch, bool gotoNextAfterSearch)
|
||||||
|
{
|
||||||
|
this.list = list;
|
||||||
|
this.source = source;
|
||||||
|
this.needSourceSearch = needSearch;
|
||||||
this.placeInSubdir = placeInSubdir;
|
this.placeInSubdir = placeInSubdir;
|
||||||
|
this.sourceCanBeSkipped = canBeSkipped;
|
||||||
|
this.needSkipExistingAfterSearch = needSkipExistingAfterSearch;
|
||||||
|
this.gotoNextAfterSearch = gotoNextAfterSearch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,21 +155,7 @@ namespace Data
|
||||||
|
|
||||||
public TrackLists() { }
|
public TrackLists() { }
|
||||||
|
|
||||||
public TrackLists(List<(List<List<Track>> list, ListType type, Track source)> lists)
|
public static TrackLists FromFlattened(IEnumerable<Track> flatList)
|
||||||
{
|
|
||||||
foreach (var (list, type, source) in lists)
|
|
||||||
{
|
|
||||||
var newList = new List<List<Track>>();
|
|
||||||
foreach (var innerList in list)
|
|
||||||
{
|
|
||||||
var innerNewList = new List<Track>(innerList);
|
|
||||||
newList.Add(innerNewList);
|
|
||||||
}
|
|
||||||
this.lists.Add(new TrackListEntry(newList, type, source));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TrackLists FromFlattened(IEnumerable<Track> flatList, bool aggregate, bool album)
|
|
||||||
{
|
{
|
||||||
var res = new TrackLists();
|
var res = new TrackLists();
|
||||||
using var enumerator = flatList.GetEnumerator();
|
using var enumerator = flatList.GetEnumerator();
|
||||||
|
@ -147,35 +164,26 @@ namespace Data
|
||||||
{
|
{
|
||||||
var track = enumerator.Current;
|
var track = enumerator.Current;
|
||||||
|
|
||||||
if (album && aggregate)
|
if (track.Type != TrackType.Normal)
|
||||||
{
|
{
|
||||||
res.AddEntry(ListType.AlbumAggregate, track);
|
res.AddEntry(new TrackListEntry(track));
|
||||||
}
|
|
||||||
else if (aggregate)
|
|
||||||
{
|
|
||||||
res.AddEntry(ListType.Aggregate, track);
|
|
||||||
}
|
|
||||||
else if (album || track.IsAlbum)
|
|
||||||
{
|
|
||||||
track.IsAlbum = true;
|
|
||||||
res.AddEntry(ListType.Album, track);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
res.AddEntry(ListType.Normal);
|
res.AddEntry(new TrackListEntry());
|
||||||
res.AddTrackToLast(track);
|
res.AddTrackToLast(track);
|
||||||
|
|
||||||
bool hasNext;
|
bool hasNext;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
hasNext = enumerator.MoveNext();
|
hasNext = enumerator.MoveNext();
|
||||||
if (!hasNext || enumerator.Current.IsAlbum)
|
if (!hasNext || enumerator.Current.Type != TrackType.Normal)
|
||||||
break;
|
break;
|
||||||
res.AddTrackToLast(enumerator.Current);
|
res.AddTrackToLast(enumerator.Current);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasNext && enumerator.Current.IsAlbum)
|
if (hasNext && enumerator.Current.Type != TrackType.Normal)
|
||||||
res.AddEntry(ListType.Album, track);
|
res.AddEntry(new TrackListEntry(track));
|
||||||
else if (!hasNext)
|
else if (!hasNext)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -195,35 +203,22 @@ namespace Data
|
||||||
lists.Add(tle);
|
lists.Add(tle);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddEntry(List<List<Track>>? list, ListType? type = null, Track? source = null)
|
|
||||||
{
|
|
||||||
type ??= ListType.Normal;
|
|
||||||
source ??= new Track();
|
|
||||||
list ??= new List<List<Track>>();
|
|
||||||
lists.Add(new TrackListEntry(list, (ListType)type, source));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddEntry(List<Track> tracks, ListType? type = null, Track? source = null)
|
|
||||||
{
|
|
||||||
var list = new List<List<Track>>() { tracks };
|
|
||||||
AddEntry(list, type, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddEntry(Track track, ListType? type = null, Track? source = null)
|
|
||||||
{
|
|
||||||
var list = new List<List<Track>>() { new List<Track>() { track } };
|
|
||||||
AddEntry(list, type, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddEntry(ListType? type = null, Track? source = null)
|
|
||||||
{
|
|
||||||
var list = new List<List<Track>>() { new List<Track>() };
|
|
||||||
AddEntry(list, type, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddTrackToLast(Track track)
|
public void AddTrackToLast(Track track)
|
||||||
{
|
{
|
||||||
|
if (lists.Count == 0)
|
||||||
|
{
|
||||||
|
AddEntry(new TrackListEntry(new List<List<Track>> { new List<Track>() { track } }, new Track()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int i = lists.Count - 1;
|
int i = lists.Count - 1;
|
||||||
|
|
||||||
|
if (lists[i].list.Count == 0)
|
||||||
|
{
|
||||||
|
lists[i].list.Add(new List<Track>() { track });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int j = lists[i].list.Count - 1;
|
int j = lists[i].list.Count - 1;
|
||||||
lists[i].list[j].Add(track);
|
lists[i].list[j].Add(track);
|
||||||
}
|
}
|
||||||
|
@ -240,13 +235,57 @@ namespace Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpgradeListTypes(bool aggregate, bool album)
|
||||||
|
{
|
||||||
|
if (!aggregate && !album)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var newLists = new List<TrackListEntry>();
|
||||||
|
|
||||||
|
for (int i = 0; i < lists.Count; i++)
|
||||||
|
{
|
||||||
|
var tle = lists[i];
|
||||||
|
|
||||||
|
if (tle.source.Type == TrackType.Album && aggregate)
|
||||||
|
{
|
||||||
|
tle.source.Type = TrackType.AlbumAggregate;
|
||||||
|
newLists.Add(tle);
|
||||||
|
}
|
||||||
|
else if (tle.source.Type == TrackType.Aggregate && album)
|
||||||
|
{
|
||||||
|
tle.source.Type = TrackType.AlbumAggregate;
|
||||||
|
newLists.Add(tle);
|
||||||
|
}
|
||||||
|
else if (tle.source.Type == TrackType.Normal && (album || aggregate))
|
||||||
|
{
|
||||||
|
foreach (var track in tle.list[0])
|
||||||
|
{
|
||||||
|
if (album && aggregate)
|
||||||
|
track.Type = TrackType.AlbumAggregate;
|
||||||
|
else if (album)
|
||||||
|
track.Type = TrackType.Album;
|
||||||
|
else if (aggregate)
|
||||||
|
track.Type = TrackType.Aggregate;
|
||||||
|
|
||||||
|
newLists.Add(new TrackListEntry(track));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newLists.Add(tle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lists = newLists;
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<Track> Flattened(bool addSources, bool addSpecialSourceTracks, bool sourcesOnly = false)
|
public IEnumerable<Track> Flattened(bool addSources, bool addSpecialSourceTracks, bool sourcesOnly = false)
|
||||||
{
|
{
|
||||||
foreach (var tle in lists)
|
foreach (var tle in lists)
|
||||||
{
|
{
|
||||||
if ((addSources || sourcesOnly) && tle.source != null)
|
if ((addSources || sourcesOnly) && tle.source != null)
|
||||||
yield return tle.source;
|
yield return tle.source;
|
||||||
if (!sourcesOnly && tle.list.Count > 0 && (tle.type == ListType.Normal || addSpecialSourceTracks))
|
if (!sourcesOnly && tle.list.Count > 0 && (tle.source.Type == TrackType.Normal || addSpecialSourceTracks))
|
||||||
{
|
{
|
||||||
foreach (var t in tle.list[0])
|
foreach (var t in tle.list[0])
|
||||||
yield return t;
|
yield return t;
|
||||||
|
|
|
@ -40,12 +40,12 @@ namespace Enums
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ListType
|
public enum TrackType
|
||||||
{
|
{
|
||||||
Normal,
|
Normal = 0,
|
||||||
Album,
|
Album = 1,
|
||||||
Aggregate,
|
Aggregate = 2,
|
||||||
AlbumAggregate,
|
AlbumAggregate = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum M3uOption
|
public enum M3uOption
|
||||||
|
|
|
@ -5,9 +5,9 @@ using System.IO;
|
||||||
|
|
||||||
namespace ExistingCheckers
|
namespace ExistingCheckers
|
||||||
{
|
{
|
||||||
public static class Registry
|
public static class ExistingCheckerRegistry
|
||||||
{
|
{
|
||||||
static IExistingChecker GetChecker(SkipMode mode, string dir, FileConditions conditions, M3uEditor m3uEditor)
|
public static ExistingChecker GetChecker(SkipMode mode, string dir, FileConditions conditions, M3uEditor m3uEditor)
|
||||||
{
|
{
|
||||||
bool noConditions = conditions.Equals(new FileConditions());
|
bool noConditions = conditions.Equals(new FileConditions());
|
||||||
return mode switch
|
return mode switch
|
||||||
|
@ -20,38 +20,16 @@ namespace ExistingCheckers
|
||||||
SkipMode.M3uCond => noConditions ? new M3uExistingChecker(m3uEditor, true) : new M3uConditionExistingChecker(m3uEditor, conditions),
|
SkipMode.M3uCond => noConditions ? new M3uExistingChecker(m3uEditor, true) : new M3uConditionExistingChecker(m3uEditor, conditions),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Dictionary<Track, string> SkipExisting(List<Track> tracks, string dir, FileConditions necessaryCond, M3uEditor m3uEditor, SkipMode mode)
|
|
||||||
{
|
|
||||||
var existing = new Dictionary<Track, string>();
|
|
||||||
|
|
||||||
var checker = GetChecker(mode, dir, necessaryCond, m3uEditor);
|
|
||||||
checker.BuildIndex();
|
|
||||||
|
|
||||||
for (int i = 0; i < tracks.Count; i++)
|
|
||||||
{
|
|
||||||
if (tracks[i].IsNotAudio)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (checker.TrackExists(tracks[i], out string? path))
|
|
||||||
{
|
|
||||||
existing.TryAdd(tracks[i], path);
|
|
||||||
tracks[i].State = TrackState.AlreadyExists;
|
|
||||||
tracks[i].DownloadPath = path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IExistingChecker
|
public abstract class ExistingChecker
|
||||||
{
|
{
|
||||||
public bool TrackExists(Track track, out string? foundPath);
|
public abstract bool TrackExists(Track track, out string? foundPath);
|
||||||
public void BuildIndex() { }
|
public virtual void BuildIndex() { IndexIsBuilt = true; }
|
||||||
|
public bool IndexIsBuilt { get; protected set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NameExistingChecker : IExistingChecker
|
public class NameExistingChecker : ExistingChecker
|
||||||
{
|
{
|
||||||
readonly string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" };
|
readonly string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" };
|
||||||
readonly string dir;
|
readonly string dir;
|
||||||
|
@ -71,7 +49,7 @@ namespace ExistingCheckers
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BuildIndex()
|
public override void BuildIndex()
|
||||||
{
|
{
|
||||||
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
|
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
|
||||||
|
|
||||||
|
@ -86,16 +64,24 @@ namespace ExistingCheckers
|
||||||
index.Add((path, ppath, pname));
|
index.Add((path, ppath, pname));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IndexIsBuilt = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TrackExists(Track track, out string? foundPath)
|
public override bool TrackExists(Track track, out string? foundPath)
|
||||||
{
|
{
|
||||||
|
foundPath = null;
|
||||||
|
|
||||||
|
if (track.OutputsDirectory)
|
||||||
|
return false;
|
||||||
|
|
||||||
string title = Preprocess(track.Title, true);
|
string title = Preprocess(track.Title, true);
|
||||||
string artist = Preprocess(track.Artist, true);
|
string artist = Preprocess(track.Artist, true);
|
||||||
|
|
||||||
foreach ((var path, var ppath, var pname) in index)
|
foreach ((var path, var ppath, var pname) in index)
|
||||||
{
|
{
|
||||||
if (pname.Contains(title) && ppath.Contains(artist))
|
if (pname.ContainsWithBoundaryIgnoreWs(title, acceptLeftDigit: true)
|
||||||
|
&& ppath.ContainsWithBoundaryIgnoreWs(artist, acceptLeftDigit: true))
|
||||||
{
|
{
|
||||||
foundPath = path;
|
foundPath = path;
|
||||||
return true;
|
return true;
|
||||||
|
@ -107,7 +93,7 @@ namespace ExistingCheckers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NameConditionExistingChecker : IExistingChecker
|
public class NameConditionExistingChecker : ExistingChecker
|
||||||
{
|
{
|
||||||
readonly string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" };
|
readonly string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" };
|
||||||
readonly string dir;
|
readonly string dir;
|
||||||
|
@ -129,7 +115,7 @@ namespace ExistingCheckers
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BuildIndex()
|
public override void BuildIndex()
|
||||||
{
|
{
|
||||||
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
|
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
|
||||||
|
|
||||||
|
@ -148,16 +134,25 @@ namespace ExistingCheckers
|
||||||
index.Add((ppath, pname, new SimpleFile(musicFile)));
|
index.Add((ppath, pname, new SimpleFile(musicFile)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IndexIsBuilt = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TrackExists(Track track, out string? foundPath)
|
public override bool TrackExists(Track track, out string? foundPath)
|
||||||
{
|
{
|
||||||
|
foundPath = null;
|
||||||
|
|
||||||
|
if (track.OutputsDirectory)
|
||||||
|
return false;
|
||||||
|
|
||||||
string title = Preprocess(track.Title, true);
|
string title = Preprocess(track.Title, true);
|
||||||
string artist = Preprocess(track.Artist, true);
|
string artist = Preprocess(track.Artist, true);
|
||||||
|
|
||||||
foreach ((var ppath, var pname, var musicFile) in index)
|
foreach ((var ppath, var pname, var musicFile) in index)
|
||||||
{
|
{
|
||||||
if (pname.Contains(title) && ppath.Contains(artist) && conditions.FileSatisfies(musicFile, track))
|
if (pname.ContainsWithBoundaryIgnoreWs(title, acceptLeftDigit: true)
|
||||||
|
&& ppath.ContainsWithBoundaryIgnoreWs(artist, acceptLeftDigit: true)
|
||||||
|
&& conditions.FileSatisfies(musicFile, track))
|
||||||
{
|
{
|
||||||
foundPath = musicFile.Path;
|
foundPath = musicFile.Path;
|
||||||
return true;
|
return true;
|
||||||
|
@ -169,7 +164,7 @@ namespace ExistingCheckers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TagExistingChecker : IExistingChecker
|
public class TagExistingChecker : ExistingChecker
|
||||||
{
|
{
|
||||||
readonly string dir;
|
readonly string dir;
|
||||||
readonly List<(string, string, string)> index = new(); // (Path, PreprocessedArtist, PreprocessedTitle)
|
readonly List<(string, string, string)> index = new(); // (Path, PreprocessedArtist, PreprocessedTitle)
|
||||||
|
@ -184,7 +179,7 @@ namespace ExistingCheckers
|
||||||
return s.Replace(" ", "").RemoveFt().ToLower();
|
return s.Replace(" ", "").RemoveFt().ToLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BuildIndex()
|
public override void BuildIndex()
|
||||||
{
|
{
|
||||||
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
|
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
|
||||||
|
|
||||||
|
@ -201,10 +196,17 @@ namespace ExistingCheckers
|
||||||
index.Add((path, partist, ptitle));
|
index.Add((path, partist, ptitle));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IndexIsBuilt = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TrackExists(Track track, out string? foundPath)
|
public override bool TrackExists(Track track, out string? foundPath)
|
||||||
{
|
{
|
||||||
|
foundPath = null;
|
||||||
|
|
||||||
|
if (track.OutputsDirectory)
|
||||||
|
return false;
|
||||||
|
|
||||||
string title = Preprocess(track.Title);
|
string title = Preprocess(track.Title);
|
||||||
string artist = Preprocess(track.Artist);
|
string artist = Preprocess(track.Artist);
|
||||||
|
|
||||||
|
@ -217,12 +219,11 @@ namespace ExistingCheckers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foundPath = null;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TagConditionExistingChecker : IExistingChecker
|
public class TagConditionExistingChecker : ExistingChecker
|
||||||
{
|
{
|
||||||
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)
|
||||||
|
@ -239,7 +240,7 @@ namespace ExistingCheckers
|
||||||
return s.Replace(" ", "").RemoveFt().ToLower();
|
return s.Replace(" ", "").RemoveFt().ToLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BuildIndex()
|
public override void BuildIndex()
|
||||||
{
|
{
|
||||||
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
|
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
|
||||||
|
|
||||||
|
@ -256,10 +257,17 @@ namespace ExistingCheckers
|
||||||
index.Add((partist, ptitle, new SimpleFile(musicFile)));
|
index.Add((partist, ptitle, new SimpleFile(musicFile)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IndexIsBuilt = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TrackExists(Track track, out string? foundPath)
|
public override bool TrackExists(Track track, out string? foundPath)
|
||||||
{
|
{
|
||||||
|
foundPath = null;
|
||||||
|
|
||||||
|
if (track.OutputsDirectory)
|
||||||
|
return false;
|
||||||
|
|
||||||
string title = Preprocess(track.Title);
|
string title = Preprocess(track.Title);
|
||||||
string artist = Preprocess(track.Artist);
|
string artist = Preprocess(track.Artist);
|
||||||
|
|
||||||
|
@ -272,12 +280,11 @@ namespace ExistingCheckers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foundPath = null;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class M3uExistingChecker : IExistingChecker
|
public class M3uExistingChecker : ExistingChecker
|
||||||
{
|
{
|
||||||
M3uEditor m3uEditor;
|
M3uEditor m3uEditor;
|
||||||
bool checkFileExists;
|
bool checkFileExists;
|
||||||
|
@ -288,16 +295,29 @@ namespace ExistingCheckers
|
||||||
this.checkFileExists = checkFileExists;
|
this.checkFileExists = checkFileExists;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TrackExists(Track track, out string? foundPath)
|
public override bool TrackExists(Track track, out string? foundPath)
|
||||||
{
|
{
|
||||||
foundPath = null;
|
foundPath = null;
|
||||||
var t = m3uEditor.PreviousRunResult(track);
|
var t = m3uEditor.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 && (t.DownloadPath.Length == 0 || !File.Exists(t.DownloadPath)))
|
if (checkFileExists)
|
||||||
{
|
{
|
||||||
return false;
|
if (t.DownloadPath.Length == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (t.OutputsDirectory)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(t.DownloadPath))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!File.Exists(t.DownloadPath))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foundPath = t.DownloadPath;
|
foundPath = t.DownloadPath;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -305,7 +325,7 @@ namespace ExistingCheckers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class M3uConditionExistingChecker : IExistingChecker
|
public class M3uConditionExistingChecker : ExistingChecker
|
||||||
{
|
{
|
||||||
M3uEditor m3uEditor;
|
M3uEditor m3uEditor;
|
||||||
FileConditions conditions;
|
FileConditions conditions;
|
||||||
|
@ -316,36 +336,71 @@ namespace ExistingCheckers
|
||||||
this.conditions = conditions;
|
this.conditions = conditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TrackExists(Track track, out string? foundPath)
|
public override bool TrackExists(Track track, out string? foundPath)
|
||||||
{
|
{
|
||||||
foundPath = null;
|
foundPath = null;
|
||||||
var t = m3uEditor.PreviousRunResult(track);
|
var t = m3uEditor.PreviousRunResult(track);
|
||||||
if (t != null && (t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists) && t.DownloadPath.Length > 0)
|
|
||||||
|
if (t == null || t.DownloadPath.Length == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!t.OutputsDirectory)
|
||||||
{
|
{
|
||||||
if (File.Exists(t.DownloadPath))
|
if (!File.Exists(t.DownloadPath))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
TagLib.File musicFile;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
TagLib.File musicFile;
|
musicFile = TagLib.File.Create(t.DownloadPath);
|
||||||
try
|
if (conditions.FileSatisfies(musicFile, track, false))
|
||||||
{
|
{
|
||||||
musicFile = TagLib.File.Create(t.DownloadPath);
|
foundPath = t.DownloadPath;
|
||||||
if (conditions.FileSatisfies(musicFile, track, false))
|
return true;
|
||||||
{
|
|
||||||
foundPath = t.DownloadPath;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
else
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
else
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(t.DownloadPath))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var files = Directory.GetFiles(t.DownloadPath, "*", SearchOption.AllDirectories);
|
||||||
|
|
||||||
|
if (t.MaxAlbumTrackCount > -1 || t.MinAlbumTrackCount > -1)
|
||||||
|
{
|
||||||
|
int count = files.Count(x=> Utils.IsMusicFile(x));
|
||||||
|
|
||||||
|
if (t.MaxAlbumTrackCount > -1 && count > t.MaxAlbumTrackCount)
|
||||||
|
return false;
|
||||||
|
if (t.MinAlbumTrackCount > -1 && count < t.MinAlbumTrackCount)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var path in files)
|
||||||
|
{
|
||||||
|
if (Utils.IsMusicFile(path))
|
||||||
|
{
|
||||||
|
TagLib.File musicFile;
|
||||||
|
try { musicFile = TagLib.File.Create(path); }
|
||||||
|
catch { return false; }
|
||||||
|
|
||||||
|
if (!conditions.FileSatisfies(musicFile, track))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foundPath = t.DownloadPath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,7 +16,7 @@ namespace Extractors
|
||||||
return input.IsInternetUrl() && input.Contains("bandcamp.com");
|
return input.IsInternetUrl() && input.Contains("bandcamp.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TrackLists> GetTracks()
|
public async Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse)
|
||||||
{
|
{
|
||||||
var trackLists = new TrackLists();
|
var trackLists = new TrackLists();
|
||||||
bool isTrack = Config.input.Contains("/track/");
|
bool isTrack = Config.input.Contains("/track/");
|
||||||
|
@ -54,9 +54,15 @@ namespace Extractors
|
||||||
{
|
{
|
||||||
Album = item.GetProperty("title").GetString(),
|
Album = item.GetProperty("title").GetString(),
|
||||||
Artist = item.GetProperty("artist_name").GetString() ?? item.GetProperty("band_name").GetString(),
|
Artist = item.GetProperty("artist_name").GetString() ?? item.GetProperty("band_name").GetString(),
|
||||||
IsAlbum = true,
|
Type = TrackType.Album,
|
||||||
};
|
};
|
||||||
trackLists.AddEntry(ListType.Album, t);
|
var tle = new TrackListEntry()
|
||||||
|
{
|
||||||
|
source = t,
|
||||||
|
placeInSubdir = true,
|
||||||
|
subdirOverride = t.ToString(true)
|
||||||
|
};
|
||||||
|
trackLists.AddEntry(tle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -70,8 +76,8 @@ namespace Extractors
|
||||||
if (isAlbum)
|
if (isAlbum)
|
||||||
{
|
{
|
||||||
var artist = nameSection.SelectSingleNode(".//h3/span/a").InnerText.UnHtmlString().Trim();
|
var artist = nameSection.SelectSingleNode(".//h3/span/a").InnerText.UnHtmlString().Trim();
|
||||||
var track = new Track() { Artist = artist, Album = name, IsAlbum = true };
|
var track = new Track() { Artist = artist, Album = name, Type = TrackType.Album };
|
||||||
trackLists.AddEntry(ListType.Album, track);
|
trackLists.AddEntry(new TrackListEntry(track));
|
||||||
|
|
||||||
if (Config.setAlbumMinTrackCount || Config.setAlbumMaxTrackCount)
|
if (Config.setAlbumMinTrackCount || Config.setAlbumMaxTrackCount)
|
||||||
{
|
{
|
||||||
|
@ -92,16 +98,20 @@ namespace Extractors
|
||||||
var album = nameSection.SelectSingleNode(".//h3[contains(@class, 'albumTitle')]/span/a").InnerText.UnHtmlString().Trim();
|
var album = nameSection.SelectSingleNode(".//h3[contains(@class, 'albumTitle')]/span/a").InnerText.UnHtmlString().Trim();
|
||||||
var artist = nameSection.SelectSingleNode(".//h3[contains(@class, 'albumTitle')]/span[last()]/a").InnerText.UnHtmlString().Trim();
|
var artist = nameSection.SelectSingleNode(".//h3[contains(@class, 'albumTitle')]/span[last()]/a").InnerText.UnHtmlString().Trim();
|
||||||
//var timeParts = doc.DocumentNode.SelectSingleNode("//span[@class='time_total']").InnerText.Trim().Split(':');
|
//var timeParts = doc.DocumentNode.SelectSingleNode("//span[@class='time_total']").InnerText.Trim().Split(':');
|
||||||
|
|
||||||
var track = new Track() { Artist = artist, Title = name, Album = album };
|
var track = new Track() { Artist = artist, Title = name, Album = album };
|
||||||
trackLists.AddEntry(track);
|
trackLists.AddEntry(new());
|
||||||
|
trackLists.AddTrackToLast(track);
|
||||||
|
|
||||||
Config.defaultFolderName = ".";
|
Config.defaultFolderName = ".";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Config.reverse)
|
if (reverse)
|
||||||
{
|
trackLists.Reverse();
|
||||||
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(Config.offset).Take(Config.maxTracks), Config.aggregate, Config.album);
|
|
||||||
}
|
if (offset > 0 || maxTracks < int.MaxValue)
|
||||||
|
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(offset).Take(maxTracks));
|
||||||
|
|
||||||
return trackLists;
|
return trackLists;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using Data;
|
using Data;
|
||||||
|
using Enums;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Extractors
|
namespace Extractors
|
||||||
|
@ -14,16 +15,30 @@ namespace Extractors
|
||||||
return !input.IsInternetUrl() && input.EndsWith(".csv");
|
return !input.IsInternetUrl() && input.EndsWith(".csv");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TrackLists> GetTracks()
|
public async Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse)
|
||||||
{
|
{
|
||||||
int max = Config.reverse ? int.MaxValue : Config.maxTracks;
|
int max = reverse ? int.MaxValue : maxTracks;
|
||||||
int off = Config.reverse ? 0 : Config.offset;
|
int off = reverse ? 0 : offset;
|
||||||
|
|
||||||
if (!File.Exists(Config.input))
|
if (!File.Exists(Config.input))
|
||||||
throw new FileNotFoundException("CSV file not found");
|
throw new FileNotFoundException("CSV file not found");
|
||||||
|
|
||||||
var tracks = await ParseCsvIntoTrackInfo(Config.input, Config.artistCol, Config.trackCol, Config.lengthCol, Config.albumCol, Config.descCol, Config.ytIdCol, Config.trackCountCol, Config.timeUnit, Config.ytParse);
|
var tracks = await ParseCsvIntoTrackInfo(Config.input, Config.artistCol, Config.trackCol, Config.lengthCol, Config.albumCol, Config.descCol, Config.ytIdCol, Config.trackCountCol, Config.timeUnit, Config.ytParse);
|
||||||
var trackLists = TrackLists.FromFlattened(tracks.Skip(off).Take(max), Config.aggregate, Config.album);
|
|
||||||
|
if (reverse)
|
||||||
|
tracks.Reverse();
|
||||||
|
|
||||||
|
var trackLists = TrackLists.FromFlattened(tracks.Skip(off).Take(max));
|
||||||
|
|
||||||
|
foreach (var tle in trackLists.lists)
|
||||||
|
{
|
||||||
|
if (tle.source.Type != TrackType.Normal)
|
||||||
|
{
|
||||||
|
tle.placeInSubdir = true;
|
||||||
|
tle.subdirOverride = tle.source.ToString(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Config.defaultFolderName = Path.GetFileNameWithoutExtension(Config.input);
|
Config.defaultFolderName = Path.GetFileNameWithoutExtension(Config.input);
|
||||||
|
|
||||||
return trackLists;
|
return trackLists;
|
||||||
|
@ -161,7 +176,8 @@ namespace Extractors
|
||||||
if (ytParse)
|
if (ytParse)
|
||||||
track = await YouTube.ParseTrackInfo(track.Title, track.Artist, track.URI, track.Length, desc);
|
track = await YouTube.ParseTrackInfo(track.Title, track.Artist, track.URI, track.Length, desc);
|
||||||
|
|
||||||
track.IsAlbum = track.Title.Length == 0 && track.Album.Length > 0;
|
if (track.Title.Length == 0 && track.Album.Length > 0)
|
||||||
|
track.Type = Enums.TrackType.Album;
|
||||||
|
|
||||||
if (track.Title.Length > 0 || track.Artist.Length > 0 || track.Album.Length > 0)
|
if (track.Title.Length > 0 || track.Artist.Length > 0 || track.Album.Length > 0)
|
||||||
tracks.Add(track);
|
tracks.Add(track);
|
||||||
|
|
|
@ -18,15 +18,15 @@ namespace Extractors
|
||||||
return input == "spotify-likes" || input.IsInternetUrl() && input.Contains("spotify.com");
|
return input == "spotify-likes" || input.IsInternetUrl() && input.Contains("spotify.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TrackLists> GetTracks()
|
public async Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse)
|
||||||
{
|
{
|
||||||
var trackLists = new TrackLists();
|
var trackLists = new TrackLists();
|
||||||
int max = Config.reverse ? int.MaxValue : Config.maxTracks;
|
int max = reverse ? int.MaxValue : maxTracks;
|
||||||
int off = Config.reverse ? 0 : Config.offset;
|
int off = reverse ? 0 : offset;
|
||||||
|
|
||||||
string playlistName = "";
|
string playlistName = "";
|
||||||
bool needLogin = Config.input == "spotify-likes" || Config.removeTracksFromSource;
|
bool needLogin = Config.input == "spotify-likes" || Config.removeTracksFromSource;
|
||||||
List<Track> tracks = new List<Track>();
|
var tle = new TrackListEntry();
|
||||||
|
|
||||||
static void readSpotifyCreds()
|
static void readSpotifyCreds()
|
||||||
{
|
{
|
||||||
|
@ -48,19 +48,16 @@ namespace Extractors
|
||||||
if (Config.input == "spotify-likes")
|
if (Config.input == "spotify-likes")
|
||||||
{
|
{
|
||||||
Console.WriteLine("Loading Spotify likes");
|
Console.WriteLine("Loading Spotify likes");
|
||||||
tracks = await spotifyClient.GetLikes(max, off);
|
var tracks = await spotifyClient.GetLikes(max, off);
|
||||||
playlistName = "Spotify Likes";
|
playlistName = "Spotify Likes";
|
||||||
|
tle.list.Add(tracks);
|
||||||
trackLists.AddEntry(tracks);
|
|
||||||
if (Config.album || Config.aggregate)
|
|
||||||
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false), Config.aggregate, Config.album);
|
|
||||||
}
|
}
|
||||||
else if (Config.input.Contains("/album/"))
|
else if (Config.input.Contains("/album/"))
|
||||||
{
|
{
|
||||||
Console.WriteLine("Loading Spotify album");
|
Console.WriteLine("Loading Spotify album");
|
||||||
(var source, tracks) = await spotifyClient.GetAlbum(Config.input);
|
(var source, var tracks) = await spotifyClient.GetAlbum(Config.input);
|
||||||
playlistName = source.ToString(noInfo: true);
|
playlistName = source.ToString(noInfo: true);
|
||||||
trackLists.AddEntry(ListType.Album, source);
|
tle.source = source;
|
||||||
|
|
||||||
if (Config.setAlbumMinTrackCount)
|
if (Config.setAlbumMinTrackCount)
|
||||||
source.MinAlbumTrackCount = tracks.Count;
|
source.MinAlbumTrackCount = tracks.Count;
|
||||||
|
@ -75,6 +72,8 @@ namespace Extractors
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var tracks = new List<Track>();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Console.WriteLine("Loading Spotify playlist");
|
Console.WriteLine("Loading Spotify playlist");
|
||||||
|
@ -105,13 +104,18 @@ namespace Extractors
|
||||||
}
|
}
|
||||||
else throw;
|
else throw;
|
||||||
}
|
}
|
||||||
trackLists.AddEntry(tracks);
|
|
||||||
if (Config.album || Config.aggregate)
|
tle.list.Add(tracks);
|
||||||
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false), Config.aggregate, Config.album);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.defaultFolderName = playlistName.ReplaceInvalidChars(Config.invalidReplaceStr);
|
Config.defaultFolderName = playlistName.ReplaceInvalidChars(Config.invalidReplaceStr);
|
||||||
|
|
||||||
|
if (reverse)
|
||||||
|
{
|
||||||
|
trackLists.Reverse();
|
||||||
|
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(offset).Take(maxTracks));
|
||||||
|
}
|
||||||
|
|
||||||
return trackLists;
|
return trackLists;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,7 +332,7 @@ namespace Extractors
|
||||||
tracks.Add(t);
|
tracks.Add(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (new Track { Album = album.Name, Artist = album.Artists.First().Name, IsAlbum = true }, tracks);
|
return (new Track { Album = album.Name, Artist = album.Artists.First().Name, Type = TrackType.Album }, tracks);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetAlbumIdFromUrl(string url)
|
private string GetAlbumIdFromUrl(string url)
|
||||||
|
|
|
@ -11,41 +11,23 @@ namespace Extractors
|
||||||
return !input.IsInternetUrl();
|
return !input.IsInternetUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TrackLists> GetTracks()
|
public async Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse)
|
||||||
{
|
{
|
||||||
var trackLists = new TrackLists();
|
var trackLists = new TrackLists();
|
||||||
var music = ParseTrackArg(Config.input, Config.album);
|
var music = ParseTrackArg(Config.input, Config.album);
|
||||||
bool isAlbum = false;
|
|
||||||
|
|
||||||
if (Config.album && Config.aggregate)
|
if (music.Title.Length == 0 && music.Album.Length > 0)
|
||||||
{
|
{
|
||||||
trackLists.AddEntry(ListType.AlbumAggregate, music);
|
music.Type = TrackType.Album;
|
||||||
}
|
trackLists.AddEntry(new TrackListEntry(music));
|
||||||
else if (Config.album)
|
|
||||||
{
|
|
||||||
music.IsAlbum = true;
|
|
||||||
trackLists.AddEntry(ListType.Album, music);
|
|
||||||
}
|
|
||||||
else if (!Config.aggregate && music.Title.Length > 0)
|
|
||||||
{
|
|
||||||
trackLists.AddEntry(music);
|
|
||||||
}
|
|
||||||
else if (Config.aggregate)
|
|
||||||
{
|
|
||||||
trackLists.AddEntry(ListType.Aggregate, music);
|
|
||||||
}
|
|
||||||
else if (music.Title.Length == 0 && music.Album.Length > 0)
|
|
||||||
{
|
|
||||||
isAlbum = true;
|
|
||||||
music.IsAlbum = true;
|
|
||||||
trackLists.AddEntry(ListType.Album, music);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Need track title or album");
|
trackLists.AddEntry(new TrackListEntry());
|
||||||
|
trackLists.AddTrackToLast(music);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Config.aggregate || isAlbum || Config.album)
|
if (Config.aggregate || Config.album || music.Type != TrackType.Normal)
|
||||||
Config.defaultFolderName = music.ToString(true).ReplaceInvalidChars(Config.invalidReplaceStr).Trim();
|
Config.defaultFolderName = music.ToString(true).ReplaceInvalidChars(Config.invalidReplaceStr).Trim();
|
||||||
else
|
else
|
||||||
Config.defaultFolderName = ".";
|
Config.defaultFolderName = ".";
|
||||||
|
@ -59,8 +41,6 @@ namespace Extractors
|
||||||
var track = new Track();
|
var track = new Track();
|
||||||
var keys = new string[] { "title", "artist", "length", "album", "artist-maybe-wrong" };
|
var keys = new string[] { "title", "artist", "length", "album", "artist-maybe-wrong" };
|
||||||
|
|
||||||
track.IsAlbum = isAlbum;
|
|
||||||
|
|
||||||
void setProperty(string key, string value)
|
void setProperty(string key, string value)
|
||||||
{
|
{
|
||||||
switch (key)
|
switch (key)
|
||||||
|
|
|
@ -20,11 +20,11 @@ namespace Extractors
|
||||||
return input.IsInternetUrl() && (input.Contains("youtu.be") || input.Contains("youtube.com"));
|
return input.IsInternetUrl() && (input.Contains("youtu.be") || input.Contains("youtube.com"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TrackLists> GetTracks()
|
public async Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse)
|
||||||
{
|
{
|
||||||
var trackLists = new TrackLists();
|
var trackLists = new TrackLists();
|
||||||
int max = Config.reverse ? int.MaxValue : Config.maxTracks;
|
int max = reverse ? int.MaxValue : maxTracks;
|
||||||
int off = Config.reverse ? 0 : Config.offset;
|
int off = reverse ? 0 : offset;
|
||||||
YouTube.apiKey = Config.ytKey;
|
YouTube.apiKey = Config.ytKey;
|
||||||
|
|
||||||
string name;
|
string name;
|
||||||
|
@ -60,12 +60,19 @@ namespace Extractors
|
||||||
}
|
}
|
||||||
|
|
||||||
YouTube.StopService();
|
YouTube.StopService();
|
||||||
trackLists.AddEntry(tracks);
|
|
||||||
|
|
||||||
if (Config.album || Config.aggregate)
|
var tle = new TrackListEntry();
|
||||||
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false), Config.aggregate, Config.album);
|
tle.list.Add(tracks);
|
||||||
|
trackLists.AddEntry(tle);
|
||||||
|
|
||||||
Config.defaultFolderName = name.ReplaceInvalidChars(Config.invalidReplaceStr);
|
Config.defaultFolderName = name.ReplaceInvalidChars(Config.invalidReplaceStr);
|
||||||
|
|
||||||
|
if (reverse)
|
||||||
|
{
|
||||||
|
trackLists.Reverse();
|
||||||
|
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(offset).Take(maxTracks));
|
||||||
|
}
|
||||||
|
|
||||||
return trackLists;
|
return trackLists;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace Extractors
|
||||||
{
|
{
|
||||||
public interface IExtractor
|
public interface IExtractor
|
||||||
{
|
{
|
||||||
Task<TrackLists> GetTracks();
|
Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse);
|
||||||
Task RemoveTrackFromSource(Track track) => Task.CompletedTask;
|
Task RemoveTrackFromSource(Track track) => Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ public static class Help
|
||||||
library. Use with --skip-existing
|
library. Use with --skip-existing
|
||||||
--skip-not-found Skip searching for tracks that weren't found on Soulseek
|
--skip-not-found Skip searching for tracks that weren't found on Soulseek
|
||||||
during the last run. Fails are read from the m3u file.
|
during the last run. Fails are read from the m3u file.
|
||||||
|
--skip-existing-pref-cond Use preferred instead of necessary conds for skip-existing
|
||||||
|
|
||||||
--display-mode <option> Changes how searches and downloads are displayed:
|
--display-mode <option> Changes how searches and downloads are displayed:
|
||||||
'single' (default): Show transfer state and percentage
|
'single' (default): Show transfer state and percentage
|
||||||
|
@ -375,23 +376,26 @@ public static class Help
|
||||||
track Track number
|
track Track number
|
||||||
disc Disc number
|
disc Disc number
|
||||||
filename Soulseek filename without extension
|
filename Soulseek filename without extension
|
||||||
foldername Default sldl folder name
|
foldername Soulseek folder name (only available for album downloads)
|
||||||
|
default-foldername Default sldl folder name
|
||||||
extractor Name of the extractor used (CSV/Spotify/YouTube/etc)
|
extractor Name of the extractor used (CSV/Spotify/YouTube/etc)
|
||||||
";
|
";
|
||||||
|
|
||||||
const string skipExistingHelp = @"
|
const string skipExistingHelp = @"
|
||||||
Skip existing
|
Skip existing
|
||||||
|
|
||||||
sldl can skip files that exist in the download directory or a specified directory configured with
|
sldl can skip downloads that exist in the output directory or a specified directory configured
|
||||||
--music-dir.
|
with --music-dir.
|
||||||
The following modes are available for --skip-mode:
|
The following modes are available for --skip-mode:
|
||||||
|
|
||||||
m3u
|
m3u
|
||||||
Default when checking in the output directory.
|
Default when checking in the output directory.
|
||||||
Checks whether the output m3u file contains the track in the '#SLDL' line. Does not check if
|
Checks whether the output m3u file contains the track in the '#SLDL' line. Does not check if
|
||||||
the audio file exists or satisfies the file conditions, use m3u-cond for that.
|
the audio file exists or satisfies the file conditions (use m3u-cond for that). m3u and
|
||||||
|
m3u-cond are the only modes that can skip album downloads.
|
||||||
|
|
||||||
name
|
name
|
||||||
|
Default when checking in the music directory.
|
||||||
Compares filenames to the track title and artist name to determine if a track already exists.
|
Compares filenames to the track title and artist name to determine if a track already exists.
|
||||||
Specifically, a track will be skipped if there exists a file whose name contains the title
|
Specifically, a track will be skipped if there exists a file whose name contains the title
|
||||||
and whose full path contains the artist name.
|
and whose full path contains the artist name.
|
||||||
|
@ -402,11 +406,11 @@ public static class Help
|
||||||
(ignoring case and ws). Slower than name mode as it needs to read all file tags.
|
(ignoring case and ws). Slower than name mode as it needs to read all file tags.
|
||||||
|
|
||||||
m3u-cond, name-cond, tag-cond
|
m3u-cond, name-cond, tag-cond
|
||||||
Default for checking in --music-dir: name-cond.
|
Same as the above modes but also checks whether the found file satisfies the configured
|
||||||
Same as the above modes but also checks whether the found file satisfies necessary conditions.
|
conditions. Uses necessary conditions by default, run with --skip-existing-pref-cond to use
|
||||||
Equivalent to the above modes if no necessary conditions have been specified (except m3u-cond
|
preferred conditions instead. Equivalent to the above modes if no necessary conditions have
|
||||||
which always checks if the file exists). May be slower and use a lot of memory for large
|
been specified (except m3u-cond, which always checks if the file exists).
|
||||||
libraries.
|
May be slower and use a lot of memory for large libraries.
|
||||||
";
|
";
|
||||||
|
|
||||||
const string configHelp = @"
|
const string configHelp = @"
|
||||||
|
|
|
@ -6,13 +6,13 @@ using System.Text;
|
||||||
public class M3uEditor
|
public class M3uEditor
|
||||||
{
|
{
|
||||||
List<string> lines;
|
List<string> lines;
|
||||||
TrackLists trackLists;
|
|
||||||
string path;
|
|
||||||
string parent;
|
|
||||||
int offset = 0;
|
|
||||||
M3uOption option = M3uOption.Index;
|
|
||||||
bool needFirstUpdate = false;
|
bool needFirstUpdate = false;
|
||||||
Dictionary<string, Track> previousRunTracks = new(); // {track.ToKey(), track }
|
readonly TrackLists trackLists;
|
||||||
|
readonly string path;
|
||||||
|
readonly string parent;
|
||||||
|
readonly int offset = 0;
|
||||||
|
readonly M3uOption option = M3uOption.Index;
|
||||||
|
readonly Dictionary<string, Track> previousRunData = new(); // { track.ToKey(), track }
|
||||||
|
|
||||||
public M3uEditor(string m3uPath, TrackLists trackLists, M3uOption option, int offset = 0)
|
public M3uEditor(string m3uPath, TrackLists trackLists, M3uOption option, int offset = 0)
|
||||||
{
|
{
|
||||||
|
@ -27,18 +27,25 @@ public class M3uEditor
|
||||||
LoadPreviousResults();
|
LoadPreviousResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadPreviousResults() // #SLDL:path,artist,album,title,length(int),state(int),failurereason(int); ... ; ...
|
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[0].StartsWith("#SLDL:"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
string sldlLine = lines[0]["#SLDL:".Length..];
|
string sldlLine = lines[0];
|
||||||
|
lines = lines.Skip(1).ToList();
|
||||||
|
|
||||||
|
int k = "#SLDL:".Length;
|
||||||
var currentItem = new StringBuilder();
|
var currentItem = new StringBuilder();
|
||||||
bool inQuotes = false;
|
bool inQuotes = false;
|
||||||
|
|
||||||
lines = lines.Skip(1).ToList();
|
for (; k < sldlLine.Length && sldlLine[k] == ' '; k++);
|
||||||
|
|
||||||
for (int k = 0; k < sldlLine.Length; k++)
|
for (; k < sldlLine.Length; k++)
|
||||||
{
|
{
|
||||||
var track = new Track();
|
var track = new Track();
|
||||||
int field = 0;
|
int field = 0;
|
||||||
|
@ -58,7 +65,7 @@ public class M3uEditor
|
||||||
inQuotes = !inQuotes;
|
inQuotes = !inQuotes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (field <= 5 && c == ',' && !inQuotes)
|
else if (field <= 6 && c == ',' && !inQuotes)
|
||||||
{
|
{
|
||||||
var x = currentItem.ToString();
|
var x = currentItem.ToString();
|
||||||
|
|
||||||
|
@ -77,12 +84,14 @@ public class M3uEditor
|
||||||
else if (field == 4)
|
else if (field == 4)
|
||||||
track.Length = int.Parse(x);
|
track.Length = int.Parse(x);
|
||||||
else if (field == 5)
|
else if (field == 5)
|
||||||
|
track.Type = (TrackType)int.Parse(currentItem.ToString());
|
||||||
|
else if (field == 6)
|
||||||
track.State = (TrackState)int.Parse(x);
|
track.State = (TrackState)int.Parse(x);
|
||||||
|
|
||||||
currentItem.Clear();
|
currentItem.Clear();
|
||||||
field++;
|
field++;
|
||||||
}
|
}
|
||||||
else if (field == 6 && c == ';')
|
else if (field == 7 && c == ';')
|
||||||
{
|
{
|
||||||
track.FailureReason = (FailureReason)int.Parse(currentItem.ToString());
|
track.FailureReason = (FailureReason)int.Parse(currentItem.ToString());
|
||||||
currentItem.Clear();
|
currentItem.Clear();
|
||||||
|
@ -94,7 +103,8 @@ public class M3uEditor
|
||||||
currentItem.Append(c);
|
currentItem.Append(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousRunTracks[track.ToKey()] = track;
|
|
||||||
|
previousRunData[track.ToKey()] = track;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,64 +118,71 @@ public class M3uEditor
|
||||||
bool needUpdate = false;
|
bool needUpdate = false;
|
||||||
int index = 1 + offset;
|
int index = 1 + offset;
|
||||||
|
|
||||||
void updateLine(string newLine)
|
bool updateLine(string newLine)
|
||||||
{
|
{
|
||||||
|
bool changed = index >= lines.Count || newLine != lines[index];
|
||||||
|
|
||||||
while (index >= lines.Count) lines.Add("");
|
while (index >= lines.Count) lines.Add("");
|
||||||
lines[index] = newLine;
|
lines[index] = newLine;
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool trackChanged(Track track, Track? indexTrack)
|
||||||
|
{
|
||||||
|
return indexTrack == null
|
||||||
|
|| indexTrack.State != track.State
|
||||||
|
|| indexTrack.FailureReason != track.FailureReason
|
||||||
|
|| indexTrack.DownloadPath != track.DownloadPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTrackIfNeeded(Track track)
|
||||||
|
{
|
||||||
|
var key = track.ToKey();
|
||||||
|
|
||||||
|
previousRunData.TryGetValue(key, out Track? indexTrack);
|
||||||
|
|
||||||
|
if (!needUpdate)
|
||||||
|
needUpdate = trackChanged(track, indexTrack);
|
||||||
|
|
||||||
|
if (needUpdate)
|
||||||
|
{
|
||||||
|
if (indexTrack == null)
|
||||||
|
previousRunData[key] = new Track(track);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
indexTrack.State = track.State;
|
||||||
|
indexTrack.FailureReason = track.FailureReason;
|
||||||
|
indexTrack.DownloadPath = track.DownloadPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var tle in trackLists.lists)
|
foreach (var tle in trackLists.lists)
|
||||||
{
|
{
|
||||||
if (tle.type != ListType.Normal)
|
if (tle.source.Type != TrackType.Normal)
|
||||||
{
|
{
|
||||||
continue;
|
if (tle.source.State != TrackState.Initial)
|
||||||
}
|
|
||||||
//if (option == M3uOption.All && source.State == TrackState.Failed)
|
|
||||||
//{
|
|
||||||
// string reason = source.FailureReason.ToString();
|
|
||||||
// updateLine(TrackToLine(source, reason));
|
|
||||||
// index++;
|
|
||||||
//}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (int k = 0; k < tle.list.Count; k++)
|
|
||||||
{
|
{
|
||||||
for (int j = 0; j < tle.list[k].Count; j++)
|
updateTrackIfNeeded(tle.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int k = 0; k < tle.list.Count; k++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < tle.list[k].Count; j++)
|
||||||
|
{
|
||||||
|
var track = tle.list[k][j];
|
||||||
|
|
||||||
|
if (track.IsNotAudio || track.State == TrackState.Initial)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
updateTrackIfNeeded(track);
|
||||||
|
|
||||||
|
if (option == M3uOption.All)
|
||||||
{
|
{
|
||||||
var track = tle.list[k][j];
|
needUpdate |= updateLine(TrackToLine(track));
|
||||||
|
index++;
|
||||||
if (track.IsNotAudio || track.State == TrackState.Initial)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
string trackKey = track.ToKey();
|
|
||||||
previousRunTracks.TryGetValue(trackKey, out Track? indexTrack);
|
|
||||||
|
|
||||||
if (!needUpdate)
|
|
||||||
{
|
|
||||||
needUpdate |= indexTrack == null
|
|
||||||
|| indexTrack.State != track.State
|
|
||||||
|| indexTrack.FailureReason != track.FailureReason
|
|
||||||
|| indexTrack.DownloadPath != track.DownloadPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
previousRunTracks[trackKey] = track;
|
|
||||||
|
|
||||||
if (option == M3uOption.All)
|
|
||||||
{
|
|
||||||
if (track.State != TrackState.AlreadyExists || k == 0)
|
|
||||||
{
|
|
||||||
string? reason = track.FailureReason != FailureReason.None ? track.FailureReason.ToString() : null;
|
|
||||||
if (reason == null && track.State == TrackState.NotFoundLastTime)
|
|
||||||
reason = nameof(FailureReason.NoSuitableFileFound);
|
|
||||||
|
|
||||||
updateLine(TrackToLine(track, reason));
|
|
||||||
if (tle.type != ListType.Normal)
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tle.type == ListType.Normal)
|
|
||||||
index++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -194,8 +211,12 @@ public class M3uEditor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteSldlLine(StreamWriter writer) // #SLDL:path,artist,album,title,length(int),state(int),failurereason(int); ... ; ...
|
private void WriteSldlLine(StreamWriter 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;
|
||||||
|
@ -221,7 +242,7 @@ public class M3uEditor
|
||||||
|
|
||||||
writer.Write("#SLDL:");
|
writer.Write("#SLDL:");
|
||||||
|
|
||||||
foreach (var val in previousRunTracks.Values)
|
foreach (var val in previousRunData.Values)
|
||||||
{
|
{
|
||||||
string p = val.DownloadPath;
|
string p = val.DownloadPath;
|
||||||
if (p.StartsWith(parent))
|
if (p.StartsWith(parent))
|
||||||
|
@ -234,6 +255,7 @@ public class M3uEditor
|
||||||
val.Album,
|
val.Album,
|
||||||
val.Title,
|
val.Title,
|
||||||
val.Length.ToString(),
|
val.Length.ToString(),
|
||||||
|
((int)val.Type).ToString(),
|
||||||
((int)val.State).ToString(),
|
((int)val.State).ToString(),
|
||||||
((int)val.FailureReason).ToString(),
|
((int)val.FailureReason).ToString(),
|
||||||
};
|
};
|
||||||
|
@ -245,10 +267,15 @@ public class M3uEditor
|
||||||
writer.Write('\n');
|
writer.Write('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
private string TrackToLine(Track track, string? failureReason = null)
|
private string TrackToLine(Track track)
|
||||||
{
|
{
|
||||||
|
string? failureReason = track.FailureReason != FailureReason.None ? track.FailureReason.ToString() : null;
|
||||||
|
if (failureReason == null && track.State == TrackState.NotFoundLastTime)
|
||||||
|
failureReason = nameof(FailureReason.NoSuitableFileFound);
|
||||||
|
|
||||||
if (failureReason != null)
|
if (failureReason != null)
|
||||||
return $"# Failed: {track} [{failureReason}]";
|
return $"# Failed: {track} [{failureReason}]";
|
||||||
|
|
||||||
if (track.DownloadPath.Length > 0)
|
if (track.DownloadPath.Length > 0)
|
||||||
{
|
{
|
||||||
if (track.DownloadPath.StartsWith(parent))
|
if (track.DownloadPath.StartsWith(parent))
|
||||||
|
@ -256,18 +283,19 @@ public class M3uEditor
|
||||||
else
|
else
|
||||||
return track.DownloadPath;
|
return track.DownloadPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $"# {track}";
|
return $"# {track}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public Track? PreviousRunResult(Track track)
|
public Track? PreviousRunResult(Track track)
|
||||||
{
|
{
|
||||||
previousRunTracks.TryGetValue(track.ToKey(), out var t);
|
previousRunData.TryGetValue(track.ToKey(), out var t);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetPreviousRunResult(Track track, out Track? result)
|
public bool TryGetPreviousRunResult(Track track, out Track? result)
|
||||||
{
|
{
|
||||||
previousRunTracks.TryGetValue(track.ToKey(), out result);
|
previousRunData.TryGetValue(track.ToKey(), out result);
|
||||||
return result != null;
|
return result != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
|
||||||
|
|
||||||
using Data;
|
using Data;
|
||||||
using Enums;
|
using Enums;
|
||||||
|
using ExistingCheckers;
|
||||||
|
|
||||||
using Directory = System.IO.Directory;
|
using Directory = System.IO.Directory;
|
||||||
using File = System.IO.File;
|
using File = System.IO.File;
|
||||||
|
@ -20,6 +21,8 @@ using SlResponse = Soulseek.SearchResponse;
|
||||||
static partial class Program
|
static partial class Program
|
||||||
{
|
{
|
||||||
public static Extractors.IExtractor? extractor;
|
public static Extractors.IExtractor? extractor;
|
||||||
|
public static ExistingChecker? outputExistingChecker;
|
||||||
|
public static ExistingChecker? musicDirExistingChecker;
|
||||||
public static SoulseekClient? client;
|
public static SoulseekClient? client;
|
||||||
public static TrackLists? trackLists;
|
public static TrackLists? trackLists;
|
||||||
public static M3uEditor? m3uEditor;
|
public static M3uEditor? m3uEditor;
|
||||||
|
@ -61,20 +64,18 @@ static partial class Program
|
||||||
|
|
||||||
WriteLine($"Using extractor: {Config.inputType}", debugOnly: true);
|
WriteLine($"Using extractor: {Config.inputType}", debugOnly: true);
|
||||||
|
|
||||||
trackLists = await extractor.GetTracks();
|
trackLists = await extractor.GetTracks(Config.maxTracks, Config.offset, Config.reverse);
|
||||||
|
|
||||||
WriteLine("Got tracks", debugOnly: true);
|
WriteLine("Got tracks", debugOnly: true);
|
||||||
|
|
||||||
Config.PostProcessArgs();
|
Config.PostProcessArgs();
|
||||||
|
|
||||||
if (Config.reverse)
|
trackLists.UpgradeListTypes(Config.aggregate, Config.album);
|
||||||
{
|
|
||||||
trackLists.Reverse();
|
|
||||||
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(Config.offset).Take(Config.maxTracks), Config.aggregate, Config.album);
|
|
||||||
}
|
|
||||||
|
|
||||||
m3uEditor = new M3uEditor(Config.m3uFilePath, trackLists, Config.m3uOption, Config.offset);
|
m3uEditor = new M3uEditor(Config.m3uFilePath, trackLists, Config.m3uOption, Config.offset);
|
||||||
|
|
||||||
|
InitExistingChecker();
|
||||||
|
|
||||||
await MainLoop();
|
await MainLoop();
|
||||||
WriteLine("Mainloop done", debugOnly: true);
|
WriteLine("Mainloop done", debugOnly: true);
|
||||||
}
|
}
|
||||||
|
@ -106,6 +107,26 @@ static partial class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void InitExistingChecker()
|
||||||
|
{
|
||||||
|
if (Config.skipExisting)
|
||||||
|
{
|
||||||
|
var cond = Config.skipExistingPrefCond ? Config.preferredCond : Config.necessaryCond;
|
||||||
|
|
||||||
|
if (Config.musicDir.Length == 0 || !Config.outputFolder.StartsWith(Config.musicDir, StringComparison.OrdinalIgnoreCase))
|
||||||
|
outputExistingChecker = ExistingCheckerRegistry.GetChecker(Config.skipMode, Config.outputFolder, cond, m3uEditor);
|
||||||
|
|
||||||
|
if (Config.musicDir.Length > 0)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(Config.musicDir))
|
||||||
|
Console.WriteLine("Error: Music directory does not exist");
|
||||||
|
else
|
||||||
|
musicDirExistingChecker = ExistingCheckerRegistry.GetChecker(Config.skipModeMusicDir, Config.outputFolder, cond, m3uEditor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void PreprocessTracks(TrackListEntry tle)
|
static void PreprocessTracks(TrackListEntry tle)
|
||||||
{
|
{
|
||||||
for (int k = 0; k < tle.list.Count; k++)
|
for (int k = 0; k < tle.list.Count; k++)
|
||||||
|
@ -154,6 +175,8 @@ static partial class Program
|
||||||
{
|
{
|
||||||
for (int i = 0; i < trackLists.lists.Count; i++)
|
for (int i = 0; i < trackLists.lists.Count; i++)
|
||||||
{
|
{
|
||||||
|
if (i > 0) Console.WriteLine();
|
||||||
|
|
||||||
var tle = trackLists[i];
|
var tle = trackLists[i];
|
||||||
|
|
||||||
Config.UpdateArgs(tle);
|
Config.UpdateArgs(tle);
|
||||||
|
@ -162,77 +185,92 @@ static partial class Program
|
||||||
|
|
||||||
var existing = new List<Track>();
|
var existing = new List<Track>();
|
||||||
var notFound = new List<Track>();
|
var notFound = new List<Track>();
|
||||||
|
var responseData = new ResponseData();
|
||||||
|
|
||||||
if (Config.skipNotFound)
|
if (Config.skipNotFound && !Config.PrintResults)
|
||||||
{
|
{
|
||||||
if (SetNotFoundLastTime(tle.source))
|
if (tle.sourceCanBeSkipped && SetNotFoundLastTime(tle.source))
|
||||||
notFound.Add(tle.source);
|
notFound.Add(tle.source);
|
||||||
|
|
||||||
foreach (var tracks in tle.list)
|
if (tle.source.State != TrackState.NotFoundLastTime && !tle.needSourceSearch)
|
||||||
notFound.AddRange(DoSkipNotFound(tracks));
|
{
|
||||||
|
foreach (var tracks in tle.list)
|
||||||
|
notFound.AddRange(DoSkipNotFound(tracks));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Config.skipExisting && tle.list != null && tle.type != ListType.Aggregate)
|
if (Config.skipExisting && !Config.PrintResults && tle.source.State != TrackState.NotFoundLastTime)
|
||||||
{
|
{
|
||||||
for (int j = 0; j < tle.list.Count; j++)
|
if (tle.sourceCanBeSkipped && SetExisting(tle.source))
|
||||||
existing.AddRange(DoSkipExisting(tle.list[0], print: i == 0 && j == 0));
|
existing.Add(tle.source);
|
||||||
|
|
||||||
|
if (tle.source.State != TrackState.AlreadyExists && !tle.needSourceSearch)
|
||||||
|
{
|
||||||
|
foreach (var tracks in tle.list)
|
||||||
|
existing.AddRange(DoSkipExisting(tracks));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Config.PrintTracks)
|
if (Config.PrintTracks)
|
||||||
{
|
{
|
||||||
if (tle.type == ListType.Normal)
|
if (tle.source.Type == TrackType.Normal)
|
||||||
{
|
{
|
||||||
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.type);
|
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var tl = new List<Track>();
|
var tl = new List<Track>();
|
||||||
if (tle.source.State == TrackState.Initial) tl.Add(tle.source);
|
if (tle.source.State == TrackState.Initial) tl.Add(tle.source);
|
||||||
PrintTracksTbd(tl, existing, notFound, tle.type);
|
PrintTracksTbd(tl, existing, notFound, tle.source.Type);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tle.needSearch)
|
if (tle.sourceCanBeSkipped)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"{tle.type} download: {tle.source.ToString(true)}, searching..");
|
if (tle.source.State == TrackState.AlreadyExists)
|
||||||
|
|
||||||
var responseData = new ResponseData();
|
|
||||||
|
|
||||||
if (tle.type == ListType.Album)
|
|
||||||
{
|
{
|
||||||
await InitClientAndUpdateIfNeeded();
|
Console.WriteLine($"{tle.source.Type} download '{tle.source.ToString(true)}' already exists at {tle.source.DownloadPath}, skipping");
|
||||||
tle.list = await GetAlbumDownloads(tle.source, responseData);
|
|
||||||
}
|
|
||||||
else if (tle.type == ListType.Aggregate)
|
|
||||||
{
|
|
||||||
await InitClientAndUpdateIfNeeded();
|
|
||||||
tle.list[0] = await GetAggregateTracks(tle.source, responseData);
|
|
||||||
if (Config.skipExisting)
|
|
||||||
{
|
|
||||||
for (int j = 0; j < tle.list.Count; j++)
|
|
||||||
existing.AddRange(DoSkipExisting(tle.list[0], print: i == 0 && j == 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (tle.type == ListType.AlbumAggregate)
|
|
||||||
{
|
|
||||||
await InitClientAndUpdateIfNeeded();
|
|
||||||
var res = await GetAggregateAlbums(tle.source, responseData);
|
|
||||||
|
|
||||||
foreach (var item in res)
|
|
||||||
trackLists.AddEntry(new TrackListEntry(item, ListType.Album, tle.source, false, true));
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tle.list.Count == 0 || !tle.list.Any(x => x.Count > 0))
|
if (tle.source.State == TrackState.NotFoundLastTime)
|
||||||
{
|
{
|
||||||
string lockedFilesStr = responseData.lockedFilesCount > 0 ? $" (Found {responseData.lockedFilesCount} locked files)" : "";
|
Console.WriteLine($"{tle.source.Type} download '{tle.source.ToString(true)}' was not found during a prior run, skipping");
|
||||||
Console.WriteLine($"No results.{lockedFilesStr}");
|
continue;
|
||||||
tle.source.State = TrackState.Failed;
|
}
|
||||||
tle.source.FailureReason = FailureReason.NoSuitableFileFound;
|
}
|
||||||
|
|
||||||
if (i < trackLists.lists.Count - 1) Console.WriteLine();
|
if (tle.needSourceSearch)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{tle.source.Type} download: {tle.source.ToString(true)}, searching..");
|
||||||
|
|
||||||
|
await InitClientAndUpdateIfNeeded();
|
||||||
|
|
||||||
|
if (tle.source.Type == TrackType.Album)
|
||||||
|
{
|
||||||
|
tle.list = await GetAlbumDownloads(tle.source, responseData);
|
||||||
|
}
|
||||||
|
else if (tle.source.Type == TrackType.Aggregate)
|
||||||
|
{
|
||||||
|
tle.list[0] = await GetAggregateTracks(tle.source, responseData);
|
||||||
|
}
|
||||||
|
else if (tle.source.Type == TrackType.AlbumAggregate)
|
||||||
|
{
|
||||||
|
var res = await GetAggregateAlbums(tle.source, responseData);
|
||||||
|
|
||||||
|
foreach (var item in res)
|
||||||
|
trackLists.AddEntry(new TrackListEntry(item, tle.source, false, true, true, false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Config.skipExisting && tle.needSkipExistingAfterSearch)
|
||||||
|
{
|
||||||
|
foreach (var tracks in tle.list)
|
||||||
|
notFound.AddRange(DoSkipExisting(tracks));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tle.gotoNextAfterSearch)
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,38 +281,44 @@ static partial class Program
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tle.needSourceSearch && (tle.list.Count == 0 || !tle.list.Any(x => x.Count > 0)))
|
||||||
|
{
|
||||||
|
string lockedFilesStr = responseData.lockedFilesCount > 0 ? $" (Found {responseData.lockedFilesCount} locked files)" : "";
|
||||||
|
Console.WriteLine($"No results.{lockedFilesStr}");
|
||||||
|
|
||||||
|
tle.source.State = TrackState.Failed;
|
||||||
|
tle.source.FailureReason = FailureReason.NoSuitableFileFound;
|
||||||
|
|
||||||
|
m3uEditor.Update();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
m3uEditor.Update();
|
m3uEditor.Update();
|
||||||
|
|
||||||
if (tle.type != ListType.Album && !Config.interactiveMode)
|
if (tle.source.Type != TrackType.Album)
|
||||||
{
|
{
|
||||||
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.type);
|
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notFound.Count + existing.Count >= tle.list.Sum(x => x.Count))
|
if (notFound.Count + existing.Count >= tle.list.Sum(x => x.Count))
|
||||||
{
|
{
|
||||||
if (i < trackLists.lists.Count - 1) Console.WriteLine();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await InitClientAndUpdateIfNeeded();
|
await InitClientAndUpdateIfNeeded();
|
||||||
|
|
||||||
if (tle.type == ListType.Normal)
|
if (tle.source.Type == TrackType.Normal)
|
||||||
{
|
{
|
||||||
await TracksDownloadNormal(tle);
|
await TracksDownloadNormal(tle);
|
||||||
}
|
}
|
||||||
else if (tle.type == ListType.Album)
|
else if (tle.source.Type == TrackType.Album)
|
||||||
{
|
{
|
||||||
await TracksDownloadAlbum(tle);
|
await TracksDownloadAlbum(tle);
|
||||||
}
|
}
|
||||||
else if (tle.type == ListType.Aggregate)
|
else if (tle.source.Type == TrackType.Aggregate)
|
||||||
{
|
{
|
||||||
await TracksDownloadNormal(tle);
|
await TracksDownloadNormal(tle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i < trackLists.lists.Count - 1)
|
|
||||||
{
|
|
||||||
Console.WriteLine();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Config.DoNotDownload && (trackLists.lists.Count > 0 || trackLists.Flattened(false, false).Skip(1).Any()))
|
if (!Config.DoNotDownload && (trackLists.lists.Count > 0 || trackLists.Flattened(false, false).Skip(1).Any()))
|
||||||
|
@ -288,17 +332,17 @@ static partial class Program
|
||||||
{
|
{
|
||||||
await InitClientAndUpdateIfNeeded();
|
await InitClientAndUpdateIfNeeded();
|
||||||
|
|
||||||
if (tle.type == ListType.Normal)
|
if (tle.source.Type == TrackType.Normal)
|
||||||
{
|
{
|
||||||
await SearchAndPrintResults(tle.list[0]);
|
await SearchAndPrintResults(tle.list[0]);
|
||||||
}
|
}
|
||||||
else if (tle.type == ListType.Aggregate)
|
else if (tle.source.Type == TrackType.Aggregate)
|
||||||
{
|
{
|
||||||
Console.WriteLine(new string('-', 60));
|
Console.WriteLine(new string('-', 60));
|
||||||
Console.WriteLine($"Results for aggregate {tle.source.ToString(true)}:");
|
Console.WriteLine($"Results for aggregate {tle.source.ToString(true)}:");
|
||||||
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.type);
|
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type);
|
||||||
}
|
}
|
||||||
else if (tle.type == ListType.Album)
|
else if (tle.source.Type == TrackType.Album)
|
||||||
{
|
{
|
||||||
Console.WriteLine(new string('-', 60));
|
Console.WriteLine(new string('-', 60));
|
||||||
Console.WriteLine($"Results for album {tle.source.ToString(true)}:");
|
Console.WriteLine($"Results for album {tle.source.ToString(true)}:");
|
||||||
|
@ -340,9 +384,9 @@ static partial class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void PrintTracksTbd(List<Track> toBeDownloaded, List<Track> existing, List<Track> notFound, ListType type)
|
static void PrintTracksTbd(List<Track> toBeDownloaded, List<Track> existing, List<Track> notFound, TrackType type)
|
||||||
{
|
{
|
||||||
if (type == ListType.Normal && !Config.PrintTracks && toBeDownloaded.Count == 1 && existing.Count + notFound.Count == 0)
|
if (type == TrackType.Normal && !Config.PrintTracks && toBeDownloaded.Count == 1 && existing.Count + notFound.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
string notFoundLastTime = notFound.Count > 0 ? $"{notFound.Count} not found" : "";
|
string notFoundLastTime = notFound.Count > 0 ? $"{notFound.Count} not found" : "";
|
||||||
|
@ -351,12 +395,12 @@ static partial class Program
|
||||||
string skippedTracks = alreadyExist.Length + notFoundLastTime.Length > 0 ? $" ({alreadyExist}{notFoundLastTime})" : "";
|
string skippedTracks = alreadyExist.Length + notFoundLastTime.Length > 0 ? $" ({alreadyExist}{notFoundLastTime})" : "";
|
||||||
bool full = Config.printOption.HasFlag(PrintOption.Full);
|
bool full = Config.printOption.HasFlag(PrintOption.Full);
|
||||||
|
|
||||||
if (type == ListType.Normal || skippedTracks.Length > 0)
|
if (type == TrackType.Normal || skippedTracks.Length > 0)
|
||||||
Console.WriteLine($"Downloading {toBeDownloaded.Count(x => !x.IsNotAudio)} tracks{skippedTracks}");
|
Console.WriteLine($"Downloading {toBeDownloaded.Count(x => !x.IsNotAudio)} tracks{skippedTracks}");
|
||||||
|
|
||||||
if (toBeDownloaded.Count > 0)
|
if (toBeDownloaded.Count > 0)
|
||||||
{
|
{
|
||||||
bool showAll = type != ListType.Normal || Config.PrintTracks || Config.PrintResults;
|
bool showAll = type != TrackType.Normal || Config.PrintTracks || Config.PrintResults;
|
||||||
PrintTracks(toBeDownloaded, showAll ? int.MaxValue : 10, full, infoFirst: Config.PrintTracks);
|
PrintTracks(toBeDownloaded, showAll ? int.MaxValue : 10, full, infoFirst: Config.PrintTracks);
|
||||||
|
|
||||||
if (full && (existing.Count > 0 || notFound.Count > 0))
|
if (full && (existing.Count > 0 || notFound.Count > 0))
|
||||||
|
@ -395,40 +439,61 @@ static partial class Program
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static List<Track> DoSkipExisting(List<Track> tracks, bool print)
|
static List<Track> DoSkipExisting(List<Track> tracks)
|
||||||
{
|
{
|
||||||
var existing = new Dictionary<Track, string>();
|
var existing = new List<Track>();
|
||||||
|
foreach (var track in tracks)
|
||||||
bool fileBasedSkip = (int)Config.skipMode < 4;
|
|
||||||
|
|
||||||
if (!(fileBasedSkip && Config.musicDir.Length > 0 && Config.outputFolder.StartsWith(Config.musicDir, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
{
|
||||||
var d = ExistingCheckers.Registry.SkipExisting(tracks, Config.outputFolder, Config.necessaryCond, m3uEditor, Config.skipMode);
|
if (SetExisting(track))
|
||||||
d.ToList().ForEach(x => existing.TryAdd(x.Key, x.Value));
|
{
|
||||||
|
existing.Add(track);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Config.musicDir.Length > 0 && System.IO.Directory.Exists(Config.musicDir))
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool SetExisting(Track track)
|
||||||
|
{
|
||||||
|
string? path = null;
|
||||||
|
|
||||||
|
if (outputExistingChecker != null)
|
||||||
{
|
{
|
||||||
if (print) Console.WriteLine($"Checking if tracks exist in library..");
|
if (!outputExistingChecker.IndexIsBuilt)
|
||||||
var d = ExistingCheckers.Registry.SkipExisting(tracks, Config.musicDir, Config.necessaryCond, m3uEditor, Config.skipModeMusicDir);
|
outputExistingChecker.BuildIndex();
|
||||||
d.ToList().ForEach(x => existing.TryAdd(x.Key, x.Value));
|
|
||||||
}
|
outputExistingChecker.TrackExists(track, out path);
|
||||||
else if (Config.musicDir.Length > 0 && !System.IO.Directory.Exists(Config.musicDir))
|
|
||||||
{
|
|
||||||
if (print) Console.WriteLine($"Music dir does not exist: {Config.musicDir}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return existing.Select(x => x.Key).ToList();
|
if (path == null && musicDirExistingChecker != null)
|
||||||
|
{
|
||||||
|
if (!musicDirExistingChecker.IndexIsBuilt)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Building music directory index..");
|
||||||
|
musicDirExistingChecker.BuildIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
musicDirExistingChecker.TrackExists(track, out path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path != null)
|
||||||
|
{
|
||||||
|
track.State = TrackState.AlreadyExists;
|
||||||
|
track.DownloadPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static List<Track> DoSkipNotFound(List<Track> tracks)
|
static List<Track> DoSkipNotFound(List<Track> tracks)
|
||||||
{
|
{
|
||||||
List<Track> notFound = new List<Track>();
|
var notFound = new List<Track>();
|
||||||
for (int i = tracks.Count - 1; i >= 0; i--)
|
foreach (var track in tracks)
|
||||||
{
|
{
|
||||||
if (SetNotFoundLastTime(tracks[i]))
|
if (SetNotFoundLastTime(track))
|
||||||
{
|
{
|
||||||
notFound.Add(tracks[i]);
|
notFound.Add(track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return notFound;
|
return notFound;
|
||||||
|
@ -453,7 +518,7 @@ static partial class Program
|
||||||
{
|
{
|
||||||
var tracks = tle.list[0];
|
var tracks = tle.list[0];
|
||||||
|
|
||||||
SemaphoreSlim semaphore = new SemaphoreSlim(Config.concurrentProcesses);
|
var semaphore = new SemaphoreSlim(Config.concurrentProcesses);
|
||||||
|
|
||||||
var copy = new List<Track>(tracks);
|
var copy = new List<Track>(tracks);
|
||||||
var downloadTasks = copy.Select(async (track, index) =>
|
var downloadTasks = copy.Select(async (track, index) =>
|
||||||
|
@ -545,7 +610,6 @@ static partial class Program
|
||||||
var tracks = new List<Track>();
|
var tracks = new List<Track>();
|
||||||
bool downloadingImages = false;
|
bool downloadingImages = false;
|
||||||
bool albumDlFailed = false;
|
bool albumDlFailed = false;
|
||||||
var listRef = list;
|
|
||||||
string savedOutputFolder = Config.outputFolder;
|
string savedOutputFolder = Config.outputFolder;
|
||||||
|
|
||||||
var curAlbumArtOption = Config.albumArtOption == AlbumArtOption.MostLargest ? AlbumArtOption.Most : Config.albumArtOption;
|
var curAlbumArtOption = Config.albumArtOption == AlbumArtOption.MostLargest ? AlbumArtOption.Most : Config.albumArtOption;
|
||||||
|
@ -602,6 +666,7 @@ static partial class Program
|
||||||
while (list.Count > 0)
|
while (list.Count > 0)
|
||||||
{
|
{
|
||||||
idx++;
|
idx++;
|
||||||
|
|
||||||
mainLoopCts = new CancellationTokenSource();
|
mainLoopCts = new CancellationTokenSource();
|
||||||
albumDlFailed = false;
|
albumDlFailed = false;
|
||||||
|
|
||||||
|
@ -612,9 +677,10 @@ static partial class Program
|
||||||
|
|
||||||
soulseekFolderPathPrefix = GetCommonPathPrefix(tracks);
|
soulseekFolderPathPrefix = GetCommonPathPrefix(tracks);
|
||||||
|
|
||||||
if (tle.placeInSubdir && (!downloadingImages || idx == 0))
|
if (tle.placeInSubdir && Config.nameFormat.Length == 0 && (!downloadingImages || idx == 0))
|
||||||
{
|
{
|
||||||
Config.outputFolder = Path.Join(savedOutputFolder, Utils.GetBaseNameSlsk(soulseekFolderPathPrefix));
|
string name = tle.subdirOverride ?? Utils.GetBaseNameSlsk(soulseekFolderPathPrefix);
|
||||||
|
Config.outputFolder = Path.Join(savedOutputFolder, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!downloadingImages)
|
if (!downloadingImages)
|
||||||
|
@ -775,7 +841,16 @@ static partial class Program
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyNamingFormatsNonAudio(listRef);
|
bool success = tracks.All(t => t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists);
|
||||||
|
|
||||||
|
ApplyNamingFormatsNonAudio(tracks);
|
||||||
|
|
||||||
|
if (!Config.albumArtOnly && success)
|
||||||
|
{
|
||||||
|
tle.source.State = TrackState.Downloaded;
|
||||||
|
tle.source.DownloadPath = Utils.GreatestCommonPath(tracks.Where(x => x.DownloadPath.Length > 0).Select(x => x.DownloadPath), Path.DirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
|
||||||
m3uEditor.Update();
|
m3uEditor.Update();
|
||||||
soulseekFolderPathPrefix = "";
|
soulseekFolderPathPrefix = "";
|
||||||
Config.outputFolder = savedOutputFolder;
|
Config.outputFolder = savedOutputFolder;
|
||||||
|
@ -1042,7 +1117,7 @@ static partial class Program
|
||||||
.Replace("{uri}", track.URI)
|
.Replace("{uri}", track.URI)
|
||||||
.Replace("{length}", track.Length.ToString())
|
.Replace("{length}", track.Length.ToString())
|
||||||
.Replace("{artist-maybe-wrong}", track.ArtistMaybeWrong.ToString())
|
.Replace("{artist-maybe-wrong}", track.ArtistMaybeWrong.ToString())
|
||||||
.Replace("{is-album}", track.IsAlbum.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)
|
||||||
|
@ -1106,39 +1181,33 @@ static partial class Program
|
||||||
return name.ReplaceInvalidChars(Config.invalidReplaceStr);
|
return name.ReplaceInvalidChars(Config.invalidReplaceStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ApplyNamingFormatsNonAudio(List<List<Track>> list)
|
static void ApplyNamingFormatsNonAudio(List<Track> tracks)
|
||||||
{
|
{
|
||||||
if (!Config.nameFormat.Replace("\\", "/").Contains('/'))
|
if (!Config.nameFormat.Replace('\\', '/').Contains('/'))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var downloadPaths = list.SelectMany(x => x)
|
var audioFilePaths = tracks.Where(t => t.DownloadPath.Length > 0 && !t.IsNotAudio).Select(t => t.DownloadPath);
|
||||||
.Where(x => x.DownloadPath.Length > 0 && !x.IsNotAudio)
|
|
||||||
.Select(x => x.DownloadPath).Distinct().ToList();
|
|
||||||
|
|
||||||
if (downloadPaths.Count == 0)
|
string outputFolder = Utils.GreatestCommonPath(audioFilePaths, Path.DirectorySeparatorChar);
|
||||||
return;
|
|
||||||
|
|
||||||
for (int i = 0; i < downloadPaths.Count; i++)
|
foreach (var track in tracks)
|
||||||
downloadPaths[i] = Path.GetFullPath(downloadPaths[i]);
|
|
||||||
|
|
||||||
for (int i = 0; i < list.Count; i++)
|
|
||||||
{
|
{
|
||||||
for (int j = 0; j < list[i].Count; j++)
|
if (!track.IsNotAudio || track.State != TrackState.Downloaded)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string newFilePath = Path.Join(outputFolder, Path.GetFileName(track.DownloadPath));
|
||||||
|
|
||||||
|
if (track.DownloadPath != newFilePath)
|
||||||
{
|
{
|
||||||
var track = list[i][j];
|
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
|
||||||
if (!track.IsNotAudio || track.State != TrackState.Downloaded)
|
Utils.Move(track.DownloadPath, newFilePath);
|
||||||
continue;
|
|
||||||
string filepath = track.DownloadPath;
|
string prevParent = Path.GetDirectoryName(track.DownloadPath);
|
||||||
string add = Path.GetRelativePath(Config.outputFolder, Path.GetDirectoryName(filepath));
|
|
||||||
string newFilePath = Path.Join(Utils.GreatestCommonPath(downloadPaths, Path.DirectorySeparatorChar), add, Path.GetFileName(filepath));
|
if (prevParent != Config.outputFolder && Utils.GetRecursiveFileCount(prevParent) == 0)
|
||||||
if (filepath != newFilePath)
|
Directory.Delete(prevParent, true);
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
|
track.DownloadPath = newFilePath;
|
||||||
Utils.Move(filepath, newFilePath);
|
|
||||||
if (add.Length > 0 && add != "." && Utils.GetRecursiveFileCount(Path.Join(Config.outputFolder, add)) == 0)
|
|
||||||
Directory.Delete(Path.Join(Config.outputFolder, add), true);
|
|
||||||
list[i][j].DownloadPath = newFilePath;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1269,6 +1338,9 @@ static partial class Program
|
||||||
case "filename":
|
case "filename":
|
||||||
return Path.GetFileNameWithoutExtension(filepath);
|
return Path.GetFileNameWithoutExtension(filepath);
|
||||||
case "foldername":
|
case "foldername":
|
||||||
|
return track.FirstDownload != null ?
|
||||||
|
Utils.GetBaseNameSlsk(Utils.GetDirectoryNameSlsk(track.FirstDownload.Filename)) : Config.defaultFolderName;
|
||||||
|
case "default-foldername":
|
||||||
return Config.defaultFolderName;
|
return Config.defaultFolderName;
|
||||||
case "extractor":
|
case "extractor":
|
||||||
return Config.inputType.ToString();
|
return Config.inputType.ToString();
|
||||||
|
@ -1347,18 +1419,18 @@ static partial class Program
|
||||||
if (!tracks[i].IsNotAudio)
|
if (!tracks[i].IsNotAudio)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" Artist: {tracks[i].Artist}");
|
Console.WriteLine($" Artist: {tracks[i].Artist}");
|
||||||
if (!string.IsNullOrEmpty(tracks[i].Title) || !tracks[i].IsAlbum)
|
if (!string.IsNullOrEmpty(tracks[i].Title) || tracks[i].Type == TrackType.Normal)
|
||||||
Console.WriteLine($" Title: {tracks[i].Title}");
|
Console.WriteLine($" Title: {tracks[i].Title}");
|
||||||
if (!string.IsNullOrEmpty(tracks[i].Album) || tracks[i].IsAlbum)
|
if (!string.IsNullOrEmpty(tracks[i].Album) || tracks[i].Type == TrackType.Album)
|
||||||
Console.WriteLine($" Album: {tracks[i].Album}");
|
Console.WriteLine($" Album: {tracks[i].Album}");
|
||||||
if (tracks[i].Length > -1 || !tracks[i].IsAlbum)
|
if (tracks[i].Length > -1 || tracks[i].Type == TrackType.Normal)
|
||||||
Console.WriteLine($" Length: {tracks[i].Length}s");
|
Console.WriteLine($" Length: {tracks[i].Length}s");
|
||||||
if (!string.IsNullOrEmpty(tracks[i].DownloadPath))
|
if (!string.IsNullOrEmpty(tracks[i].DownloadPath))
|
||||||
Console.WriteLine($" Local path: {tracks[i].DownloadPath}");
|
Console.WriteLine($" Local path: {tracks[i].DownloadPath}");
|
||||||
if (!string.IsNullOrEmpty(tracks[i].URI))
|
if (!string.IsNullOrEmpty(tracks[i].URI))
|
||||||
Console.WriteLine($" URL/ID: {tracks[i].URI}");
|
Console.WriteLine($" URL/ID: {tracks[i].URI}");
|
||||||
if (tracks[i].IsAlbum)
|
if (tracks[i].Type != TrackType.Normal)
|
||||||
Console.WriteLine($" Is album: true");
|
Console.WriteLine($" Type: {tracks[i].Type}");
|
||||||
if (!string.IsNullOrEmpty(tracks[i].Other))
|
if (!string.IsNullOrEmpty(tracks[i].Other))
|
||||||
Console.WriteLine($" Other: {tracks[i].Other}");
|
Console.WriteLine($" Other: {tracks[i].Other}");
|
||||||
if (tracks[i].ArtistMaybeWrong)
|
if (tracks[i].ArtistMaybeWrong)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using Data;
|
using Data;
|
||||||
using Enums;
|
using Enums;
|
||||||
|
using ExistingCheckers;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
@ -260,7 +261,7 @@ namespace Test
|
||||||
{
|
{
|
||||||
Config.input = strings[i];
|
Config.input = strings[i];
|
||||||
Console.WriteLine(Config.input);
|
Console.WriteLine(Config.input);
|
||||||
var res = await extractor.GetTracks();
|
var res = await extractor.GetTracks(0, 0, false);
|
||||||
var t = res[0].list[0][0];
|
var t = res[0].list[0][0];
|
||||||
Assert(Extractors.StringExtractor.InputMatches(Config.input));
|
Assert(Extractors.StringExtractor.InputMatches(Config.input));
|
||||||
Assert(t.ToKey() == tracks[i].ToKey());
|
Assert(t.ToKey() == tracks[i].ToKey());
|
||||||
|
@ -273,7 +274,7 @@ namespace Test
|
||||||
{
|
{
|
||||||
Config.input = strings[i];
|
Config.input = strings[i];
|
||||||
Console.WriteLine(Config.input);
|
Console.WriteLine(Config.input);
|
||||||
var t = (await extractor.GetTracks())[0].source;
|
var t = (await extractor.GetTracks(0, 0, false))[0].source;
|
||||||
Assert(Extractors.StringExtractor.InputMatches(Config.input));
|
Assert(Extractors.StringExtractor.InputMatches(Config.input));
|
||||||
Assert(t.ToKey() == albums[i].ToKey());
|
Assert(t.ToKey() == albums[i].ToKey());
|
||||||
}
|
}
|
||||||
|
@ -287,6 +288,7 @@ namespace Test
|
||||||
|
|
||||||
Config.m3uOption = M3uOption.All;
|
Config.m3uOption = M3uOption.All;
|
||||||
Config.skipMode = SkipMode.M3u;
|
Config.skipMode = SkipMode.M3u;
|
||||||
|
Config.musicDir = "";
|
||||||
Config.printOption = PrintOption.Tracks | PrintOption.Full;
|
Config.printOption = PrintOption.Tracks | PrintOption.Full;
|
||||||
Config.skipExisting = true;
|
Config.skipExisting = true;
|
||||||
|
|
||||||
|
@ -296,11 +298,12 @@ namespace Test
|
||||||
File.Delete(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,3,0;" +
|
$"{Path.Join(Directory.GetCurrentDirectory(), "file1.5")},\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;" +
|
||||||
$"path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,3,0;" +
|
$"path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;" +
|
||||||
$"path/to/file2,\"Artist, 2\",,Title2,-1,3,0;,\"Artist; ,3\",,Title3 ;a,-1,4,0;" +
|
$"path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;" +
|
||||||
$",\"Artist,,, ;4\",,Title4,-1,4,3;" +
|
$",\"Artist; ,3\",,Title3 ;a,-1,0,4,0;" +
|
||||||
$",,,,-1,0,0;");
|
$",\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
|
||||||
|
$",,,,-1,0,0,0;");
|
||||||
|
|
||||||
var notFoundInitial = new List<Track>()
|
var notFoundInitial = new List<Track>()
|
||||||
{
|
{
|
||||||
|
@ -320,7 +323,7 @@ namespace Test
|
||||||
};
|
};
|
||||||
|
|
||||||
var trackLists = new TrackLists();
|
var trackLists = new TrackLists();
|
||||||
trackLists.AddEntry();
|
trackLists.AddEntry(new TrackListEntry());
|
||||||
foreach (var t in notFoundInitial)
|
foreach (var t in notFoundInitial)
|
||||||
trackLists.AddTrackToLast(t);
|
trackLists.AddTrackToLast(t);
|
||||||
foreach (var t in existingInitial)
|
foreach (var t in existingInitial)
|
||||||
|
@ -330,20 +333,22 @@ namespace Test
|
||||||
|
|
||||||
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption);
|
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption);
|
||||||
|
|
||||||
|
Program.outputExistingChecker = new M3uExistingChecker(Program.m3uEditor, false);
|
||||||
|
|
||||||
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], false });
|
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 toBeDownloaded = trackLists[0].list[0].Where(t => t.State == TrackState.Initial).ToList();
|
||||||
|
|
||||||
Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial));
|
Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial));
|
||||||
Assert(existing.SequenceEqualUpToPermutation(existingInitial));
|
Assert(existing.SequenceEqualUpToPermutation(existingInitial));
|
||||||
Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
|
Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
|
||||||
|
|
||||||
ProgramInvoke("PrintTracksTbd", new object[] { toBeDownloaded, existing, notFound, ListType.Normal });
|
ProgramInvoke("PrintTracksTbd", new object[] { toBeDownloaded, existing, notFound, TrackType.Normal });
|
||||||
|
|
||||||
Program.m3uEditor.Update();
|
Program.m3uEditor.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,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,3,0;,\"Artist; ,3\",,Title3 ;a,-1,4,0;,\"Artist,,, ;4\",,Title4,-1,4,3;,,,,-1,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;" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
|
"\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
|
||||||
"\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
|
"\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
|
||||||
|
@ -362,8 +367,8 @@ namespace Test
|
||||||
Program.m3uEditor.Update();
|
Program.m3uEditor.Update();
|
||||||
output = File.ReadAllText(path);
|
output = File.ReadAllText(path);
|
||||||
need =
|
need =
|
||||||
"#SLDL:/other/new/file/path,\"Artist, 1.5\",,\"Title, , 1.5\",-1,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,3,0;,\"Artist; ,3\",,Title3 ;a,-1,4,0;,\"Artist,,, ;4\",,Title4,-1,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;" +
|
||||||
",,,,-1,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,1,0;,ArtistB,Albumm,TitleB,-1,2,3;" +
|
",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
|
"\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
|
||||||
"\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
|
"\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
|
||||||
|
@ -394,6 +399,44 @@ namespace Test
|
||||||
output = File.ReadAllText(path);
|
output = File.ReadAllText(path);
|
||||||
Assert(output == need);
|
Assert(output == need);
|
||||||
|
|
||||||
|
|
||||||
|
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.AddEntry(new TrackListEntry(t));
|
||||||
|
|
||||||
|
File.WriteAllText(path, "");
|
||||||
|
Config.m3uOption = M3uOption.Index;
|
||||||
|
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption);
|
||||||
|
Program.m3uEditor.Update();
|
||||||
|
|
||||||
|
Assert(File.ReadAllText(path) == "");
|
||||||
|
|
||||||
|
test[0].State = TrackState.Downloaded;
|
||||||
|
test[0].DownloadPath = "download/path";
|
||||||
|
test[1].State = TrackState.Failed;
|
||||||
|
test[1].FailureReason = FailureReason.NoSuitableFileFound;
|
||||||
|
test[2].State = TrackState.AlreadyExists;
|
||||||
|
|
||||||
|
Program.m3uEditor.Update();
|
||||||
|
|
||||||
|
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption);
|
||||||
|
|
||||||
|
foreach (var t in test)
|
||||||
|
{
|
||||||
|
Program.m3uEditor.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);
|
File.Delete(path);
|
||||||
|
|
||||||
Passed();
|
Passed();
|
||||||
|
|
Loading…
Reference in a new issue