diff --git a/README.md b/README.md
index f5c8f80..350a9c6 100644
--- a/README.md
+++ b/README.md
@@ -126,6 +126,8 @@ Usage: sldl [OPTIONS]
Spotify
--spotify-id spotify client ID
--spotify-secret spotify client secret
+ --spotify-token spotify access token
+ --spotify-refresh spotify refresh token
--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
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
-a private playlist or liked music.
-The id and secret can be obtained at https://developer.spotify.com/dashboard/applications.
-Create an app and add http://localhost:48721/callback as a redirect url in its settings.
+liked songs. Credentials are required when downloading a private playlist or liked music.
+
+#### Using Credential/Application
+
+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
An bandcamp url: Download a single track, and album, or an artist's entire discography.
diff --git a/slsk-batchdl/Config.cs b/slsk-batchdl/Config.cs
index bbd84df..36c82f4 100644
--- a/slsk-batchdl/Config.cs
+++ b/slsk-batchdl/Config.cs
@@ -30,6 +30,8 @@ static class Config
public static string defaultFolderName = "";
public static string spotifyId = "";
public static string spotifySecret = "";
+ public static string spotifyToken = "";
+ public static string spotifyRefresh = "";
public static string ytKey = "";
public static string username = "";
public static string password = "";
@@ -642,6 +644,14 @@ static class Config
case "--spotify-secret":
spotifySecret = args[++i];
break;
+ case "--stk":
+ case "--spotify-token":
+ spotifyToken = args[++i];
+ break;
+ case "--str":
+ case "--spotify-refresh":
+ spotifyRefresh = args[++i];
+ break;
case "--yk":
case "--youtube-key":
ytKey = args[++i];
diff --git a/slsk-batchdl/Extractors/Spotify.cs b/slsk-batchdl/Extractors/Spotify.cs
index 2958ca0..8cf8028 100644
--- a/slsk-batchdl/Extractors/Spotify.cs
+++ b/slsk-batchdl/Extractors/Spotify.cs
@@ -4,6 +4,7 @@ using Swan;
using Data;
using Enums;
+using System.Security;
namespace Extractors
{
@@ -34,15 +35,19 @@ namespace Extractors
Config.spotifyId = Console.ReadLine();
Console.Write("Spotify client secret:");
Config.spotifySecret = Console.ReadLine();
+ Console.Write("Spotify token:");
+ Config.spotifyToken = Console.ReadLine();
+ Console.Write("Spotify refresh token:");
+ Config.spotifyRefresh = Console.ReadLine();
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();
}
- spotifyClient = new Spotify(Config.spotifyId, Config.spotifySecret);
+ spotifyClient = new Spotify(Config.spotifyId, Config.spotifySecret, Config.spotifyToken, Config.spotifyRefresh);
await spotifyClient.Authorize(needLogin, Config.removeTracksFromSource);
if (Config.input == "spotify-likes")
@@ -132,6 +137,8 @@ namespace Extractors
private EmbedIOAuthServer _server;
private readonly string _clientId;
private readonly string _clientSecret;
+ private string _clientToken;
+ private string _clientRefreshToken;
private SpotifyClient? _client;
private bool loggedIn = false;
@@ -140,12 +147,14 @@ namespace Extractors
public const string encodedSpotifySecret = "Y2JlM2QxYTE5MzJkNDQ2MmFiOGUy3shTuf4Y2JhY2M3ZDdjYWU=";
public bool UsedDefaultCredentials { get; private set; }
- public Spotify(string clientId = "", string clientSecret = "")
+ public Spotify(string clientId = "", string clientSecret = "", string token = "", string refreshToken = "")
{
_clientId = clientId;
_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", "")));
_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);
await _server.Start();
- _server.AuthorizationCodeReceived += OnAuthorizationCodeReceived;
- _server.ErrorReceived += OnErrorReceived;
+ var existingOk = false;
+ 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 {
+ var scope = new List {
Scopes.UserLibraryRead, Scopes.PlaylistReadPrivate, Scopes.PlaylistReadCollaborative
};
- if (needModify)
- {
- scope.Add(Scopes.PlaylistModifyPublic);
- scope.Add(Scopes.PlaylistModifyPrivate);
+ if (needModify)
+ {
+ scope.Add(Scopes.PlaylistModifyPublic);
+ 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();
}
}
+ private async Task 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)
{
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);
loggedIn = true;
}