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 library. Use with --skip-existing
--skip-not-found Skip searching for tracks that weren't found on Soulseek --skip-not-found Skip searching for tracks that weren't found on Soulseek
during the last run. Fails are read from the m3u file. during the last run. Fails are read from the m3u file.
--skip-existing-pref-cond Use preferred instead of necessary conds for skip-existing
--display-mode <option> Changes how searches and downloads are displayed: --display-mode <option> Changes how searches and downloads are displayed:
'single' (default): Show transfer state and percentage 'single' (default): Show transfer state and percentage
@ -402,23 +403,25 @@ salbum Source album name
year Track year or date year Track year or date
track Track number track Track number
disc Disc number disc Disc number
filename Soulseek filename without extension foldername Soulseek folder name (only available for album downloads)
foldername Default sldl folder name default-foldername Default sldl folder name
extractor Name of the extractor used (CSV/Spotify/YouTube/etc) extractor Name of the extractor used (CSV/Spotify/YouTube/etc)
``` ```
## Skip existing ## Skip existing
sldl can skip files that exist in the download directory or a specified directory configured with sldl can skip downloads that exist in the output directory or a specified directory configured
--music-dir. with --music-dir.
The following modes are available for --skip-mode: The following modes are available for --skip-mode:
### m3u ### m3u
Default when checking in the output directory. Default when checking in the output directory.
Checks whether the output m3u file contains the track in the '#SLDL' line. Does not check if Checks whether the output m3u file contains the track in the '#SLDL' line. Does not check if
the audio file exists or satisfies the file conditions (use m3u-cond for that). the audio file exists or satisfies the file conditions (use m3u-cond for that). m3u and
m3u-cond are the only modes that can skip album downloads.
### name ### name
Default when checking in the music directory.
Compares filenames to the track title and artist name to determine if a track already exists. Compares filenames to the track title and artist name to determine if a track already exists.
Specifically, a track will be skipped if there exists a file whose name contains the title Specifically, a track will be skipped if there exists a file whose name contains the title
and whose full path contains the artist name. and whose full path contains the artist name.
@ -429,11 +432,11 @@ Default when checking in the output directory.
(ignoring case and ws). Slower than name mode as it needs to read all file tags. (ignoring case and ws). Slower than name mode as it needs to read all file tags.
### m3u-cond, name-cond, tag-cond ### m3u-cond, name-cond, tag-cond
Default for checking in --music-dir: name-cond. Same as the above modes but also checks whether the found file satisfies the configured
Same as the above modes but also checks whether the found file satisfies necessary conditions. conditions. Uses necessary conditions by default, run with --skip-existing-pref-cond to use
Equivalent to the above modes if no necessary conditions have been specified (except m3u-cond preferred conditions instead. Equivalent to the above modes if no necessary conditions have
which always checks if the file exists). May be slower and use a lot of memory for large been specified (except m3u-cond, which always checks if the file exists).
libraries. May be slower and use a lot of memory for large libraries.
## Configuration ## Configuration
### Config Location: ### Config Location:

View file

@ -74,13 +74,14 @@ static class Config
public static bool noModifyShareCount = false; public static bool noModifyShareCount = false;
public static bool useRandomLogin = false; public static bool useRandomLogin = false;
public static bool noBrowseFolder = false; public static bool noBrowseFolder = false;
public static bool skipExistingPrefCond = false;
public static int downrankOn = -1; public static int downrankOn = -1;
public static int ignoreOn = -2; public static int ignoreOn = -2;
public static int minAlbumTrackCount = -1; public static int minAlbumTrackCount = -1;
public static int maxAlbumTrackCount = -1; public static int maxAlbumTrackCount = -1;
public static int fastSearchDelay = 300; public static int fastSearchDelay = 300;
public static int maxTracks = int.MaxValue;
public static int minUsersAggregate = 2; public static int minUsersAggregate = 2;
public static int maxTracks = int.MaxValue;
public static int offset = 0; public static int offset = 0;
public static int maxStaleTime = 50000; public static int maxStaleTime = 50000;
public static int updateDelay = 100; public static int updateDelay = 100;
@ -98,8 +99,8 @@ static class Config
public static M3uOption m3uOption = M3uOption.Index; public static M3uOption m3uOption = M3uOption.Index;
public static DisplayMode displayMode = DisplayMode.Single; public static DisplayMode displayMode = DisplayMode.Single;
public static InputType inputType = InputType.None; public static InputType inputType = InputType.None;
public static SkipMode skipMode = SkipMode.M3uCond; public static SkipMode skipMode = SkipMode.M3u;
public static SkipMode skipModeMusicDir = SkipMode.NameCond; public static SkipMode skipModeMusicDir = SkipMode.Name;
public static PrintOption printOption = PrintOption.None; public static PrintOption printOption = PrintOption.None;
static readonly Dictionary<string, (List<string> args, string? cond)> profiles = new(); static readonly Dictionary<string, (List<string> args, string? cond)> profiles = new();
@ -226,7 +227,7 @@ static class Config
m3uOption = M3uOption.None; m3uOption = M3uOption.None;
else if (!hasConfiguredM3uMode && inputType == InputType.String) else if (!hasConfiguredM3uMode && inputType == InputType.String)
m3uOption = M3uOption.None; m3uOption = M3uOption.None;
else if (!hasConfiguredM3uMode && !aggregate && Program.trackLists != null && Program.trackLists.Flattened(true, false, true).All(t => t.IsAlbum)) else if (!hasConfiguredM3uMode && Program.trackLists != null && !aggregate &&!Program.trackLists.Flattened(true, false, true).Skip(1).Any())
m3uOption = M3uOption.None; m3uOption = M3uOption.None;
parentFolder = Utils.ExpandUser(parentFolder); parentFolder = Utils.ExpandUser(parentFolder);
@ -238,7 +239,7 @@ static class Config
if (folderName == ".") if (folderName == ".")
folderName = ""; folderName = "";
folderName = folderName.Replace("\\", "/"); folderName = folderName.Replace('\\', '/');
folderName = string.Join('/', folderName.Split('/').Select(x => x.ReplaceInvalidChars(invalidReplaceStr).Trim())); folderName = string.Join('/', folderName.Split('/').Select(x => x.ReplaceInvalidChars(invalidReplaceStr).Trim()));
folderName = folderName.Replace('/', Path.DirectorySeparatorChar); folderName = folderName.Replace('/', Path.DirectorySeparatorChar);
@ -370,7 +371,7 @@ static class Config
return var switch return var switch
{ {
"input-type" => inputType.ToString().ToLower(), "input-type" => inputType.ToString().ToLower(),
"download-mode" => tle != null ? toKebab(tle.type.ToString()) "download-mode" => tle != null ? toKebab(tle.source.Type.ToString())
: album && aggregate ? "album-aggregate" : album ? "album" : aggregate ? "aggregate" : "normal", : album && aggregate ? "album-aggregate" : album ? "album" : aggregate ? "aggregate" : "normal",
"interactive" => interactiveMode, "interactive" => interactiveMode,
"album" => album, "album" => album,
@ -1148,6 +1149,10 @@ static class Config
case "--no-browse-folder": case "--no-browse-folder":
setFlag(ref noBrowseFolder, ref i); setFlag(ref noBrowseFolder, ref i);
break; break;
case "--sepc":
case "--skip-existing-pref-cond":
setFlag(ref skipExistingPrefCond, ref i);
break;
default: default:
throw new ArgumentException($"Unknown argument: {args[i]}"); throw new ArgumentException($"Unknown argument: {args[i]}");
} }

View file

