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-04-27 21:46:35 +02:00
parent 78177932ee
commit 39eccadd2e
2 changed files with 82 additions and 59 deletions

View file

@ -2,40 +2,42 @@
A batch downloader for Soulseek built with Soulseek.NET. Accepts CSV files or Spotify and YouTube urls. 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 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 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. 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" 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. 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" 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 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" slsk-batchdl "artist=MC MENTAL" --aggregate --print tracks-full --skip-existing --music-dir "path\to\music"
``` ```
### Options ## Options
``` ```
Usage: slsk-batchdl <input> [OPTIONS] Usage: slsk-batchdl <input> [OPTIONS]
@ -107,6 +109,9 @@ Options:
--pref-max-samplerate <rate> Preferred maximum sample rate (default: 96000) --pref-max-samplerate <rate> Preferred maximum sample rate (default: 96000)
--pref-strict-artist Prefer download if filepath contains track artist --pref-strict-artist Prefer download if filepath contains track artist
--pref-banned-users <list> Comma-separated list of users to deprioritize --pref-banned-users <list> 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 -a --aggregate When input is a string: Instead of downloading a single
track matching the search string, find and download all track matching the search string, find and download all
@ -122,7 +127,7 @@ Options:
--interactive When downloading albums: Allows to select the wanted album --interactive When downloading albums: Allows to select the wanted album
--album-track-count <num> Specify the exact number of tracks in the album. Folders --album-track-count <num> Specify the exact number of tracks in the album. Folders
with a different number of tracks will be ignored. Append 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 --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 skip to the next source and do not delete all successfully
downloaded files downloaded files
@ -179,17 +184,18 @@ Options:
'results-full': Print search results including full paths 'results-full': Print search results including full paths
--debug Print extra debug info --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 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. 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. 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} {artist( - )title|album_artist( - )title|filename}
{album(/)}{track(. )}{artist|(unknown artist)} - {title|(unknown title)} {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: Create a file named `slsk-batchdl.conf` in the same directory as the executable and write your arguments there, e.g:
``` ```
--username "fakename" --username "fakename"
@ -197,7 +203,7 @@ Create a file named `slsk-batchdl.conf` in the same directory as the executable
--pref-format "flac" --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` - 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. - 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. - `--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.

View file

@ -4,6 +4,7 @@ using Soulseek;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Data; using System.Data;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using TagLib;
using ProgressBar = Konsole.ProgressBar; using ProgressBar = Konsole.ProgressBar;
using SearchResponse = Soulseek.SearchResponse; using SearchResponse = Soulseek.SearchResponse;
@ -159,7 +160,6 @@ static class Program
static object consoleLock = new object(); static object consoleLock = new object();
static DateTime lastUpdate;
static bool skipUpdate = false; static bool skipUpdate = false;
static bool debugDisableDownload = false; static bool debugDisableDownload = false;
static bool debugPrintTracks = false; static bool debugPrintTracks = false;
@ -265,6 +265,9 @@ static class Program
"\n --pref-max-samplerate <rate> Preferred maximum sample rate (default: 96000)" + "\n --pref-max-samplerate <rate> Preferred maximum sample rate (default: 96000)" +
"\n --pref-strict-artist Prefer download if filepath contains track artist" + "\n --pref-strict-artist Prefer download if filepath contains track artist" +
"\n --pref-banned-users <list> Comma-separated list of users to deprioritize" + "\n --pref-banned-users <list> 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" +
"\n -a --aggregate When input is a string: Instead of downloading a single" + "\n -a --aggregate When input is a string: Instead of downloading a single" +
"\n track matching the search string, find and download all" + "\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 --interactive When downloading albums: Allows to select the wanted album" +
"\n --album-track-count <num> Specify the exact number of tracks in the album. Folders" + "\n --album-track-count <num> Specify the exact number of tracks in the album. Folders" +
"\n with a different number of tracks will be ignored. Append" + "\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 --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 skip to the next source and do not delete all successfully" +
"\n downloaded files" + "\n downloaded files" +
@ -359,7 +362,7 @@ static class Program
} }
bool confPathChanged = false; bool confPathChanged = false;
int idx = Array.IndexOf(args, "--config"); int idx = Array.LastIndexOf(args, "--config");
if (idx != -1) if (idx != -1)
{ {
confPath = args[idx + 1]; confPath = args[idx + 1];
@ -375,6 +378,18 @@ static class Program
args = finalArgs.ToArray(); 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++) for (int i = 0; i < args.Length; i++)
{ {
if (args[i].StartsWith("-")) if (args[i].StartsWith("-"))
@ -693,6 +708,10 @@ static class Program
case "--debug": case "--debug":
debugInfo = true; debugInfo = true;
break; break;
case "--strict":
preferredCond.AcceptMissingProps = false;
necessaryCond.AcceptMissingProps = false;
break;
default: default:
throw new ArgumentException($"Unknown argument: {args[i]}"); throw new ArgumentException($"Unknown argument: {args[i]}");
} }
@ -1475,7 +1494,7 @@ static class Program
{ {
foreach (var res in ytResults) foreach (var res in ytResults)
{ {
if (necessaryCond.LengthToleranceSatisfies(track, res.length)) if (necessaryCond.LengthToleranceSatisfies(res.length, track.Length))
{ {
string saveFilePathNoExt = GetSavePathNoExt(res.title, track); string saveFilePathNoExt = GetSavePathNoExt(res.title, track);
downloading = true; downloading = true;
@ -1802,9 +1821,9 @@ static class Program
.ThenByDescending(x => preferredCond.FileSatisfies(x.file, track, x.response)) .ThenByDescending(x => preferredCond.FileSatisfies(x.file, track, x.response))
.ThenByDescending(x => x.response.HasFreeUploadSlot) .ThenByDescending(x => x.response.HasFreeUploadSlot)
.ThenByDescending(x => x.response.UploadSpeed / 600) .ThenByDescending(x => x.response.UploadSpeed / 600)
.ThenByDescending(x => albumMode || FileConditions.StrictString(x.file.Filename, track.TrackTitle)) .ThenByDescending(x => albumMode || FileConditions.StrictString(x.file.Filename, track.TrackTitle, ignoreCase: true))
.ThenByDescending(x => !albumMode || FileConditions.StrictString(GetDirectoryNameSlsk(x.file.Filename), track.Album)) .ThenByDescending(x => !albumMode || FileConditions.StrictString(GetDirectoryNameSlsk(x.file.Filename), track.Album, ignoreCase: true))
.ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.ArtistName)) .ThenByDescending(x => FileConditions.StrictString(x.file.Filename, track.ArtistName, ignoreCase: true))
.ThenByDescending(x => !useLevenshtein || levenshtein(x) <= 5) .ThenByDescending(x => !useLevenshtein || levenshtein(x) <= 5)
.ThenByDescending(x => x.response.UploadSpeed / 300) .ThenByDescending(x => x.response.UploadSpeed / 300)
.ThenByDescending(x => (x.file.BitRate ?? 0) / 70) .ThenByDescending(x => (x.file.BitRate ?? 0) / 70)
@ -2140,8 +2159,6 @@ static class Program
{ {
while (true) while (true)
{ {
lastUpdate = DateTime.Now;
if (!skipUpdate) if (!skipUpdate)
{ {
try try
@ -2329,6 +2346,7 @@ static class Program
public string StrictStringRegexRemove = ""; public string StrictStringRegexRemove = "";
public bool StrictStringDiacrRemove = true; public bool StrictStringDiacrRemove = true;
public bool AcceptNoLength = false; public bool AcceptNoLength = false;
public bool AcceptMissingProps = true;
public FileConditions() { } public FileConditions() { }
@ -2394,7 +2412,7 @@ static class Program
return true; return true;
fname = noPath ? GetFileNameWithoutExtSlsk(fname) : fname; 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) public bool StrictArtistSatisfies(string fname, string aname)
@ -2402,7 +2420,7 @@ static class Program
if (!StrictArtist || aname == "") if (!StrictArtist || aname == "")
return true; return true;
return StrictString(fname, aname, StrictStringRegexRemove, StrictStringDiacrRemove); return StrictString(fname, aname, StrictStringRegexRemove, StrictStringDiacrRemove, ignoreCase: true);
} }
public bool StrictAlbumSatisfies(string fname, string alname) public bool StrictAlbumSatisfies(string fname, string alname)
@ -2410,10 +2428,10 @@ static class Program
if (!StrictAlbum || alname == "") if (!StrictAlbum || alname == "")
return true; 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)) if (string.IsNullOrEmpty(tname))
return true; return true;
@ -2436,66 +2454,65 @@ static class Program
return Formats.Length == 0 || (ext != "" && Formats.Any(f => f == ext)); 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 LengthToleranceSatisfies(file.Length, wantedLength);
return true;
if (file.Length == null)
return AcceptNoLength;
return Math.Abs((int)file.Length - actualLength) <= LengthTolerance;
} }
public bool LengthToleranceSatisfies(TagLib.File file, int actualLength) public bool LengthToleranceSatisfies(TagLib.File file, int wantedLength)
{ {
if (LengthTolerance < 0 || actualLength < 0) return LengthToleranceSatisfies((int)file.Properties.Duration.TotalSeconds, wantedLength);
return true;
int fileLength = (int)file.Properties.Duration.TotalSeconds;
if (Math.Abs(fileLength - actualLength) <= LengthTolerance)
return true;
return false;
} }
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; return true;
if (Math.Abs(track.Length - actualLength) <= LengthTolerance) if (length == null || length < 0)
return true; return AcceptNoLength && AcceptMissingProps;
return false; return Math.Abs((int)length - wantedLength) <= LengthTolerance;
} }
public bool BitrateSatisfies(Soulseek.File file) public bool BitrateSatisfies(Soulseek.File file)
{ {
if ((MinBitrate < 0 && MaxBitrate < 0) || file.BitRate == null) return BitrateSatisfies(file.BitRate);
return true;
if (MinBitrate >= 0 && file.BitRate.Value < MinBitrate)
return false;
if (MaxBitrate >= 0 && file.BitRate.Value > MaxBitrate)
return false;
return true;
} }
public bool BitrateSatisfies(TagLib.File file) public bool BitrateSatisfies(TagLib.File file)
{ {
if ((MinBitrate < 0 && MaxBitrate < 0) || file.Properties.AudioBitrate <= 0) return BitrateSatisfies(file.Properties.AudioBitrate);
return true; }
if (MinBitrate >= 0 && file.Properties.AudioBitrate < MinBitrate)
return false;
if (MaxBitrate >= 0 && file.Properties.AudioBitrate > MaxBitrate)
return false;
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; return true;
} }
public bool SampleRateSatisfies(Soulseek.File file) 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) 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) public bool BannedUsersSatisfies(SearchResponse? response)