diff --git a/README.md b/README.md
index 635ef08..f5c8f80 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ Usage: sldl [OPTIONS]
library. Use with --skip-existing
--skip-not-found Skip searching for tracks that weren't found on Soulseek
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 Changes how searches and downloads are displayed:
'single' (default): Show transfer state and percentage
@@ -402,23 +403,25 @@ salbum Source album name
year Track year or date
track Track number
disc Disc number
-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)
```
## Skip existing
-sldl can skip files that exist in the download directory or a specified directory configured with
---music-dir.
+sldl can skip downloads that exist in the output directory or a specified directory configured
+with --music-dir.
The following modes are available for --skip-mode:
### m3u
Default when checking in the output directory.
- 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).
+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). m3u and
+m3u-cond are the only modes that can skip album downloads.
### name
+Default when checking in the music directory.
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
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.
### 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 necessary conditions.
- Equivalent to the above modes if no necessary conditions have been specified (except m3u-cond
- which always checks if the file exists). May be slower and use a lot of memory for large
- libraries.
+Same as the above modes but also checks whether the found file satisfies the configured
+conditions. Uses necessary conditions by default, run with --skip-existing-pref-cond to use
+preferred conditions instead. Equivalent to the above modes if no necessary conditions have
+been specified (except m3u-cond, which always checks if the file exists).
+May be slower and use a lot of memory for large libraries.
## Configuration
### Config Location:
diff --git a/slsk-batchdl/Config.cs b/slsk-batchdl/Config.cs
index 1461641..bbd84df 100644
--- a/slsk-batchdl/Config.cs
+++ b/slsk-batchdl/Config.cs
@@ -74,13 +74,14 @@ static class Config
public static bool noModifyShareCount = false;
public static bool useRandomLogin = false;
public static bool noBrowseFolder = false;
+ public static bool skipExistingPrefCond = false;
public static int downrankOn = -1;
public static int ignoreOn = -2;
public static int minAlbumTrackCount = -1;
public static int maxAlbumTrackCount = -1;
public static int fastSearchDelay = 300;
- public static int maxTracks = int.MaxValue;
public static int minUsersAggregate = 2;
+ public static int maxTracks = int.MaxValue;
public static int offset = 0;
public static int maxStaleTime = 50000;
public static int updateDelay = 100;
@@ -98,8 +99,8 @@ static class Config
public static M3uOption m3uOption = M3uOption.Index;
public static DisplayMode displayMode = DisplayMode.Single;
public static InputType inputType = InputType.None;
- public static SkipMode skipMode = SkipMode.M3uCond;
- public static SkipMode skipModeMusicDir = SkipMode.NameCond;
+ public static SkipMode skipMode = SkipMode.M3u;
+ public static SkipMode skipModeMusicDir = SkipMode.Name;
public static PrintOption printOption = PrintOption.None;
static readonly Dictionary args, string? cond)> profiles = new();
@@ -226,7 +227,7 @@ static class Config
m3uOption = M3uOption.None;
else if (!hasConfiguredM3uMode && inputType == InputType.String)
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;
parentFolder = Utils.ExpandUser(parentFolder);
@@ -238,7 +239,7 @@ static class Config
if (folderName == ".")
folderName = "";
- folderName = folderName.Replace("\\", "/");
+ folderName = folderName.Replace('\\', '/');
folderName = string.Join('/', folderName.Split('/').Select(x => x.ReplaceInvalidChars(invalidReplaceStr).Trim()));
folderName = folderName.Replace('/', Path.DirectorySeparatorChar);
@@ -370,7 +371,7 @@ static class Config
return var switch
{
"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",
"interactive" => interactiveMode,
"album" => album,
@@ -1148,6 +1149,10 @@ static class Config
case "--no-browse-folder":
setFlag(ref noBrowseFolder, ref i);
break;
+ case "--sepc":
+ case "--skip-existing-pref-cond":
+ setFlag(ref skipExistingPrefCond, ref i);
+ break;
default:
throw new ArgumentException($"Unknown argument: {args[i]}");
}
diff --git a/slsk-batchdl/Data.cs b/slsk-batchdl/Data.cs
index 8c9cc7d..72fe363 100644
--- a/slsk-batchdl/Data.cs
+++ b/slsk-batchdl/Data.cs
@@ -12,17 +12,20 @@ namespace Data
public string URI = "";
public int Length = -1;
public bool ArtistMaybeWrong = false;
- public bool IsAlbum = false;
public int MinAlbumTrackCount = -1;
public int MaxAlbumTrackCount = -1;
public bool IsNotAudio = false;
public string DownloadPath = "";
public string Other = "";
public int CsvRow = -1;
+ public TrackType Type = TrackType.Normal;
public FailureReason FailureReason = FailureReason.None;
public TrackState State = TrackState.Initial;
public SlDictionary? Downloads = null;
+ public bool OutputsDirectory => Type != TrackType.Normal;
+ public Soulseek.File? FirstDownload => Downloads?.FirstOrDefault().Value.Item2;
+
public Track() { }
public Track(Track other)
@@ -34,7 +37,7 @@ namespace Data
URI = other.URI;
ArtistMaybeWrong = other.ArtistMaybeWrong;
Downloads = other.Downloads;
- IsAlbum = other.IsAlbum;
+ Type = other.Type;
IsNotAudio = other.IsNotAudio;
State = other.State;
FailureReason = other.FailureReason;
@@ -47,7 +50,7 @@ namespace Data
public string ToKey()
{
- return $"{Artist};{Album};{Title};{Length}";
+ return $"{Artist};{Album};{Title};{Length};{(int)Type}";
}
public override string ToString()
@@ -61,7 +64,7 @@ namespace Data
return $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}";
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)}";
}
@@ -69,7 +72,7 @@ namespace Data
{
if (str.Length > 0)
str += " - ";
- if (IsAlbum)
+ if (Type == TrackType.Album)
str += Album;
else if (Title.Length > 0)
str += Title;
@@ -77,7 +80,7 @@ namespace Data
{
if (Length > 0)
str += $" ({Length}s)";
- if (IsAlbum)
+ if (Type == TrackType.Album)
str += " (album)";
}
}
@@ -93,28 +96,56 @@ namespace Data
public class TrackListEntry
{
public List> list;
- public ListType type;
public Track source;
- public bool needSearch;
- public bool placeInSubdir;
+ public bool needSourceSearch = false;
+ public bool sourceCanBeSkipped = false;
+ public bool needSkipExistingAfterSearch = false;
+ public bool gotoNextAfterSearch = false;
+ public bool placeInSubdir = false;
+ public string? subdirOverride;
- public TrackListEntry(List> list, ListType type, Track source)
+ public TrackListEntry()
{
- this.list = list;
- this.type = type;
- this.source = source;
-
- needSearch = type != ListType.Normal;
- placeInSubdir = false;
+ list = new List>();
+ source = new Track();
}
- public TrackListEntry(List> list, ListType type, Track source, bool needSearch, bool placeInSubdir)
+ public TrackListEntry(Track source)
+ {
+ list = new List>();
+ 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 source)
{
this.list = list;
- this.type = type;
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 source, bool needSearch, bool placeInSubdir,
+ bool canBeSkipped, bool needSkipExistingAfterSearch, bool gotoNextAfterSearch)
+ {
+ this.list = list;
+ this.source = source;
+ this.needSourceSearch = needSearch;
this.placeInSubdir = placeInSubdir;
+ this.sourceCanBeSkipped = canBeSkipped;
+ this.needSkipExistingAfterSearch = needSkipExistingAfterSearch;
+ this.gotoNextAfterSearch = gotoNextAfterSearch;
}
}
@@ -124,21 +155,7 @@ namespace Data
public TrackLists() { }
- public TrackLists(List<(List> list, ListType type, Track source)> lists)
- {
- foreach (var (list, type, source) in lists)
- {
- var newList = new List>();
- foreach (var innerList in list)
- {
- var innerNewList = new List(innerList);
- newList.Add(innerNewList);
- }
- this.lists.Add(new TrackListEntry(newList, type, source));
- }
- }
-
- public static TrackLists FromFlattened(IEnumerable flatList, bool aggregate, bool album)
+ public static TrackLists FromFlattened(IEnumerable flatList)
{
var res = new TrackLists();
using var enumerator = flatList.GetEnumerator();
@@ -147,35 +164,26 @@ namespace Data
{
var track = enumerator.Current;
- if (album && aggregate)
+ if (track.Type != TrackType.Normal)
{
- res.AddEntry(ListType.AlbumAggregate, track);
- }
- else if (aggregate)
- {
- res.AddEntry(ListType.Aggregate, track);
- }
- else if (album || track.IsAlbum)
- {
- track.IsAlbum = true;
- res.AddEntry(ListType.Album, track);
+ res.AddEntry(new TrackListEntry(track));
}
else
{
- res.AddEntry(ListType.Normal);
+ res.AddEntry(new TrackListEntry());
res.AddTrackToLast(track);
bool hasNext;
while (true)
{
hasNext = enumerator.MoveNext();
- if (!hasNext || enumerator.Current.IsAlbum)
+ if (!hasNext || enumerator.Current.Type != TrackType.Normal)
break;
res.AddTrackToLast(enumerator.Current);
}
- if (hasNext && enumerator.Current.IsAlbum)
- res.AddEntry(ListType.Album, track);
+ if (hasNext && enumerator.Current.Type != TrackType.Normal)
+ res.AddEntry(new TrackListEntry(track));
else if (!hasNext)
break;
}
@@ -195,35 +203,22 @@ namespace Data
lists.Add(tle);
}
- public void AddEntry(List>? list, ListType? type = null, Track? source = null)
- {
- type ??= ListType.Normal;
- source ??= new Track();
- list ??= new List>();
- lists.Add(new TrackListEntry(list, (ListType)type, source));
- }
-
- public void AddEntry(List tracks, ListType? type = null, Track? source = null)
- {
- var list = new List>() { tracks };
- AddEntry(list, type, source);
- }
-
- public void AddEntry(Track track, ListType? type = null, Track? source = null)
- {
- var list = new List>() { new List() { track } };
- AddEntry(list, type, source);
- }
-
- public void AddEntry(ListType? type = null, Track? source = null)
- {
- var list = new List>() { new List() };
- AddEntry(list, type, source);
- }
-
public void AddTrackToLast(Track track)
{
+ if (lists.Count == 0)
+ {
+ AddEntry(new TrackListEntry(new List> { new List() { track } }, new Track()));
+ return;
+ }
+
int i = lists.Count - 1;
+
+ if (lists[i].list.Count == 0)
+ {
+ lists[i].list.Add(new List() { track });
+ return;
+ }
+
int j = lists[i].list.Count - 1;
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();
+
+ 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 Flattened(bool addSources, bool addSpecialSourceTracks, bool sourcesOnly = false)
{
foreach (var tle in lists)
{
if ((addSources || sourcesOnly) && tle.source != null)
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])
yield return t;
diff --git a/slsk-batchdl/Enums.cs b/slsk-batchdl/Enums.cs
index dd43662..3045139 100644
--- a/slsk-batchdl/Enums.cs
+++ b/slsk-batchdl/Enums.cs
@@ -40,12 +40,12 @@ namespace Enums
None,
}
- public enum ListType
+ public enum TrackType
{
- Normal,
- Album,
- Aggregate,
- AlbumAggregate,
+ Normal = 0,
+ Album = 1,
+ Aggregate = 2,
+ AlbumAggregate = 3,
}
public enum M3uOption
diff --git a/slsk-batchdl/ExistingChecker.cs b/slsk-batchdl/ExistingChecker.cs
index babbb00..d02fb3d 100644
--- a/slsk-batchdl/ExistingChecker.cs
+++ b/slsk-batchdl/ExistingChecker.cs
@@ -5,9 +5,9 @@ using System.IO;
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());
return mode switch
@@ -20,38 +20,16 @@ namespace ExistingCheckers
SkipMode.M3uCond => noConditions ? new M3uExistingChecker(m3uEditor, true) : new M3uConditionExistingChecker(m3uEditor, conditions),
};
}
-
- public static Dictionary SkipExisting(List tracks, string dir, FileConditions necessaryCond, M3uEditor m3uEditor, SkipMode mode)
- {
- var existing = new Dictionary();
-
- 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 void BuildIndex() { }
+ public abstract bool TrackExists(Track track, out string? foundPath);
+ 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 dir;
@@ -71,7 +49,7 @@ namespace ExistingCheckers
return s;
}
- public void BuildIndex()
+ public override void BuildIndex()
{
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
@@ -86,16 +64,24 @@ namespace ExistingCheckers
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 artist = Preprocess(track.Artist, true);
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;
return true;
@@ -107,7 +93,7 @@ namespace ExistingCheckers
}
}
- public class NameConditionExistingChecker : IExistingChecker
+ public class NameConditionExistingChecker : ExistingChecker
{
readonly string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" };
readonly string dir;
@@ -129,7 +115,7 @@ namespace ExistingCheckers
return s;
}
- public void BuildIndex()
+ public override void BuildIndex()
{
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
@@ -148,16 +134,25 @@ namespace ExistingCheckers
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 artist = Preprocess(track.Artist, true);
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;
return true;
@@ -169,7 +164,7 @@ namespace ExistingCheckers
}
}
- public class TagExistingChecker : IExistingChecker
+ public class TagExistingChecker : ExistingChecker
{
readonly string dir;
readonly List<(string, string, string)> index = new(); // (Path, PreprocessedArtist, PreprocessedTitle)
@@ -184,7 +179,7 @@ namespace ExistingCheckers
return s.Replace(" ", "").RemoveFt().ToLower();
}
- public void BuildIndex()
+ public override void BuildIndex()
{
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
@@ -201,10 +196,17 @@ namespace ExistingCheckers
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 artist = Preprocess(track.Artist);
@@ -217,12 +219,11 @@ namespace ExistingCheckers
}
}
- foundPath = null;
return false;
}
}
- public class TagConditionExistingChecker : IExistingChecker
+ public class TagConditionExistingChecker : ExistingChecker
{
readonly string dir;
readonly List<(string, string, SimpleFile)> index = new(); // (PreprocessedArtist, PreprocessedTitle, file)
@@ -239,7 +240,7 @@ namespace ExistingCheckers
return s.Replace(" ", "").RemoveFt().ToLower();
}
- public void BuildIndex()
+ public override void BuildIndex()
{
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
@@ -256,10 +257,17 @@ namespace ExistingCheckers
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 artist = Preprocess(track.Artist);
@@ -272,12 +280,11 @@ namespace ExistingCheckers
}
}
- foundPath = null;
return false;
}
}
- public class M3uExistingChecker : IExistingChecker
+ public class M3uExistingChecker : ExistingChecker
{
M3uEditor m3uEditor;
bool checkFileExists;
@@ -288,16 +295,29 @@ namespace ExistingCheckers
this.checkFileExists = checkFileExists;
}
- public bool TrackExists(Track track, out string? foundPath)
+ public override bool TrackExists(Track track, out string? foundPath)
{
foundPath = null;
var t = m3uEditor.PreviousRunResult(track);
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;
return true;
}
@@ -305,7 +325,7 @@ namespace ExistingCheckers
}
}
- public class M3uConditionExistingChecker : IExistingChecker
+ public class M3uConditionExistingChecker : ExistingChecker
{
M3uEditor m3uEditor;
FileConditions conditions;
@@ -316,36 +336,71 @@ namespace ExistingCheckers
this.conditions = conditions;
}
- public bool TrackExists(Track track, out string? foundPath)
+ public override bool TrackExists(Track track, out string? foundPath)
{
foundPath = null;
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))
- {
- TagLib.File musicFile;
- try
- {
- musicFile = TagLib.File.Create(t.DownloadPath);
- if (conditions.FileSatisfies(musicFile, track, false))
- {
- foundPath = t.DownloadPath;
- return true;
- }
- else
- {
- return false;
- }
+ if (!File.Exists(t.DownloadPath))
+ return false;
+
+ TagLib.File musicFile;
+ try
+ {
+ musicFile = TagLib.File.Create(t.DownloadPath);
+ if (conditions.FileSatisfies(musicFile, track, false))
+ {
+ foundPath = t.DownloadPath;
+ return true;
}
- catch
- {
+ else
+ {
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;
+ }
}
}
}
\ No newline at end of file
diff --git a/slsk-batchdl/Extractors/Bandcamp.cs b/slsk-batchdl/Extractors/Bandcamp.cs
index 4c58f77..675401d 100644
--- a/slsk-batchdl/Extractors/Bandcamp.cs
+++ b/slsk-batchdl/Extractors/Bandcamp.cs
@@ -16,7 +16,7 @@ namespace Extractors
return input.IsInternetUrl() && input.Contains("bandcamp.com");
}
- public async Task GetTracks()
+ public async Task GetTracks(int maxTracks, int offset, bool reverse)
{
var trackLists = new TrackLists();
bool isTrack = Config.input.Contains("/track/");
@@ -54,9 +54,15 @@ namespace Extractors
{
Album = item.GetProperty("title").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
@@ -70,8 +76,8 @@ namespace Extractors
if (isAlbum)
{
var artist = nameSection.SelectSingleNode(".//h3/span/a").InnerText.UnHtmlString().Trim();
- var track = new Track() { Artist = artist, Album = name, IsAlbum = true };
- trackLists.AddEntry(ListType.Album, track);
+ var track = new Track() { Artist = artist, Album = name, Type = TrackType.Album };
+ trackLists.AddEntry(new TrackListEntry(track));
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 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 track = new Track() { Artist = artist, Title = name, Album = album };
- trackLists.AddEntry(track);
+ trackLists.AddEntry(new());
+ trackLists.AddTrackToLast(track);
+
Config.defaultFolderName = ".";
}
}
- if (!Config.reverse)
- {
- trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(Config.offset).Take(Config.maxTracks), Config.aggregate, Config.album);
- }
+ if (reverse)
+ trackLists.Reverse();
+
+ if (offset > 0 || maxTracks < int.MaxValue)
+ trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(offset).Take(maxTracks));
return trackLists;
}
diff --git a/slsk-batchdl/Extractors/Csv.cs b/slsk-batchdl/Extractors/Csv.cs
index f28f35e..634bd31 100644
--- a/slsk-batchdl/Extractors/Csv.cs
+++ b/slsk-batchdl/Extractors/Csv.cs
@@ -1,4 +1,5 @@
using Data;
+using Enums;
using System.Text.RegularExpressions;
namespace Extractors
@@ -14,16 +15,30 @@ namespace Extractors
return !input.IsInternetUrl() && input.EndsWith(".csv");
}
- public async Task GetTracks()
+ public async Task GetTracks(int maxTracks, int offset, bool reverse)
{
- int max = Config.reverse ? int.MaxValue : Config.maxTracks;
- int off = Config.reverse ? 0 : Config.offset;
+ int max = reverse ? int.MaxValue : maxTracks;
+ int off = reverse ? 0 : offset;
if (!File.Exists(Config.input))
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 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);
return trackLists;
@@ -161,7 +176,8 @@ namespace Extractors
if (ytParse)
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)
tracks.Add(track);
diff --git a/slsk-batchdl/Extractors/Spotify.cs b/slsk-batchdl/Extractors/Spotify.cs
index 7423fea..2958ca0 100644
--- a/slsk-batchdl/Extractors/Spotify.cs
+++ b/slsk-batchdl/Extractors/Spotify.cs
@@ -18,15 +18,15 @@ namespace Extractors
return input == "spotify-likes" || input.IsInternetUrl() && input.Contains("spotify.com");
}
- public async Task GetTracks()
+ public async Task GetTracks(int maxTracks, int offset, bool reverse)
{
var trackLists = new TrackLists();
- int max = Config.reverse ? int.MaxValue : Config.maxTracks;
- int off = Config.reverse ? 0 : Config.offset;
+ int max = reverse ? int.MaxValue : maxTracks;
+ int off = reverse ? 0 : offset;
string playlistName = "";
bool needLogin = Config.input == "spotify-likes" || Config.removeTracksFromSource;
- List tracks = new List();
+ var tle = new TrackListEntry();
static void readSpotifyCreds()
{
@@ -48,19 +48,16 @@ namespace Extractors
if (Config.input == "spotify-likes")
{
Console.WriteLine("Loading Spotify likes");
- tracks = await spotifyClient.GetLikes(max, off);
+ var tracks = await spotifyClient.GetLikes(max, off);
playlistName = "Spotify Likes";
-
- trackLists.AddEntry(tracks);
- if (Config.album || Config.aggregate)
- trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false), Config.aggregate, Config.album);
+ tle.list.Add(tracks);
}
else if (Config.input.Contains("/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);
- trackLists.AddEntry(ListType.Album, source);
+ tle.source = source;
if (Config.setAlbumMinTrackCount)
source.MinAlbumTrackCount = tracks.Count;
@@ -75,6 +72,8 @@ namespace Extractors
}
else
{
+ var tracks = new List();
+
try
{
Console.WriteLine("Loading Spotify playlist");
@@ -105,13 +104,18 @@ namespace Extractors
}
else throw;
}
- trackLists.AddEntry(tracks);
- if (Config.album || Config.aggregate)
- trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false), Config.aggregate, Config.album);
+
+ tle.list.Add(tracks);
}
Config.defaultFolderName = playlistName.ReplaceInvalidChars(Config.invalidReplaceStr);
+ if (reverse)
+ {
+ trackLists.Reverse();
+ trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(offset).Take(maxTracks));
+ }
+
return trackLists;
}
@@ -328,7 +332,7 @@ namespace Extractors
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)
diff --git a/slsk-batchdl/Extractors/String.cs b/slsk-batchdl/Extractors/String.cs
index 2580c48..2bae45e 100644
--- a/slsk-batchdl/Extractors/String.cs
+++ b/slsk-batchdl/Extractors/String.cs
@@ -11,41 +11,23 @@ namespace Extractors
return !input.IsInternetUrl();
}
- public async Task GetTracks()
+ public async Task GetTracks(int maxTracks, int offset, bool reverse)
{
var trackLists = new TrackLists();
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);
- }
- 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);
+ music.Type = TrackType.Album;
+ trackLists.AddEntry(new TrackListEntry(music));
}
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();
else
Config.defaultFolderName = ".";
@@ -59,8 +41,6 @@ namespace Extractors
var track = new Track();
var keys = new string[] { "title", "artist", "length", "album", "artist-maybe-wrong" };
- track.IsAlbum = isAlbum;
-
void setProperty(string key, string value)
{
switch (key)
diff --git a/slsk-batchdl/Extractors/YouTube.cs b/slsk-batchdl/Extractors/YouTube.cs
index 7b9e276..4e16b22 100644
--- a/slsk-batchdl/Extractors/YouTube.cs
+++ b/slsk-batchdl/Extractors/YouTube.cs
@@ -20,11 +20,11 @@ namespace Extractors
return input.IsInternetUrl() && (input.Contains("youtu.be") || input.Contains("youtube.com"));
}
- public async Task GetTracks()
+ public async Task GetTracks(int maxTracks, int offset, bool reverse)
{
var trackLists = new TrackLists();
- int max = Config.reverse ? int.MaxValue : Config.maxTracks;
- int off = Config.reverse ? 0 : Config.offset;
+ int max = reverse ? int.MaxValue : maxTracks;
+ int off = reverse ? 0 : offset;
YouTube.apiKey = Config.ytKey;
string name;
@@ -60,12 +60,19 @@ namespace Extractors
}
YouTube.StopService();
- trackLists.AddEntry(tracks);
- if (Config.album || Config.aggregate)
- trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false), Config.aggregate, Config.album);
+ var tle = new TrackListEntry();
+ tle.list.Add(tracks);
+ trackLists.AddEntry(tle);
+
Config.defaultFolderName = name.ReplaceInvalidChars(Config.invalidReplaceStr);
+ if (reverse)
+ {
+ trackLists.Reverse();
+ trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(offset).Take(maxTracks));
+ }
+
return trackLists;
}
}
diff --git a/slsk-batchdl/Extractors/_Extractor.cs b/slsk-batchdl/Extractors/_Extractor.cs
index f19672d..617eef9 100644
--- a/slsk-batchdl/Extractors/_Extractor.cs
+++ b/slsk-batchdl/Extractors/_Extractor.cs
@@ -6,7 +6,7 @@ namespace Extractors
{
public interface IExtractor
{
- Task GetTracks();
+ Task GetTracks(int maxTracks, int offset, bool reverse);
Task RemoveTrackFromSource(Track track) => Task.CompletedTask;
}
diff --git a/slsk-batchdl/Help.cs b/slsk-batchdl/Help.cs
index e3609bf..916c30c 100644
--- a/slsk-batchdl/Help.cs
+++ b/slsk-batchdl/Help.cs
@@ -43,6 +43,7 @@ public static class Help
library. Use with --skip-existing
--skip-not-found Skip searching for tracks that weren't found on Soulseek
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 Changes how searches and downloads are displayed:
'single' (default): Show transfer state and percentage
@@ -375,23 +376,26 @@ public static class Help
track Track number
disc Disc number
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)
";
const string skipExistingHelp = @"
Skip existing
- sldl can skip files that exist in the download directory or a specified directory configured with
- --music-dir.
+ sldl can skip downloads that exist in the output directory or a specified directory configured
+ with --music-dir.
The following modes are available for --skip-mode:
m3u
Default when checking in the output directory.
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
+ Default when checking in the music directory.
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
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.
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 necessary conditions.
- Equivalent to the above modes if no necessary conditions have been specified (except m3u-cond
- which always checks if the file exists). May be slower and use a lot of memory for large
- libraries.
+ Same as the above modes but also checks whether the found file satisfies the configured
+ conditions. Uses necessary conditions by default, run with --skip-existing-pref-cond to use
+ preferred conditions instead. Equivalent to the above modes if no necessary conditions have
+ been specified (except m3u-cond, which always checks if the file exists).
+ May be slower and use a lot of memory for large libraries.
";
const string configHelp = @"
diff --git a/slsk-batchdl/M3uEditor.cs b/slsk-batchdl/M3uEditor.cs
index 5d34aa5..8e2e4ed 100644
--- a/slsk-batchdl/M3uEditor.cs
+++ b/slsk-batchdl/M3uEditor.cs
@@ -6,13 +6,13 @@ using System.Text;
public class M3uEditor
{
List lines;
- TrackLists trackLists;
- string path;
- string parent;
- int offset = 0;
- M3uOption option = M3uOption.Index;
bool needFirstUpdate = false;
- Dictionary 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 previousRunData = new(); // { track.ToKey(), track }
public M3uEditor(string m3uPath, TrackLists trackLists, M3uOption option, int offset = 0)
{
@@ -27,18 +27,25 @@ public class M3uEditor
LoadPreviousResults();
}
- private void LoadPreviousResults() // #SLDL:path,artist,album,title,length(int),state(int),failurereason(int); ... ; ...
+ private void LoadPreviousResults()
{
+ // Format:
+ // #SLDL:;; ...
+ // where = filepath,artist,album,title,length(int),tracktype(int),state(int),failurereason(int)
+
if (lines.Count == 0 || !lines[0].StartsWith("#SLDL:"))
return;
- string sldlLine = lines[0]["#SLDL:".Length..];
+ string sldlLine = lines[0];
+ lines = lines.Skip(1).ToList();
+
+ int k = "#SLDL:".Length;
var currentItem = new StringBuilder();
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();
int field = 0;
@@ -58,7 +65,7 @@ public class M3uEditor
inQuotes = !inQuotes;
}
}
- else if (field <= 5 && c == ',' && !inQuotes)
+ else if (field <= 6 && c == ',' && !inQuotes)
{
var x = currentItem.ToString();
@@ -77,12 +84,14 @@ public class M3uEditor
else if (field == 4)
track.Length = int.Parse(x);
else if (field == 5)
+ track.Type = (TrackType)int.Parse(currentItem.ToString());
+ else if (field == 6)
track.State = (TrackState)int.Parse(x);
currentItem.Clear();
field++;
}
- else if (field == 6 && c == ';')
+ else if (field == 7 && c == ';')
{
track.FailureReason = (FailureReason)int.Parse(currentItem.ToString());
currentItem.Clear();
@@ -94,7 +103,8 @@ public class M3uEditor
currentItem.Append(c);
}
}
- previousRunTracks[track.ToKey()] = track;
+
+ previousRunData[track.ToKey()] = track;
}
}
@@ -108,64 +118,71 @@ public class M3uEditor
bool needUpdate = false;
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("");
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)
{
- if (tle.type != ListType.Normal)
+ if (tle.source.Type != TrackType.Normal)
{
- continue;
- }
- //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++)
+ if (tle.source.State != TrackState.Initial)
{
- 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];
-
- 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++;
+ needUpdate |= updateLine(TrackToLine(track));
+ 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:;; ...
+ // where = filepath,artist,album,title,length(int),tracktype(int),state(int),failurereason(int)
+
void writeCsvLine(string[] items)
{
bool comma = false;
@@ -221,7 +242,7 @@ public class M3uEditor
writer.Write("#SLDL:");
- foreach (var val in previousRunTracks.Values)
+ foreach (var val in previousRunData.Values)
{
string p = val.DownloadPath;
if (p.StartsWith(parent))
@@ -234,6 +255,7 @@ public class M3uEditor
val.Album,
val.Title,
val.Length.ToString(),
+ ((int)val.Type).ToString(),
((int)val.State).ToString(),
((int)val.FailureReason).ToString(),
};
@@ -245,10 +267,15 @@ public class M3uEditor
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)
return $"# Failed: {track} [{failureReason}]";
+
if (track.DownloadPath.Length > 0)
{
if (track.DownloadPath.StartsWith(parent))
@@ -256,18 +283,19 @@ public class M3uEditor
else
return track.DownloadPath;
}
+
return $"# {track}";
}
public Track? PreviousRunResult(Track track)
{
- previousRunTracks.TryGetValue(track.ToKey(), out var t);
+ previousRunData.TryGetValue(track.ToKey(), out var t);
return t;
}
public bool TryGetPreviousRunResult(Track track, out Track? result)
{
- previousRunTracks.TryGetValue(track.ToKey(), out result);
+ previousRunData.TryGetValue(track.ToKey(), out result);
return result != null;
}
diff --git a/slsk-batchdl/Program.cs b/slsk-batchdl/Program.cs
index ac5d482..001a76e 100644
--- a/slsk-batchdl/Program.cs
+++ b/slsk-batchdl/Program.cs
@@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
using Data;
using Enums;
+using ExistingCheckers;
using Directory = System.IO.Directory;
using File = System.IO.File;
@@ -20,6 +21,8 @@ using SlResponse = Soulseek.SearchResponse;
static partial class Program
{
public static Extractors.IExtractor? extractor;
+ public static ExistingChecker? outputExistingChecker;
+ public static ExistingChecker? musicDirExistingChecker;
public static SoulseekClient? client;
public static TrackLists? trackLists;
public static M3uEditor? m3uEditor;
@@ -61,20 +64,18 @@ static partial class Program
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);
Config.PostProcessArgs();
- if (Config.reverse)
- {
- trackLists.Reverse();
- trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(Config.offset).Take(Config.maxTracks), Config.aggregate, Config.album);
- }
+ trackLists.UpgradeListTypes(Config.aggregate, Config.album);
m3uEditor = new M3uEditor(Config.m3uFilePath, trackLists, Config.m3uOption, Config.offset);
+ InitExistingChecker();
+
await MainLoop();
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)
{
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++)
{
+ if (i > 0) Console.WriteLine();
+
var tle = trackLists[i];
Config.UpdateArgs(tle);
@@ -162,77 +185,92 @@ static partial class Program
var existing = new List();
var notFound = new List();
+ 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);
- foreach (var tracks in tle.list)
- notFound.AddRange(DoSkipNotFound(tracks));
+ if (tle.source.State != TrackState.NotFoundLastTime && !tle.needSourceSearch)
+ {
+ 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++)
- existing.AddRange(DoSkipExisting(tle.list[0], print: i == 0 && j == 0));
+ if (tle.sourceCanBeSkipped && SetExisting(tle.source))
+ 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 (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
{
var tl = new List();
if (tle.source.State == TrackState.Initial) tl.Add(tle.source);
- PrintTracksTbd(tl, existing, notFound, tle.type);
+ PrintTracksTbd(tl, existing, notFound, tle.source.Type);
}
continue;
}
- if (tle.needSearch)
+ if (tle.sourceCanBeSkipped)
{
- Console.WriteLine($"{tle.type} download: {tle.source.ToString(true)}, searching..");
-
- var responseData = new ResponseData();
-
- if (tle.type == ListType.Album)
+ if (tle.source.State == TrackState.AlreadyExists)
+ {
+ Console.WriteLine($"{tle.source.Type} download '{tle.source.ToString(true)}' already exists at {tle.source.DownloadPath}, skipping");
+ continue;
+ }
+
+ if (tle.source.State == TrackState.NotFoundLastTime)
+ {
+ Console.WriteLine($"{tle.source.Type} download '{tle.source.ToString(true)}' was not found during a prior run, skipping");
+ continue;
+ }
+ }
+
+ if (tle.needSourceSearch)
+ {
+ Console.WriteLine($"{tle.source.Type} download: {tle.source.ToString(true)}, searching..");
+
+ await InitClientAndUpdateIfNeeded();
+
+ if (tle.source.Type == TrackType.Album)
{
- await InitClientAndUpdateIfNeeded();
tle.list = await GetAlbumDownloads(tle.source, responseData);
}
- else if (tle.type == ListType.Aggregate)
+ else if (tle.source.Type == TrackType.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)
+ else if (tle.source.Type == TrackType.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;
+ trackLists.AddEntry(new TrackListEntry(item, tle.source, false, true, true, false, false));
}
- if (tle.list.Count == 0 || !tle.list.Any(x => x.Count > 0))
+ if (Config.skipExisting && tle.needSkipExistingAfterSearch)
{
- 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;
+ foreach (var tracks in tle.list)
+ notFound.AddRange(DoSkipExisting(tracks));
+ }
- if (i < trackLists.lists.Count - 1) Console.WriteLine();
+ if (tle.gotoNextAfterSearch)
+ {
continue;
}
}
@@ -243,38 +281,44 @@ static partial class Program
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();
- 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 (i < trackLists.lists.Count - 1) Console.WriteLine();
continue;
}
await InitClientAndUpdateIfNeeded();
- if (tle.type == ListType.Normal)
+ if (tle.source.Type == TrackType.Normal)
{
await TracksDownloadNormal(tle);
}
- else if (tle.type == ListType.Album)
+ else if (tle.source.Type == TrackType.Album)
{
await TracksDownloadAlbum(tle);
}
- else if (tle.type == ListType.Aggregate)
+ else if (tle.source.Type == TrackType.Aggregate)
{
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()))
@@ -288,17 +332,17 @@ static partial class Program
{
await InitClientAndUpdateIfNeeded();
- if (tle.type == ListType.Normal)
+ if (tle.source.Type == TrackType.Normal)
{
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($"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($"Results for album {tle.source.ToString(true)}:");
@@ -340,9 +384,9 @@ static partial class Program
}
- static void PrintTracksTbd(List toBeDownloaded, List existing, List notFound, ListType type)
+ static void PrintTracksTbd(List toBeDownloaded, List existing, List 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;
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})" : "";
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}");
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);
if (full && (existing.Count > 0 || notFound.Count > 0))
@@ -395,40 +439,61 @@ static partial class Program
}
- static List DoSkipExisting(List tracks, bool print)
+ static List DoSkipExisting(List tracks)
{
- var existing = new Dictionary();
-
- bool fileBasedSkip = (int)Config.skipMode < 4;
-
- if (!(fileBasedSkip && Config.musicDir.Length > 0 && Config.outputFolder.StartsWith(Config.musicDir, StringComparison.OrdinalIgnoreCase)))
+ var existing = new List();
+ foreach (var track in tracks)
{
- var d = ExistingCheckers.Registry.SkipExisting(tracks, Config.outputFolder, Config.necessaryCond, m3uEditor, Config.skipMode);
- d.ToList().ForEach(x => existing.TryAdd(x.Key, x.Value));
+ if (SetExisting(track))
+ {
+ 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..");
- var d = ExistingCheckers.Registry.SkipExisting(tracks, Config.musicDir, Config.necessaryCond, m3uEditor, Config.skipModeMusicDir);
- d.ToList().ForEach(x => existing.TryAdd(x.Key, x.Value));
- }
- else if (Config.musicDir.Length > 0 && !System.IO.Directory.Exists(Config.musicDir))
- {
- if (print) Console.WriteLine($"Music dir does not exist: {Config.musicDir}");
+ if (!outputExistingChecker.IndexIsBuilt)
+ outputExistingChecker.BuildIndex();
+
+ outputExistingChecker.TrackExists(track, out path);
}
- 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 DoSkipNotFound(List tracks)
{
- List notFound = new List();
- for (int i = tracks.Count - 1; i >= 0; i--)
+ var notFound = new List();
+ foreach (var track in tracks)
{
- if (SetNotFoundLastTime(tracks[i]))
+ if (SetNotFoundLastTime(track))
{
- notFound.Add(tracks[i]);
+ notFound.Add(track);
}
}
return notFound;
@@ -453,7 +518,7 @@ static partial class Program
{
var tracks = tle.list[0];
- SemaphoreSlim semaphore = new SemaphoreSlim(Config.concurrentProcesses);
+ var semaphore = new SemaphoreSlim(Config.concurrentProcesses);
var copy = new List(tracks);
var downloadTasks = copy.Select(async (track, index) =>
@@ -545,7 +610,6 @@ static partial class Program
var tracks = new List();
bool downloadingImages = false;
bool albumDlFailed = false;
- var listRef = list;
string savedOutputFolder = Config.outputFolder;
var curAlbumArtOption = Config.albumArtOption == AlbumArtOption.MostLargest ? AlbumArtOption.Most : Config.albumArtOption;
@@ -602,6 +666,7 @@ static partial class Program
while (list.Count > 0)
{
idx++;
+
mainLoopCts = new CancellationTokenSource();
albumDlFailed = false;
@@ -612,9 +677,10 @@ static partial class Program
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)
@@ -775,7 +841,16 @@ static partial class Program
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();
soulseekFolderPathPrefix = "";
Config.outputFolder = savedOutputFolder;
@@ -1042,7 +1117,7 @@ static partial class Program
.Replace("{uri}", track.URI)
.Replace("{length}", track.Length.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("{failure-reason}", track.FailureReason.ToString())
.Replace("{path}", track.DownloadPath)
@@ -1106,39 +1181,33 @@ static partial class Program
return name.ReplaceInvalidChars(Config.invalidReplaceStr);
}
- static void ApplyNamingFormatsNonAudio(List> list)
+ static void ApplyNamingFormatsNonAudio(List tracks)
{
- if (!Config.nameFormat.Replace("\\", "/").Contains('/'))
+ if (!Config.nameFormat.Replace('\\', '/').Contains('/'))
return;
- var downloadPaths = list.SelectMany(x => x)
- .Where(x => x.DownloadPath.Length > 0 && !x.IsNotAudio)
- .Select(x => x.DownloadPath).Distinct().ToList();
+ var audioFilePaths = tracks.Where(t => t.DownloadPath.Length > 0 && !t.IsNotAudio).Select(t => t.DownloadPath);
- if (downloadPaths.Count == 0)
- return;
+ string outputFolder = Utils.GreatestCommonPath(audioFilePaths, Path.DirectorySeparatorChar);
- for (int i = 0; i < downloadPaths.Count; i++)
- downloadPaths[i] = Path.GetFullPath(downloadPaths[i]);
-
- for (int i = 0; i < list.Count; i++)
+ foreach (var track in tracks)
{
- 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];
- if (!track.IsNotAudio || track.State != TrackState.Downloaded)
- continue;
- string filepath = 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 (filepath != newFilePath)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(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;
- }
+ Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
+ Utils.Move(track.DownloadPath, newFilePath);
+
+ string prevParent = Path.GetDirectoryName(track.DownloadPath);
+
+ if (prevParent != Config.outputFolder && Utils.GetRecursiveFileCount(prevParent) == 0)
+ Directory.Delete(prevParent, true);
+
+ track.DownloadPath = newFilePath;
}
}
}
@@ -1269,6 +1338,9 @@ static partial class Program
case "filename":
return Path.GetFileNameWithoutExtension(filepath);
case "foldername":
+ return track.FirstDownload != null ?
+ Utils.GetBaseNameSlsk(Utils.GetDirectoryNameSlsk(track.FirstDownload.Filename)) : Config.defaultFolderName;
+ case "default-foldername":
return Config.defaultFolderName;
case "extractor":
return Config.inputType.ToString();
@@ -1347,18 +1419,18 @@ static partial class Program
if (!tracks[i].IsNotAudio)
{
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}");
- 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}");
- 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");
if (!string.IsNullOrEmpty(tracks[i].DownloadPath))
Console.WriteLine($" Local path: {tracks[i].DownloadPath}");
if (!string.IsNullOrEmpty(tracks[i].URI))
Console.WriteLine($" URL/ID: {tracks[i].URI}");
- if (tracks[i].IsAlbum)
- Console.WriteLine($" Is album: true");
+ if (tracks[i].Type != TrackType.Normal)
+ Console.WriteLine($" Type: {tracks[i].Type}");
if (!string.IsNullOrEmpty(tracks[i].Other))
Console.WriteLine($" Other: {tracks[i].Other}");
if (tracks[i].ArtistMaybeWrong)
diff --git a/slsk-batchdl/Test.cs b/slsk-batchdl/Test.cs
index 3c6c366..150450d 100644
--- a/slsk-batchdl/Test.cs
+++ b/slsk-batchdl/Test.cs
@@ -1,5 +1,6 @@
using Data;
using Enums;
+using ExistingCheckers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@@ -260,7 +261,7 @@ namespace Test
{
Config.input = strings[i];
Console.WriteLine(Config.input);
- var res = await extractor.GetTracks();
+ var res = await extractor.GetTracks(0, 0, false);
var t = res[0].list[0][0];
Assert(Extractors.StringExtractor.InputMatches(Config.input));
Assert(t.ToKey() == tracks[i].ToKey());
@@ -273,7 +274,7 @@ namespace Test
{
Config.input = strings[i];
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(t.ToKey() == albums[i].ToKey());
}
@@ -287,6 +288,7 @@ namespace Test
Config.m3uOption = M3uOption.All;
Config.skipMode = SkipMode.M3u;
+ Config.musicDir = "";
Config.printOption = PrintOption.Tracks | PrintOption.Full;
Config.skipExisting = true;
@@ -296,11 +298,12 @@ namespace Test
File.Delete(path);
File.WriteAllText(path, $"#SLDL:" +
- $"{Path.Join(Directory.GetCurrentDirectory(), "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;");
+ $"{Path.Join(Directory.GetCurrentDirectory(), "file1.5")},\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;" +
+ $"path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;" +
+ $"path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;" +
+ $",\"Artist; ,3\",,Title3 ;a,-1,0,4,0;" +
+ $",\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
+ $",,,,-1,0,0,0;");
var notFoundInitial = new List()
{
@@ -320,7 +323,7 @@ namespace Test
};
var trackLists = new TrackLists();
- trackLists.AddEntry();
+ trackLists.AddEntry(new TrackListEntry());
foreach (var t in notFoundInitial)
trackLists.AddTrackToLast(t);
foreach (var t in existingInitial)
@@ -330,20 +333,22 @@ namespace Test
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption);
+ Program.outputExistingChecker = new M3uExistingChecker(Program.m3uEditor, false);
+
var notFound = (List)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
- var existing = (List)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0], false });
+ var existing = (List)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0] });
var toBeDownloaded = trackLists[0].list[0].Where(t => t.State == TrackState.Initial).ToList();
Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial));
Assert(existing.SequenceEqualUpToPermutation(existingInitial));
Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
- ProgramInvoke("PrintTracksTbd", new object[] { toBeDownloaded, existing, notFound, ListType.Normal });
+ ProgramInvoke("PrintTracksTbd", new object[] { toBeDownloaded, existing, notFound, TrackType.Normal });
Program.m3uEditor.Update();
string output = File.ReadAllText(path);
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# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
"\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
@@ -362,8 +367,8 @@ namespace Test
Program.m3uEditor.Update();
output = File.ReadAllText(path);
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;" +
- ",,,,-1,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,1,0;,ArtistB,Albumm,TitleB,-1,2,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,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
"\n" +
"\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
"\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
@@ -394,6 +399,44 @@ namespace Test
output = File.ReadAllText(path);
Assert(output == need);
+
+ var test = new List
+ {
+ new() { Artist = "ArtistA", Album = "AlbumA", Type = TrackType.Album },
+ new() { Artist = "ArtistB", Album = "AlbumB", Type = TrackType.Album },
+ new() { Artist = "ArtistC", Album = "AlbumC", Type = TrackType.Album },
+ };
+
+ trackLists = new TrackLists();
+ foreach (var t in test)
+ trackLists.AddEntry(new TrackListEntry(t));
+
+ File.WriteAllText(path, "");
+ 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);
Passed();