@ -12,17 +12,20 @@ namespace Data
public string URI = ""; public string URI = "";
public int Length = -1; public int Length = -1;
public bool ArtistMaybeWrong = false; public bool ArtistMaybeWrong = false;
public bool IsAlbum = false;
public int MinAlbumTrackCount = -1; public int MinAlbumTrackCount = -1;
public int MaxAlbumTrackCount = -1; public int MaxAlbumTrackCount = -1;
public bool IsNotAudio = false; public bool IsNotAudio = false;
public string DownloadPath = ""; public string DownloadPath = "";
public string Other = ""; public string Other = "";
public int CsvRow = -1; public int CsvRow = -1;
public TrackType Type = TrackType.Normal;
public FailureReason FailureReason = FailureReason.None; public FailureReason FailureReason = FailureReason.None;
public TrackState State = TrackState.Initial; public TrackState State = TrackState.Initial;
public SlDictionary? Downloads = null; public SlDictionary? Downloads = null;
public bool OutputsDirectory => Type != TrackType.Normal;
public Soulseek.File? FirstDownload => Downloads?.FirstOrDefault().Value.Item2;
public Track() { } public Track() { }
public Track(Track other) public Track(Track other)
@ -34,7 +37,7 @@ namespace Data
URI = other.URI; URI = other.URI;
ArtistMaybeWrong = other.ArtistMaybeWrong; ArtistMaybeWrong = other.ArtistMaybeWrong;
Downloads = other.Downloads; Downloads = other.Downloads;
IsAlbum = other.IsAlbum; Type = other.Type;
IsNotAudio = other.IsNotAudio; IsNotAudio = other.IsNotAudio;
State = other.State; State = other.State;
FailureReason = other.FailureReason; FailureReason = other.FailureReason;
@ -47,7 +50,7 @@ namespace Data
public string ToKey() public string ToKey()
{ {
return $"{Artist};{Album};{Title};{Length}"; return $"{Artist};{Album};{Title};{Length};{(int)Type}";
} }
public override string ToString() public override string ToString()
@ -61,7 +64,7 @@ namespace Data
return $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}"; return $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}";
string str = Artist; string str = Artist;
if (!IsAlbum && Title.Length == 0 && Downloads != null && !Downloads.IsEmpty) if (Type == TrackType.Normal && Title.Length == 0 && Downloads != null && !Downloads.IsEmpty)
{ {
str = $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}"; str = $"{Utils.GetFileNameSlsk(Downloads.First().Value.Item2.Filename)}";
} }
@ -69,7 +72,7 @@ namespace Data
{ {
if (str.Length > 0) if (str.Length > 0)
str += " - "; str += " - ";
if (IsAlbum) if (Type == TrackType.Album)
str += Album; str += Album;
else if (Title.Length > 0) else if (Title.Length > 0)
str += Title; str += Title;
@ -77,7 +80,7 @@ namespace Data
{ {
if (Length > 0) if (Length > 0)
str += $" ({Length}s)"; str += $" ({Length}s)";
if (IsAlbum) if (Type == TrackType.Album)
str += " (album)"; str += " (album)";
} }
} }
@ -93,28 +96,56 @@ namespace Data
public class TrackListEntry public class TrackListEntry
{ {
public List<List<Track>> list; public List<List<Track>> list;
public ListType type;
public Track source; public Track source;
public bool needSearch; public bool needSourceSearch = false;
public bool placeInSubdir; public bool sourceCanBeSkipped = false;
public bool needSkipExistingAfterSearch = false;
public bool gotoNextAfterSearch = false;
public bool placeInSubdir = false;
public string? subdirOverride;
public TrackListEntry(List<List<Track>> list, ListType type, Track source) public TrackListEntry()
{ {
this.list = list; list = new List<List<Track>>();
this.type = type; source = new Track();
this.source = source;
needSearch = type != ListType.Normal;
placeInSubdir = false;
} }
public TrackListEntry(List<List<Track>> list, ListType type, Track source, bool needSearch, bool placeInSubdir) public TrackListEntry(Track source)
{
list = new List<List<Track>>();
this.source = source;
needSourceSearch = source.Type != TrackType.Normal;
needSkipExistingAfterSearch = source.Type == TrackType.Aggregate;
gotoNextAfterSearch = source.Type == TrackType.AlbumAggregate;
sourceCanBeSkipped = source.Type != TrackType.Normal
&& source.Type != TrackType.Aggregate
&& source.Type != TrackType.AlbumAggregate;
}
public TrackListEntry(List<List<Track>> list, Track source)
{ {
this.list = list; this.list = list;
this.type = type;
this.source = source; this.source = source;
this.needSearch = needSearch;
needSourceSearch = source.Type != TrackType.Normal;
needSkipExistingAfterSearch = source.Type == TrackType.Aggregate;
gotoNextAfterSearch = source.Type == TrackType.AlbumAggregate;
sourceCanBeSkipped = source.Type != TrackType.Normal
&& source.Type != TrackType.Aggregate
&& source.Type != TrackType.AlbumAggregate;
}
public TrackListEntry(List<List<Track>> list, Track source, bool needSearch, bool placeInSubdir,
bool canBeSkipped, bool needSkipExistingAfterSearch, bool gotoNextAfterSearch)
{
this.list = list;
this.source = source;
this.needSourceSearch = needSearch;
this.placeInSubdir = placeInSubdir; this.placeInSubdir = placeInSubdir;
this.sourceCanBeSkipped = canBeSkipped;
this.needSkipExistingAfterSearch = needSkipExistingAfterSearch;
this.gotoNextAfterSearch = gotoNextAfterSearch;
} }
} }
@ -124,21 +155,7 @@ namespace Data
public TrackLists() { } public TrackLists() { }
public TrackLists(List<(List<List<Track>> list, ListType type, Track source)> lists) public static TrackLists FromFlattened(IEnumerable<Track> flatList)
{
foreach (var (list, type, source) in lists)
{
var newList = new List<List<Track>>();
foreach (var innerList in list)
{
var innerNewList = new List<Track>(innerList);
newList.Add(innerNewList);
}
this.lists.Add(new TrackListEntry(newList, type, source));
}
}
public static TrackLists FromFlattened(IEnumerable<Track> flatList, bool aggregate, bool album)
{ {
var res = new TrackLists(); var res = new TrackLists();
using var enumerator = flatList.GetEnumerator(); using var enumerator = flatList.GetEnumerator();
@ -147,35 +164,26 @@ namespace Data
{ {
var track = enumerator.Current; var track = enumerator.Current;
if (album && aggregate) if (track.Type != TrackType.Normal)
{ {
res.AddEntry(ListType.AlbumAggregate, track); res.AddEntry(new TrackListEntry(track));
}
else if (aggregate)
{
res.AddEntry(ListType.Aggregate, track);
}
else if (album || track.IsAlbum)
{
track.IsAlbum = true;
res.AddEntry(ListType.Album, track);
} }
else else
{ {
res.AddEntry(ListType.Normal); res.AddEntry(new TrackListEntry());
res.AddTrackToLast(track); res.AddTrackToLast(track);
bool hasNext; bool hasNext;
while (true) while (true)
{ {
hasNext = enumerator.MoveNext(); hasNext = enumerator.MoveNext();
if (!hasNext || enumerator.Current.IsAlbum) if (!hasNext || enumerator.Current.Type != TrackType.Normal)
break; break;
res.AddTrackToLast(enumerator.Current); res.AddTrackToLast(enumerator.Current);
} }
if (hasNext && enumerator.Current.IsAlbum) if (hasNext && enumerator.Current.Type != TrackType.Normal)
res.AddEntry(ListType.Album, track); res.AddEntry(new TrackListEntry(track));
else if (!hasNext) else if (!hasNext)
break; break;
} }
@ -195,35 +203,22 @@ namespace Data
lists.Add(tle); lists.Add(tle);
} }
public void AddEntry(List<List<Track>>? list, ListType? type = null, Track? source = null)
{
type ??= ListType.Normal;
source ??= new Track();
list ??= new List<List<Track>>();
lists.Add(new TrackListEntry(list, (ListType)type, source));
}
public void AddEntry(List<Track> tracks, ListType? type = null, Track? source = null)
{
var list = new List<List<Track>>() { tracks };
AddEntry(list, type, source);
}
public void AddEntry(Track track, ListType? type = null, Track? source = null)
{
var list = new List<List<Track>>() { new List<Track>() { track } };
AddEntry(list, type, source);
}
public void AddEntry(ListType? type = null, Track? source = null)
{
var list = new List<List<Track>>() { new List<Track>() };
AddEntry(list, type, source);
}
public void AddTrackToLast(Track track) public void AddTrackToLast(Track track)
{ {
if (lists.Count == 0)
{
AddEntry(new TrackListEntry(new List<List<Track>> { new List<Track>() { track } }, new Track()));
return;
}
int i = lists.Count - 1; int i = lists.Count - 1;
if (lists[i].list.Count == 0)
{
lists[i].list.Add(new List<Track>() { track });
return;
}
int j = lists[i].list.Count - 1; int j = lists[i].list.Count - 1;
lists[i].list[j].Add(track); lists[i].list[j].Add(track);
} }
@ -240,13 +235,57 @@ namespace Data
} }
} }
public void UpgradeListTypes(bool aggregate, bool album)
{
if (!aggregate && !album)
return;
var newLists = new List<TrackListEntry>();
for (int i = 0; i < lists.Count; i++)
{
var tle = lists[i];
if (tle.source.Type == TrackType.Album && aggregate)
{
tle.source.Type = TrackType.AlbumAggregate;
newLists.Add(tle);
}
else if (tle.source.Type == TrackType.Aggregate && album)
{
tle.source.Type = TrackType.AlbumAggregate;
newLists.Add(tle);
}
else if (tle.source.Type == TrackType.Normal && (album || aggregate))
{
foreach (var track in tle.list[0])
{
if (album && aggregate)
track.Type = TrackType.AlbumAggregate;
else if (album)
track.Type = TrackType.Album;
else if (aggregate)
track.Type = TrackType.Aggregate;
newLists.Add(new TrackListEntry(track));
}
}
else
{
newLists.Add(tle);
}
}
lists = newLists;
}
public IEnumerable<Track> Flattened(bool addSources, bool addSpecialSourceTracks, bool sourcesOnly = false) public IEnumerable<Track> Flattened(bool addSources, bool addSpecialSourceTracks, bool sourcesOnly = false)
{ {
foreach (var tle in lists) foreach (var tle in lists)
{ {
if ((addSources || sourcesOnly) && tle.source != null) if ((addSources || sourcesOnly) && tle.source != null)
yield return tle.source; yield return tle.source;
if (!sourcesOnly && tle.list.Count > 0 && (tle.type == ListType.Normal || addSpecialSourceTracks)) if (!sourcesOnly && tle.list.Count > 0 && (tle.source.Type == TrackType.Normal || addSpecialSourceTracks))
{ {
foreach (var t in tle.list[0]) foreach (var t in tle.list[0])
yield return t; yield return t;

View file

@ -40,12 +40,12 @@ namespace Enums
None, None,
} }
public enum ListType public enum TrackType
{ {
Normal, Normal = 0,
Album, Album = 1,
Aggregate, Aggregate = 2,
AlbumAggregate, AlbumAggregate = 3,
} }
public enum M3uOption public enum M3uOption

View file

@ -5,9 +5,9 @@ using System.IO;
namespace ExistingCheckers namespace ExistingCheckers
{ {
public static class Registry public static class ExistingCheckerRegistry
{ {
static IExistingChecker GetChecker(SkipMode mode, string dir, FileConditions conditions, M3uEditor m3uEditor) public static ExistingChecker GetChecker(SkipMode mode, string dir, FileConditions conditions, M3uEditor m3uEditor)
{ {
bool noConditions = conditions.Equals(new FileConditions()); bool noConditions = conditions.Equals(new FileConditions());
return mode switch return mode switch
@ -20,38 +20,16 @@ namespace ExistingCheckers
SkipMode.M3uCond => noConditions ? new M3uExistingChecker(m3uEditor, true) : new M3uConditionExistingChecker(m3uEditor, conditions), SkipMode.M3uCond => noConditions ? new M3uExistingChecker(m3uEditor, true) : new M3uConditionExistingChecker(m3uEditor, conditions),
}; };
} }
}
public static Dictionary<Track, string> SkipExisting(List<Track> tracks, string dir, FileConditions necessaryCond, M3uEditor m3uEditor, SkipMode mode) public abstract class ExistingChecker
{ {
var existing = new Dictionary<Track, string>(); public abstract bool TrackExists(Track track, out string? foundPath);
public virtual void BuildIndex() { IndexIsBuilt = true; }
var checker = GetChecker(mode, dir, necessaryCond, m3uEditor); public bool IndexIsBuilt { get; protected set; } = false;
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 class NameExistingChecker : ExistingChecker
}
}
public interface IExistingChecker
{
public bool TrackExists(Track track, out string? foundPath);
public void BuildIndex() { }
}
public class NameExistingChecker : IExistingChecker
{ {
readonly string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" }; readonly string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" };
readonly string dir; readonly string dir;
@ -71,7 +49,7 @@ namespace ExistingCheckers
return s; return s;
} }
public void BuildIndex() public override void BuildIndex()
{ {
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories); var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
@ -86,16 +64,24 @@ namespace ExistingCheckers
index.Add((path, ppath, pname)); index.Add((path, ppath, pname));
} }
} }
IndexIsBuilt = true;
} }
public bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, out string? foundPath)
{ {
foundPath = null;
if (track.OutputsDirectory)
return false;
string title = Preprocess(track.Title, true); string title = Preprocess(track.Title, true);
string artist = Preprocess(track.Artist, true); string artist = Preprocess(track.Artist, true);
foreach ((var path, var ppath, var pname) in index) foreach ((var path, var ppath, var pname) in index)
{ {
if (pname.Contains(title) && ppath.Contains(artist)) if (pname.ContainsWithBoundaryIgnoreWs(title, acceptLeftDigit: true)
&& ppath.ContainsWithBoundaryIgnoreWs(artist, acceptLeftDigit: true))
{ {
foundPath = path; foundPath = path;
return true; return true;
@ -107,7 +93,7 @@ namespace ExistingCheckers
} }
} }
public class NameConditionExistingChecker : IExistingChecker public class NameConditionExistingChecker : ExistingChecker
{ {
readonly string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" }; readonly string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" };
readonly string dir; readonly string dir;
@ -129,7 +115,7 @@ namespace ExistingCheckers
return s; return s;
} }
public void BuildIndex() public override void BuildIndex()
{ {
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories); var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
@ -148,16 +134,25 @@ namespace ExistingCheckers
index.Add((ppath, pname, new SimpleFile(musicFile))); index.Add((ppath, pname, new SimpleFile(musicFile)));
} }
} }
IndexIsBuilt = true;
} }
public bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, out string? foundPath)
{ {
foundPath = null;
if (track.OutputsDirectory)
return false;
string title = Preprocess(track.Title, true); string title = Preprocess(track.Title, true);
string artist = Preprocess(track.Artist, true); string artist = Preprocess(track.Artist, true);
foreach ((var ppath, var pname, var musicFile) in index) foreach ((var ppath, var pname, var musicFile) in index)
{ {
if (pname.Contains(title) && ppath.Contains(artist) && conditions.FileSatisfies(musicFile, track)) if (pname.ContainsWithBoundaryIgnoreWs(title, acceptLeftDigit: true)
&& ppath.ContainsWithBoundaryIgnoreWs(artist, acceptLeftDigit: true)
&& conditions.FileSatisfies(musicFile, track))
{ {
foundPath = musicFile.Path; foundPath = musicFile.Path;
return true; return true;
@ -169,7 +164,7 @@ namespace ExistingCheckers
} }
} }
public class TagExistingChecker : IExistingChecker public class TagExistingChecker : ExistingChecker
{ {
readonly string dir; readonly string dir;
readonly List<(string, string, string)> index = new(); // (Path, PreprocessedArtist, PreprocessedTitle) readonly List<(string, string, string)> index = new(); // (Path, PreprocessedArtist, PreprocessedTitle)
@ -184,7 +179,7 @@ namespace ExistingCheckers
return s.Replace(" ", "").RemoveFt().ToLower(); return s.Replace(" ", "").RemoveFt().ToLower();
} }
public void BuildIndex() public override void BuildIndex()
{ {
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories); var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
@ -201,10 +196,17 @@ namespace ExistingCheckers
index.Add((path, partist, ptitle)); index.Add((path, partist, ptitle));
} }
} }
IndexIsBuilt = true;
} }
public bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, out string? foundPath)
{ {
foundPath = null;
if (track.OutputsDirectory)
return false;
string title = Preprocess(track.Title); string title = Preprocess(track.Title);
string artist = Preprocess(track.Artist); string artist = Preprocess(track.Artist);
@ -217,12 +219,11 @@ namespace ExistingCheckers
} }
} }
foundPath = null;
return false; return false;
} }
} }
public class TagConditionExistingChecker : IExistingChecker public class TagConditionExistingChecker : ExistingChecker
{ {
readonly string dir; readonly string dir;
readonly List<(string, string, SimpleFile)> index = new(); // (PreprocessedArtist, PreprocessedTitle, file) readonly List<(string, string, SimpleFile)> index = new(); // (PreprocessedArtist, PreprocessedTitle, file)
@ -239,7 +240,7 @@ namespace ExistingCheckers
return s.Replace(" ", "").RemoveFt().ToLower(); return s.Replace(" ", "").RemoveFt().ToLower();
} }
public void BuildIndex() public override void BuildIndex()
{ {
var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories); var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
@ -256,10 +257,17 @@ namespace ExistingCheckers
index.Add((partist, ptitle, new SimpleFile(musicFile))); index.Add((partist, ptitle, new SimpleFile(musicFile)));
} }
} }
IndexIsBuilt = true;
} }
public bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, out string? foundPath)
{ {
foundPath = null;
if (track.OutputsDirectory)
return false;
string title = Preprocess(track.Title); string title = Preprocess(track.Title);
string artist = Preprocess(track.Artist); string artist = Preprocess(track.Artist);
@ -272,12 +280,11 @@ namespace ExistingCheckers
} }
} }
foundPath = null;
return false; return false;
} }
} }
public class M3uExistingChecker : IExistingChecker public class M3uExistingChecker : ExistingChecker
{ {
M3uEditor m3uEditor; M3uEditor m3uEditor;
bool checkFileExists; bool checkFileExists;
@ -288,16 +295,29 @@ namespace ExistingCheckers
this.checkFileExists = checkFileExists; this.checkFileExists = checkFileExists;
} }
public bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, out string? foundPath)
{ {
foundPath = null; foundPath = null;
var t = m3uEditor.PreviousRunResult(track); var t = m3uEditor.PreviousRunResult(track);
if (t != null && (t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists)) if (t != null && (t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists))
{ {
if (checkFileExists && (t.DownloadPath.Length == 0 || !File.Exists(t.DownloadPath))) if (checkFileExists)
{ {
if (t.DownloadPath.Length == 0)
return false;
if (t.OutputsDirectory)
{
if (!Directory.Exists(t.DownloadPath))
return false; return false;
} }
else
{
if (!File.Exists(t.DownloadPath))
return false;
}
}
foundPath = t.DownloadPath; foundPath = t.DownloadPath;
return true; return true;
} }
@ -305,7 +325,7 @@ namespace ExistingCheckers
} }
} }
public class M3uConditionExistingChecker : IExistingChecker public class M3uConditionExistingChecker : ExistingChecker
{ {
M3uEditor m3uEditor; M3uEditor m3uEditor;
FileConditions conditions; FileConditions conditions;
@ -316,14 +336,19 @@ namespace ExistingCheckers
this.conditions = conditions; this.conditions = conditions;
} }
public bool TrackExists(Track track, out string? foundPath) public override bool TrackExists(Track track, out string? foundPath)
{ {
foundPath = null; foundPath = null;
var t = m3uEditor.PreviousRunResult(track); var t = m3uEditor.PreviousRunResult(track);
if (t != null && (t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists) && t.DownloadPath.Length > 0)
{ if (t == null || t.DownloadPath.Length == 0)
if (File.Exists(t.DownloadPath)) return false;
if (!t.OutputsDirectory)
{ {
if (!File.Exists(t.DownloadPath))
return false;
TagLib.File musicFile; TagLib.File musicFile;
try try
{ {
@ -342,10 +367,40 @@ namespace ExistingCheckers
{ {
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; return false;
} }
} }
foundPath = t.DownloadPath;
return true;
}
}
}
} }

