1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2025-01-10 23:42:42 +00:00

remove spotify limit, liked songs download,

reverse download order option,
youtube download
This commit is contained in:
fiso64 2023-03-28 23:16:10 +02:00
parent c37287d926
commit 5c49877040
6 changed files with 299 additions and 120 deletions

View file

@ -1,6 +1,6 @@
# slsk-batchdl # slsk-batchdl
A batch downloader for Soulseek using Soulseek.NET. Accepts csv files and spotify playlist urls. A batch downloader for Soulseek using Soulseek.NET. Accepts CSV files, Spotify & YouTube urls.
``` ```
Usage: slsk-batchdl.exe [OPTIONS] Usage: slsk-batchdl.exe [OPTIONS]
@ -10,14 +10,16 @@ Options:
--username <username> Soulseek username --username <username> Soulseek username
--password <password> Soulseek password --password <password> Soulseek password
--spotify <url> Download a spotify playlist --spotify <url> Download a spotify playlist. "likes" to download all your liked music.
--spotify-id <id> Your spotify client id (use if the default fails or if playlist private) --spotify-id <id> Your spotify client id (use if the default fails or if playlist private)
--spotify-secret <sec> Your spotify client secret (use if the default fails or if playlist private) --spotify-secret <sec> Your spotify client secret (use if the default fails or if playlist private)
--youtube <url> Download YouTube playlist
--csv <path> Use a csv file containing track info to download --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
--album-col <unit> CSV album column name. Optional, may improve searching --album-col <unit> CSV album column name. Optional, may improve searching, slower
--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
--uploader-col <column> Specify when using full title col if there is also an uploader column in the csv (fallback in case artist name cannot be extracted from title) --uploader-col <column> Specify when using full title col if there is also an uploader column in the csv (fallback in case artist name cannot be extracted from title)
--length-col <column> CSV duration column name. Recommended, will improve accuracy --length-col <column> CSV duration column name. Recommended, will improve accuracy
@ -34,8 +36,10 @@ 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
--album-search Also search for "[Album name] [track name]". Occasionally helps to find more
--skip-existing Skip if a track matching the conditions is found in the output folder or your music library (if provided) --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 --music-dir <path> Specify to also skip downloading tracks which are in your library, use with --skip-existing
--reverse Download tracks in reverse order
--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. --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 --create-m3u Create an m3u playlist file
--m3u-only Only create an m3u playlist file with existing tracks and exit --m3u-only Only create an m3u playlist file with existing tracks and exit
@ -62,5 +66,6 @@ slsk-batchdl.exe --spotify <url> -p "C:\Users\fiso64\Music\Playlists" --m3u "C:\
You might need to provide an id and secret when using spotify (which you can get here https://developer.spotify.com/dashboard/applications, under "Create an app"). You might need to provide an id and secret when using spotify (which you can get here https://developer.spotify.com/dashboard/applications, under "Create an app").
## Notes: ## Notes:
- YouTube playlist downloading is bad: There is no way of reliably parsing video information into artist & track name. Also, many videos may be unavailable (or unavailable specifically through the api, for reasons that are beyond me), in which case they won't be downloaded.
- The console output tends to break after a while - The console output tends to break after a while
- Much of the code was written by ChatGPT - Much of the code was written by ChatGPT

View file

