mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 14:32:40 +00:00
commit
This commit is contained in:
parent
11579385a9
commit
c5806e5d24
2 changed files with 138 additions and 52 deletions
11
README.md
11
README.md
|
@ -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)
|
||||
|
||||
### 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.
|
||||
```
|
||||
{artist( - )title|album_artist( - )title|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`.
|
||||
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|filename}`: If artist and title is not null, name it 'Artist - Title', otherwise use the original filename.
|
||||
|
||||
### Quality vs Speed
|
||||
The following options will make it go faster, but may decrease search result quality or cause instability:
|
||||
|
|
|
@ -23,7 +23,7 @@ using SlDictionary = System.Collections.Concurrent.ConcurrentDictionary<string,
|
|||
|
||||
// undocumented options
|
||||
// --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,
|
||||
// --fails-to-deprioritize (=1), --fails-to-ignore (=2)
|
||||
// --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 ytIdCol = "";
|
||||
static string descCol = "";
|
||||
static string trackCountCol = "";
|
||||
static string lengthCol = "";
|
||||
static bool aggregate = false;
|
||||
static bool album = false;
|
||||
|
@ -470,6 +471,9 @@ static class Program
|
|||
case "--yt-desc-col":
|
||||
descCol = args[++i];
|
||||
break;
|
||||
case "--album-track-count-col":
|
||||
trackCountCol = args[++i];
|
||||
break;
|
||||
case "--yt-id-col":
|
||||
ytIdCol = args[++i];
|
||||
break;
|
||||
|
@ -630,10 +634,19 @@ static class Program
|
|||
case "--atc":
|
||||
case "--album-track-count":
|
||||
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));
|
||||
}
|
||||
else if (a.Last() == '+')
|
||||
{
|
||||
minAlbumTrackCount = int.Parse(a.Substring(0, a.Length - 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
minAlbumTrackCount = int.Parse(a);
|
||||
|
@ -917,7 +930,7 @@ static class Program
|
|||
if (debugDisableDownload)
|
||||
maxConcurrentProcesses = 1;
|
||||
|
||||
ignoreOn = ignoreOn > deprioritizeOn ? deprioritizeOn : ignoreOn;
|
||||
ignoreOn = Math.Min(ignoreOn, deprioritizeOn);
|
||||
|
||||
if (inputType == "youtube" || (inputType == "" && input.StartsWith("http") && input.Contains("youtu")))
|
||||
{
|
||||
|
@ -1192,7 +1205,7 @@ static class Program
|
|||
if (!File.Exists(csvPath))
|
||||
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();
|
||||
trackLists = TrackLists.FromFlatList(tracks, aggregate, album);
|
||||
defaultFolderName = Path.GetFileNameWithoutExtension(csvPath);
|
||||
|
@ -2125,7 +2138,7 @@ static class Program
|
|||
}
|
||||
|
||||
if (nameFormat != "" && !useYtdlp)
|
||||
saveFilePath = ApplyNamingFormat(saveFilePath);
|
||||
saveFilePath = ApplyNamingFormat(saveFilePath, track);
|
||||
|
||||
return saveFilePath;
|
||||
}
|
||||
|
@ -2238,10 +2251,22 @@ static class Program
|
|||
x.Item2.Count(x => Utils.IsMusicFile(x.file.Filename))
|
||||
).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
|
||||
.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 => {
|
||||
var t = new Track
|
||||
{
|
||||
|
@ -3291,7 +3316,7 @@ static class Program
|
|||
|
||||
|
||||
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>();
|
||||
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() != ""))
|
||||
header = parser.ReadNextRow();
|
||||
|
||||
string[] cols = { artistCol, albumCol, trackCol, lengthCol, descCol, ytIdCol };
|
||||
string[] cols = { artistCol, albumCol, trackCol, lengthCol, descCol, ytIdCol, trackCountCol };
|
||||
string[][] aliases = {
|
||||
new[] { "artist", "artist name", "artists", "artist names" },
|
||||
new[] { "album", "album name", "album title" },
|
||||
new[] { "title", "song", "track title", "track name", "song name", "track" },
|
||||
new[] { "length", "duration", "track length", "track duration", "song length", "song duration" },
|
||||
new[] { "description", "youtube description" },
|
||||
new[] { "id", "youtube id", "url" }
|
||||
new[] { "url", "id", "youtube id" },
|
||||
new[] { "track count", "album track count" }
|
||||
};
|
||||
|
||||
string usingColumns = "";
|
||||
|
@ -3338,8 +3364,8 @@ static class Program
|
|||
throw new Exception("No columns specified and couldn't determine automatically");
|
||||
|
||||
int[] indices = cols.Select(col => col == "" ? -1 : header.IndexOf(col)).ToArray();
|
||||
int artistIndex, albumIndex, trackIndex, lengthIndex, descIndex, ytIdIndex;
|
||||
(artistIndex, albumIndex, trackIndex, lengthIndex, descIndex, ytIdIndex) = (indices[0], indices[1], indices[2], indices[3], indices[4], indices[5]);
|
||||
int artistIndex, albumIndex, trackIndex, lengthIndex, descIndex, ytIdIndex, trackCountIndex;
|
||||
(artistIndex, albumIndex, trackIndex, lengthIndex, descIndex, ytIdIndex, trackCountIndex) = (indices[0], indices[1], indices[2], indices[3], indices[4], indices[5], indices[6]);
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
@ -3359,6 +3385,28 @@ static class Program
|
|||
if (albumIndex >= 0) track.Album = values[albumIndex];
|
||||
if (descIndex >= 0) desc = values[descIndex];
|
||||
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)
|
||||
{
|
||||
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))
|
||||
return filepath;
|
||||
|
||||
string add = Path.GetRelativePath(outputFolder, Path.GetDirectoryName(filepath));
|
||||
string newFilePath = NamingFormat(filepath, nameFormat);
|
||||
string newFilePath = NamingFormat(filepath, nameFormat, track);
|
||||
if (filepath != newFilePath)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath));
|
||||
|
@ -3487,7 +3535,7 @@ static class Program
|
|||
return newFilePath;
|
||||
}
|
||||
|
||||
static string NamingFormat(string filepath, string format)
|
||||
static string NamingFormat(string filepath, string format, Track track)
|
||||
{
|
||||
string newName = format;
|
||||
TagLib.File? file = null;
|
||||
|
@ -3511,7 +3559,7 @@ static class Program
|
|||
{
|
||||
string[] parts = Regex.Split(opt, @"\([^\)]*\)");
|
||||
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;
|
||||
break;
|
||||
}
|
||||
|
@ -3522,7 +3570,7 @@ static class Program
|
|||
if (match.Value.StartsWith("(") && match.Value.EndsWith(")"))
|
||||
return match.Value.Substring(1, match.Value.Length-2);
|
||||
else
|
||||
return GetTagValue(file, match.Value);
|
||||
return GetVarValue(match.Value, file, track);
|
||||
});
|
||||
string old = match.Groups[1].Value;
|
||||
old = old.StartsWith("{{") ? old.Substring(1) : old;
|
||||
|
@ -3548,22 +3596,29 @@ static class Program
|
|||
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":
|
||||
return (file.Tag.FirstPerformer ?? "").RemoveFt();
|
||||
return file.Tag.FirstPerformer ?? "";
|
||||
case "artists":
|
||||
return string.Join(" & ", file.Tag.Performers).RemoveFt();
|
||||
case "album_artist":
|
||||
return (file.Tag.FirstAlbumArtist ?? "").RemoveFt();
|
||||
case "album_artists":
|
||||
return string.Join(" & ", file.Tag.AlbumArtists).RemoveFt();
|
||||
return string.Join(" & ", file.Tag.Performers);
|
||||
case "albumartist":
|
||||
return file.Tag.FirstAlbumArtist ?? "";
|
||||
case "albumartists":
|
||||
return string.Join(" & ", file.Tag.AlbumArtists);
|
||||
case "title":
|
||||
return file.Tag.Title ?? "";
|
||||
case "album":
|
||||
return file.Tag.Album ?? "";
|
||||
case "sartist":
|
||||
case "sartists":
|
||||
return track.Artist;
|
||||
case "stitle":
|
||||
return track.Title;
|
||||
case "salbum":
|
||||
return track.Album;
|
||||
case "year":
|
||||
return file.Tag.Year.ToString() ?? "";
|
||||
case "track":
|
||||
|
@ -3572,7 +3627,7 @@ static class Program
|
|||
return file.Tag.Disc.ToString() ?? "";
|
||||
case "filename":
|
||||
return Path.GetFileNameWithoutExtension(file.Name);
|
||||
case "default_foldername":
|
||||
case "foldername":
|
||||
return defaultFolderName;
|
||||
default:
|
||||
return "";
|
||||
|
@ -3581,34 +3636,66 @@ static class Program
|
|||
|
||||
static bool TrackMatchesFilename(Track track, string filename)
|
||||
{
|
||||
string[] ignore = new string[] { " ", "_", "-", ".", "(", ")" };
|
||||
string searchName = track.Title.Replace(ignore, "").ToLower();
|
||||
searchName = searchName.ReplaceInvalidChars("").RemoveFt().RemoveSquareBrackets();
|
||||
searchName = searchName == "" ? track.Title : searchName;
|
||||
if (track.Title.Trim() == "" || filename.Trim() == "")
|
||||
return false;
|
||||
|
||||
string searchName2 = "";
|
||||
if (searchName.Length <= 3) {
|
||||
searchName2 = track.Artist.Replace(ignore, "").ToLower();
|
||||
searchName2 = searchName2.ReplaceInvalidChars("").RemoveFt().RemoveSquareBrackets();
|
||||
searchName2 = searchName2 == "" ? track.Artist : searchName2;
|
||||
string[] ignore = new string[] { " ", "_", "-", ".", "(", ")", "[", "]" };
|
||||
|
||||
string preprocess1(string s, bool removeSlash = true)
|
||||
{
|
||||
s = s.ReplaceInvalidChars("", false, removeSlash).Replace(ignore, "").ToLower();
|
||||
s = s.RemoveFt().RemoveDiacritics();
|
||||
return s;
|
||||
}
|
||||
|
||||
string fullpath = filename;
|
||||
filename = Path.GetFileNameWithoutExtension(filename);
|
||||
filename = filename.ReplaceInvalidChars("");
|
||||
filename = filename.Replace(ignore, "").ToLower();
|
||||
string preprocess2(string s, bool removeSlash = true)
|
||||
{
|
||||
s = s.ReplaceInvalidChars("", false, removeSlash).ToLower().RemoveDiacritics();
|
||||
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;
|
||||
}
|
||||
else if ((track.ArtistMaybeWrong || track.Artist == "") && track.Title.Contains(" - "))
|
||||
{
|
||||
searchName = track.Title.Substring(track.Title.IndexOf(" - ") + 3).Replace(ignore, "").ToLower();
|
||||
searchName = searchName.ReplaceInvalidChars("").RemoveFt().RemoveSquareBrackets();
|
||||
if (searchName != "")
|
||||
title = preprocess1(track.Title.Substring(track.Title.IndexOf(" - ") + 3));
|
||||
if (title != "")
|
||||
{
|
||||
if (filename.Contains(searchName))
|
||||
if (preprocess1(filename, false).Contains(title))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -4372,6 +4459,8 @@ public struct Track
|
|||
public int Length = -1;
|
||||
public bool ArtistMaybeWrong = false;
|
||||
public bool IsAlbum = false;
|
||||
public int MinAlbumTrackCount = -1;
|
||||
public int MaxAlbumTrackCount = -1;
|
||||
public bool IsNotAudio = false;
|
||||
public string FailureReason = "";
|
||||
public string DownloadPath = "";
|
||||
|
@ -4406,6 +4495,8 @@ public struct Track
|
|||
FailureReason = other.FailureReason;
|
||||
DownloadPath = other.DownloadPath;
|
||||
Other = other.Other;
|
||||
MinAlbumTrackCount = other.MinAlbumTrackCount;
|
||||
MaxAlbumTrackCount = other.MaxAlbumTrackCount;
|
||||
}
|
||||
|
||||
public override readonly string ToString()
|
||||
|
|
Loading…
Reference in a new issue