View file

@ -16,7 +16,7 @@ namespace Extractors
return input.IsInternetUrl() && input.Contains("bandcamp.com"); return input.IsInternetUrl() && input.Contains("bandcamp.com");
} }
public async Task<TrackLists> GetTracks() public async Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse)
{ {
var trackLists = new TrackLists(); var trackLists = new TrackLists();
bool isTrack = Config.input.Contains("/track/"); bool isTrack = Config.input.Contains("/track/");
@ -54,9 +54,15 @@ namespace Extractors
{ {
Album = item.GetProperty("title").GetString(), Album = item.GetProperty("title").GetString(),
Artist = item.GetProperty("artist_name").GetString() ?? item.GetProperty("band_name").GetString(), Artist = item.GetProperty("artist_name").GetString() ?? item.GetProperty("band_name").GetString(),
IsAlbum = true, Type = TrackType.Album,
}; };
trackLists.AddEntry(ListType.Album, t); var tle = new TrackListEntry()
{
source = t,
placeInSubdir = true,
subdirOverride = t.ToString(true)
};
trackLists.AddEntry(tle);
} }
} }
else else
@ -70,8 +76,8 @@ namespace Extractors
if (isAlbum) if (isAlbum)
{ {
var artist = nameSection.SelectSingleNode(".//h3/span/a").InnerText.UnHtmlString().Trim(); var artist = nameSection.SelectSingleNode(".//h3/span/a").InnerText.UnHtmlString().Trim();
var track = new Track() { Artist = artist, Album = name, IsAlbum = true }; var track = new Track() { Artist = artist, Album = name, Type = TrackType.Album };
trackLists.AddEntry(ListType.Album, track); trackLists.AddEntry(new TrackListEntry(track));
if (Config.setAlbumMinTrackCount || Config.setAlbumMaxTrackCount) if (Config.setAlbumMinTrackCount || Config.setAlbumMaxTrackCount)
{ {
@ -92,16 +98,20 @@ namespace Extractors
var album = nameSection.SelectSingleNode(".//h3[contains(@class, 'albumTitle')]/span/a").InnerText.UnHtmlString().Trim(); var album = nameSection.SelectSingleNode(".//h3[contains(@class, 'albumTitle')]/span/a").InnerText.UnHtmlString().Trim();
var artist = nameSection.SelectSingleNode(".//h3[contains(@class, 'albumTitle')]/span[last()]/a").InnerText.UnHtmlString().Trim(); var artist = nameSection.SelectSingleNode(".//h3[contains(@class, 'albumTitle')]/span[last()]/a").InnerText.UnHtmlString().Trim();
//var timeParts = doc.DocumentNode.SelectSingleNode("//span[@class='time_total']").InnerText.Trim().Split(':'); //var timeParts = doc.DocumentNode.SelectSingleNode("//span[@class='time_total']").InnerText.Trim().Split(':');
var track = new Track() { Artist = artist, Title = name, Album = album }; var track = new Track() { Artist = artist, Title = name, Album = album };
trackLists.AddEntry(track); trackLists.AddEntry(new());
trackLists.AddTrackToLast(track);
Config.defaultFolderName = "."; Config.defaultFolderName = ".";
} }
} }
if (!Config.reverse) if (reverse)
{ trackLists.Reverse();
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(Config.offset).Take(Config.maxTracks), Config.aggregate, Config.album);
} if (offset > 0 || maxTracks < int.MaxValue)
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(offset).Take(maxTracks));
return trackLists; return trackLists;
} }

View file

