1
0
Fork 0
mirror of https://github.com/fiso64/slsk-batchdl.git synced 2024-12-22 14:32:40 +00:00

Implement spotify token and refresh flow from configuration

* makes headless usage of sldl easier #42
* fallback to outputting login flow URL if opening a browser fails (in headless environment)
* output token and refresh token on login complete
* parse token and refresh token from config and attempt to use before invoking login flow
This commit is contained in:
FoxxMD 2024-08-27 14:50:25 -04:00
parent 6e7b8d5d67
commit 5357911054
3 changed files with 125 additions and 19 deletions

View file

@ -126,6 +126,8 @@ Usage: sldl <input> [OPTIONS]
Spotify Spotify
--spotify-id <id> spotify client ID --spotify-id <id> spotify client ID
--spotify-secret <secret> spotify client secret --spotify-secret <secret> spotify client secret
--spotify-token <token> spotify access token
--spotify-refresh <token> spotify refresh token
--remove-from-source Remove downloaded tracks from source playlist --remove-from-source Remove downloaded tracks from source playlist
``` ```
``` ```
@ -243,10 +245,26 @@ Tip: For playlists containing music videos, it may be better to remove all text
### Spotify ### Spotify
A playlist/album url or 'spotify-likes': Download a spotify playlist, album, or your A playlist/album url or 'spotify-likes': Download a spotify playlist, album, or your
liked songs. --spotify-id and --spotify-secret are required in addition when downloading liked songs. Credentials are required when downloading a private playlist or liked music.
a private playlist or liked music.
The id and secret can be obtained at https://developer.spotify.com/dashboard/applications. #### Using Credential/Application
Create an app and add http://localhost:48721/callback as a redirect url in its settings.
Create a [Spotify application](https://developer.spotify.com/dashboard/applications) with a redirect url of `http://localhost:48721/callback`. Obtain an application **ID** and **Secret** from the created application dashboard.
Start sldl with the obtained credentials and an authorized action to trigger the Spotify app login flow:
```shell
sldl spotify-likes --number 1 --spotify-id 123456 --spotify-secret 123456 ...
```
sldl will try to open a browser automatically but will fallback to logging the login flow URL to output. After login flow is complete sldl will output a **Token** and **Refresh Token** and finish running the current command.
To skip requiring login flow every time `sldl` is used the **Token** and **Refresh Token** can be provided to sldl (hint: use `--config` and store this info in the config file to make commands less verbose):
```shell
sldl spotify-likes --number 1 --spotify-id 123456 --spotify-secret 123456 --spotify-refresh 123456 --spotify-token 123456 ...
```
`spotify-token` access is only valid for 1 hour. `spotify-refresh` will enable sldl to renew access every time it is run (and can be used without including `spotify-token`)
### Bandcamp ### Bandcamp
An bandcamp url: Download a single track, and album, or an artist's entire discography. An bandcamp url: Download a single track, and album, or an artist's entire discography.

View file

@ -30,6 +30,8 @@ static class Config
public static string defaultFolderName = ""; public static string defaultFolderName = "";
public static string spotifyId = ""; public static string spotifyId = "";
public static string spotifySecret = ""; public static string spotifySecret = "";
public static string spotifyToken = "";
public static string spotifyRefresh = "";
public static string ytKey = ""; public static string ytKey = "";
public static string username = ""; public static string username = "";
public static string password = ""; public static string password = "";
@ -642,6 +644,14 @@ static class Config
case "--spotify-secret": case "--spotify-secret":
spotifySecret = args[++i]; spotifySecret = args[++i];
break; break;
case "--stk":
case "--spotify-token":
spotifyToken = args[++i];
break;
case "--str":
case "--spotify-refresh":
spotifyRefresh = args[++i];
break;
case "--yk": case "--yk":
case "--youtube-key": case "--youtube-key":
ytKey = args[++i]; ytKey = args[++i];

View file

@ -4,6 +4,7 @@ using Swan;
using Data; using Data;
using Enums; using Enums;
using System.Security;
namespace Extractors namespace Extractors
{ {
@ -34,15 +35,19 @@ namespace Extractors
Config.spotifyId = Console.ReadLine(); Config.spotifyId = Console.ReadLine();
Console.Write("Spotify client secret:"); Console.Write("Spotify client secret:");
Config.spotifySecret = Console.ReadLine(); Config.spotifySecret = Console.ReadLine();
Console.Write("Spotify token:");
Config.spotifyToken = Console.ReadLine();
Console.Write("Spotify refresh token:");
Config.spotifyRefresh = Console.ReadLine();
Console.WriteLine(); Console.WriteLine();
} }
if (needLogin && (Config.spotifyId.Length == 0 || Config.spotifySecret.Length == 0)) if (needLogin && Config.spotifyToken.Length == 0 && (Config.spotifyId.Length == 0 || Config.spotifySecret.Length == 0))
{ {
readSpotifyCreds(); readSpotifyCreds();
} }
spotifyClient = new Spotify(Config.spotifyId, Config.spotifySecret); spotifyClient = new Spotify(Config.spotifyId, Config.spotifySecret, Config.spotifyToken, Config.spotifyRefresh);
await spotifyClient.Authorize(needLogin, Config.removeTracksFromSource); await spotifyClient.Authorize(needLogin, Config.removeTracksFromSource);
if (Config.input == "spotify-likes") if (Config.input == "spotify-likes")
@ -132,6 +137,8 @@ namespace Extractors
private EmbedIOAuthServer _server; private EmbedIOAuthServer _server;
private readonly string _clientId; private readonly string _clientId;
private readonly string _clientSecret; private readonly string _clientSecret;
private string _clientToken;
private string _clientRefreshToken;
private SpotifyClient? _client; private SpotifyClient? _client;
private bool loggedIn = false; private bool loggedIn = false;
@ -140,12 +147,14 @@ namespace Extractors
public const string encodedSpotifySecret = "Y2JlM2QxYTE5MzJkNDQ2MmFiOGUy3shTuf4Y2JhY2M3ZDdjYWU="; public const string encodedSpotifySecret = "Y2JlM2QxYTE5MzJkNDQ2MmFiOGUy3shTuf4Y2JhY2M3ZDdjYWU=";
public bool UsedDefaultCredentials { get; private set; } public bool UsedDefaultCredentials { get; private set; }
public Spotify(string clientId = "", string clientSecret = "") public Spotify(string clientId = "", string clientSecret = "", string token = "", string refreshToken = "")
{ {
_clientId = clientId; _clientId = clientId;
_clientSecret = clientSecret; _clientSecret = clientSecret;
_clientToken = token;
_clientRefreshToken = refreshToken;
if (_clientId.Length == 0 || _clientSecret.Length == 0) if (_clientToken.Length == 0 && (_clientId.Length == 0 || _clientSecret.Length == 0))
{ {
_clientId = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encodedSpotifyId.Replace("1bLaH9", ""))); _clientId = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encodedSpotifyId.Replace("1bLaH9", "")));
_clientSecret = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encodedSpotifySecret.Replace("3shTuf4", ""))); _clientSecret = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encodedSpotifySecret.Replace("3shTuf4", "")));
@ -172,27 +181,91 @@ namespace Extractors
_server = new EmbedIOAuthServer(new Uri("http://localhost:48721/callback"), 48721); _server = new EmbedIOAuthServer(new Uri("http://localhost:48721/callback"), 48721);
await _server.Start(); await _server.Start();
_server.AuthorizationCodeReceived += OnAuthorizationCodeReceived; var existingOk = false;
_server.ErrorReceived += OnErrorReceived; if (_clientToken.Length != 0 || _clientRefreshToken.Length != 0)
{
existingOk = await this.TryExistingToken();
loggedIn = true;
//new OAuthClient(config).RequestToken()
}
if (!existingOk)
{
_server.AuthorizationCodeReceived += OnAuthorizationCodeReceived;
_server.ErrorReceived += OnErrorReceived;
var scope = new List<string> { var scope = new List<string> {
Scopes.UserLibraryRead, Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative Scopes.UserLibraryRead, Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative
}; };
if (needModify) if (needModify)
{ {
scope.Add(Scopes.PlaylistModifyPublic); scope.Add(Scopes.PlaylistModifyPublic);
scope.Add(Scopes.PlaylistModifyPrivate); scope.Add(Scopes.PlaylistModifyPrivate);
}
var request = new LoginRequest(_server.BaseUri, _clientId, LoginRequest.ResponseType.Code) { Scope = scope };
try
{
BrowserUtil.Open(request.ToUri());
}
catch (Exception)
{
Console.WriteLine("Unable to open URL, manually open: {0}", request.ToUri());
}
} }
var request = new LoginRequest(_server.BaseUri, _clientId, LoginRequest.ResponseType.Code) { Scope = scope };
BrowserUtil.Open(request.ToUri());
await IsClientReady(); await IsClientReady();
} }
} }
private async Task<bool> TryExistingToken()
{
if (_clientToken.Length != 0)
{
Console.WriteLine("Testing Spotify access with existing token...");
var client = new SpotifyClient(_clientToken);
try
{
var me = await client.UserProfile.Current();
Console.WriteLine("Spotify access is good!");
_client = client;
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Could not make an API call with existing token: {ex}");
}
}
if (_clientRefreshToken.Length != 0)
{
Console.WriteLine("Trying to renew access with refresh token...");
// var refreshRequest = new TokenSwapRefreshRequest(
// new Uri("http://localhost:48721/refresh"),
// _clientRefreshToken
// );
var refreshRequest = new AuthorizationCodeRefreshRequest(_clientId, _clientSecret, _clientRefreshToken);
try
{
var oauthClient = new OAuthClient();
var refreshResponse = await oauthClient.RequestToken(refreshRequest);
Console.WriteLine($"We got a new refreshed access token from server: {refreshResponse.AccessToken}");
_clientToken = refreshResponse.AccessToken;
_client = new SpotifyClient(_clientToken);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Could not refresh access token with refresh token: {ex}");
}
} else {
Console.WriteLine("No refresh token present, cannot refresh existing access");
}
Console.WriteLine("Not possible to access Spotify API without login! Falling back to login flow...");
return false;
}
private async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response) private async Task OnAuthorizationCodeReceived(object sender, AuthorizationCodeResponse response)
{ {
await _server.Stop(); await _server.Stop();
@ -204,6 +277,11 @@ namespace Extractors
) )
); );
Console.WriteLine("Spotify token: " + tokenResponse.AccessToken);
_clientToken = tokenResponse.AccessToken;
Console.WriteLine("Spotify refresh token: " + tokenResponse.RefreshToken);
_clientRefreshToken = tokenResponse.RefreshToken;
_client = new SpotifyClient(tokenResponse.AccessToken); _client = new SpotifyClient(tokenResponse.AccessToken);
loggedIn = true; loggedIn = true;
} }