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:
parent
c37287d926
commit
5c49877040
6 changed files with 299 additions and 120 deletions
11
README.md
11
README.md
|
@ -1,6 +1,6 @@
|
|||
# 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]
|
||||
|
@ -10,14 +10,16 @@ Options:
|
|||
--username <username> Soulseek username
|
||||
--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-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
|
||||
--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
|
||||
--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
|
||||
--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
|
||||
|
@ -34,8 +36,10 @@ Options:
|
|||
--nec-max-bitrate <rate> Necessary maximum bitrate
|
||||
--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)
|
||||
--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.
|
||||
--create-m3u Create an m3u playlist file
|
||||
--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").
|
||||
|
||||
## 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
|
||||
- Much of the code was written by ChatGPT
|
||||
|
|
|
@ -13,7 +13,6 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Soulseek;
|
||||
using Spotify;
|
||||
using TagLib.Matroska;
|
||||
using static System.Formats.Asn1.AsnWriter;
|
||||
using static System.Net.WebRequestMethods;
|
||||
|
@ -45,14 +44,16 @@ class Program
|
|||
Console.WriteLine(" --username <username> Soulseek username");
|
||||
Console.WriteLine(" --password <password> Soulseek password");
|
||||
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-secret <sec> Your spotify client secret (use if the default fails or if playlist private)");
|
||||
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(" --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(" --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(" --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");
|
||||
|
@ -69,8 +70,10 @@ class Program
|
|||
Console.WriteLine(" --nec-max-bitrate <rate> Necessary maximum bitrate");
|
||||
Console.WriteLine(" --nec-max-sample-rate <rate> Necessary maximum sample rate");
|
||||
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(" --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(" --create-m3u Create an m3u playlist file");
|
||||
Console.WriteLine(" --m3u-only Only create an m3u playlist file with existing tracks and exit");
|
||||
|
@ -101,6 +104,7 @@ class Program
|
|||
musicDir = "";
|
||||
string parentFolder = "";
|
||||
string folderName = "";
|
||||
string ytUrl = "";
|
||||
string spotifyUrl = "";
|
||||
string spotifyId = "";
|
||||
string spotifySecret = "";
|
||||
|
@ -117,9 +121,11 @@ class Program
|
|||
string lengthCol = "";
|
||||
string timeUnit = "s";
|
||||
ytdlpFormat = "bestaudio/best";
|
||||
bool reverse = false;
|
||||
bool useYtdlp = false;
|
||||
bool skipExisting = false;
|
||||
bool skipIfPrefFailed = false;
|
||||
bool albumSearch = false;
|
||||
bool createM3u = false;
|
||||
bool m3uOnly = false;
|
||||
int searchTimeout = 15000;
|
||||
|
@ -161,6 +167,9 @@ class Program
|
|||
case "--csv":
|
||||
tracksCsv = args[++i];
|
||||
break;
|
||||
case "--youtube":
|
||||
ytUrl = args[++i];
|
||||
break;
|
||||
case "--spotify":
|
||||
spotifyUrl = args[++i];
|
||||
break;
|
||||
|
@ -185,6 +194,9 @@ class Program
|
|||
case "--album-col":
|
||||
albumCol = args[++i];
|
||||
break;
|
||||
case "--album-search":
|
||||
albumSearch = true;
|
||||
break;
|
||||
case "--full-title-col":
|
||||
fullTitleCol = args[++i];
|
||||
break;
|
||||
|
@ -206,6 +218,9 @@ class Program
|
|||
case "--skip-existing":
|
||||
skipExisting = true;
|
||||
break;
|
||||
case "--reverse":
|
||||
reverse = true;
|
||||
break;
|
||||
case "--skip-if-pref-failed":
|
||||
skipIfPrefFailed = true;
|
||||
break;
|
||||
|
@ -279,6 +294,15 @@ class Program
|
|||
if (spotifyUrl == "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);
|
||||
}
|
||||
else
|
||||
|
@ -310,7 +334,15 @@ class Program
|
|||
}
|
||||
}
|
||||
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 != "")
|
||||
{
|
||||
|
@ -327,7 +359,7 @@ class Program
|
|||
folderName = Path.GetFileNameWithoutExtension(tracksCsv);
|
||||
}
|
||||
else
|
||||
throw new Exception("No csv or spotify url provided");
|
||||
throw new Exception("No csv, spotify or youtube url provided");
|
||||
|
||||
|
||||
folderName = RemoveInvalidChars(folderName, " ");
|
||||
|
@ -423,7 +455,10 @@ class Program
|
|||
}
|
||||
}
|
||||
|
||||
albumSearch |= albumCol != "";
|
||||
int tracksRemaining = tracks.Count;
|
||||
if (reverse)
|
||||
tracks.Reverse();
|
||||
|
||||
//foreach (var track in tracks)
|
||||
// WriteLastLine($"{track.Title}, {track.ArtistName} - {track.TackTitle} ({track.Length}s)");
|
||||
|
@ -441,7 +476,7 @@ class Program
|
|||
await semaphore.WaitAsync();
|
||||
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 != "")
|
||||
{
|
||||
tracksRemaining--;
|
||||
|
@ -471,7 +506,7 @@ class Program
|
|||
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}";
|
||||
if (track.TrackTitle == "")
|
||||
|
@ -544,13 +579,13 @@ class Program
|
|||
}
|
||||
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\"");
|
||||
string searchText = $"{track.Album} {track.TrackTitle}";
|
||||
searchOptions = new SearchOptions
|
||||
(
|
||||
minimumPeerUploadSpeed: 1, searchTimeout: albumSearchTimeout,
|
||||
minimumPeerUploadSpeed: 1, searchTimeout: 5000,
|
||||
responseFilter: (response) =>
|
||||
{
|
||||
return response.UploadSpeed > 0;
|
||||
|
@ -647,7 +682,7 @@ class Program
|
|||
try {
|
||||
downloading = true;
|
||||
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 + ".*");
|
||||
foreach (string file in files)
|
||||
{
|
||||
|
@ -799,14 +834,20 @@ class Program
|
|||
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();
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo();
|
||||
|
||||
startInfo.FileName = "yt-dlp";
|
||||
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.RedirectStandardError = true;
|
||||
|
@ -822,7 +863,7 @@ class Program
|
|||
|
||||
List<(int, string, string)> results = new List<(int, string, string)>();
|
||||
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)
|
||||
{
|
||||
Match match = regex.Match(output);
|
||||
|
@ -842,25 +883,11 @@ class Program
|
|||
|
||||
foreach (var res in results)
|
||||
{
|
||||
bool possibleMatch = false;
|
||||
if (conditions.LengthToleranceSatisfies(track, res.Item1))
|
||||
{
|
||||
WriteLastLine($"[yt-dlp] Downloading: {res.Item3} ({res.Item1}s)");
|
||||
process = new Process();
|
||||
startInfo = new ProcessStartInfo();
|
||||
|
||||
startInfo.FileName = "yt-dlp";
|
||||
startInfo.Arguments = $"{res.Item2} -f {ytdlpFormat} -ci -o \"{savePath}.%(ext)s\" --extract-audio";
|
||||
WriteLastLine($"{startInfo.FileName} {startInfo.Arguments}");
|
||||
|
||||
startInfo.RedirectStandardOutput = true;
|
||||
startInfo.RedirectStandardError = true;
|
||||
startInfo.UseShellExecute = false;
|
||||
process.StartInfo = startInfo;
|
||||
process.OutputDataReceived += (sender, e) => { WriteLastLine(e.Data); };
|
||||
process.ErrorDataReceived += (sender, e) => { WriteLastLine(e.Data); };
|
||||
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
YtdlpDownload(res.Item2, savePathNoExt);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -868,6 +895,26 @@ class Program
|
|||
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.Arguments = $"{id} -f {ytdlpFormat} -ci -o \"{savePathNoExt}.%(ext)s\" --extract-audio";
|
||||
WriteLastLine($"{startInfo.FileName} {startInfo.Arguments}");
|
||||
|
||||
startInfo.RedirectStandardOutput = true;
|
||||
startInfo.RedirectStandardError = true;
|
||||
startInfo.UseShellExecute = false;
|
||||
process.StartInfo = startInfo;
|
||||
process.OutputDataReceived += (sender, e) => { WriteLastLine(e.Data); };
|
||||
process.ErrorDataReceived += (sender, e) => { WriteLastLine(e.Data); };
|
||||
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
}
|
||||
|
||||
class DownloadInfo
|
||||
{
|
||||
public string savePath;
|
||||
|
@ -1038,7 +1085,7 @@ class Program
|
|||
|
||||
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)
|
||||
{
|
||||
await spotify.AuthorizeLogin();
|
||||
|
@ -1053,7 +1100,7 @@ class Program
|
|||
|
||||
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.IsClientReady();
|
||||
|
||||
|
@ -1209,6 +1256,16 @@ class Program
|
|||
str = str.Replace(c.ToString(), replaceStr);
|
||||
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
|
||||
|
@ -1218,6 +1275,7 @@ public struct Track
|
|||
public string TrackTitle = "";
|
||||
public string ArtistName = "";
|
||||
public string Album = "";
|
||||
public string YtID = "";
|
||||
public int Length = -1;
|
||||
public Track() { }
|
||||
}
|
||||
|
|
|
@ -7,9 +7,13 @@
|
|||
"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"
|
||||
},
|
||||
"Profile 2": {
|
||||
"Profile 3": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,82 +6,85 @@ using SpotifyAPI.Web.Auth;
|
|||
using Swan;
|
||||
using TagLib.IFD.Tags;
|
||||
|
||||
namespace Spotify
|
||||
public class Spotify
|
||||
{
|
||||
public class Client
|
||||
private EmbedIOAuthServer _server;
|
||||
private readonly string _clientId;
|
||||
private readonly string _clientSecret;
|
||||
private SpotifyClient _client;
|
||||
private bool loggedIn = false;
|
||||
|
||||
public Spotify(string clientId, string clientSecret)
|
||||
{
|
||||
private EmbedIOAuthServer _server;
|
||||
private readonly string _clientId;
|
||||
private readonly string _clientSecret;
|
||||
private SpotifyClient _client;
|
||||
private bool loggedIn = false;
|
||||
_clientId = clientId;
|
||||
_clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public Client(string clientId, string 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)
|
||||
{
|
||||
_clientId = clientId;
|
||||
_clientSecret = clientSecret;
|
||||
}
|
||||
Scope = new List<string> { Scopes.UserLibraryRead, Scopes.PlaylistReadPrivate }
|
||||
};
|
||||
BrowserUtil.Open(request.ToUri());
|
||||
loggedIn = true;
|
||||
}
|
||||
|
||||
public async Task Authorize()
|
||||
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<List<Track>> GetLikes()
|
||||
{
|
||||
if (!loggedIn)
|
||||
throw new Exception("Can't get liked music, not logged in");
|
||||
|
||||
List<Track> res = new List<Track>();
|
||||
int offset = 0;
|
||||
int limit = 50;
|
||||
|
||||
while (true)
|
||||
{
|
||||
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.UserLibraryRead, Scopes.PlaylistReadPrivate }
|
||||
};
|
||||
BrowserUtil.Open(request.ToUri());
|
||||
loggedIn = true;
|
||||
}
|
||||
|
||||
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<List<Track>> GetLikes()
|
||||
{
|
||||
if (!loggedIn)
|
||||
throw new Exception("Can't get liked music, not logged in");
|
||||
|
||||
var tracks = await _client.Library.GetTracks();
|
||||
List<Track> res = new List<Track>();
|
||||
var tracks = await _client.Library.GetTracks(new LibraryTracksRequest { Limit = limit, Offset = offset });
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
return res;
|
||||
if (tracks.Items.Count < limit)
|
||||
break;
|
||||
|
||||
offset += limit;
|
||||
}
|
||||
|
||||
public async Task<(string?, List<Track>)> GetPlaylist(string url)
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
public async Task<(string?, List<Track>)> GetPlaylist(string url)
|
||||
{
|
||||
var playlistId = GetPlaylistIdFromUrl(url);
|
||||
var p = await _client.Playlists.Get(playlistId);
|
||||
|
||||
List<Track> res = new List<Track>();
|
||||
int offset = 0;
|
||||
int limit = 100;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var playlistId = GetPlaylistIdFromUrl(url);
|
||||
var p = await _client.Playlists.Get(playlistId);
|
||||
var tracks = await _client.Playlists.GetItems(playlistId);
|
||||
List<Track> res = new List<Track>();
|
||||
var tracks = await _client.Playlists.GetItems(playlistId, new PlaylistGetItemsRequest { Limit = limit, Offset = offset });
|
||||
|
||||
foreach (var track in tracks.Items)
|
||||
{
|
||||
|
@ -110,17 +126,22 @@ namespace Spotify
|
|||
string name = (string)track.Track.ReadProperty("name");
|
||||
string album = (string)track.Track.ReadProperty("album").ReadProperty("name");
|
||||
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 });
|
||||
}
|
||||
|
||||
return (p.Name, res);
|
||||
if (tracks.Items.Count < limit)
|
||||
break;
|
||||
|
||||
offset += limit;
|
||||
}
|
||||
|
||||
private string GetPlaylistIdFromUrl(string url)
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
var segments = uri.Segments;
|
||||
return segments[segments.Length - 1].TrimEnd('/');
|
||||
}
|
||||
return (p.Name, res);
|
||||
}
|
||||
|
||||
private string GetPlaylistIdFromUrl(string url)
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
var segments = uri.Segments;
|
||||
return segments[segments.Length - 1].TrimEnd('/');
|
||||
}
|
||||
}
|
||||
|
|
90
slsk-batchdl/YouTube.cs
Normal file
90
slsk-batchdl/YouTube.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
<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="YoutubeExplode" Version="6.2.11" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
Loading…
Reference in a new issue