@ -1,4 +1,5 @@
using Data; using Data;
using Enums;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace Extractors namespace Extractors
@ -14,16 +15,30 @@ namespace Extractors
return !input.IsInternetUrl() && input.EndsWith(".csv"); return !input.IsInternetUrl() && input.EndsWith(".csv");
} }
public async Task<TrackLists> GetTracks() public async Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse)
{ {
int max = Config.reverse ? int.MaxValue : Config.maxTracks; int max = reverse ? int.MaxValue : maxTracks;
int off = Config.reverse ? 0 : Config.offset; int off = reverse ? 0 : offset;
if (!File.Exists(Config.input)) if (!File.Exists(Config.input))
throw new FileNotFoundException("CSV file not found"); throw new FileNotFoundException("CSV file not found");
var tracks = await ParseCsvIntoTrackInfo(Config.input, Config.artistCol, Config.trackCol, Config.lengthCol, Config.albumCol, Config.descCol, Config.ytIdCol, Config.trackCountCol, Config.timeUnit, Config.ytParse); var tracks = await ParseCsvIntoTrackInfo(Config.input, Config.artistCol, Config.trackCol, Config.lengthCol, Config.albumCol, Config.descCol, Config.ytIdCol, Config.trackCountCol, Config.timeUnit, Config.ytParse);
var trackLists = TrackLists.FromFlattened(tracks.Skip(off).Take(max), Config.aggregate, Config.album);
if (reverse)
tracks.Reverse();
var trackLists = TrackLists.FromFlattened(tracks.Skip(off).Take(max));
foreach (var tle in trackLists.lists)
{
if (tle.source.Type != TrackType.Normal)
{
tle.placeInSubdir = true;
tle.subdirOverride = tle.source.ToString(true);
}
}
Config.defaultFolderName = Path.GetFileNameWithoutExtension(Config.input); Config.defaultFolderName = Path.GetFileNameWithoutExtension(Config.input);
return trackLists; return trackLists;
@ -161,7 +176,8 @@ namespace Extractors
if (ytParse) if (ytParse)
track = await YouTube.ParseTrackInfo(track.Title, track.Artist, track.URI, track.Length, desc); track = await YouTube.ParseTrackInfo(track.Title, track.Artist, track.URI, track.Length, desc);
track.IsAlbum = track.Title.Length == 0 && track.Album.Length > 0; if (track.Title.Length == 0 && track.Album.Length > 0)
track.Type = Enums.TrackType.Album;
if (track.Title.Length > 0 || track.Artist.Length > 0 || track.Album.Length > 0) if (track.Title.Length > 0 || track.Artist.Length > 0 || track.Album.Length > 0)
tracks.Add(track); tracks.Add(track);

View file

@ -18,15 +18,15 @@ namespace Extractors
return input == "spotify-likes" || input.IsInternetUrl() && input.Contains("spotify.com"); return input == "spotify-likes" || input.IsInternetUrl() && input.Contains("spotify.com");
} }
public async Task<TrackLists> GetTracks() public async Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse)
{ {
var trackLists = new TrackLists(); var trackLists = new TrackLists();
int max = Config.reverse ? int.MaxValue : Config.maxTracks; int max = reverse ? int.MaxValue : maxTracks;
int off = Config.reverse ? 0 : Config.offset; int off = reverse ? 0 : offset;
string playlistName = ""; string playlistName = "";
bool needLogin = Config.input == "spotify-likes" || Config.removeTracksFromSource; bool needLogin = Config.input == "spotify-likes" || Config.removeTracksFromSource;
List<Track> tracks = new List<Track>(); var tle = new TrackListEntry();
static void readSpotifyCreds() static void readSpotifyCreds()
{ {
@ -48,19 +48,16 @@ namespace Extractors
if (Config.input == "spotify-likes") if (Config.input == "spotify-likes")
{ {
Console.WriteLine("Loading Spotify likes"); Console.WriteLine("Loading Spotify likes");
tracks = await spotifyClient.GetLikes(max, off); var tracks = await spotifyClient.GetLikes(max, off);
playlistName = "Spotify Likes"; playlistName = "Spotify Likes";
tle.list.Add(tracks);
trackLists.AddEntry(tracks);
if (Config.album || Config.aggregate)
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false), Config.aggregate, Config.album);
} }
else if (Config.input.Contains("/album/")) else if (Config.input.Contains("/album/"))
{ {
Console.WriteLine("Loading Spotify album"); Console.WriteLine("Loading Spotify album");
(var source, tracks) = await spotifyClient.GetAlbum(Config.input); (var source, var tracks) = await spotifyClient.GetAlbum(Config.input);
playlistName = source.ToString(noInfo: true); playlistName = source.ToString(noInfo: true);
trackLists.AddEntry(ListType.Album, source); tle.source = source;
if (Config.setAlbumMinTrackCount) if (Config.setAlbumMinTrackCount)
source.MinAlbumTrackCount = tracks.Count; source.MinAlbumTrackCount = tracks.Count;
@ -75,6 +72,8 @@ namespace Extractors
} }
else else
{ {
var tracks = new List<Track>();
try try
{ {
Console.WriteLine("Loading Spotify playlist"); Console.WriteLine("Loading Spotify playlist");
@ -105,13 +104,18 @@ namespace Extractors
} }
else throw; else throw;
} }
trackLists.AddEntry(tracks);
if (Config.album || Config.aggregate) tle.list.Add(tracks);
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false), Config.aggregate, Config.album);
} }
Config.defaultFolderName = playlistName.ReplaceInvalidChars(Config.invalidReplaceStr); Config.defaultFolderName = playlistName.ReplaceInvalidChars(Config.invalidReplaceStr);
if (reverse)
{
trackLists.Reverse();
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(offset).Take(maxTracks));
}
return trackLists; return trackLists;
} }
@ -328,7 +332,7 @@ namespace Extractors
tracks.Add(t); tracks.Add(t);
} }
return (new Track { Album = album.Name, Artist = album.Artists.First().Name, IsAlbum = true }, tracks); return (new Track { Album = album.Name, Artist = album.Artists.First().Name, Type = TrackType.Album }, tracks);
} }
private string GetAlbumIdFromUrl(string url) private string GetAlbumIdFromUrl(string url)

View file

@ -11,41 +11,23 @@ namespace Extractors
return !input.IsInternetUrl(); return !input.IsInternetUrl();
} }
public async Task<TrackLists> GetTracks() public async Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse)
{ {
var trackLists = new TrackLists(); var trackLists = new TrackLists();
var music = ParseTrackArg(Config.input, Config.album); var music = ParseTrackArg(Config.input, Config.album);
bool isAlbum = false;
if (Config.album && Config.aggregate) if (music.Title.Length == 0 && music.Album.Length > 0)
{ {
trackLists.AddEntry(ListType.AlbumAggregate, music); music.Type = TrackType.Album;
} trackLists.AddEntry(new TrackListEntry(music));
else if (Config.album)
{
music.IsAlbum = true;
trackLists.AddEntry(ListType.Album, music);
}
else if (!Config.aggregate && music.Title.Length > 0)
{
trackLists.AddEntry(music);
}
else if (Config.aggregate)
{
trackLists.AddEntry(ListType.Aggregate, music);
}
else if (music.Title.Length == 0 && music.Album.Length > 0)
{
isAlbum = true;
music.IsAlbum = true;
trackLists.AddEntry(ListType.Album, music);
} }
else else
{ {
throw new ArgumentException("Need track title or album"); trackLists.AddEntry(new TrackListEntry());
trackLists.AddTrackToLast(music);
} }
if (Config.aggregate || isAlbum || Config.album) if (Config.aggregate || Config.album || music.Type != TrackType.Normal)
Config.defaultFolderName = music.ToString(true).ReplaceInvalidChars(Config.invalidReplaceStr).Trim(); Config.defaultFolderName = music.ToString(true).ReplaceInvalidChars(Config.invalidReplaceStr).Trim();
else else
Config.defaultFolderName = "."; Config.defaultFolderName = ".";
@ -59,8 +41,6 @@ namespace Extractors
var track = new Track(); var track = new Track();
var keys = new string[] { "title", "artist", "length", "album", "artist-maybe-wrong" }; var keys = new string[] { "title", "artist", "length", "album", "artist-maybe-wrong" };
track.IsAlbum = isAlbum;
void setProperty(string key, string value) void setProperty(string key, string value)
{ {
switch (key) switch (key)

View file

@ -20,11 +20,11 @@ namespace Extractors
return input.IsInternetUrl() && (input.Contains("youtu.be") || input.Contains("youtube.com")); return input.IsInternetUrl() && (input.Contains("youtu.be") || input.Contains("youtube.com"));
} }
public async Task<TrackLists> GetTracks() public async Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse)
{ {
var trackLists = new TrackLists(); var trackLists = new TrackLists();
int max = Config.reverse ? int.MaxValue : Config.maxTracks; int max = reverse ? int.MaxValue : maxTracks;
int off = Config.reverse ? 0 : Config.offset; int off = reverse ? 0 : offset;
YouTube.apiKey = Config.ytKey; YouTube.apiKey = Config.ytKey;
string name; string name;
@ -60,12 +60,19 @@ namespace Extractors
} }
YouTube.StopService(); YouTube.StopService();
trackLists.AddEntry(tracks);
if (Config.album || Config.aggregate) var tle = new TrackListEntry();
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false), Config.aggregate, Config.album); tle.list.Add(tracks);
trackLists.AddEntry(tle);
Config.defaultFolderName = name.ReplaceInvalidChars(Config.invalidReplaceStr); Config.defaultFolderName = name.ReplaceInvalidChars(Config.invalidReplaceStr);
if (reverse)
{
trackLists.Reverse();
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(offset).Take(maxTracks));
}
return trackLists; return trackLists;
} }
} }

View file

