From 39eccadd2efa300725320516815fcc48dac8fc18 Mon Sep 17 00:00:00 2001 From: fiso64 Date: Sat, 27 Apr 2024 21:46:35 +0200 Subject: [PATCH] --strict --- README.md | 30 ++++++----- slsk-batchdl/Program.cs | 111 +++++++++++++++++++++++----------------- 2 files changed, 82 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 3cc1b4c..869f5ca 100644 --- a/README.md +++ b/README.md @@ -2,40 +2,42 @@ A batch downloader for Soulseek built with Soulseek.NET. Accepts CSV files or Spotify and YouTube urls. -#### Download tracks from a csv file: +## Examples + +### Download tracks from a csv file: ``` slsk-batchdl test.csv ``` -Use `--print tracks` before downloading to check if everything has been parsed correctly. The names of the columns in the csv should be: `Artist`, `Title`, `Album`, `Length`, though alternative names are sometimes inferred as well. Only the title column is required, but any additional info improves search results. +The names of the columns in the csv should be: `Artist`, `Title`, `Album`, `Length`, though alternatives can sometimes be inferred as well. You can use `--print tracks` before downloading to check if everything has been parsed correctly. Only the title column is required, but any additional info improves search results. -#### Download spotify likes while skipping existing songs: +### Download spotify likes while skipping existing songs: ``` slsk-batchdl spotify-likes --skip-existing ``` To download private playlists or liked songs you will need to provide a client id and secret, which you can get here https://developer.spotify.com/dashboard/applications. Create an app and add `http://localhost:48721/callback` as a redirect url in its settings. -#### Download youtube playlist (with fallback to yt-dlp), including deleted videos: +### Download from youtube playlist (w. yt-dlp fallback), including deleted videos: ``` slsk-batchdl --get-deleted --yt-dlp "https://www.youtube.com/playlist?list=PLI_eFW8NAFzYAXZ5DrU6E6mQ_XfhaLBUX" ``` Playlists are retrieved using the YoutubeExplode library which unfortunately doesn't always return all videos. You can use the official API by providing a key with `--youtube-key`. Get it here https://console.cloud.google.com. Create a new project, click "Enable Api" and search for "youtube data", then follow the prompts. -#### Search & download a specific song: +### Search & download a specific song: ``` slsk-batchdl "title=MC MENTAL @ HIS BEST,length=242" --pref-format "flac,wav" ``` -#### Interactive album download: +### Interactive album download: ``` slsk-batchdl "album=Some Album" --interactive ``` -#### See which songs by an artist are missing in your library: +### Find an artist's songs that aren't in your library: ``` slsk-batchdl "artist=MC MENTAL" --aggregate --print tracks-full --skip-existing --music-dir "path\to\music" ``` -### Options +## Options ``` Usage: slsk-batchdl [OPTIONS] @@ -107,6 +109,9 @@ Options: --pref-max-samplerate Preferred maximum sample rate (default: 96000) --pref-strict-artist Prefer download if filepath contains track artist --pref-banned-users Comma-separated list of users to deprioritize + --strict Skip files with missing properties instead of accepting by + default; if --min-bitrate is set, ignores any files with + unknown bitrate. -a --aggregate When input is a string: Instead of downloading a single track matching the search string, find and download all @@ -122,7 +127,7 @@ Options: --interactive When downloading albums: Allows to select the wanted album --album-track-count Specify the exact number of tracks in the album. Folders with a different number of tracks will be ignored. Append - a '+' or '-' to the number for the inequalities >= and <=. + a '+' or '-' after the number for the inequalities >= and <= --album-ignore-fails When downloading an album and one of the files fails, do not skip to the next source and do not delete all successfully downloaded files @@ -179,17 +184,18 @@ Options: 'results-full': Print search results including full paths --debug Print extra debug info ``` +### File conditions: Files not satisfying the conditions will not be downloaded. For example, `--length-tol` is set to 3 by default, meaning that files whose duration differs from the supplied duration by more than 3 seconds will not be downloaded (disable it by setting it to 99999). Files satisfying `pref-` conditions will be preferred. For example, setting `--pref-format "flac,wav"` will make it download high quality files if they exist and only download low quality files if there's nothing else. -#### 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. ``` {artist( - )title|album_artist( - )title|filename} {album(/)}{track(. )}{artist|(unknown artist)} - {title|(unknown title)} ``` -### Configuration files +## Configuration files Create a file named `slsk-batchdl.conf` in the same directory as the executable and write your arguments there, e.g: ``` --username "fakename" @@ -197,7 +203,7 @@ Create a file named `slsk-batchdl.conf` in the same directory as the executable --pref-format "flac" ``` -### Notes +## Notes - For macOS builds you can use publish.sh to build the app. Download dotnet from https://dotnet.microsoft.com/en-us/download/dotnet/6.0, then run `chmod +x publish.sh && sh publish.sh` - The CSV file must use `"` as string delimiter and be encoded with UTF8. - `--display single` and especially `double` can cause the printed lines to be duplicated or overwritten on some configurations. Use `simple` if that's an issue. diff --git a/slsk-batchdl/Program.cs b/slsk-batchdl/Program.cs index 44d04fa..4e2aaf7 100644 --- a/slsk-batchdl/Program.cs +++ b/slsk-batchdl/Program.cs @@ -4,6 +4,7 @@ using Soulseek; using System.Collections.Concurrent; using System.Data; using System.Text.RegularExpressions; +using TagLib; using ProgressBar = Konsole.ProgressBar; using SearchResponse = Soulseek.SearchResponse; @@ -159,7 +160,6 @@ static class Program static object consoleLock = new object(); - static DateTime lastUpdate; static bool skipUpdate = false; static bool debugDisableDownload = false; static bool debugPrintTracks = false; @@ -265,6 +265,9 @@ static class Program "\n --pref-max-samplerate Preferred maximum sample rate (default: 96000)" + "\n --pref-strict-artist Prefer download if filepath contains track artist" + "\n --pref-banned-users Comma-separated list of users to deprioritize" + + "\n --strict Skip files with missing properties instead of accepting by" + + "\n default; if --min-bitrate is set, ignores any files with" + + "\n unknown bitrate." + "\n" + "\n -a --aggregate When input is a string: Instead of downloading a single" + "\n track matching the search string, find and download all" + @@ -280,7 +283,7 @@ static class Program "\n --interactive When downloading albums: Allows to select the wanted album" + "\n --album-track-count Specify the exact number of tracks in the album. Folders" + "\n with a different number of tracks will be ignored. Append" + - "\n a '+' or '-' to the number for the inequalities >= and <=." + + "\n a '+' or '-' after the number for the inequalities >= and <=" + "\n --album-ignore-fails When downloading an album and one of the files fails, do not" + "\n skip to the next source and do not delete all successfully" + "\n downloaded files" + @@ -359,7 +362,7 @@ static class Program } bool confPathChanged = false; - int idx = Array.IndexOf(args, "--config"); + int idx = Array.LastIndexOf(args, "--config"); if (idx != -1) { confPath = args[idx + 1]; @@ -375,6 +378,18 @@ static class Program args = finalArgs.ToArray(); } + if (args.Contains("--strict")) + { + preferredCond.AcceptMissingProps = false; + necessaryCond.AcceptMissingProps = false; + preferredCond.MaxBitrate = -1; + necessaryCond.MaxBitrate = -1; + preferredCond.MinBitrate = -1; + necessaryCond.MinBitrate = -1; + preferredCond.MaxSampleRate = -1; + necessaryCond.MaxSampleRate = -1; + } + for (int i = 0; i < args.Length; i++) { if (args[i].StartsWith("-")) @@ -693,6 +708,10 @@ static class Program case "--debug": debugInfo = true; break; + case "--strict": + preferredCond.AcceptMissingProps = false; + necessaryCond.AcceptMissingProps = false; + break; default: throw new ArgumentException($"Unknown argument: {args[i]}"); } @@ -1475,7 +1494,7 @@ static class Program { foreach (var res in ytResults) { - if (necessaryCond.LengthToleranceSatisfies(track, res.length)) + if (necessaryCond.LengthToleranceSatisfies(res.length, track.Length)) { string saveFilePathNoExt = GetSavePathNoExt(res.title, track); downloading = true; @@ -1802,9 +1821,9 @@ static class Program .ThenByDescending(x => preferredCond.FileSatisfies(x.file, track, x.response)) .ThenByDescending(x => x.response.HasFreeUploadSlot) .ThenByDescending(x => x.response.UploadSpeed / 600) - .ThenByDescending(x => albumMode || FileConditions.StrictString(x.file.Filename, track.TrackTitle)) - .ThenByDescending(x => !albumMode || FileConditions.StrictString(GetDirectoryNameSlsk(x.file.Filename), track.Album)) - .ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.ArtistName)) + .ThenByDescending(x => albumMode || FileConditions.StrictString(x.file.Filename, track.TrackTitle, ignoreCase: true)) + .ThenByDescending(x => !albumMode || FileConditions.StrictString(GetDirectoryNameSlsk(x.file.Filename), track.Album, ignoreCase: true)) + .ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.ArtistName, ignoreCase: true)) .ThenByDescending(x => !useLevenshtein || levenshtein(x) <= 5) .ThenByDescending(x => x.response.UploadSpeed / 300) .ThenByDescending(x => (x.file.BitRate ?? 0) / 70) @@ -2140,8 +2159,6 @@ static class Program { while (true) { - lastUpdate = DateTime.Now; - if (!skipUpdate) { try @@ -2329,6 +2346,7 @@ static class Program public string StrictStringRegexRemove = ""; public bool StrictStringDiacrRemove = true; public bool AcceptNoLength = false; + public bool AcceptMissingProps = true; public FileConditions() { } @@ -2394,7 +2412,7 @@ static class Program return true; fname = noPath ? GetFileNameWithoutExtSlsk(fname) : fname; - return StrictString(fname, tname, StrictStringRegexRemove, StrictStringDiacrRemove); + return StrictString(fname, tname, StrictStringRegexRemove, StrictStringDiacrRemove, ignoreCase: true); } public bool StrictArtistSatisfies(string fname, string aname) @@ -2402,7 +2420,7 @@ static class Program if (!StrictArtist || aname == "") return true; - return StrictString(fname, aname, StrictStringRegexRemove, StrictStringDiacrRemove); + return StrictString(fname, aname, StrictStringRegexRemove, StrictStringDiacrRemove, ignoreCase: true); } public bool StrictAlbumSatisfies(string fname, string alname) @@ -2410,10 +2428,10 @@ static class Program if (!StrictAlbum || alname == "") return true; - return StrictString(GetDirectoryNameSlsk(fname), alname, StrictStringRegexRemove, StrictStringDiacrRemove); + return StrictString(GetDirectoryNameSlsk(fname), alname, StrictStringRegexRemove, StrictStringDiacrRemove, ignoreCase: true); } - public static bool StrictString(string fname, string tname, string regexRemove = "", bool diacrRemove = true, bool ignoreCase = false) + public static bool StrictString(string fname, string tname, string regexRemove = "", bool diacrRemove = true, bool ignoreCase = true) { if (string.IsNullOrEmpty(tname)) return true; @@ -2436,66 +2454,65 @@ static class Program return Formats.Length == 0 || (ext != "" && Formats.Any(f => f == ext)); } - public bool LengthToleranceSatisfies(Soulseek.File file, int actualLength) + public bool LengthToleranceSatisfies(Soulseek.File file, int wantedLength) { - if (LengthTolerance < 0 || actualLength < 0) - return true; - if (file.Length == null) - return AcceptNoLength; - return Math.Abs((int)file.Length - actualLength) <= LengthTolerance; + return LengthToleranceSatisfies(file.Length, wantedLength); } - public bool LengthToleranceSatisfies(TagLib.File file, int actualLength) + public bool LengthToleranceSatisfies(TagLib.File file, int wantedLength) { - if (LengthTolerance < 0 || actualLength < 0) - return true; - int fileLength = (int)file.Properties.Duration.TotalSeconds; - if (Math.Abs(fileLength - actualLength) <= LengthTolerance) - return true; - return false; + return LengthToleranceSatisfies((int)file.Properties.Duration.TotalSeconds, wantedLength); } - public bool LengthToleranceSatisfies(Track track, int actualLength) + public bool LengthToleranceSatisfies(int? length, int wantedLength) { - if (LengthTolerance < 0 || actualLength < 0 || track.Length < 0) + if (LengthTolerance < 0 || wantedLength < 0) return true; - if (Math.Abs(track.Length - actualLength) <= LengthTolerance) - return true; - return false; + if (length == null || length < 0) + return AcceptNoLength && AcceptMissingProps; + return Math.Abs((int)length - wantedLength) <= LengthTolerance; } public bool BitrateSatisfies(Soulseek.File file) { - if ((MinBitrate < 0 && MaxBitrate < 0) || file.BitRate == null) - return true; - if (MinBitrate >= 0 && file.BitRate.Value < MinBitrate) - return false; - if (MaxBitrate >= 0 && file.BitRate.Value > MaxBitrate) - return false; - - return true; + return BitrateSatisfies(file.BitRate); } public bool BitrateSatisfies(TagLib.File file) { - if ((MinBitrate < 0 && MaxBitrate < 0) || file.Properties.AudioBitrate <= 0) - return true; - if (MinBitrate >= 0 && file.Properties.AudioBitrate < MinBitrate) - return false; - if (MaxBitrate >= 0 && file.Properties.AudioBitrate > MaxBitrate) - return false; + return BitrateSatisfies(file.Properties.AudioBitrate); + } + public bool BitrateSatisfies(int? bitrate) + { + if (MinBitrate < 0 && MaxBitrate < 0) + return true; + if (bitrate == null || bitrate < 0) + return AcceptMissingProps; + if (MinBitrate >= 0 && bitrate < MinBitrate) + return false; + if (MaxBitrate >= 0 && bitrate > MaxBitrate) + return false; return true; } public bool SampleRateSatisfies(Soulseek.File file) { - return MaxSampleRate < 0 || file.SampleRate == null || file.SampleRate.Value <= MaxSampleRate; + return SampleRateSatisfies(file.SampleRate); } public bool SampleRateSatisfies(TagLib.File file) { - return MaxSampleRate < 0 || file.Properties.AudioSampleRate <= MaxSampleRate; + return SampleRateSatisfies(file.Properties.AudioSampleRate); + } + + public bool SampleRateSatisfies(int? sampleRate) + { + if (MaxSampleRate < 0) + return true; + if (sampleRate == null || sampleRate < 0) + return AcceptMissingProps; + return sampleRate <= MaxSampleRate; } public bool BannedUsersSatisfies(SearchResponse? response)