@ -13,7 +13,6 @@ 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;
@ -45,14 +44,16 @@ class Program
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 <url> Download a spotify playlist. \"likes\" to download all your liked music.");
Console.WriteLine(" --spotify-id <id> Your spotify client id (use if the default fails or if playlist private)"); Console.WriteLine(" --spotify-id <id> Your spotify client id (use if the default fails or if playlist private)");
Console.WriteLine(" --spotify-secret <sec> Your spotify client secret (use if the default fails or if playlist private)"); Console.WriteLine(" --spotify-secret <sec> Your spotify client secret (use if the default fails or if playlist private)");
Console.WriteLine(); Console.WriteLine();
Console.WriteLine(" --youtube <url> Download YouTube playlist");
Console.WriteLine();
Console.WriteLine(" --csv <path> Use a csv file containing track info to download"); 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(" --album-col <unit> CSV album column name. Optional, may improve searching"); Console.WriteLine(" --album-col <unit> CSV album column name. Optional, may improve searching, slower");
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");
Console.WriteLine(" --uploader-col <column> Specify when using full title col if there is also an uploader column in the csv (fallback in case artist name cannot be extracted from title)"); Console.WriteLine(" --uploader-col <column> Specify when using full title col if there is also an uploader column in the csv (fallback in case artist name cannot be extracted from title)");
Console.WriteLine(" --length-col <column> CSV duration column name. Recommended, will improve accuracy"); Console.WriteLine(" --length-col <column> CSV duration column name. Recommended, will improve accuracy");
@ -69,8 +70,10 @@ 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(" --album-search Also search for \"[Album name] [track name]\". Occasionally helps to find more");
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(" --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(" --music-dir <path> Specify to also skip downloading tracks which are in your library, use with --skip-existing");
Console.WriteLine(" --reverse Download tracks in reverse order");
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(" --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(" --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-only Only create an m3u playlist file with existing tracks and exit");
@ -101,6 +104,7 @@ class Program
musicDir = ""; musicDir = "";
string parentFolder = ""; string parentFolder = "";
string folderName = ""; string folderName = "";
string ytUrl = "";
string spotifyUrl = ""; string spotifyUrl = "";
string spotifyId = ""; string spotifyId = "";
string spotifySecret = ""; string spotifySecret = "";
@ -117,9 +121,11 @@ class Program
string lengthCol = ""; string lengthCol = "";
string timeUnit = "s"; string timeUnit = "s";
ytdlpFormat = "bestaudio/best"; ytdlpFormat = "bestaudio/best";
bool reverse = false;
bool useYtdlp = false; bool useYtdlp = false;
bool skipExisting = false; bool skipExisting = false;
bool skipIfPrefFailed = false; bool skipIfPrefFailed = false;
bool albumSearch = false;
bool createM3u = false; bool createM3u = false;
bool m3uOnly = false; bool m3uOnly = false;
int searchTimeout = 15000; int searchTimeout = 15000;
@ -161,6 +167,9 @@ class Program
case "--csv": case "--csv":
tracksCsv = args[++i]; tracksCsv = args[++i];
break; break;
case "--youtube":
ytUrl = args[++i];
break;
case "--spotify": case "--spotify":
spotifyUrl = args[++i]; spotifyUrl = args[++i];
break; break;
@ -185,6 +194,9 @@ class Program
case "--album-col": case "--album-col":
albumCol = args[++i]; albumCol = args[++i];
break; break;
case "--album-search":
albumSearch = true;
break;
case "--full-title-col": case "--full-title-col":
fullTitleCol = args[++i]; fullTitleCol = args[++i];
break; break;
@ -206,6 +218,9 @@ class Program
case "--skip-existing": case "--skip-existing":
skipExisting = true; skipExisting = true;
break; break;
case "--reverse":
reverse = true;
break;
case "--skip-if-pref-failed": case "--skip-if-pref-failed":
skipIfPrefFailed = true; skipIfPrefFailed = true;
break; break;
@ -279,6 +294,15 @@ class Program
if (spotifyUrl == "likes") if (spotifyUrl == "likes")
{ {
playlistName = "Spotify Likes"; playlistName = "Spotify Likes";
if (usedDefaultId)
{
WriteLastLine("");
Console.Write("Spotify client ID:");
spotifyId = Console.ReadLine();
WriteLastLine("");
Console.Write("Spotify client secret:");
spotifySecret = Console.ReadLine();
}
tracks = await GetSpotifyLikes(spotifyId, spotifySecret); tracks = await GetSpotifyLikes(spotifyId, spotifySecret);
} }
else else
@ -310,7 +334,15 @@ class Program
} }
} }
if (folderName == "") if (folderName == "")
folderName = playlistName; folderName = RemoveInvalidChars(playlistName, " ");
}
else if (ytUrl != "")
{
WriteLastLine("Loading youtube playlist...");
(string name, tracks) = await YouTube.GetTracks(ytUrl);
if (folderName == "")
folderName = RemoveInvalidChars(name, " ");
} }
else if (tracksCsv != "") else if (tracksCsv != "")
{ {
@ -327,7 +359,7 @@ class Program
folderName = Path.GetFileNameWithoutExtension(tracksCsv); folderName = Path.GetFileNameWithoutExtension(tracksCsv);
} }
else else
throw new Exception("No csv or spotify url provided"); throw new Exception("No csv, spotify or youtube url provided");
folderName = RemoveInvalidChars(folderName, " "); folderName = RemoveInvalidChars(folderName, " ");
@ -423,7 +455,10 @@ class Program
} }
} }
albumSearch |= albumCol != "";
int tracksRemaining = tracks.Count; int tracksRemaining = tracks.Count;
if (reverse)
tracks.Reverse();
//foreach (var track in tracks) //foreach (var track in tracks)
// WriteLastLine($"{track.Title}, {track.ArtistName} - {track.TackTitle} ({track.Length}s)"); // WriteLastLine($"{track.Title}, {track.ArtistName} - {track.TackTitle} ({track.Length}s)");
@ -441,7 +476,7 @@ class Program
await semaphore.WaitAsync(); await semaphore.WaitAsync();
try try
{ {
var savedFilePath = await SearchAndDownload(track, preferredCond, necessaryCond, skipIfPrefFailed, maxRetriesPerFile, searchTimeout, albumSearchTimeout: 5000, useYtdlp); var savedFilePath = await SearchAndDownload(track, preferredCond, necessaryCond, skipIfPrefFailed, maxRetriesPerFile, searchTimeout, albumSearch, useYtdlp);
if (savedFilePath != "") if (savedFilePath != "")
{ {
tracksRemaining--; tracksRemaining--;
@ -471,7 +506,7 @@ class Program
WriteLastLine($"Failed to download:\n{System.IO.File.ReadAllText(failsFilePath)}"); WriteLastLine($"Failed to download:\n{System.IO.File.ReadAllText(failsFilePath)}");
} }
static async Task<string> SearchAndDownload(Track track, FileConditions preferredCond, FileConditions necessaryCond, bool skipIfPrefFailed, int maxRetriesPerFile, int searchTimeout, int albumSearchTimeout, bool useYtdlp) static async Task<string> SearchAndDownload(Track track, FileConditions preferredCond, FileConditions necessaryCond, bool skipIfPrefFailed, int maxRetriesPerFile, int searchTimeout, bool albumSearch, bool useYtdlp)
{ {
var title = track.TrackTitle == "" ? $"{track.UnparsedTitle}" : $"{track.ArtistName} - {track.TrackTitle}"; var title = track.TrackTitle == "" ? $"{track.UnparsedTitle}" : $"{track.ArtistName} - {track.TrackTitle}";
if (track.TrackTitle == "") if (track.TrackTitle == "")
@ -544,13 +579,13 @@ class Program
} }
catch (OperationCanceledException ex) { } catch (OperationCanceledException ex) { }
if (responses.Count == 0 && track.Album != "" && track.TrackTitle != "") if (albumSearch && responses.Count == 0 && track.Album != "" && track.TrackTitle != "")
{ {
Debug.WriteLine("\"Artist - Track\" not found, trying \"Album Track\""); Debug.WriteLine("\"Artist - Track\" not found, trying \"Album Track\"");
string searchText = $"{track.Album} {track.TrackTitle}"; string searchText = $"{track.Album} {track.TrackTitle}";
searchOptions = new SearchOptions searchOptions = new SearchOptions
( (
minimumPeerUploadSpeed: 1, searchTimeout: albumSearchTimeout, minimumPeerUploadSpeed: 1, searchTimeout: 5000,
responseFilter: (response) => responseFilter: (response) =>
{ {
return response.UploadSpeed > 0; return response.UploadSpeed > 0;
@ -647,7 +682,7 @@ class Program
try { try {
downloading = true; downloading = true;
string fname = GetSaveName(track); string fname = GetSaveName(track);
YtdlpDownload(track, necessaryCond, Path.Combine(outputFolder, fname)); YtdlpSearchAndDownload(track, necessaryCond, Path.Combine(outputFolder, fname));
string[] files = System.IO.Directory.GetFiles(outputFolder, fname + ".*"); string[] files = System.IO.Directory.GetFiles(outputFolder, fname + ".*");
foreach (string file in files) foreach (string file in files)
{ {
@ -799,14 +834,20 @@ class Program
return RemoveInvalidChars(name, " "); return RemoveInvalidChars(name, " ");
} }
static void YtdlpDownload(Track track, FileConditions conditions, string savePath) static void YtdlpSearchAndDownload(Track track, FileConditions conditions, string savePathNoExt)
{ {
if (track.YtID != "")
{
YtdlpDownload(track.YtID, savePathNoExt);
return;
}
Process process = new Process(); Process process = new Process();
ProcessStartInfo startInfo = new ProcessStartInfo(); ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName = "yt-dlp"; startInfo.FileName = "yt-dlp";
string search = track.TrackTitle == "" ? track.UnparsedTitle : $"{track.ArtistName} - {track.TrackTitle}"; string search = track.TrackTitle == "" ? track.UnparsedTitle : $"{track.ArtistName} - {track.TrackTitle}";
startInfo.Arguments = $"\"ytsearch3:{search}\" --print \"%(duration>%H:%M:%S)s - %(id)s - %(title)s\""; startInfo.Arguments = $"\"ytsearch3:{search}\" --print \"%(duration>%H:%M:%S)s ¦¦ %(id)s ¦¦ %(title)s\"";
startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true; startInfo.RedirectStandardError = true;
@ -822,7 +863,7 @@ class Program
List<(int, string, string)> results = new List<(int, string, string)>(); List<(int, string, string)> results = new List<(int, string, string)>();
string output; string output;
Regex regex = new Regex(@"^(\d+):(\d+):(\d+) - ([\w-]+) - (.+)$"); // I LOVE CHATGPT !!!! Regex regex = new Regex(@"^(\d+):(\d+):(\d+) ¦¦ ([\w-]+) ¦¦ (.+)$"); // I LOVE CHATGPT !!!!
while ((output = process.StandardOutput.ReadLine()) != null) while ((output = process.StandardOutput.ReadLine()) != null)
{ {
Match match = regex.Match(output); Match match = regex.Match(output);
@ -842,14 +883,25 @@ class Program
foreach (var res in results) foreach (var res in results)
{ {
bool possibleMatch = false;
if (conditions.LengthToleranceSatisfies(track, res.Item1)) if (conditions.LengthToleranceSatisfies(track, res.Item1))
{ {
WriteLastLine($"[yt-dlp] Downloading: {res.Item3} ({res.Item1}s)"); WriteLastLine($"[yt-dlp] Downloading: {res.Item3} ({res.Item1}s)");
process = new Process(); YtdlpDownload(res.Item2, savePathNoExt);
startInfo = new ProcessStartInfo(); return;
}
}
throw new Exception($"[yt-dlp] No matching files found");
}
static void YtdlpDownload(string id, string savePathNoExt)
{
Process process = new Process();
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName = "yt-dlp"; startInfo.FileName = "yt-dlp";
startInfo.Arguments = $"{res.Item2} -f {ytdlpFormat} -ci -o \"{savePath}.%(ext)s\" --extract-audio"; startInfo.Arguments = $"{id} -f {ytdlpFormat} -ci -o \"{savePathNoExt}.%(ext)s\" --extract-audio";
WriteLastLine($"{startInfo.FileName} {startInfo.Arguments}"); WriteLastLine($"{startInfo.FileName} {startInfo.Arguments}");
startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardOutput = true;
@ -861,11 +913,6 @@ class Program
process.Start(); process.Start();
process.WaitForExit(); process.WaitForExit();
return;
}
}
throw new Exception($"[yt-dlp] No matching files found");
} }
class DownloadInfo class DownloadInfo
@ -1038,7 +1085,7 @@ class Program
static async Task<(string?, List<Track>)> GetSpotifyPlaylist(string url, string id, string secret, bool login) static async Task<(string?, List<Track>)> GetSpotifyPlaylist(string url, string id, string secret, bool login)
{ {
var spotify = new Client(id, secret); var spotify = new Spotify(id, secret);
if (login) if (login)
{ {
await spotify.AuthorizeLogin(); await spotify.AuthorizeLogin();
@ -1053,7 +1100,7 @@ class Program
static async Task<List<Track>> GetSpotifyLikes(string id, string secret) static async Task<List<Track>> GetSpotifyLikes(string id, string secret)
{ {
var spotify = new Client(id, secret); var spotify = new Spotify(id, secret);
await spotify.AuthorizeLogin(); await spotify.AuthorizeLogin();
await spotify.IsClientReady(); await spotify.IsClientReady();
@ -1209,6 +1256,16 @@ class Program
str = str.Replace(c.ToString(), replaceStr); str = str.Replace(c.ToString(), replaceStr);
return str; return str;
} }
static void PrintTracks(List<Track> tracks)
{
foreach (var track in tracks)
{
string title = track.TrackTitle == "" ? track.UnparsedTitle : $"{track.ArtistName} - {track.TrackTitle}";
WriteLastLine($"{title} ({track.Length})");
}
WriteLastLine(tracks.Count);
}
} }
public struct Track public struct Track
@ -1218,6 +1275,7 @@ public struct Track
public string TrackTitle = ""; public string TrackTitle = "";
public string ArtistName = ""; public string ArtistName = "";
public string Album = ""; public string Album = "";
public string YtID = "";
public int Length = -1; public int Length = -1;
public Track() { } public Track() { }
} }

View file

@ -7,9 +7,13 @@
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "-p \"C:\\Users\\fiso64\\Music\\Playlists\" --csv \"C:\\Users\\fiso64\\Downloads\\test.csv\" --username \"fakename99123\" --password \"fakepass123123\" --artist-col \"Artist Name(s)\" --album-col \"Album Name\" --track-col \"Track Name\" --length-col \"Duration (ms)\" --time-unit \"ms\" --pref-format \"flac\" --yt-dlp" "commandLineArgs": "-p \"C:\\Users\\fiso64\\Music\\Playlists\" --csv \"C:\\Users\\fiso64\\Downloads\\test.csv\" --username \"fakename99123\" --password \"fakepass123123\" --artist-col \"Artist Name(s)\" --album-col \"Album Name\" --track-col \"Track Name\" --length-col \"Duration (ms)\" --time-unit \"ms\" --pref-format \"flac\" --yt-dlp"
}, },
"Profile 2": { "Profile 3": {
"commandName": "Project", "commandName": "Project",
"commandLineArgs": "--spotify \"\" -p \"C:\\Users\\fiso64\\Music\\Playlists\" --username \"fakename99123\" --password \"fakepass123123\" --pref-format \"flac\" --yt-dlp" "commandLineArgs": "--spotify \"likes\" -p \"C:\\Users\\fiso64\\Music\\Playlists\" --username \"fakename99123\" --password \"fakepass123123\" --yt-dlp"
},
"Profile 4": {
"commandName": "Project",
"commandLineArgs": "--youtube \"https://www.youtube.com/playlist?list=PLI_eFW8NAFzYAXZ5DrU6E6mQ_XfhaLBUX\" -p \"C:\\Users\\fiso64\\Music\\Playlists\" --username \"fakename99123\" --password \"fakepass123123\" --yt-dlp"
} }
} }
} }

View file

@ -6,17 +6,15 @@ using SpotifyAPI.Web.Auth;
using Swan; using Swan;
using TagLib.IFD.Tags; using TagLib.IFD.Tags;
namespace Spotify public class Spotify
{ {
public class Client
{
private EmbedIOAuthServer _server; private EmbedIOAuthServer _server;
private readonly string _clientId; private readonly string _clientId;
private readonly string _clientSecret; private readonly string _clientSecret;
private SpotifyClient _client; private SpotifyClient _client;
private bool loggedIn = false; private bool loggedIn = false;
public Client(string clientId, string clientSecret) public Spotify(string clientId, string clientSecret)
{ {
_clientId = clientId; _clientId = clientId;
_clientSecret = clientSecret; _clientSecret = clientSecret;
@ -80,8 +78,13 @@ namespace Spotify
if (!loggedIn) if (!loggedIn)
throw new Exception("Can't get liked music, not logged in"); throw new Exception("Can't get liked music, not logged in");
var tracks = await _client.Library.GetTracks();
List<Track> res = new List<Track>(); List<Track> res = new List<Track>();
int offset = 0;
int limit = 50;
while (true)
{
var tracks = await _client.Library.GetTracks(new LibraryTracksRequest { Limit = limit, Offset = offset });
foreach (var track in tracks.Items) foreach (var track in tracks.Items)
{ {
@ -93,15 +96,28 @@ namespace Spotify
res.Add(new Track { Album = album, ArtistName = artist, TrackTitle = name, Length = duration / 1000 }); res.Add(new Track { Album = album, ArtistName = artist, TrackTitle = name, Length = duration / 1000 });
} }
if (tracks.Items.Count < limit)
break;
offset += limit;
}
return res; return res;
} }
public async Task<(string?, List<Track>)> GetPlaylist(string url) public async Task<(string?, List<Track>)> GetPlaylist(string url)
{ {
var playlistId = GetPlaylistIdFromUrl(url); var playlistId = GetPlaylistIdFromUrl(url);
var p = await _client.Playlists.Get(playlistId); var p = await _client.Playlists.Get(playlistId);
var tracks = await _client.Playlists.GetItems(playlistId);
List<Track> res = new List<Track>(); List<Track> res = new List<Track>();
int offset = 0;
int limit = 100;
while (true)
{
var tracks = await _client.Playlists.GetItems(playlistId, new PlaylistGetItemsRequest { Limit = limit, Offset = offset });
foreach (var track in tracks.Items) foreach (var track in tracks.Items)
{ {
@ -110,7 +126,13 @@ namespace Spotify
string name = (string)track.Track.ReadProperty("name"); string name = (string)track.Track.ReadProperty("name");
string album = (string)track.Track.ReadProperty("album").ReadProperty("name"); string album = (string)track.Track.ReadProperty("album").ReadProperty("name");
int duration = (int)track.Track.ReadProperty("durationMs"); int duration = (int)track.Track.ReadProperty("durationMs");
res.Add(new Track { Album=album, ArtistName=artist, TrackTitle=name, Length=duration / 1000 }); res.Add(new Track { Album = album, ArtistName = artist, TrackTitle = name, Length = duration / 1000 });
}
if (tracks.Items.Count < limit)
break;
offset += limit;
} }
return (p.Name, res); return (p.Name, res);
@ -122,5 +144,4 @@ namespace Spotify
var segments = uri.Segments; var segments = uri.Segments;
return segments[segments.Length - 1].TrimEnd('/'); return segments[segments.Length - 1].TrimEnd('/');
} }
}
} }

90
slsk-batchdl/YouTube.cs Normal file
View file

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using YoutubeExplode;
public static class YouTube
{
public static async Task<(string, List<Track>)> GetTracks(string url)
{
var youtube = new YoutubeClient();
var playlist = await youtube.Playlists.GetAsync(url);
var playlistTitle = playlist.Title;
var tracks = new List<Track>();
var videoTasks = new List<(ValueTask<YoutubeExplode.Videos.Video>, int)>();
await foreach (var video in youtube.Playlists.GetVideosAsync(playlist.Id))
{
var title = video.Title;
var uploader = video.Author.Title;
var ytId = video.Id.Value;
var length = (int)video.Duration.Value.TotalSeconds;
title = title.Replace("", "-");
var trackTitle = title.Trim();
var artist = uploader.Trim();
if (artist.EndsWith("- Topic"))
{
artist = artist.Substring(0, artist.Length - 7).Trim();
trackTitle = title;
if (artist == "Various Artists")
{
//var vid = await youtube.Videos.GetAsync(video.Id);
videoTasks.Add((youtube.Videos.GetAsync(video.Id), tracks.Count));
//Thread.Sleep(20);
}
}
else
{
int idx = title.IndexOf('-');
var split = title.Split(new[] { '-' }, 2);
if (idx > 0 && idx < title.Length - 1 && (title[idx - 1] == ' ' || title[idx + 1] == ' ') && split[0].Trim() != "" && split[1].Trim() != "")
{
artist = title.Split(new[] { '-' }, 2)[0].Trim();
trackTitle = title.Split(new[] { '-' }, 2)[1].Trim();
}
else
{
artist = uploader;
trackTitle = title;
}
}
var track = new Track
{
UnparsedTitle = video.Title,
Uploader = uploader,
TrackTitle = trackTitle,
ArtistName = artist,
YtID = ytId,
Length = length
};
tracks.Add(track);
}
foreach ((var vidTask, int idx) in videoTasks)
{
var vid = await vidTask;
var lines = vid.Description.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
var dotLine = lines.FirstOrDefault(line => line.Contains(" · "));
if (dotLine != null)
{
var t = tracks[idx];
t.ArtistName = dotLine.Split(new[] { " · " }, StringSplitOptions.None)[1]; // can't be asked to do it properly
tracks[idx] = t;
}
}
return (playlistTitle, tracks);
}
}

View file

@ -12,6 +12,7 @@
<PackageReference Include="SpotifyAPI.Web" Version="7.0.0" /> <PackageReference Include="SpotifyAPI.Web" Version="7.0.0" />
<PackageReference Include="SpotifyAPI.Web.Auth" 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" />
<PackageReference Include="YoutubeExplode" Version="6.2.11" />
</ItemGroup> </ItemGroup>
</Project> </Project>