@ -6,7 +6,7 @@ namespace Extractors
{ {
public interface IExtractor public interface IExtractor
{ {
Task<TrackLists> GetTracks(); Task<TrackLists> GetTracks(int maxTracks, int offset, bool reverse);
Task RemoveTrackFromSource(Track track) => Task.CompletedTask; Task RemoveTrackFromSource(Track track) => Task.CompletedTask;
} }

View file

@ -43,6 +43,7 @@ public static class Help
library. Use with --skip-existing library. Use with --skip-existing
--skip-not-found Skip searching for tracks that weren't found on Soulseek --skip-not-found Skip searching for tracks that weren't found on Soulseek
during the last run. Fails are read from the m3u file. during the last run. Fails are read from the m3u file.
--skip-existing-pref-cond Use preferred instead of necessary conds for skip-existing
--display-mode <option> Changes how searches and downloads are displayed: --display-mode <option> Changes how searches and downloads are displayed:
'single' (default): Show transfer state and percentage 'single' (default): Show transfer state and percentage
@ -375,23 +376,26 @@ public static class Help
track Track number track Track number
disc Disc number disc Disc number
filename Soulseek filename without extension filename Soulseek filename without extension
foldername Default sldl folder name foldername Soulseek folder name (only available for album downloads)
default-foldername Default sldl folder name
extractor Name of the extractor used (CSV/Spotify/YouTube/etc) extractor Name of the extractor used (CSV/Spotify/YouTube/etc)
"; ";
const string skipExistingHelp = @" const string skipExistingHelp = @"
Skip existing Skip existing
sldl can skip files that exist in the download directory or a specified directory configured with sldl can skip downloads that exist in the output directory or a specified directory configured
--music-dir. with --music-dir.
The following modes are available for --skip-mode: The following modes are available for --skip-mode:
m3u m3u
Default when checking in the output directory. Default when checking in the output directory.
Checks whether the output m3u file contains the track in the '#SLDL' line. Does not check if Checks whether the output m3u file contains the track in the '#SLDL' line. Does not check if
the audio file exists or satisfies the file conditions, use m3u-cond for that. the audio file exists or satisfies the file conditions (use m3u-cond for that). m3u and
m3u-cond are the only modes that can skip album downloads.
name name
Default when checking in the music directory.
Compares filenames to the track title and artist name to determine if a track already exists. Compares filenames to the track title and artist name to determine if a track already exists.
Specifically, a track will be skipped if there exists a file whose name contains the title Specifically, a track will be skipped if there exists a file whose name contains the title
and whose full path contains the artist name. and whose full path contains the artist name.
@ -402,11 +406,11 @@ public static class Help
(ignoring case and ws). Slower than name mode as it needs to read all file tags. (ignoring case and ws). Slower than name mode as it needs to read all file tags.
m3u-cond, name-cond, tag-cond m3u-cond, name-cond, tag-cond
Default for checking in --music-dir: name-cond. Same as the above modes but also checks whether the found file satisfies the configured
Same as the above modes but also checks whether the found file satisfies necessary conditions. conditions. Uses necessary conditions by default, run with --skip-existing-pref-cond to use
Equivalent to the above modes if no necessary conditions have been specified (except m3u-cond preferred conditions instead. Equivalent to the above modes if no necessary conditions have
which always checks if the file exists). May be slower and use a lot of memory for large been specified (except m3u-cond, which always checks if the file exists).
libraries. May be slower and use a lot of memory for large libraries.
"; ";
const string configHelp = @" const string configHelp = @"

View file

@ -6,13 +6,13 @@ using System.Text;
public class M3uEditor public class M3uEditor
{ {
List<string> lines; List<string> lines;
TrackLists trackLists;
string path;
string parent;
int offset = 0;
M3uOption option = M3uOption.Index;
bool needFirstUpdate = false; bool needFirstUpdate = false;
Dictionary<string, Track> previousRunTracks = new(); // {track.ToKey(), track } readonly TrackLists trackLists;
readonly string path;
readonly string parent;
readonly int offset = 0;
readonly M3uOption option = M3uOption.Index;
readonly Dictionary<string, Track> previousRunData = new(); // { track.ToKey(), track }
public M3uEditor(string m3uPath, TrackLists trackLists, M3uOption option, int offset = 0) public M3uEditor(string m3uPath, TrackLists trackLists, M3uOption option, int offset = 0)
{ {
@ -27,18 +27,25 @@ public class M3uEditor
LoadPreviousResults(); LoadPreviousResults();
} }
private void LoadPreviousResults() // #SLDL:path,artist,album,title,length(int),state(int),failurereason(int); ... ; ... private void LoadPreviousResults()
{ {
// Format:
// #SLDL:<trackinfo>;<trackinfo>; ...
// where <trackinfo> = filepath,artist,album,title,length(int),tracktype(int),state(int),failurereason(int)
if (lines.Count == 0 || !lines[0].StartsWith("#SLDL:")) if (lines.Count == 0 || !lines[0].StartsWith("#SLDL:"))
return; return;
string sldlLine = lines[0]["#SLDL:".Length..]; string sldlLine = lines[0];
lines = lines.Skip(1).ToList();
int k = "#SLDL:".Length;
var currentItem = new StringBuilder(); var currentItem = new StringBuilder();
bool inQuotes = false; bool inQuotes = false;
lines = lines.Skip(1).ToList(); for (; k < sldlLine.Length && sldlLine[k] == ' '; k++);
for (int k = 0; k < sldlLine.Length; k++) for (; k < sldlLine.Length; k++)
{ {
var track = new Track(); var track = new Track();
int field = 0; int field = 0;
@ -58,7 +65,7 @@ public class M3uEditor
inQuotes = !inQuotes; inQuotes = !inQuotes;
} }
} }
else if (field <= 5 && c == ',' && !inQuotes) else if (field <= 6 && c == ',' && !inQuotes)
{ {
var x = currentItem.ToString(); var x = currentItem.ToString();
@ -77,12 +84,14 @@ public class M3uEditor
else if (field == 4) else if (field == 4)
track.Length = int.Parse(x); track.Length = int.Parse(x);
else if (field == 5) else if (field == 5)
track.Type = (TrackType)int.Parse(currentItem.ToString());
else if (field == 6)
track.State = (TrackState)int.Parse(x); track.State = (TrackState)int.Parse(x);
currentItem.Clear(); currentItem.Clear();
field++; field++;
} }
else if (field == 6 && c == ';') else if (field == 7 && c == ';')
{ {
track.FailureReason = (FailureReason)int.Parse(currentItem.ToString()); track.FailureReason = (FailureReason)int.Parse(currentItem.ToString());
currentItem.Clear(); currentItem.Clear();
@ -94,7 +103,8 @@ public class M3uEditor
currentItem.Append(c); currentItem.Append(c);
} }
} }
previousRunTracks[track.ToKey()] = track;
previousRunData[track.ToKey()] = track;
} }
} }
@ -108,26 +118,56 @@ public class M3uEditor
bool needUpdate = false; bool needUpdate = false;
int index = 1 + offset; int index = 1 + offset;
void updateLine(string newLine) bool updateLine(string newLine)
{ {
bool changed = index >= lines.Count || newLine != lines[index];
while (index >= lines.Count) lines.Add(""); while (index >= lines.Count) lines.Add("");
lines[index] = newLine; lines[index] = newLine;
return changed;
}
bool trackChanged(Track track, Track? indexTrack)
{
return indexTrack == null
|| indexTrack.State != track.State
|| indexTrack.FailureReason != track.FailureReason
|| indexTrack.DownloadPath != track.DownloadPath;
}
void updateTrackIfNeeded(Track track)
{
var key = track.ToKey();
previousRunData.TryGetValue(key, out Track? indexTrack);
if (!needUpdate)
needUpdate = trackChanged(track, indexTrack);
if (needUpdate)
{
if (indexTrack == null)
previousRunData[key] = new Track(track);
else
{
indexTrack.State = track.State;
indexTrack.FailureReason = track.FailureReason;
indexTrack.DownloadPath = track.DownloadPath;
}
}
} }
foreach (var tle in trackLists.lists) foreach (var tle in trackLists.lists)
{ {
if (tle.type != ListType.Normal) if (tle.source.Type != TrackType.Normal)
{ {
continue; if (tle.source.State != TrackState.Initial)
{
updateTrackIfNeeded(tle.source);
} }
//if (option == M3uOption.All && source.State == TrackState.Failed) }
//{
// string reason = source.FailureReason.ToString();
// updateLine(TrackToLine(source, reason));
// index++;
//}
else
{
for (int k = 0; k < tle.list.Count; k++) for (int k = 0; k < tle.list.Count; k++)
{ {
for (int j = 0; j < tle.list[k].Count; j++) for (int j = 0; j < tle.list[k].Count; j++)
@ -137,34 +177,11 @@ public class M3uEditor
if (track.IsNotAudio || track.State == TrackState.Initial) if (track.IsNotAudio || track.State == TrackState.Initial)
continue; continue;
string trackKey = track.ToKey(); updateTrackIfNeeded(track);
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 (option == M3uOption.All)
{ {
if (track.State != TrackState.AlreadyExists || k == 0) needUpdate |= updateLine(TrackToLine(track));
{
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++; index++;
} }
} }
@ -194,8 +211,12 @@ public class M3uEditor
} }
} }
private void WriteSldlLine(StreamWriter writer) // #SLDL:path,artist,album,title,length(int),state(int),failurereason(int); ... ; ... private void WriteSldlLine(StreamWriter writer)
{ {
// Format:
// #SLDL:<trackinfo>;<trackinfo>; ...
// where <trackinfo> = filepath,artist,album,title,length(int),tracktype(int),state(int),failurereason(int)
void writeCsvLine(string[] items) void writeCsvLine(string[] items)
{ {
bool comma = false; bool comma = false;
@ -221,7 +242,7 @@ public class M3uEditor
writer.Write("#SLDL:"); writer.Write("#SLDL:");
foreach (var val in previousRunTracks.Values) foreach (var val in previousRunData.Values)
{ {
string p = val.DownloadPath; string p = val.DownloadPath;
if (p.StartsWith(parent)) if (p.StartsWith(parent))
@ -234,6 +255,7 @@ public class M3uEditor
val.Album, val.Album,
val.Title, val.Title,
val.Length.ToString(), val.Length.ToString(),
((int)val.Type).ToString(),
((int)val.State).ToString(), ((int)val.State).ToString(),
((int)val.FailureReason).ToString(), ((int)val.FailureReason).ToString(),
}; };
@ -245,10 +267,15 @@ public class M3uEditor
writer.Write('\n'); writer.Write('\n');
} }
private string TrackToLine(Track track, string? failureReason = null) private string TrackToLine(Track track)
{ {
string? failureReason = track.FailureReason != FailureReason.None ? track.FailureReason.ToString() : null;
if (failureReason == null && track.State == TrackState.NotFoundLastTime)
failureReason = nameof(FailureReason.NoSuitableFileFound);
if (failureReason != null) if (failureReason != null)
return $"# Failed: {track} [{failureReason}]"; return $"# Failed: {track} [{failureReason}]";
if (track.DownloadPath.Length > 0) if (track.DownloadPath.Length > 0)
{ {
if (track.DownloadPath.StartsWith(parent)) if (track.DownloadPath.StartsWith(parent))
@ -256,18 +283,19 @@ public class M3uEditor
else else
return track.DownloadPath; return track.DownloadPath;
} }
return $"# {track}"; return $"# {track}";
} }
public Track? PreviousRunResult(Track track) public Track? PreviousRunResult(Track track)
{ {
previousRunTracks.TryGetValue(track.ToKey(), out var t); previousRunData.TryGetValue(track.ToKey(), out var t);
return t; return t;
} }
public bool TryGetPreviousRunResult(Track track, out Track? result) public bool TryGetPreviousRunResult(Track track, out Track? result)
{ {
previousRunTracks.TryGetValue(track.ToKey(), out result); previousRunData.TryGetValue(track.ToKey(), out result);
return result != null; return result != null;
} }

View file

@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
using Data; using Data;
using Enums; using Enums;
using ExistingCheckers;
using Directory = System.IO.Directory; using Directory = System.IO.Directory;
using File = System.IO.File; using File = System.IO.File;
@ -20,6 +21,8 @@ using SlResponse = Soulseek.SearchResponse;
static partial class Program static partial class Program
{ {
public static Extractors.IExtractor? extractor; public static Extractors.IExtractor? extractor;
public static ExistingChecker? outputExistingChecker;
public static ExistingChecker? musicDirExistingChecker;
public static SoulseekClient? client; public static SoulseekClient? client;
public static TrackLists? trackLists; public static TrackLists? trackLists;
public static M3uEditor? m3uEditor; public static M3uEditor? m3uEditor;
@ -61,20 +64,18 @@ static partial class Program
WriteLine($"Using extractor: {Config.inputType}", debugOnly: true); WriteLine($"Using extractor: {Config.inputType}", debugOnly: true);
trackLists = await extractor.GetTracks(); trackLists = await extractor.GetTracks(Config.maxTracks, Config.offset, Config.reverse);
WriteLine("Got tracks", debugOnly: true); WriteLine("Got tracks", debugOnly: true);
Config.PostProcessArgs(); Config.PostProcessArgs();
if (Config.reverse) trackLists.UpgradeListTypes(Config.aggregate, Config.album);
{
trackLists.Reverse();
trackLists = TrackLists.FromFlattened(trackLists.Flattened(true, false).Skip(Config.offset).Take(Config.maxTracks), Config.aggregate, Config.album);
}
m3uEditor = new M3uEditor(Config.m3uFilePath, trackLists, Config.m3uOption, Config.offset); m3uEditor = new M3uEditor(Config.m3uFilePath, trackLists, Config.m3uOption, Config.offset);
InitExistingChecker();
await MainLoop(); await MainLoop();
WriteLine("Mainloop done", debugOnly: true); WriteLine("Mainloop done", debugOnly: true);
} }
@ -106,6 +107,26 @@ static partial class Program
} }
static void InitExistingChecker()
{
if (Config.skipExisting)
{
var cond = Config.skipExistingPrefCond ? Config.preferredCond : Config.necessaryCond;
if (Config.musicDir.Length == 0 || !Config.outputFolder.StartsWith(Config.musicDir, StringComparison.OrdinalIgnoreCase))
outputExistingChecker = ExistingCheckerRegistry.GetChecker(Config.skipMode, Config.outputFolder, cond, m3uEditor);
if (Config.musicDir.Length > 0)
{
if (!Directory.Exists(Config.musicDir))
Console.WriteLine("Error: Music directory does not exist");
else
musicDirExistingChecker = ExistingCheckerRegistry.GetChecker(Config.skipModeMusicDir, Config.outputFolder, cond, m3uEditor);
}
}
}
static void PreprocessTracks(TrackListEntry tle) static void PreprocessTracks(TrackListEntry tle)
{ {
for (int k = 0; k < tle.list.Count; k++) for (int k = 0; k < tle.list.Count; k++)
@ -154,6 +175,8 @@ static partial class Program
{ {
for (int i = 0; i < trackLists.lists.Count; i++) for (int i = 0; i < trackLists.lists.Count; i++)
{ {
if (i > 0) Console.WriteLine();
var tle = trackLists[i]; var tle = trackLists[i];
Config.UpdateArgs(tle); Config.UpdateArgs(tle);
@ -162,77 +185,92 @@ static partial class Program
var existing = new List<Track>(); var existing = new List<Track>();
var notFound = new List<Track>(); var notFound = new List<Track>();
var responseData = new ResponseData();
if (Config.skipNotFound) if (Config.skipNotFound && !Config.PrintResults)
{ {
if (SetNotFoundLastTime(tle.source)) if (tle.sourceCanBeSkipped && SetNotFoundLastTime(tle.source))
notFound.Add(tle.source); notFound.Add(tle.source);
if (tle.source.State != TrackState.NotFoundLastTime && !tle.needSourceSearch)
{
foreach (var tracks in tle.list) foreach (var tracks in tle.list)
notFound.AddRange(DoSkipNotFound(tracks)); notFound.AddRange(DoSkipNotFound(tracks));
} }
}
if (Config.skipExisting && tle.list != null && tle.type != ListType.Aggregate) if (Config.skipExisting && !Config.PrintResults && tle.source.State != TrackState.NotFoundLastTime)
{ {
for (int j = 0; j < tle.list.Count; j++) if (tle.sourceCanBeSkipped && SetExisting(tle.source))
existing.AddRange(DoSkipExisting(tle.list[0], print: i == 0 && j == 0)); existing.Add(tle.source);
if (tle.source.State != TrackState.AlreadyExists && !tle.needSourceSearch)
{
foreach (var tracks in tle.list)
existing.AddRange(DoSkipExisting(tracks));
}
} }
if (Config.PrintTracks) if (Config.PrintTracks)
{ {
if (tle.type == ListType.Normal) if (tle.source.Type == TrackType.Normal)
{ {
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.type); PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type);
} }
else else
{ {
var tl = new List<Track>(); var tl = new List<Track>();
if (tle.source.State == TrackState.Initial) tl.Add(tle.source); if (tle.source.State == TrackState.Initial) tl.Add(tle.source);
PrintTracksTbd(tl, existing, notFound, tle.type); PrintTracksTbd(tl, existing, notFound, tle.source.Type);
} }
continue; continue;
} }
if (tle.needSearch) if (tle.sourceCanBeSkipped)
{ {
Console.WriteLine($"{tle.type} download: {tle.source.ToString(true)}, searching.."); if (tle.source.State == TrackState.AlreadyExists)
var responseData = new ResponseData();
if (tle.type == ListType.Album)
{ {
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(); await InitClientAndUpdateIfNeeded();
if (tle.source.Type == TrackType.Album)
{
tle.list = await GetAlbumDownloads(tle.source, responseData); 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); 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.source.Type == TrackType.AlbumAggregate)
else if (tle.type == ListType.AlbumAggregate)
{ {
await InitClientAndUpdateIfNeeded();
var res = await GetAggregateAlbums(tle.source, responseData); var res = await GetAggregateAlbums(tle.source, responseData);
foreach (var item in res) foreach (var item in res)
trackLists.AddEntry(new TrackListEntry(item, ListType.Album, tle.source, false, true)); trackLists.AddEntry(new TrackListEntry(item, tle.source, false, true, true, false, false));
continue;
} }
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)" : ""; foreach (var tracks in tle.list)
Console.WriteLine($"No results.{lockedFilesStr}"); notFound.AddRange(DoSkipExisting(tracks));
tle.source.State = TrackState.Failed; }
tle.source.FailureReason = FailureReason.NoSuitableFileFound;
if (i < trackLists.lists.Count - 1) Console.WriteLine(); if (tle.gotoNextAfterSearch)
{
continue; continue;
} }
} }
@ -243,38 +281,44 @@ static partial class Program
continue; continue;
} }
if (tle.needSourceSearch && (tle.list.Count == 0 || !tle.list.Any(x => x.Count > 0)))
{
string lockedFilesStr = responseData.lockedFilesCount > 0 ? $" (Found {responseData.lockedFilesCount} locked files)" : "";
Console.WriteLine($"No results.{lockedFilesStr}");
tle.source.State = TrackState.Failed;
tle.source.FailureReason = FailureReason.NoSuitableFileFound;
m3uEditor.Update();
continue;
}
m3uEditor.Update(); m3uEditor.Update();
if (tle.type != ListType.Album && !Config.interactiveMode) if (tle.source.Type != TrackType.Album)
{ {
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.type); PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type);
} }
if (notFound.Count + existing.Count >= tle.list.Sum(x => x.Count)) if (notFound.Count + existing.Count >= tle.list.Sum(x => x.Count))
{ {
if (i < trackLists.lists.Count - 1) Console.WriteLine();
continue; continue;
} }
await InitClientAndUpdateIfNeeded(); await InitClientAndUpdateIfNeeded();
if (tle.type == ListType.Normal) if (tle.source.Type == TrackType.Normal)
{ {
await TracksDownloadNormal(tle); await TracksDownloadNormal(tle);
} }
else if (tle.type == ListType.Album) else if (tle.source.Type == TrackType.Album)
{ {
await TracksDownloadAlbum(tle); await TracksDownloadAlbum(tle);
} }
else if (tle.type == ListType.Aggregate) else if (tle.source.Type == TrackType.Aggregate)
{ {
await TracksDownloadNormal(tle); await TracksDownloadNormal(tle);
} }
if (i < trackLists.lists.Count - 1)
{
Console.WriteLine();
}
} }
if (!Config.DoNotDownload && (trackLists.lists.Count > 0 || trackLists.Flattened(false, false).Skip(1).Any())) if (!Config.DoNotDownload && (trackLists.lists.Count > 0 || trackLists.Flattened(false, false).Skip(1).Any()))
@ -288,17 +332,17 @@ static partial class Program
{ {
await InitClientAndUpdateIfNeeded(); await InitClientAndUpdateIfNeeded();
if (tle.type == ListType.Normal) if (tle.source.Type == TrackType.Normal)
{ {
await SearchAndPrintResults(tle.list[0]); await SearchAndPrintResults(tle.list[0]);
} }
else if (tle.type == ListType.Aggregate) else if (tle.source.Type == TrackType.Aggregate)
{ {
Console.WriteLine(new string('-', 60)); Console.WriteLine(new string('-', 60));
Console.WriteLine($"Results for aggregate {tle.source.ToString(true)}:"); Console.WriteLine($"Results for aggregate {tle.source.ToString(true)}:");
PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.type); PrintTracksTbd(tle.list[0].Where(t => t.State == TrackState.Initial).ToList(), existing, notFound, tle.source.Type);
} }
else if (tle.type == ListType.Album) else if (tle.source.Type == TrackType.Album)
{ {
Console.WriteLine(new string('-', 60)); Console.WriteLine(new string('-', 60));
Console.WriteLine($"Results for album {tle.source.ToString(true)}:"); Console.WriteLine($"Results for album {tle.source.ToString(true)}:");
@ -340,9 +384,9 @@ static partial class Program
} }
static void PrintTracksTbd(List<Track> toBeDownloaded, List<Track> existing, List<Track> notFound, ListType type) static void PrintTracksTbd(List<Track> toBeDownloaded, List<Track> existing, List<Track> notFound, TrackType type)
{ {
if (type == ListType.Normal && !Config.PrintTracks && toBeDownloaded.Count == 1 && existing.Count + notFound.Count == 0) if (type == TrackType.Normal && !Config.PrintTracks && toBeDownloaded.Count == 1 && existing.Count + notFound.Count == 0)
return; return;
string notFoundLastTime = notFound.Count > 0 ? $"{notFound.Count} not found" : ""; string notFoundLastTime = notFound.Count > 0 ? $"{notFound.Count} not found" : "";
@ -351,12 +395,12 @@ static partial class Program
string skippedTracks = alreadyExist.Length + notFoundLastTime.Length > 0 ? $" ({alreadyExist}{notFoundLastTime})" : ""; string skippedTracks = alreadyExist.Length + notFoundLastTime.Length > 0 ? $" ({alreadyExist}{notFoundLastTime})" : "";
bool full = Config.printOption.HasFlag(PrintOption.Full); bool full = Config.printOption.HasFlag(PrintOption.Full);
if (type == ListType.Normal || skippedTracks.Length > 0) if (type == TrackType.Normal || skippedTracks.Length > 0)
Console.WriteLine($"Downloading {toBeDownloaded.Count(x => !x.IsNotAudio)} tracks{skippedTracks}"); Console.WriteLine($"Downloading {toBeDownloaded.Count(x => !x.IsNotAudio)} tracks{skippedTracks}");
if (toBeDownloaded.Count > 0) if (toBeDownloaded.Count > 0)
{ {
bool showAll = type != ListType.Normal || Config.PrintTracks || Config.PrintResults; bool showAll = type != TrackType.Normal || Config.PrintTracks || Config.PrintResults;
PrintTracks(toBeDownloaded, showAll ? int.MaxValue : 10, full, infoFirst: Config.PrintTracks); PrintTracks(toBeDownloaded, showAll ? int.MaxValue : 10, full, infoFirst: Config.PrintTracks);
if (full && (existing.Count > 0 || notFound.Count > 0)) if (full && (existing.Count > 0 || notFound.Count > 0))
@ -395,40 +439,61 @@ static partial class Program
} }
static List<Track> DoSkipExisting(List<Track> tracks, bool print) static List<Track> DoSkipExisting(List<Track> tracks)
{ {
var existing = new Dictionary<Track, string>(); var existing = new List<Track>();
foreach (var track in tracks)
bool fileBasedSkip = (int)Config.skipMode < 4;
if (!(fileBasedSkip && Config.musicDir.Length > 0 && Config.outputFolder.StartsWith(Config.musicDir, StringComparison.OrdinalIgnoreCase)))
{ {
var d = ExistingCheckers.Registry.SkipExisting(tracks, Config.outputFolder, Config.necessaryCond, m3uEditor, Config.skipMode); if (SetExisting(track))
d.ToList().ForEach(x => existing.TryAdd(x.Key, x.Value)); {
existing.Add(track);
} }
if (Config.musicDir.Length > 0 && System.IO.Directory.Exists(Config.musicDir))
{
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)) return existing;
{
if (print) Console.WriteLine($"Music dir does not exist: {Config.musicDir}");
} }
return existing.Select(x => x.Key).ToList();
static bool SetExisting(Track track)
{
string? path = null;
if (outputExistingChecker != null)
{
if (!outputExistingChecker.IndexIsBuilt)
outputExistingChecker.BuildIndex();
outputExistingChecker.TrackExists(track, out path);
}
if (path == null && musicDirExistingChecker != null)
{
if (!musicDirExistingChecker.IndexIsBuilt)
{
Console.WriteLine($"Building music directory index..");
musicDirExistingChecker.BuildIndex();
}
musicDirExistingChecker.TrackExists(track, out path);
}
if (path != null)
{
track.State = TrackState.AlreadyExists;
track.DownloadPath = path;
}
return path != null;
} }
static List<Track> DoSkipNotFound(List<Track> tracks) static List<Track> DoSkipNotFound(List<Track> tracks)
{ {
List<Track> notFound = new List<Track>(); var notFound = new List<Track>();
for (int i = tracks.Count - 1; i >= 0; i--) foreach (var track in tracks)
{ {
if (SetNotFoundLastTime(tracks[i])) if (SetNotFoundLastTime(track))
{ {
notFound.Add(tracks[i]); notFound.Add(track);
} }
} }
return notFound; return notFound;
@ -453,7 +518,7 @@ static partial class Program
{ {
var tracks = tle.list[0]; var tracks = tle.list[0];
SemaphoreSlim semaphore = new SemaphoreSlim(Config.concurrentProcesses); var semaphore = new SemaphoreSlim(Config.concurrentProcesses);
var copy = new List<Track>(tracks); var copy = new List<Track>(tracks);
var downloadTasks = copy.Select(async (track, index) => var downloadTasks = copy.Select(async (track, index) =>
@ -545,7 +610,6 @@ static partial class Program
var tracks = new List<Track>(); var tracks = new List<Track>();
bool downloadingImages = false; bool downloadingImages = false;
bool albumDlFailed = false; bool albumDlFailed = false;
var listRef = list;
string savedOutputFolder = Config.outputFolder; string savedOutputFolder = Config.outputFolder;
var curAlbumArtOption = Config.albumArtOption == AlbumArtOption.MostLargest ? AlbumArtOption.Most : Config.albumArtOption; var curAlbumArtOption = Config.albumArtOption == AlbumArtOption.MostLargest ? AlbumArtOption.Most : Config.albumArtOption;
@ -602,6 +666,7 @@ static partial class Program
while (list.Count > 0) while (list.Count > 0)
{ {
idx++; idx++;
mainLoopCts = new CancellationTokenSource(); mainLoopCts = new CancellationTokenSource();
albumDlFailed = false; albumDlFailed = false;
@ -612,9 +677,10 @@ static partial class Program
soulseekFolderPathPrefix = GetCommonPathPrefix(tracks); soulseekFolderPathPrefix = GetCommonPathPrefix(tracks);
if (tle.placeInSubdir && (!downloadingImages || idx == 0)) if (tle.placeInSubdir && Config.nameFormat.Length == 0 && (!downloadingImages || idx == 0))
{ {
Config.outputFolder = Path.Join(savedOutputFolder, Utils.GetBaseNameSlsk(soulseekFolderPathPrefix)); string name = tle.subdirOverride ?? Utils.GetBaseNameSlsk(soulseekFolderPathPrefix);
Config.outputFolder = Path.Join(savedOutputFolder, name);
} }
if (!downloadingImages) if (!downloadingImages)
@ -775,7 +841,16 @@ static partial class Program
break; break;
} }
ApplyNamingFormatsNonAudio(listRef); bool success = tracks.All(t => t.State == TrackState.Downloaded || t.State == TrackState.AlreadyExists);
ApplyNamingFormatsNonAudio(tracks);
if (!Config.albumArtOnly && success)
{
tle.source.State = TrackState.Downloaded;
tle.source.DownloadPath = Utils.GreatestCommonPath(tracks.Where(x => x.DownloadPath.Length > 0).Select(x => x.DownloadPath), Path.DirectorySeparatorChar);
}
m3uEditor.Update(); m3uEditor.Update();
soulseekFolderPathPrefix = ""; soulseekFolderPathPrefix = "";
Config.outputFolder = savedOutputFolder; Config.outputFolder = savedOutputFolder;
@ -1042,7 +1117,7 @@ static partial class Program
.Replace("{uri}", track.URI) .Replace("{uri}", track.URI)
.Replace("{length}", track.Length.ToString()) .Replace("{length}", track.Length.ToString())
.Replace("{artist-maybe-wrong}", track.ArtistMaybeWrong.ToString()) .Replace("{artist-maybe-wrong}", track.ArtistMaybeWrong.ToString())
.Replace("{is-album}", track.IsAlbum.ToString()) .Replace("{type}", track.Type.ToString())
.Replace("{is-not-audio}", track.IsNotAudio.ToString()) .Replace("{is-not-audio}", track.IsNotAudio.ToString())
.Replace("{failure-reason}", track.FailureReason.ToString()) .Replace("{failure-reason}", track.FailureReason.ToString())
.Replace("{path}", track.DownloadPath) .Replace("{path}", track.DownloadPath)
@ -1106,39 +1181,33 @@ static partial class Program
return name.ReplaceInvalidChars(Config.invalidReplaceStr); return name.ReplaceInvalidChars(Config.invalidReplaceStr);
} }
static void ApplyNamingFormatsNonAudio(List<List<Track>> list) static void ApplyNamingFormatsNonAudio(List<Track> tracks)
{ {
if (!Config.nameFormat.Replace("\\", "/").Contains('/')) if (!Config.nameFormat.Replace('\\', '/').Contains('/'))
return; return;
var downloadPaths = list.SelectMany(x => x) var audioFilePaths = tracks.Where(t => t.DownloadPath.Length > 0 && !t.IsNotAudio).Select(t => t.DownloadPath);
.Where(x => x.DownloadPath.Length > 0 && !x.IsNotAudio)
.Select(x => x.DownloadPath).Distinct().ToList();
if (downloadPaths.Count == 0) string outputFolder = Utils.GreatestCommonPath(audioFilePaths, Path.DirectorySeparatorChar);
return;
for (int i = 0; i < downloadPaths.Count; i++) foreach (var track in tracks)
downloadPaths[i] = Path.GetFullPath(downloadPaths[i]);
for (int i = 0; i < list.Count; i++)
{ {
for (int j = 0; j < list[i].Count; j++)
{
var track = list[i][j];
if (!track.IsNotAudio || track.State != TrackState.Downloaded) if (!track.IsNotAudio || track.State != TrackState.Downloaded)
continue; continue;
string filepath = track.DownloadPath;
string add = Path.GetRelativePath(Config.outputFolder, Path.GetDirectoryName(filepath)); string newFilePath = Path.Join(outputFolder, Path.GetFileName(track.DownloadPath));
string newFilePath = Path.Join(Utils.GreatestCommonPath(downloadPaths, Path.DirectorySeparatorChar), add, Path.GetFileName(filepath));
if (filepath != newFilePath) if (track.DownloadPath != newFilePath)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
Utils.Move(filepath, newFilePath); Utils.Move(track.DownloadPath, newFilePath);
if (add.Length > 0 && add != "." && Utils.GetRecursiveFileCount(Path.Join(Config.outputFolder, add)) == 0)
Directory.Delete(Path.Join(Config.outputFolder, add), true); string prevParent = Path.GetDirectoryName(track.DownloadPath);
list[i][j].DownloadPath = newFilePath;
} 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": case "filename":
return Path.GetFileNameWithoutExtension(filepath); return Path.GetFileNameWithoutExtension(filepath);
case "foldername": case "foldername":
return track.FirstDownload != null ?
Utils.GetBaseNameSlsk(Utils.GetDirectoryNameSlsk(track.FirstDownload.Filename)) : Config.defaultFolderName;
case "default-foldername":
return Config.defaultFolderName; return Config.defaultFolderName;
case "extractor": case "extractor":
return Config.inputType.ToString(); return Config.inputType.ToString();
@ -1347,18 +1419,18 @@ static partial class Program
if (!tracks[i].IsNotAudio) if (!tracks[i].IsNotAudio)
{ {
Console.WriteLine($" Artist: {tracks[i].Artist}"); Console.WriteLine($" Artist: {tracks[i].Artist}");
if (!string.IsNullOrEmpty(tracks[i].Title) || !tracks[i].IsAlbum) if (!string.IsNullOrEmpty(tracks[i].Title) || tracks[i].Type == TrackType.Normal)
Console.WriteLine($" Title: {tracks[i].Title}"); Console.WriteLine($" Title: {tracks[i].Title}");
if (!string.IsNullOrEmpty(tracks[i].Album) || tracks[i].IsAlbum) if (!string.IsNullOrEmpty(tracks[i].Album) || tracks[i].Type == TrackType.Album)
Console.WriteLine($" Album: {tracks[i].Album}"); Console.WriteLine($" Album: {tracks[i].Album}");
if (tracks[i].Length > -1 || !tracks[i].IsAlbum) if (tracks[i].Length > -1 || tracks[i].Type == TrackType.Normal)
Console.WriteLine($" Length: {tracks[i].Length}s"); Console.WriteLine($" Length: {tracks[i].Length}s");
if (!string.IsNullOrEmpty(tracks[i].DownloadPath)) if (!string.IsNullOrEmpty(tracks[i].DownloadPath))
Console.WriteLine($" Local path: {tracks[i].DownloadPath}"); Console.WriteLine($" Local path: {tracks[i].DownloadPath}");
if (!string.IsNullOrEmpty(tracks[i].URI)) if (!string.IsNullOrEmpty(tracks[i].URI))
Console.WriteLine($" URL/ID: {tracks[i].URI}"); Console.WriteLine($" URL/ID: {tracks[i].URI}");
if (tracks[i].IsAlbum) if (tracks[i].Type != TrackType.Normal)
Console.WriteLine($" Is album: true"); Console.WriteLine($" Type: {tracks[i].Type}");
if (!string.IsNullOrEmpty(tracks[i].Other)) if (!string.IsNullOrEmpty(tracks[i].Other))
Console.WriteLine($" Other: {tracks[i].Other}"); Console.WriteLine($" Other: {tracks[i].Other}");
if (tracks[i].ArtistMaybeWrong) if (tracks[i].ArtistMaybeWrong)

View file

@ -1,5 +1,6 @@
using Data; using Data;
using Enums; using Enums;
using ExistingCheckers;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@ -260,7 +261,7 @@ namespace Test
{ {
Config.input = strings[i]; Config.input = strings[i];
Console.WriteLine(Config.input); Console.WriteLine(Config.input);
var res = await extractor.GetTracks(); var res = await extractor.GetTracks(0, 0, false);
var t = res[0].list[0][0]; var t = res[0].list[0][0];
Assert(Extractors.StringExtractor.InputMatches(Config.input)); Assert(Extractors.StringExtractor.InputMatches(Config.input));
Assert(t.ToKey() == tracks[i].ToKey()); Assert(t.ToKey() == tracks[i].ToKey());
@ -273,7 +274,7 @@ namespace Test
{ {
Config.input = strings[i]; Config.input = strings[i];
Console.WriteLine(Config.input); Console.WriteLine(Config.input);
var t = (await extractor.GetTracks())[0].source; var t = (await extractor.GetTracks(0, 0, false))[0].source;
Assert(Extractors.StringExtractor.InputMatches(Config.input)); Assert(Extractors.StringExtractor.InputMatches(Config.input));
Assert(t.ToKey() == albums[i].ToKey()); Assert(t.ToKey() == albums[i].ToKey());
} }
@ -287,6 +288,7 @@ namespace Test
Config.m3uOption = M3uOption.All; Config.m3uOption = M3uOption.All;
Config.skipMode = SkipMode.M3u; Config.skipMode = SkipMode.M3u;
Config.musicDir = "";
Config.printOption = PrintOption.Tracks | PrintOption.Full; Config.printOption = PrintOption.Tracks | PrintOption.Full;
Config.skipExisting = true; Config.skipExisting = true;
@ -296,11 +298,12 @@ namespace Test
File.Delete(path); File.Delete(path);
File.WriteAllText(path, $"#SLDL:" + File.WriteAllText(path, $"#SLDL:" +
$"{Path.Join(Directory.GetCurrentDirectory(), "file1.5")},\"Artist, 1.5\",,\"Title, , 1.5\",-1,3,0;" + $"{Path.Join(Directory.GetCurrentDirectory(), "file1.5")},\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;" +
$"path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,3,0;" + $"path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;" +
$"path/to/file2,\"Artist, 2\",,Title2,-1,3,0;,\"Artist; ,3\",,Title3 ;a,-1,4,0;" + $"path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;" +
$",\"Artist,,, ;4\",,Title4,-1,4,3;" + $",\"Artist; ,3\",,Title3 ;a,-1,0,4,0;" +
$",,,,-1,0,0;"); $",\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
$",,,,-1,0,0,0;");
var notFoundInitial = new List<Track>() var notFoundInitial = new List<Track>()
{ {
@ -320,7 +323,7 @@ namespace Test
}; };
var trackLists = new TrackLists(); var trackLists = new TrackLists();
trackLists.AddEntry(); trackLists.AddEntry(new TrackListEntry());
foreach (var t in notFoundInitial) foreach (var t in notFoundInitial)
trackLists.AddTrackToLast(t); trackLists.AddTrackToLast(t);
foreach (var t in existingInitial) foreach (var t in existingInitial)
@ -330,20 +333,22 @@ namespace Test
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption); Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption);
Program.outputExistingChecker = new M3uExistingChecker(Program.m3uEditor, false);
var notFound = (List<Track>)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] }); var notFound = (List<Track>)ProgramInvoke("DoSkipNotFound", new object[] { trackLists[0].list[0] });
var existing = (List<Track>)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0], false }); var existing = (List<Track>)ProgramInvoke("DoSkipExisting", new object[] { trackLists[0].list[0] });
var toBeDownloaded = trackLists[0].list[0].Where(t => t.State == TrackState.Initial).ToList(); var toBeDownloaded = trackLists[0].list[0].Where(t => t.State == TrackState.Initial).ToList();
Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial)); Assert(notFound.SequenceEqualUpToPermutation(notFoundInitial));
Assert(existing.SequenceEqualUpToPermutation(existingInitial)); Assert(existing.SequenceEqualUpToPermutation(existingInitial));
Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial)); Assert(toBeDownloaded.SequenceEqualUpToPermutation(toBeDownloadedInitial));
ProgramInvoke("PrintTracksTbd", new object[] { toBeDownloaded, existing, notFound, ListType.Normal }); ProgramInvoke("PrintTracksTbd", new object[] { toBeDownloaded, existing, notFound, TrackType.Normal });
Program.m3uEditor.Update(); Program.m3uEditor.Update();
string output = File.ReadAllText(path); string output = File.ReadAllText(path);
string need = string need =
"#SLDL:./file1.5,\"Artist, 1.5\",,\"Title, , 1.5\",-1,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,3,0;,\"Artist; ,3\",,Title3 ;a,-1,4,0;,\"Artist,,, ;4\",,Title4,-1,4,3;,,,,-1,0,0;" + "#SLDL:./file1.5,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;,,,,-1,0,0,0;" +
"\n" + "\n" +
"\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" + "\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
"\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" + "\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
@ -362,8 +367,8 @@ namespace Test
Program.m3uEditor.Update(); Program.m3uEditor.Update();
output = File.ReadAllText(path); output = File.ReadAllText(path);
need = need =
"#SLDL:/other/new/file/path,\"Artist, 1.5\",,\"Title, , 1.5\",-1,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,3,0;,\"Artist; ,3\",,Title3 ;a,-1,4,0;,\"Artist,,, ;4\",,Title4,-1,4,3;" + "#SLDL:/other/new/file/path,\"Artist, 1.5\",,\"Title, , 1.5\",-1,0,3,0;path/to/file1,\"Artist, 1\",,\"Title, , 1\",-1,0,3,0;path/to/file2,\"Artist, 2\",,Title2,-1,0,3,0;,\"Artist; ,3\",,Title3 ;a,-1,0,4,0;,\"Artist,,, ;4\",,Title4,-1,0,4,3;" +
",,,,-1,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,1,0;,ArtistB,Albumm,TitleB,-1,2,3;" + ",,,,-1,0,0,0;new/file/path,ArtistA,Albumm,TitleA,-1,0,1,0;,ArtistB,Albumm,TitleB,-1,0,2,3;" +
"\n" + "\n" +
"\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" + "\n# Failed: Artist; ,3 - Title3 ;a [NoSuitableFileFound]" +
"\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" + "\n# Failed: Artist,,, ;4 - Title4 [NoSuitableFileFound]" +
@ -394,6 +399,44 @@ namespace Test
output = File.ReadAllText(path); output = File.ReadAllText(path);
Assert(output == need); Assert(output == need);
var test = new List<Track>
{
new() { Artist = "ArtistA", Album = "AlbumA", Type = TrackType.Album },
new() { Artist = "ArtistB", Album = "AlbumB", Type = TrackType.Album },
new() { Artist = "ArtistC", Album = "AlbumC", Type = TrackType.Album },
};
trackLists = new TrackLists();
foreach (var t in test)
trackLists.AddEntry(new TrackListEntry(t));
File.WriteAllText(path, "");
Config.m3uOption = M3uOption.Index;
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption);
Program.m3uEditor.Update();
Assert(File.ReadAllText(path) == "");
test[0].State = TrackState.Downloaded;
test[0].DownloadPath = "download/path";
test[1].State = TrackState.Failed;
test[1].FailureReason = FailureReason.NoSuitableFileFound;
test[2].State = TrackState.AlreadyExists;
Program.m3uEditor.Update();
Program.m3uEditor = new M3uEditor(path, trackLists, Config.m3uOption);
foreach (var t in test)
{
Program.m3uEditor.TryGetPreviousRunResult(t, out var tt);
Assert(tt != null);
Assert(tt.ToKey() == t.ToKey());
t.DownloadPath = "this should not change tt.DownloadPath";
Assert(t.DownloadPath != tt.DownloadPath);
}
File.Delete(path); File.Delete(path);
Passed(); Passed();