mirror of
https://github.com/fiso64/slsk-batchdl.git
synced 2024-12-22 14:32:40 +00:00
added spotify url option
This commit is contained in:
parent
b859d02a16
commit
7245c4179b
5 changed files with 288 additions and 61 deletions
35
README.md
35
README.md
|
@ -1,15 +1,20 @@
|
||||||
# slsk-batchdl
|
# slsk-batchdl
|
||||||
|
|
||||||
A barely-functional batch downloader for Soulseek using Soulseek.NET. Accepts csv files.
|
A batch downloader for Soulseek using Soulseek.NET. Accepts csv files and spotify playlist urls.
|
||||||
|
|
||||||
```
|
```
|
||||||
Usage: slsk-batchdl.exe [OPTIONS]
|
Usage: slsk-batchdl.exe [OPTIONS]
|
||||||
Options:
|
Options:
|
||||||
--output <path> Downloaded files will be placed here
|
-p --parent <path> Downloaded music will be placed here
|
||||||
--csv <path> The csv file containing track information (in case it's not in the output folder)
|
-n --name <name> Folder / playlist name. If not specified, the name of the csv file / spotify playlist is used.
|
||||||
--username <username> Soulseek username
|
--username <username> Soulseek username
|
||||||
--password <password> Soulseek password
|
--password <password> Soulseek password
|
||||||
|
|
||||||
|
--spotify <url> Download a spotify playlist
|
||||||
|
--spotify-id <id> Your spotify client id (in case the default one failed)
|
||||||
|
--spotify-secret <sec> Your spotify client secret (in case the default one failed)
|
||||||
|
|
||||||
|
--csv <path> Use a csv file containing track info to download
|
||||||
--artist-col <column> Specify if the csv file contains an artist name column
|
--artist-col <column> Specify if the csv file contains an artist name column
|
||||||
--track-col <column> Specify if if the csv file contains an track name column
|
--track-col <column> Specify if if the csv file contains an track name column
|
||||||
--full-title-col <column> Specify only if there are no separate artist and track name columns in the csv
|
--full-title-col <column> Specify only if there are no separate artist and track name columns in the csv
|
||||||
|
@ -17,12 +22,6 @@ Options:
|
||||||
--length-col <column> Specify the name of the track duration column, if exists
|
--length-col <column> Specify the name of the track duration column, if exists
|
||||||
--time-unit <unit> Time unit for the track duration column, ms or s (default: s)
|
--time-unit <unit> Time unit for the track duration column, ms or s (default: s)
|
||||||
|
|
||||||
--skip-existing Skip if a track matching the conditions is found in the output folder or your music library (if provided)
|
|
||||||
--music-dir <path> Specify to also skip downloading tracks which are in your library, use with --skip-existing
|
|
||||||
--skip-if-pref-failed Skip if preferred versions of a track exist but failed to download. If no pref. versions were found, download as normal.
|
|
||||||
--create-m3u Create an m3u playlist file in the output dir
|
|
||||||
--m3u-only Only create an m3u playlist file with existing tracks and exit
|
|
||||||
|
|
||||||
--pref-format <format> Preferred file format (default: mp3)
|
--pref-format <format> Preferred file format (default: mp3)
|
||||||
--pref-length-tolerance <tol> Preferred length tolerance (if length col provided) (default: 3)
|
--pref-length-tolerance <tol> Preferred length tolerance (if length col provided) (default: 3)
|
||||||
--pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)
|
--pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)
|
||||||
|
@ -34,18 +33,28 @@ Options:
|
||||||
--nec-max-bitrate <rate> Necessary maximum bitrate
|
--nec-max-bitrate <rate> Necessary maximum bitrate
|
||||||
--nec-max-sample-rate <rate> Necessary maximum sample rate
|
--nec-max-sample-rate <rate> Necessary maximum sample rate
|
||||||
|
|
||||||
|
--skip-existing Skip if a track matching the conditions is found in the output folder or your music library (if provided)
|
||||||
|
--music-dir <path> Specify to also skip downloading tracks which are in your library, use with --skip-existing
|
||||||
|
--skip-if-pref-failed Skip if preferred versions of a track exist but failed to download. If no pref. versions were found, download as normal.
|
||||||
|
--create-m3u Create an m3u playlist file
|
||||||
|
--m3u-only Only create an m3u playlist file with existing tracks and exit
|
||||||
|
--m3u <path> Where to place created m3u files (--parent by default)
|
||||||
|
|
||||||
--search-timeout <timeout> Maximal search time (default: 15000)
|
--search-timeout <timeout> Maximal search time (default: 15000)
|
||||||
--download-max-stale-time <time> Maximal download time with no progress (default: 80000)
|
--download-max-stale-time <time> Maximal download time with no progress (default: 80000)
|
||||||
--max-concurrent-processes <num> Max concurrent searches / downloads (default: 2)
|
--max-concurrent-processes <num> Max concurrent searches / downloads (default: 2)
|
||||||
--max-retries-per-file <num> Maximum number of users to try downloading from before skipping track (default: 30)
|
--max-retries-per-file <num> Maximum number of users to try downloading from before skipping track (default: 30)
|
||||||
```
|
```
|
||||||
- Provide either both a track-col and artist-col (ideally), or full-title-col in case separate artist and track names are unavailable. You can also specify --uploader-col (channel names) in that case to use as artist names whenever full-title-col doesn't contain them.
|
|
||||||
- Always provide a length-col or get wrong results
|
|
||||||
- Files satisfying `pref` conditions will be preferred. Files not satisfying `nec` conditions will not be downloaded.
|
- Files satisfying `pref` conditions will be preferred. Files not satisfying `nec` conditions will not be downloaded.
|
||||||
|
- When using csv, provide either both a track-col and artist-col (ideally), or full-title-col in case separate artist and track names are unavailable. You can also specify --uploader-col (channel names) in that case to use as artist names whenever full-title-col doesn't contain them. Always provide a length-col or get wrong results
|
||||||
|
|
||||||
Example use (with a csv from https://exportify.net/):
|
Download tracks from a csv file and create m3u:
|
||||||
```
|
```
|
||||||
slsk-batchdl.exe --output "C:\Users\fiso64\Music\Playlists\test" --csv "C:\Users\fiso64\Downloads\test.csv" --username "fakename" --password "fakepass" --artist-col "Artist Name(s)" --track-col "Track Name" --length-col "Duration (ms)" --time-unit "ms" --skip-existing --create-m3u --pref-format "flac"
|
slsk-batchdl.exe -p "C:\Users\fiso64\Music\Playlists" --csv "C:\Users\fiso64\Downloads\test.csv" --username "fakename" --password "fakepass" --artist-col "Artist Name(s)" --track-col "Track Name" --length-col "Duration (ms)" --time-unit "ms" --skip-existing --create-m3u --pref-format "flac"
|
||||||
|
```
|
||||||
|
Download spotify playlist and create m3u:
|
||||||
|
```
|
||||||
|
slsk-batchdl.exe --spotify <url> -p "C:\Users\fiso64\Music\Playlists" --m3u "C:\Users\fiso64\Documents\MusicBee\Playlists" --music-dir "C:\Users\fiso64\Music" --username "fakename" --password "fakepass" --skip-existing --pref-format "flac"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes:
|
## Notes:
|
||||||
|
|
|
@ -12,6 +12,7 @@ using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Soulseek;
|
using Soulseek;
|
||||||
|
using Spotify;
|
||||||
using TagLib.Matroska;
|
using TagLib.Matroska;
|
||||||
using static System.Formats.Asn1.AsnWriter;
|
using static System.Formats.Asn1.AsnWriter;
|
||||||
using static System.Net.WebRequestMethods;
|
using static System.Net.WebRequestMethods;
|
||||||
|
@ -37,11 +38,16 @@ class Program
|
||||||
{
|
{
|
||||||
Console.WriteLine("Usage: slsk-batchdl.exe [OPTIONS]");
|
Console.WriteLine("Usage: slsk-batchdl.exe [OPTIONS]");
|
||||||
Console.WriteLine("Options:");
|
Console.WriteLine("Options:");
|
||||||
Console.WriteLine(" --output <path> Downloaded files will be placed here");
|
Console.WriteLine(" -p --parent <path> Downloaded music will be placed here");
|
||||||
Console.WriteLine(" --csv <path> The csv file containing track information (in case it's not in the output folder)");
|
Console.WriteLine(" -n --name <name> Folder / playlist name. If not specified, the name of the csv file / spotify playlist is used.");
|
||||||
Console.WriteLine(" --username <username> Soulseek username");
|
Console.WriteLine(" --username <username> Soulseek username");
|
||||||
Console.WriteLine(" --password <password> Soulseek password");
|
Console.WriteLine(" --password <password> Soulseek password");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(" --spotify <url> Download a spotify playlist");
|
||||||
|
Console.WriteLine(" --spotify-id <id> Your spotify client id (in case the default one failed)");
|
||||||
|
Console.WriteLine(" --spotify-secret <sec> Your spotify client secret (in case the default one failed)");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(" --csv <path> Use a csv file containing track info to download");
|
||||||
Console.WriteLine(" --artist-col <column> Specify if the csv file contains an artist name column");
|
Console.WriteLine(" --artist-col <column> Specify if the csv file contains an artist name column");
|
||||||
Console.WriteLine(" --track-col <column> Specify if if the csv file contains an track name column");
|
Console.WriteLine(" --track-col <column> Specify if if the csv file contains an track name column");
|
||||||
Console.WriteLine(" --full-title-col <column> Specify only if there are no separate artist and track name columns in the csv");
|
Console.WriteLine(" --full-title-col <column> Specify only if there are no separate artist and track name columns in the csv");
|
||||||
|
@ -49,12 +55,6 @@ class Program
|
||||||
Console.WriteLine(" --length-col <column> Specify the name of the track duration column, if exists");
|
Console.WriteLine(" --length-col <column> Specify the name of the track duration column, if exists");
|
||||||
Console.WriteLine(" --time-unit <unit> Time unit for the track duration column, ms or s (default: s)");
|
Console.WriteLine(" --time-unit <unit> Time unit for the track duration column, ms or s (default: s)");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine(" --skip-existing Skip if a track matching the conditions is found in the output folder or your music library (if provided)");
|
|
||||||
Console.WriteLine(" --music-dir <path> Specify to also skip downloading tracks which are in your library, use with --skip-existing");
|
|
||||||
Console.WriteLine(" --skip-if-pref-failed Skip if preferred versions of a track exist but failed to download. If no pref. versions were found, download as normal.");
|
|
||||||
Console.WriteLine(" --create-m3u Create an m3u playlist file in the output dir");
|
|
||||||
Console.WriteLine(" --m3u-only Only create an m3u playlist file with existing tracks and exit");
|
|
||||||
Console.WriteLine();
|
|
||||||
Console.WriteLine(" --pref-format <format> Preferred file format (default: mp3)");
|
Console.WriteLine(" --pref-format <format> Preferred file format (default: mp3)");
|
||||||
Console.WriteLine(" --pref-length-tolerance <tol> Preferred length tolerance (if length col provided) (default: 3)");
|
Console.WriteLine(" --pref-length-tolerance <tol> Preferred length tolerance (if length col provided) (default: 3)");
|
||||||
Console.WriteLine(" --pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)");
|
Console.WriteLine(" --pref-min-bitrate <rate> Preferred minimum bitrate (default: 200)");
|
||||||
|
@ -66,6 +66,13 @@ class Program
|
||||||
Console.WriteLine(" --nec-max-bitrate <rate> Necessary maximum bitrate");
|
Console.WriteLine(" --nec-max-bitrate <rate> Necessary maximum bitrate");
|
||||||
Console.WriteLine(" --nec-max-sample-rate <rate> Necessary maximum sample rate");
|
Console.WriteLine(" --nec-max-sample-rate <rate> Necessary maximum sample rate");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(" --skip-existing Skip if a track matching the conditions is found in the output folder or your music library (if provided)");
|
||||||
|
Console.WriteLine(" --music-dir <path> Specify to also skip downloading tracks which are in your library, use with --skip-existing");
|
||||||
|
Console.WriteLine(" --skip-if-pref-failed Skip if preferred versions of a track exist but failed to download. If no pref. versions were found, download as normal.");
|
||||||
|
Console.WriteLine(" --create-m3u Create an m3u playlist file");
|
||||||
|
Console.WriteLine(" --m3u-only Only create an m3u playlist file with existing tracks and exit");
|
||||||
|
Console.WriteLine(" --m3u <path> Where to place created m3u files (--parent by default)");
|
||||||
|
Console.WriteLine();
|
||||||
Console.WriteLine(" --search-timeout <timeout> Maximal search time (default: 15000)");
|
Console.WriteLine(" --search-timeout <timeout> Maximal search time (default: 15000)");
|
||||||
Console.WriteLine(" --download-max-stale-time <time> Maximal download time with no progress (default: 80000)");
|
Console.WriteLine(" --download-max-stale-time <time> Maximal download time with no progress (default: 80000)");
|
||||||
Console.WriteLine(" --max-concurrent-processes <num> Max concurrent searches / downloads (default: 2)");
|
Console.WriteLine(" --max-concurrent-processes <num> Max concurrent searches / downloads (default: 2)");
|
||||||
|
@ -80,14 +87,19 @@ class Program
|
||||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
lastLine = Console.CursorTop;
|
lastLine = Console.CursorTop;
|
||||||
if (args.Contains("--help") || args.Length <= 1 || !args.Contains("--output"))
|
if (args.Contains("--help"))
|
||||||
{
|
{
|
||||||
PrintHelp();
|
PrintHelp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
outputFolder = "";
|
|
||||||
musicDir = "";
|
musicDir = "";
|
||||||
|
string parentFolder = "";
|
||||||
|
string folderName = "";
|
||||||
|
string spotifyUrl = "";
|
||||||
|
string spotifyId = "1bf4691bbb1a4f41bced9b2c1cfdbbd2";
|
||||||
|
string spotifySecret = "e79992e56f4642169acef68c742303f1";
|
||||||
string tracksCsv = "";
|
string tracksCsv = "";
|
||||||
string username = "";
|
string username = "";
|
||||||
string password = "";
|
string password = "";
|
||||||
|
@ -127,8 +139,13 @@ class Program
|
||||||
{
|
{
|
||||||
switch (args[i])
|
switch (args[i])
|
||||||
{
|
{
|
||||||
case "--output":
|
case "-p":
|
||||||
outputFolder = args[++i];
|
case "--parent":
|
||||||
|
parentFolder = args[++i];
|
||||||
|
break;
|
||||||
|
case "-n":
|
||||||
|
case "--name":
|
||||||
|
folderName = args[++i];
|
||||||
break;
|
break;
|
||||||
case "--music-dir":
|
case "--music-dir":
|
||||||
musicDir = args[++i];
|
musicDir = args[++i];
|
||||||
|
@ -136,6 +153,15 @@ class Program
|
||||||
case "--csv":
|
case "--csv":
|
||||||
tracksCsv = args[++i];
|
tracksCsv = args[++i];
|
||||||
break;
|
break;
|
||||||
|
case "--spotify":
|
||||||
|
spotifyUrl = args[++i];
|
||||||
|
break;
|
||||||
|
case "--spotify-id":
|
||||||
|
spotifyId = args[++i];
|
||||||
|
break;
|
||||||
|
case "--spotify-secret":
|
||||||
|
spotifySecret = args[++i];
|
||||||
|
break;
|
||||||
case "--username":
|
case "--username":
|
||||||
username = args[++i];
|
username = args[++i];
|
||||||
break;
|
break;
|
||||||
|
@ -172,6 +198,9 @@ class Program
|
||||||
case "--m3u-only":
|
case "--m3u-only":
|
||||||
m3uOnly = true;
|
m3uOnly = true;
|
||||||
break;
|
break;
|
||||||
|
case "--m3u":
|
||||||
|
m3uFilePath = args[++i];
|
||||||
|
break;
|
||||||
case "--search-timeout":
|
case "--search-timeout":
|
||||||
searchTimeout = int.Parse(args[++i]);
|
searchTimeout = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
|
@ -215,41 +244,85 @@ class Program
|
||||||
necessaryCond.MaxSampleRate = int.Parse(args[++i]);
|
necessaryCond.MaxSampleRate = int.Parse(args[++i]);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
Console.WriteLine($"Unknown argument: {args[i]}");
|
WriteLastLine($"Unknown argument: {args[i]}", ConsoleColor.Red);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (spotifyUrl != "")
|
||||||
|
{
|
||||||
|
string? playlistName;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(playlistName, tracks) = await GetSpotifyPlaylist(spotifyUrl, spotifyId, spotifySecret, false);
|
||||||
|
}
|
||||||
|
catch (SpotifyAPI.Web.APIException)
|
||||||
|
{
|
||||||
|
WriteLastLine("Spotify playlist not found. It may be set to private. Login? [Y/n]");
|
||||||
|
string answer = Console.ReadLine();
|
||||||
|
if (answer.ToLower() == "y")
|
||||||
|
{
|
||||||
|
try { (playlistName, tracks) = await GetSpotifyPlaylist(spotifyUrl, spotifyId, spotifySecret, true); }
|
||||||
|
catch (SpotifyAPI.Web.APIException) { throw; }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (folderName == "")
|
||||||
|
folderName = playlistName;
|
||||||
|
}
|
||||||
|
else if (tracksCsv != "")
|
||||||
|
{
|
||||||
|
if (!System.IO.File.Exists(tracksCsv))
|
||||||
|
throw new Exception("csv file not found");
|
||||||
if ((trackCol == "" && artistCol == "" && fullTitleCol == "") || (trackCol != "" && artistCol == "") || (fullTitleCol != "" && (artistCol != "" || trackCol != "")))
|
if ((trackCol == "" && artistCol == "" && fullTitleCol == "") || (trackCol != "" && artistCol == "") || (fullTitleCol != "" && (artistCol != "" || trackCol != "")))
|
||||||
throw new Exception("Use one of: full title column, (artist column AND track name)");
|
throw new Exception("Use one of: full title column, (artist column AND track name)");
|
||||||
if (lengthCol == "")
|
if (lengthCol == "")
|
||||||
WriteLastLine($"Warning: No length column specified, results may be imprecise.");
|
WriteLastLine($"Warning: No length column specified, results may be imprecise.");
|
||||||
|
|
||||||
System.IO.Directory.CreateDirectory(outputFolder);
|
|
||||||
|
|
||||||
string[] csvFiles = System.IO.Directory.GetFiles(outputFolder, "*.csv");
|
|
||||||
if ((tracksCsv != "" && !System.IO.File.Exists(tracksCsv)) && csvFiles.Length == 0)
|
|
||||||
throw new Exception("csv file not found");
|
|
||||||
if (tracksCsv == "" && csvFiles.Length > 0)
|
|
||||||
tracksCsv = csvFiles[0];
|
|
||||||
|
|
||||||
tracks = ParseCsvIntoTrackInfo(tracksCsv, artistCol, trackCol, lengthCol, fullTitleCol, uploaderCol, timeUnit: timeUnit);
|
tracks = ParseCsvIntoTrackInfo(tracksCsv, artistCol, trackCol, lengthCol, fullTitleCol, uploaderCol, timeUnit: timeUnit);
|
||||||
Track[] tmp = new Track[tracks.Count];
|
|
||||||
tracks.CopyTo(tmp);
|
|
||||||
var tracksStart = tmp.ToList();
|
|
||||||
|
|
||||||
failsFilePath = Path.Combine(outputFolder, "_failed.txt");
|
if (folderName == "")
|
||||||
if (System.IO.File.Exists(failsFilePath))
|
folderName = Path.GetFileNameWithoutExtension(tracksCsv);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new Exception("No csv or spotify url provided");
|
||||||
|
|
||||||
|
|
||||||
|
folderName = RemoveInvalidChars(folderName, " ");
|
||||||
|
|
||||||
|
if (parentFolder == "" && !m3uOnly)
|
||||||
|
throw new Exception("No folder provided (-p <path>)");
|
||||||
|
else if (parentFolder != "")
|
||||||
|
{
|
||||||
|
outputFolder = Path.Combine(parentFolder, folderName);
|
||||||
|
System.IO.Directory.CreateDirectory(outputFolder);
|
||||||
|
failsFilePath = Path.Combine(outputFolder, $"{folderName}_failed.txt");
|
||||||
|
if (!m3uOnly && System.IO.File.Exists(failsFilePath))
|
||||||
{
|
{
|
||||||
WriteAllLinesOutputFile("");
|
WriteAllLinesOutputFile("");
|
||||||
try { System.IO.File.Delete(failsFilePath); }
|
try { System.IO.File.Delete(failsFilePath); }
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m3uFilePath != "")
|
||||||
|
{
|
||||||
|
m3uFilePath = Path.Combine(m3uFilePath, folderName + ".m3u");
|
||||||
|
createM3u = true;
|
||||||
|
}
|
||||||
|
else if (outputFolder != "")
|
||||||
|
m3uFilePath = Path.Combine(outputFolder, folderName + ".m3u");
|
||||||
|
|
||||||
|
Track[] tmp = new Track[tracks.Count];
|
||||||
|
tracks.CopyTo(tmp);
|
||||||
|
var tracksStart = tmp.ToList();
|
||||||
|
|
||||||
createM3u |= m3uOnly;
|
createM3u |= m3uOnly;
|
||||||
m3uFilePath = Path.Combine(outputFolder, "playlist.m3u");
|
|
||||||
List<string> m3uLines = Enumerable.Repeat("", tracksStart.Count).ToList();
|
List<string> m3uLines = Enumerable.Repeat("", tracksStart.Count).ToList();
|
||||||
|
|
||||||
if (skipExisting || m3uOnly)
|
if (skipExisting || m3uOnly || musicDir != "")
|
||||||
|
{
|
||||||
|
if (outputFolder != "")
|
||||||
{
|
{
|
||||||
WriteLastLine("Checking if tracks exist in output folder...");
|
WriteLastLine("Checking if tracks exist in output folder...");
|
||||||
var outputDirFiles = System.IO.Directory.GetFiles(outputFolder, "*", SearchOption.AllDirectories);
|
var outputDirFiles = System.IO.Directory.GetFiles(outputFolder, "*", SearchOption.AllDirectories);
|
||||||
|
@ -261,13 +334,14 @@ class Program
|
||||||
m3uLines[tracksStart.IndexOf(x)] = path;
|
m3uLines[tracksStart.IndexOf(x)] = path;
|
||||||
return !exists;
|
return !exists;
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
if (musicDir != "")
|
if (musicDir != "")
|
||||||
{
|
{
|
||||||
WriteLastLine($"Checking if tracks exist in library...");
|
WriteLastLine($"Checking if tracks exist in library...");
|
||||||
var musicDirFiles = System.IO.Directory.GetFiles(musicDir, "*", SearchOption.AllDirectories);
|
var musicDirFiles = System.IO.Directory.GetFiles(musicDir, "*", SearchOption.AllDirectories);
|
||||||
musicFiles = musicDirFiles
|
var musicFiles = musicDirFiles
|
||||||
.Where(filename => !filename.Contains(outputFolder))
|
.Where(filename => outputFolder == "" || !filename.Contains(outputFolder))
|
||||||
.Where(filename => IsMusicFile(filename)).ToArray();
|
.Where(filename => IsMusicFile(filename)).ToArray();
|
||||||
tracks = tracks.Where(x =>
|
tracks = tracks.Where(x =>
|
||||||
{
|
{
|
||||||
|
@ -281,6 +355,7 @@ class Program
|
||||||
|
|
||||||
if (createM3u)
|
if (createM3u)
|
||||||
{
|
{
|
||||||
|
System.IO.Directory.CreateDirectory(Path.GetDirectoryName(m3uFilePath));
|
||||||
if (System.IO.File.Exists(m3uFilePath))
|
if (System.IO.File.Exists(m3uFilePath))
|
||||||
using (var fileStream = new FileStream(m3uFilePath, FileMode.Truncate, FileAccess.Write, FileShare.ReadWrite)) { fileStream.SetLength(0); }
|
using (var fileStream = new FileStream(m3uFilePath, FileMode.Truncate, FileAccess.Write, FileShare.ReadWrite)) { fileStream.SetLength(0); }
|
||||||
if (tracks.Count < tracksStart.Count)
|
if (tracks.Count < tracksStart.Count)
|
||||||
|
@ -296,6 +371,12 @@ class Program
|
||||||
if (m3uOnly)
|
if (m3uOnly)
|
||||||
{
|
{
|
||||||
WriteLastLine($"Created m3u file: {tracksStart.Count - tracks.Count} of {tracksStart.Count} found as local files");
|
WriteLastLine($"Created m3u file: {tracksStart.Count - tracks.Count} of {tracksStart.Count} found as local files");
|
||||||
|
if (tracks.Count > 0)
|
||||||
|
{
|
||||||
|
WriteLastLine($"Missing:");
|
||||||
|
foreach (var t in tracks)
|
||||||
|
WriteLastLine((t.TrackTitle == "" ? t.UnparsedTitle : $"{t.TrackTitle} - {t.ArtistName}") + (t.Length > 0 ? $" ({t.Length}s)" : ""));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -458,7 +539,8 @@ class Program
|
||||||
if (fileResponses.Count == 0)
|
if (fileResponses.Count == 0)
|
||||||
{
|
{
|
||||||
WriteLastLine($"Failed to find: {title}, skipping", ConsoleColor.Red);
|
WriteLastLine($"Failed to find: {title}, skipping", ConsoleColor.Red);
|
||||||
var failedDownloadInfo = $"{title} ({track.Length}s) [Reason: No file found with matching criteria]";
|
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
||||||
|
var failedDownloadInfo = $"{title} {length}[Reason: No file found with matching criteria]";
|
||||||
WriteLineOutputFile(failedDownloadInfo);
|
WriteLineOutputFile(failedDownloadInfo);
|
||||||
cts.Dispose();
|
cts.Dispose();
|
||||||
return "";
|
return "";
|
||||||
|
@ -471,7 +553,8 @@ class Program
|
||||||
if (skipIfPrefFailed && attemptedDownloadPref && !pref)
|
if (skipIfPrefFailed && attemptedDownloadPref && !pref)
|
||||||
{
|
{
|
||||||
WriteLastLine($"Pref. version of the file exists, but couldn't be downloaded: {title}, skipping", ConsoleColor.Red);
|
WriteLastLine($"Pref. version of the file exists, but couldn't be downloaded: {title}, skipping", ConsoleColor.Red);
|
||||||
var failedDownloadInfo = $"{title} ({track.Length}s) [Preferred version of the file exists, but couldn't be downloaded]";
|
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
||||||
|
var failedDownloadInfo = $"{title} {length}[Preferred version of the file exists, but couldn't be downloaded]";
|
||||||
WriteLineOutputFile(failedDownloadInfo);
|
WriteLineOutputFile(failedDownloadInfo);
|
||||||
cts.Dispose();
|
cts.Dispose();
|
||||||
return "";
|
return "";
|
||||||
|
@ -493,7 +576,8 @@ class Program
|
||||||
if (--downloadRetries <= 0)
|
if (--downloadRetries <= 0)
|
||||||
{
|
{
|
||||||
WriteLastLine($"Failed to download: {title}, skipping", ConsoleColor.Red);
|
WriteLastLine($"Failed to download: {title}, skipping", ConsoleColor.Red);
|
||||||
var failedDownloadInfo = $"{title} ({track.Length}s) [Reason: Out of download retries]";
|
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
||||||
|
var failedDownloadInfo = $"{title} {length}[Reason: Out of download retries]";
|
||||||
WriteLineOutputFile(failedDownloadInfo);
|
WriteLineOutputFile(failedDownloadInfo);
|
||||||
cts.Dispose();
|
cts.Dispose();
|
||||||
return "";
|
return "";
|
||||||
|
@ -504,7 +588,8 @@ class Program
|
||||||
if (!downloading)
|
if (!downloading)
|
||||||
{
|
{
|
||||||
WriteLastLine($"Failed to download: {title}", ConsoleColor.Red);
|
WriteLastLine($"Failed to download: {title}", ConsoleColor.Red);
|
||||||
var failedDownloadInfo = $"{title} ({track.Length}s) [Reason: All downloads failed]";
|
var length = track.Length > 0 ? $"({track.Length}s) " : "";
|
||||||
|
var failedDownloadInfo = $"{title} {length}[Reason: All downloads failed]";
|
||||||
WriteLineOutputFile(failedDownloadInfo);
|
WriteLineOutputFile(failedDownloadInfo);
|
||||||
cts.Dispose();
|
cts.Dispose();
|
||||||
return "";
|
return "";
|
||||||
|
@ -783,6 +868,29 @@ class Program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async Task<(string?, List<Track>)> GetSpotifyPlaylist(string url, string id, string secret, bool login)
|
||||||
|
{
|
||||||
|
var spotify = new Client(id, secret);
|
||||||
|
if (login)
|
||||||
|
{
|
||||||
|
await spotify.AuthorizeLogin();
|
||||||
|
await spotify.IsClientReady();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await spotify.Authorize();
|
||||||
|
|
||||||
|
(string? name, var res) = await spotify.GetPlaylist(url);
|
||||||
|
|
||||||
|
List<Track> trackList = res.Select(t =>
|
||||||
|
new Track
|
||||||
|
{
|
||||||
|
TrackTitle = t.Item2,
|
||||||
|
ArtistName = t.Item1,
|
||||||
|
Length = t.Item3
|
||||||
|
}).ToList();
|
||||||
|
return (name, trackList);
|
||||||
|
}
|
||||||
|
|
||||||
static List<Track> ParseCsvIntoTrackInfo(string path, string artistCol = "", string trackCol = "", string lengthCol = "", string titleCol = "", string uploaderCol = "", string timeUnit = "s")
|
static List<Track> ParseCsvIntoTrackInfo(string path, string artistCol = "", string trackCol = "", string lengthCol = "", string titleCol = "", string uploaderCol = "", string timeUnit = "s")
|
||||||
{
|
{
|
||||||
var tracks = new List<Track>();
|
var tracks = new List<Track>();
|
||||||
|
|
7
slsk-batchdl/Properties/launchSettings.json
Normal file
7
slsk-batchdl/Properties/launchSettings.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"slsk-batchdl": {
|
||||||
|
"commandName": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
101
slsk-batchdl/Spotify.cs
Normal file
101
slsk-batchdl/Spotify.cs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using SpotifyAPI.Web;
|
||||||
|
using SpotifyAPI.Web.Auth;
|
||||||
|
using Swan;
|
||||||
|
|
||||||
|
namespace Spotify
|
||||||
|
{
|
||||||
|
public class Client
|
||||||
|
{
|
||||||
|
private EmbedIOAuthServer _server;
|
||||||
|
private readonly string _clientId;
|
||||||
|
private readonly string _clientSecret;
|
||||||
|
private SpotifyClient _client;
|
||||||
|
|
||||||
|
public Client(string clientId, string clientSecret)
|
||||||
|
{
|
||||||
|
_clientId = clientId;
|
||||||
|
_clientSecret = clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Authorize()
|
||||||
|
{
|
||||||
|
var config = SpotifyClientConfig.CreateDefault();
|
||||||
|
|
||||||
|
var request = new ClientCredentialsRequest(_clientId, _clientSecret);
|
||||||
|
var response = await new OAuthClient(config).RequestToken(request);
|
||||||
|
|
||||||
|
_client = new SpotifyClient(config.WithToken(response.AccessToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AuthorizeLogin()
|
||||||
|
{
|
||||||
|
Swan.Logging.Logger.NoLogging();
|
||||||
|
_server = new EmbedIOAuthServer(new Uri("http://localhost:5000/callback"), 5000);
|
||||||
|
await _server.Start();
|
||||||
|
|
||||||
|
_server.AuthorizationCodeReceived += OnAuthorizationCodeReceived;
|
||||||
|
_server.ErrorReceived += OnErrorReceived;
|
||||||
|
|
||||||
|
var request = new LoginRequest(_server.BaseUri, _clientId, LoginRequest.ResponseType.Code)
|
||||||
|
{
|
||||||
|
Scope = new List<string> { Scopes.UserReadEmail }
|
||||||
|
};
|
||||||
|
BrowserUtil.Open(request.ToUri());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response)
|
||||||
|
{
|
||||||
|
await _server.Stop();
|
||||||
|
|
||||||
|
var config = SpotifyClientConfig.CreateDefault();
|
||||||
|
var tokenResponse = await new OAuthClient(config).RequestToken(
|
||||||
|
new AuthorizationCodeTokenRequest(
|
||||||
|
_clientId, _clientSecret, response.Code, new Uri("http://localhost:5000/callback")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
_client = new SpotifyClient(tokenResponse.AccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnErrorReceived(object sender, string error, string state)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Aborting authorization, error received: {error}");
|
||||||
|
await _server.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsClientReady()
|
||||||
|
{
|
||||||
|
while (_client == null)
|
||||||
|
await Task.Delay(1000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(string?, List<(string, string, int)> )> GetPlaylist(string url)
|
||||||
|
{
|
||||||
|
var playlistId = GetPlaylistIdFromUrl(url);
|
||||||
|
var p = await _client.Playlists.Get(playlistId);
|
||||||
|
var tracks = await _client.Playlists.GetItems(playlistId);
|
||||||
|
List<(string, string, int)> res = new List<(string, string, int)>();
|
||||||
|
|
||||||
|
foreach (var track in tracks.Items)
|
||||||
|
{
|
||||||
|
string[] artists = ((IEnumerable<object>)track.Track.ReadProperty("artists")).Select(a => (string)a.ReadProperty("name")).ToArray();
|
||||||
|
string artist = artists[0];
|
||||||
|
string name = (string)track.Track.ReadProperty("name");
|
||||||
|
int duration = (int)track.Track.ReadProperty("durationMs");
|
||||||
|
res.Add((artist, name, duration / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (p.Name, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPlaylistIdFromUrl(string url)
|
||||||
|
{
|
||||||
|
var uri = new Uri(url);
|
||||||
|
var segments = uri.Segments;
|
||||||
|
return segments[segments.Length - 1].TrimEnd('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,8 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Soulseek" Version="6.1.1" />
|
<PackageReference Include="Soulseek" Version="6.1.1" />
|
||||||
|
<PackageReference Include="SpotifyAPI.Web" Version="7.0.0" />
|
||||||
|
<PackageReference Include="SpotifyAPI.Web.Auth" Version="7.0.0" />
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue