1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 14:32:40 +00:00
This commit is contained in:
fiso64 2024-08-27 18:00:31 +02:00
parent 71160047d8
commit 6e7b8d5d67
15 changed files with 704 additions and 438 deletions

View file

@ -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:

View file

@ -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]}");
}

View file

@ -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;

View file

@ -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

View file

@ -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;
}
}
}
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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)

View file

@ -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)

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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 = @"

View file

@ -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;
}

View file

@ -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)

View file

@ -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();