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
|
||||
--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 <option> 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:
|
||||
|
|
|
@ -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<string, (List<string> 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]}");
|
||||
}
|
||||
|
|
|
@ -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<Track>> 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<Track>> 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<List<Track>>();
|
||||
source = new Track();
|
||||
}
|
||||
|
||||
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.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>> 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<Track>> list, ListType type, Track source)> lists)
|
||||
{
|
||||
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)
|
||||
public static TrackLists FromFlattened(IEnumerable<Track> 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<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)
|
||||
{
|
||||
if (lists.Count == 0)
|
||||
{
|
||||
AddEntry(new TrackListEntry(new List<List<Track>> { new List<Track>() { track } }, new Track()));
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
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)
|
||||
{
|
||||
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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ namespace Extractors
|
|||
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();
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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<TrackLists> GetTracks()
|
||||
public async Task<TrackLists> 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);
|
||||
|
|
|
@ -18,15 +18,15 @@ namespace Extractors
|
|||
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();
|
||||
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<Track> tracks = new List<Track>();
|
||||
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<Track>();
|
||||
|
||||
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)
|
||||
|
|
|
@ -11,41 +11,23 @@ namespace Extractors
|
|||
return !input.IsInternetUrl();
|
||||
}
|
||||
|
||||
public async Task<TrackLists> GetTracks()
|
||||
public async Task<TrackLists> 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)
|
||||
|
|
|
@ -20,11 +20,11 @@ namespace Extractors
|
|||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace Extractors
|
|||
{
|
||||
public interface IExtractor
|
||||
{
|
||||
Task<TrackLists> GetTracks();
|
||||
Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse);
|
||||
Task RemoveTrackFromSource(Track track) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <option> 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 = @"
|
||||
|
|
|
@ -6,13 +6,13 @@ using System.Text;
|
|||
public class M3uEditor
|
||||
{
|
||||
List<string> lines;
|
||||
TrackLists trackLists;
|
||||
string path;
|
||||
string parent;
|
||||
int offset = 0;
|
||||
M3uOption option = M3uOption.Index;
|
||||
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)
|
||||
{
|
||||
|
@ -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:<trackinfo>;<trackinfo>; ...
|
||||
// where <trackinfo> = filepath,artist,album,title,length(int),tracktype(int),state(int),failurereason(int)
|
||||
|
||||
if (lines.Count == 0 || !lines[0].StartsWith("#SLDL:"))
|
||||
return;
|
||||
|
||||
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:<trackinfo>;<trackinfo>; ...
|
||||
// where <trackinfo> = 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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);
|
||||
|
||||
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<Track>();
|
||||
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<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;
|
||||
|
||||
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<Track> DoSkipExisting(List<Track> tracks, bool print)
|
||||
static List<Track> DoSkipExisting(List<Track> tracks)
|
||||
{
|
||||
var existing = new Dictionary<Track, string>();
|
||||
|
||||
bool fileBasedSkip = (int)Config.skipMode < 4;
|
||||
|
||||
if (!(fileBasedSkip && Config.musicDir.Length > 0 && Config.outputFolder.StartsWith(Config.musicDir, StringComparison.OrdinalIgnoreCase)))
|
||||
var existing = new List<Track>();
|
||||
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<Track> DoSkipNotFound(List<Track> tracks)
|
||||
{
|
||||
List<Track> notFound = new List<Track>();
|
||||
for (int i = tracks.Count - 1; i >= 0; i--)
|
||||
var notFound = new List<Track>();
|
||||
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<Track>(tracks);
|
||||
var downloadTasks = copy.Select(async (track, index) =>
|
||||
|
@ -545,7 +610,6 @@ static partial class Program
|
|||
var tracks = new List<Track>();
|
||||
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<Track>> list)
|
||||
static void ApplyNamingFormatsNonAudio(List<Track> 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)
|
||||
|
|
|
@ -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<Track>()
|
||||
{
|
||||
|
@ -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<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();
|
||||
|
||||
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<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);
|
||||
|
||||
Passed();
|
||||
|
|
Loading…
Reference in a new issue