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.
#### 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 <input> [OPTIONS]
@ -107,6 +109,9 @@ Options:
--pref-max-samplerate <rate> Preferred maximum sample rate (default: 96000)
--pref-strict-artist Prefer download if filepath contains track artist
--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
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 <num> 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.

View file

@ -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 <rate> Preferred maximum sample rate (default: 96000)" +
"\n --pref-strict-artist Prefer download if filepath contains track artist" +
"\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 -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 <num> 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)