1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 22:42:41 +00:00
This commit is contained in:
fiso64 2024-05-31 12:26:55 +02:00
parent 11579385a9
commit c5806e5d24
2 changed files with 138 additions and 52 deletions

View file

@ -250,14 +250,9 @@ Files satisfying `pref-` conditions will be preferred; setting `--pref-format "f
**Important note**: Some info may be unavailable depending on the client used by the peer. For example, the default Soulseek client does not share the file bitrate. By default, if `--min-bitrate` is set, then files with unknown bitrate will still be downloaded. You can configure it to reject all files where one of the checked properties is unavailable by enabling `--strict-conditions`. (As a consequence, if `--min-bitrate` is also set then any files shared by users with the default client will be ignored) **Important note**: Some info may be unavailable depending on the client used by the peer. For example, the default Soulseek client does not share the file bitrate. By default, if `--min-bitrate` is set, then files with unknown bitrate will still be downloaded. You can configure it to reject all files where one of the checked properties is unavailable by enabling `--strict-conditions`. (As a consequence, if `--min-bitrate` is also set then any files shared by users with the default client will be ignored)
### Name format ### Name format
Available tags are: artist, artists, album_artist, album_artists, title, album, year, track, disc, filename, default_foldername. Name format supports subdirectories as well as conditional expressions: `{str1|str2}` If any tags in str1 are null, choose str2. String literals enclosed in parentheses are ignored in the null check. Variables enclosed in {} will be replaced by the corresponding file tag value. Available variables are: artist, sartist, artists, albumartist, albumartists, title, stitle, album, salbum, year, track, disc, filename, foldername. The variables sartist, stitle and salbum will be replaced by the source artist, title and album respectively (i.e what is shown in spotify/youtube/csv file) instead of the tag values of the downloaded file. Name format supports subdirectories as well as conditional expressions like `{tag1|tag2}` If tag1 is null, choose tag2. String literals enclosed in parentheses are ignored in the null check. Examples:
``` - `{artist} - {title}`: Always name it 'Artist - Title'. Because some files on Soulseek do not have tags, the second example is preferred:
{artist( - )title|album_artist( - )title|filename} - `{artist( - )title|filename}`: If artist and title is not null, name it 'Artist - Title', otherwise use the original filename.
```
```
{album(/)}{track(. )}{artist|(unknown artist)} - {title|(unknown title)}
```
Here `{album(/)}` will conditionally put the download into a subfolder; if the album tag is null, then the slash won't be added to the path. An alternative is `{album|(missing album)}/` which will save all songs with unknown album under `missing album`.
### Quality vs Speed ### Quality vs Speed
The following options will make it go faster, but may decrease search result quality or cause instability: The following options will make it go faster, but may decrease search result quality or cause instability:

View file

@ -23,7 +23,7 @@ using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string,
// undocumented options // undocumented options
// --on-complete // --on-complete
// --artist-col, --title-col, --album-col, --length-col, --yt-desc-col, --yt-id-col // --artist-col, --title-col, --album-col, --length-col, --yt-desc-col, --yt-id-col, --album-track-count-col
// --input-type, --login, --random-login, --no-modify-share-count --fast-search-delay, // --input-type, --login, --random-login, --no-modify-share-count --fast-search-delay,
// --fails-to-deprioritize (=1), --fails-to-ignore (=2) // --fails-to-deprioritize (=1), --fails-to-ignore (=2)
// --cond, --pref, --danger-words, --pref-danger-words, --strict-title, --strict-artist, --strict-album // --cond, --pref, --danger-words, --pref-danger-words, --strict-title, --strict-artist, --strict-album
@ -77,6 +77,7 @@ static class Program
static string trackCol = ""; static string trackCol = "";
static string ytIdCol = ""; static string ytIdCol = "";
static string descCol = ""; static string descCol = "";
static string trackCountCol = "";
static string lengthCol = ""; static string lengthCol = "";
static bool aggregate = false; static bool aggregate = false;
static bool album = false; static bool album = false;
@ -470,6 +471,9 @@ static class Program
case "--yt-desc-col": case "--yt-desc-col":
descCol = args[++i]; descCol = args[++i];
break; break;
case "--album-track-count-col":
trackCountCol = args[++i];
break;
case "--yt-id-col": case "--yt-id-col":
ytIdCol = args[++i]; ytIdCol = args[++i];
break; break;
@ -630,10 +634,19 @@ static class Program
case "--atc": case "--atc":
case "--album-track-count": case "--album-track-count":
string a = args[++i]; string a = args[++i];
if (a.Last() == '-') if (a == "-1")
{
minAlbumTrackCount = -1;
maxAlbumTrackCount = -1;
}
else if (a.Last() == '-')
{
maxAlbumTrackCount = int.Parse(a.Substring(0, a.Length - 1)); maxAlbumTrackCount = int.Parse(a.Substring(0, a.Length - 1));
}
else if (a.Last() == '+') else if (a.Last() == '+')
{
minAlbumTrackCount = int.Parse(a.Substring(0, a.Length - 1)); minAlbumTrackCount = int.Parse(a.Substring(0, a.Length - 1));
}
else else
{ {
minAlbumTrackCount = int.Parse(a); minAlbumTrackCount = int.Parse(a);
@ -917,7 +930,7 @@ static class Program
if (debugDisableDownload) if (debugDisableDownload)
maxConcurrentProcesses = 1; maxConcurrentProcesses = 1;
ignoreOn = ignoreOn > deprioritizeOn ? deprioritizeOn : ignoreOn; ignoreOn = Math.Min(ignoreOn, deprioritizeOn);
if (inputType == "youtube" || (inputType == "" && input.StartsWith("http") && input.Contains("youtu"))) if (inputType == "youtube" || (inputType == "" && input.StartsWith("http") && input.Contains("youtu")))
{ {
@ -1192,7 +1205,7 @@ static class Program
if (!File.Exists(csvPath)) if (!File.Exists(csvPath))
throw new FileNotFoundException("CSV file not found"); throw new FileNotFoundException("CSV file not found");
var tracks = await ParseCsvIntoTrackInfo(csvPath, artistCol, trackCol, lengthCol, albumCol, descCol, ytIdCol, timeUnit, ytParse); var tracks = await ParseCsvIntoTrackInfo(csvPath, artistCol, trackCol, lengthCol, albumCol, descCol, ytIdCol, trackCountCol, timeUnit, ytParse);
tracks = tracks.Skip(off).Take(max).ToList(); tracks = tracks.Skip(off).Take(max).ToList();
trackLists = TrackLists.FromFlatList(tracks, aggregate, album); trackLists = TrackLists.FromFlatList(tracks, aggregate, album);
defaultFolderName = Path.GetFileNameWithoutExtension(csvPath); defaultFolderName = Path.GetFileNameWithoutExtension(csvPath);
@ -2125,7 +2138,7 @@ static class Program
} }
if (nameFormat != "" && !useYtdlp) if (nameFormat != "" && !useYtdlp)
saveFilePath = ApplyNamingFormat(saveFilePath); saveFilePath = ApplyNamingFormat(saveFilePath, track);
return saveFilePath; return saveFilePath;
} }
@ -2238,10 +2251,22 @@ static class Program
x.Item2.Count(x => Utils.IsMusicFile(x.file.Filename)) x.Item2.Count(x => Utils.IsMusicFile(x.file.Filename))
).ToList(); ).ToList();
bool countIsGood(int count, int min, int max) => count >= min && (max == -1 || count <= max); int min, max;
if (track.MinAlbumTrackCount != -1 || track.MaxAlbumTrackCount != -1)
{
min = track.MinAlbumTrackCount;
max = track.MaxAlbumTrackCount;
}
else
{
min = minAlbumTrackCount;
max = maxAlbumTrackCount;
}
bool countIsGood(int count) => count >= min && (max == -1 || count <= max);
var result = musicFolders var result = musicFolders
.Where(x => countIsGood(x.Item2.Count(rf => Utils.IsMusicFile(rf.file.Filename)), minAlbumTrackCount, maxAlbumTrackCount)) .Where(x => countIsGood(x.Item2.Count(rf => Utils.IsMusicFile(rf.file.Filename))))
.Select(ls => ls.Item2.Select(x => { .Select(ls => ls.Item2.Select(x => {
var t = new Track var t = new Track
{ {
@ -3291,7 +3316,7 @@ static class Program
static async Task<List<Track>> ParseCsvIntoTrackInfo(string path, string artistCol = "", string trackCol = "", static async Task<List<Track>> ParseCsvIntoTrackInfo(string path, string artistCol = "", string trackCol = "",
string lengthCol = "", string albumCol = "", string descCol = "", string ytIdCol = "", string timeUnit = "s", bool ytParse = false) string lengthCol = "", string albumCol = "", string descCol = "", string ytIdCol = "", string trackCountCol = "", string timeUnit = "s", bool ytParse = false)
{ {
var tracks = new List<Track>(); var tracks = new List<Track>();
using var sr = new StreamReader(path, System.Text.Encoding.UTF8); using var sr = new StreamReader(path, System.Text.Encoding.UTF8);
@ -3301,14 +3326,15 @@ static class Program
while (header == null || header.Count == 0 || !header.Any(t => t.Trim() != "")) while (header == null || header.Count == 0 || !header.Any(t => t.Trim() != ""))
header = parser.ReadNextRow(); header = parser.ReadNextRow();
string[] cols = { artistCol, albumCol, trackCol, lengthCol, descCol, ytIdCol }; string[] cols = { artistCol, albumCol, trackCol, lengthCol, descCol, ytIdCol, trackCountCol };
string[][] aliases = { string[][] aliases = {
new[] { "artist", "artist name", "artists", "artist names" }, new[] { "artist", "artist name", "artists", "artist names" },
new[] { "album", "album name", "album title" }, new[] { "album", "album name", "album title" },
new[] { "title", "song", "track title", "track name", "song name", "track" }, new[] { "title", "song", "track title", "track name", "song name", "track" },
new[] { "length", "duration", "track length", "track duration", "song length", "song duration" }, new[] { "length", "duration", "track length", "track duration", "song length", "song duration" },
new[] { "description", "youtube description" }, new[] { "description", "youtube description" },
new[] { "id", "youtube id", "url" } new[] { "url", "id", "youtube id" },
new[] { "track count", "album track count" }
}; };
string usingColumns = ""; string usingColumns = "";
@ -3338,8 +3364,8 @@ static class Program
throw new Exception("No columns specified and couldn't determine automatically"); throw new Exception("No columns specified and couldn't determine automatically");
int[] indices = cols.Select(col => col == "" ? -1 : header.IndexOf(col)).ToArray(); int[] indices = cols.Select(col => col == "" ? -1 : header.IndexOf(col)).ToArray();
int artistIndex, albumIndex, trackIndex, lengthIndex, descIndex, ytIdIndex; int artistIndex, albumIndex, trackIndex, lengthIndex, descIndex, ytIdIndex, trackCountIndex;
(artistIndex, albumIndex, trackIndex, lengthIndex, descIndex, ytIdIndex) = (indices[0], indices[1], indices[2], indices[3], indices[4], indices[5]); (artistIndex, albumIndex, trackIndex, lengthIndex, descIndex, ytIdIndex, trackCountIndex) = (indices[0], indices[1], indices[2], indices[3], indices[4], indices[5], indices[6]);
while (true) while (true)
{ {
@ -3359,6 +3385,28 @@ static class Program
if (albumIndex >= 0) track.Album = values[albumIndex]; if (albumIndex >= 0) track.Album = values[albumIndex];
if (descIndex >= 0) desc = values[descIndex]; if (descIndex >= 0) desc = values[descIndex];
if (ytIdIndex >= 0) track.URI = values[ytIdIndex]; if (ytIdIndex >= 0) track.URI = values[ytIdIndex];
if (trackCountIndex >= 0)
{
string a = values[trackCountIndex].Trim();
if (a == "-1")
{
track.MinAlbumTrackCount = -1;
track.MaxAlbumTrackCount = -1;
}
else if (a.Last() == '-' && int.TryParse(a.AsSpan(0, a.Length - 1), out int n))
{
track.MaxAlbumTrackCount = n;
}
else if (a.Last() == '+' && int.TryParse(a.AsSpan(0, a.Length - 1), out n))
{
track.MinAlbumTrackCount = n;
}
else if (int.TryParse(a, out n))
{
track.MinAlbumTrackCount = n;
track.MaxAlbumTrackCount = n;
}
}
if (lengthIndex >= 0) if (lengthIndex >= 0)
{ {
try try
@ -3469,13 +3517,13 @@ static class Program
} }
} }
static string ApplyNamingFormat(string filepath) static string ApplyNamingFormat(string filepath, Track track)
{ {
if (nameFormat == "" || !Utils.IsMusicFile(filepath)) if (nameFormat == "" || !Utils.IsMusicFile(filepath))
return filepath; return filepath;
string add = Path.GetRelativePath(outputFolder, Path.GetDirectoryName(filepath)); string add = Path.GetRelativePath(outputFolder, Path.GetDirectoryName(filepath));
string newFilePath = NamingFormat(filepath, nameFormat); string newFilePath = NamingFormat(filepath, nameFormat, track);
if (filepath != newFilePath) if (filepath != newFilePath)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
@ -3487,7 +3535,7 @@ static class Program
return newFilePath; return newFilePath;
} }
static string NamingFormat(string filepath, string format) static string NamingFormat(string filepath, string format, Track track)
{ {
string newName = format; string newName = format;
TagLib.File? file = null; TagLib.File? file = null;
@ -3511,7 +3559,7 @@ static class Program
{ {
string[] parts = Regex.Split(opt, @"\([^\)]*\)"); string[] parts = Regex.Split(opt, @"\([^\)]*\)");
string[] result = parts.Where(part => !string.IsNullOrWhiteSpace(part)).ToArray(); string[] result = parts.Where(part => !string.IsNullOrWhiteSpace(part)).ToArray();
if (result.All(x => GetTagValue(file, x) != "")) { if (result.All(x => GetVarValue(x, file, track) != "")) {
chosenOpt = opt; chosenOpt = opt;
break; break;
} }
@ -3522,7 +3570,7 @@ static class Program
if (match.Value.StartsWith("(") && match.Value.EndsWith(")")) if (match.Value.StartsWith("(") && match.Value.EndsWith(")"))
return match.Value.Substring(1, match.Value.Length-2); return match.Value.Substring(1, match.Value.Length-2);
else else
return GetTagValue(file, match.Value); return GetVarValue(match.Value, file, track);
}); });
string old = match.Groups[1].Value; string old = match.Groups[1].Value;
old = old.StartsWith("{{") ? old.Substring(1) : old; old = old.StartsWith("{{") ? old.Substring(1) : old;
@ -3548,22 +3596,29 @@ static class Program
return filepath; return filepath;
} }
static string GetTagValue(TagLib.File file, string tag) static string GetVarValue(string x, TagLib.File file, Track track)
{ {
switch (tag) switch (x)
{ {
case "artist": case "artist":
return (file.Tag.FirstPerformer ?? "").RemoveFt(); return file.Tag.FirstPerformer ?? "";
case "artists": case "artists":
return string.Join(" & ", file.Tag.Performers).RemoveFt(); return string.Join(" & ", file.Tag.Performers);
case "album_artist": case "albumartist":
return (file.Tag.FirstAlbumArtist ?? "").RemoveFt(); return file.Tag.FirstAlbumArtist ?? "";
case "album_artists": case "albumartists":
return string.Join(" & ", file.Tag.AlbumArtists).RemoveFt(); return string.Join(" & ", file.Tag.AlbumArtists);
case "title": case "title":
return file.Tag.Title ?? ""; return file.Tag.Title ?? "";
case "album": case "album":
return file.Tag.Album ?? ""; return file.Tag.Album ?? "";
case "sartist":
case "sartists":
return track.Artist;
case "stitle":
return track.Title;
case "salbum":
return track.Album;
case "year": case "year":
return file.Tag.Year.ToString() ?? ""; return file.Tag.Year.ToString() ?? "";
case "track": case "track":
@ -3572,7 +3627,7 @@ static class Program
return file.Tag.Disc.ToString() ?? ""; return file.Tag.Disc.ToString() ?? "";
case "filename": case "filename":
return Path.GetFileNameWithoutExtension(file.Name); return Path.GetFileNameWithoutExtension(file.Name);
case "default_foldername": case "foldername":
return defaultFolderName; return defaultFolderName;
default: default:
return ""; return "";
@ -3581,34 +3636,66 @@ static class Program
static bool TrackMatchesFilename(Track track, string filename) static bool TrackMatchesFilename(Track track, string filename)
{ {
string[] ignore = new string[] { " ", "_", "-", ".", "(", ")" }; if (track.Title.Trim() == "" || filename.Trim() == "")
string searchName = track.Title.Replace(ignore, "").ToLower(); return false;
searchName = searchName.ReplaceInvalidChars("").RemoveFt().RemoveSquareBrackets();
searchName = searchName == "" ? track.Title : searchName;
string searchName2 = ""; string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" };
if (searchName.Length <= 3) {
searchName2 = track.Artist.Replace(ignore, "").ToLower(); string preprocess1(string s, bool removeSlash = true)
searchName2 = searchName2.ReplaceInvalidChars("").RemoveFt().RemoveSquareBrackets(); {
searchName2 = searchName2 == "" ? track.Artist : searchName2; s = s.ReplaceInvalidChars("", false, removeSlash).Replace(ignore, "").ToLower();
s = s.RemoveFt().RemoveDiacritics();
return s;
} }
string fullpath = filename; string preprocess2(string s, bool removeSlash = true)
filename = Path.GetFileNameWithoutExtension(filename); {
filename = filename.ReplaceInvalidChars(""); s = s.ReplaceInvalidChars("", false, removeSlash).ToLower().RemoveDiacritics();
filename = filename.Replace(ignore, "").ToLower(); return s;
}
if (filename.Contains(searchName) && FileConditions.StrictString(fullpath, searchName2, ignoreCase:true, boundarySkipWs:true)) string preprocess3(string s)
{
s = s.ToLower().RemoveDiacritics();
return s;
}
string title = preprocess1(track.Title);
string artist = preprocess1(track.Artist);
string fname = preprocess1(Path.GetFileNameWithoutExtension(filename));
string path = preprocess1(filename, false);
if (title == "" || fname == "")
{
title = preprocess2(track.Title);
artist = preprocess2(track.Artist);
fname = preprocess2(Path.GetFileNameWithoutExtension(filename));
path = preprocess2(filename, false);
if (title == "" || fname == "")
{
title = preprocess3(track.Title);
artist = preprocess3(track.Artist);
fname = preprocess3(Path.GetFileNameWithoutExtension(filename));
path = preprocess3(filename);
if (title == "" || fname == "")
{
return false;
}
}
}
if (fname.Contains(title) && path.Contains(artist))
{ {
return true; return true;
} }
else if ((track.ArtistMaybeWrong || track.Artist == "") && track.Title.Contains(" - ")) else if ((track.ArtistMaybeWrong || track.Artist == "") && track.Title.Contains(" - "))
{ {
searchName = track.Title.Substring(track.Title.IndexOf(" - ") + 3).Replace(ignore, "").ToLower(); title = preprocess1(track.Title.Substring(track.Title.IndexOf(" - ") + 3));
searchName = searchName.ReplaceInvalidChars("").RemoveFt().RemoveSquareBrackets(); if (title != "")
if (searchName != "")
{ {
if (filename.Contains(searchName)) if (preprocess1(filename, false).Contains(title))
return true; return true;
} }
} }
@ -4372,6 +4459,8 @@ public struct Track
public int Length = -1; public int Length = -1;
public bool ArtistMaybeWrong = false; public bool ArtistMaybeWrong = false;
public bool IsAlbum = false; public bool IsAlbum = false;
public int MinAlbumTrackCount = -1;
public int MaxAlbumTrackCount = -1;
public bool IsNotAudio = false; public bool IsNotAudio = false;
public string FailureReason = ""; public string FailureReason = "";
public string DownloadPath = ""; public string DownloadPath = "";
@ -4406,6 +4495,8 @@ public struct Track
FailureReason = other.FailureReason; FailureReason = other.FailureReason;
DownloadPath = other.DownloadPath; DownloadPath = other.DownloadPath;
Other = other.Other; Other = other.Other;
MinAlbumTrackCount = other.MinAlbumTrackCount;
MaxAlbumTrackCount = other.MaxAlbumTrackCount;
} }
public override readonly string ToString() public override readonly string ToString()