From 206cf0355350f96dc8f784dc5cd0f7c47c8b538e Mon Sep 17 00:00:00 2001 From: werwolf2303 Date: Thu, 14 Aug 2025 11:20:48 +0200 Subject: [PATCH] Request token from Login5 Changed token request from Mercury to Login5 Removed scoped token functionality --- .../java/xyz/gianlu/librespot/api/Main.java | 8 +- .../api/handlers/MetadataHandler.java | 8 +- .../librespot/api/handlers/TokensHandler.java | 13 +-- .../librespot/api/handlers/WebApiHandler.java | 7 +- .../xyz/gianlu/librespot/ZeroconfServer.java | 4 +- .../audio/PlayableContentFeeder.java | 14 +-- .../librespot/audio/cdn/CdnManager.java | 6 +- .../xyz/gianlu/librespot/core/ApResolver.java | 1 - .../java/xyz/gianlu/librespot/core/OAuth.java | 5 +- .../xyz/gianlu/librespot/core/Session.java | 6 +- .../gianlu/librespot/core/TimeProvider.java | 5 +- .../gianlu/librespot/core/TokenProvider.java | 109 ++++++++---------- .../gianlu/librespot/dealer/ApiClient.java | 42 +++---- .../gianlu/librespot/dealer/DealerClient.java | 8 +- .../xyz/gianlu/librespot/player/Main.java | 4 +- .../gianlu/librespot/player/StateWrapper.java | 3 +- .../player/playback/PlayerQueueEntry.java | 7 +- .../player/state/DeviceStateHandler.java | 4 +- 18 files changed, 117 insertions(+), 137 deletions(-) diff --git a/api/src/main/java/xyz/gianlu/librespot/api/Main.java b/api/src/main/java/xyz/gianlu/librespot/api/Main.java index 1556f918..d962a1b4 100644 --- a/api/src/main/java/xyz/gianlu/librespot/api/Main.java +++ b/api/src/main/java/xyz/gianlu/librespot/api/Main.java @@ -21,7 +21,7 @@ import org.jetbrains.annotations.NotNull; import xyz.gianlu.librespot.common.Log4JUncaughtExceptionHandler; import xyz.gianlu.librespot.core.Session; -import xyz.gianlu.librespot.mercury.MercuryClient; +import xyz.gianlu.librespot.core.TokenProvider; import xyz.gianlu.librespot.player.FileConfiguration; import xyz.gianlu.librespot.player.FileConfiguration.AuthStrategy; @@ -33,7 +33,7 @@ */ public class Main { - public static void main(String[] args) throws IOException, MercuryClient.MercuryException, GeneralSecurityException, Session.SpotifyAuthenticationException { + public static void main(String[] args) throws IOException, TokenProvider.TokenException, GeneralSecurityException, Session.SpotifyAuthenticationException { FileConfiguration conf = new FileConfiguration(args); Configurator.setRootLevel(conf.loggingLevel()); Thread.setDefaultUncaughtExceptionHandler(new Log4JUncaughtExceptionHandler()); @@ -45,7 +45,7 @@ public static void main(String[] args) throws IOException, MercuryClient.Mercury else withPlayer(port, host, conf); } - private static void withPlayer(int port, @NotNull String host, @NotNull FileConfiguration conf) throws IOException, MercuryClient.MercuryException, GeneralSecurityException, Session.SpotifyAuthenticationException { + private static void withPlayer(int port, @NotNull String host, @NotNull FileConfiguration conf) throws IOException, TokenProvider.TokenException, GeneralSecurityException, Session.SpotifyAuthenticationException { PlayerWrapper wrapper; if (conf.authStrategy() == AuthStrategy.ZEROCONF) wrapper = PlayerWrapper.fromZeroconf(conf.initZeroconfBuilder().create(), conf.toPlayer(), conf.toEventsShell()); @@ -57,7 +57,7 @@ private static void withPlayer(int port, @NotNull String host, @NotNull FileConf server.start(); } - private static void withoutPlayer(int port, @NotNull String host, @NotNull FileConfiguration conf) throws IOException, MercuryClient.MercuryException, GeneralSecurityException, Session.SpotifyAuthenticationException { + private static void withoutPlayer(int port, @NotNull String host, @NotNull FileConfiguration conf) throws IOException, TokenProvider.TokenException, GeneralSecurityException, Session.SpotifyAuthenticationException { SessionWrapper wrapper; if (conf.authStrategy() == AuthStrategy.ZEROCONF) wrapper = SessionWrapper.fromZeroconf(conf.initZeroconfBuilder().create(), conf.toEventsShell()); diff --git a/api/src/main/java/xyz/gianlu/librespot/api/handlers/MetadataHandler.java b/api/src/main/java/xyz/gianlu/librespot/api/handlers/MetadataHandler.java index c0b58c6c..e8cd0a77 100644 --- a/api/src/main/java/xyz/gianlu/librespot/api/handlers/MetadataHandler.java +++ b/api/src/main/java/xyz/gianlu/librespot/api/handlers/MetadataHandler.java @@ -26,8 +26,8 @@ import xyz.gianlu.librespot.api.Utils; import xyz.gianlu.librespot.common.ProtobufToJson; import xyz.gianlu.librespot.core.Session; +import xyz.gianlu.librespot.core.TokenProvider; import xyz.gianlu.librespot.dealer.ApiClient; -import xyz.gianlu.librespot.mercury.MercuryClient; import xyz.gianlu.librespot.metadata.*; import java.io.IOException; @@ -91,7 +91,7 @@ public void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Session Utils.internalError(exchange, ex); LOGGER.error("Failed handling api request. {type: {}, uri: {}, code: {}}", type, uri, ex.code, ex); - } catch (IOException | MercuryClient.MercuryException ex) { + } catch (IOException | TokenProvider.TokenException ex) { Utils.internalError(exchange, ex); LOGGER.error("Failed handling api request. {type: {}, uri: {}}", type, uri, ex); } catch (IllegalArgumentException ex) { @@ -100,7 +100,7 @@ public void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Session } @NotNull - private JsonObject handle(@NotNull Session session, @NotNull MetadataType type, @NotNull String uri) throws IOException, MercuryClient.MercuryException, IllegalArgumentException { + private JsonObject handle(@NotNull Session session, @NotNull MetadataType type, @NotNull String uri) throws IOException, TokenProvider.TokenException, IllegalArgumentException { switch (type) { case ALBUM: return ProtobufToJson.convert(session.api().getMetadata4Album(AlbumId.fromUri(uri))); @@ -120,7 +120,7 @@ private JsonObject handle(@NotNull Session session, @NotNull MetadataType type, } @NotNull - private JsonObject handlePlaylist(@NotNull Session session, @NotNull String uri) throws IOException, MercuryClient.MercuryException { + private JsonObject handlePlaylist(@NotNull Session session, @NotNull String uri) throws IOException, TokenProvider.TokenException { return ProtobufToJson.convert(session.api().getPlaylist(PlaylistId.fromUri(uri))); } diff --git a/api/src/main/java/xyz/gianlu/librespot/api/handlers/TokensHandler.java b/api/src/main/java/xyz/gianlu/librespot/api/handlers/TokensHandler.java index 62588f58..10bbdfdf 100644 --- a/api/src/main/java/xyz/gianlu/librespot/api/handlers/TokensHandler.java +++ b/api/src/main/java/xyz/gianlu/librespot/api/handlers/TokensHandler.java @@ -20,13 +20,9 @@ import io.undertow.server.HttpServerExchange; import org.jetbrains.annotations.NotNull; import xyz.gianlu.librespot.api.SessionWrapper; -import xyz.gianlu.librespot.api.Utils; import xyz.gianlu.librespot.core.Session; import xyz.gianlu.librespot.core.TokenProvider; -import java.util.Deque; -import java.util.Map; - public final class TokensHandler extends AbsSessionHandler { public TokensHandler(@NotNull SessionWrapper wrapper) { @@ -41,14 +37,7 @@ protected void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Sess return; } - Map> params = Utils.readParameters(exchange); - String scope = Utils.getFirstString(params, "scope"); - if (scope == null) { - Utils.invalidParameter(exchange, "scope"); - return; - } - - TokenProvider.StoredToken token = session.tokens().getToken(scope); + TokenProvider.StoredToken token = session.tokens().getToken(); JsonObject obj = new JsonObject(); obj.addProperty("token", token.accessToken); obj.addProperty("timestamp", token.timestamp); diff --git a/api/src/main/java/xyz/gianlu/librespot/api/handlers/WebApiHandler.java b/api/src/main/java/xyz/gianlu/librespot/api/handlers/WebApiHandler.java index b5c60f20..98a26e68 100644 --- a/api/src/main/java/xyz/gianlu/librespot/api/handlers/WebApiHandler.java +++ b/api/src/main/java/xyz/gianlu/librespot/api/handlers/WebApiHandler.java @@ -28,7 +28,6 @@ import xyz.gianlu.librespot.core.TokenProvider; public final class WebApiHandler extends AbsSessionHandler { - private static final String[] API_TOKENS_ALL = new String[]{"ugc-image-upload", "playlist-read-collaborative", "playlist-modify-private", "playlist-modify-public", "playlist-read-private", "user-read-playback-position", "user-read-recently-played", "user-top-read", "user-modify-playback-state", "user-read-currently-playing", "user-read-playback-state", "user-read-private", "user-read-email", "user-library-modify", "user-library-read", "user-follow-modify", "user-follow-read", "streaming", "app-remote-control"}; private static final HttpUrl BASE_API_URL = HttpUrl.get("https://api.spotify.com"); private static final HttpString HEADER_X_SCOPE = HttpString.tryFromString("X-Spotify-Scope"); @@ -47,11 +46,7 @@ protected void handleRequest(@NotNull HttpServerExchange exchange, @NotNull Sess String body = FileUtils.readFile(exchange.getInputStream()); HeaderValues contentType = exchange.getRequestHeaders().get(Headers.CONTENT_TYPE); - String[] scopes = API_TOKENS_ALL; - if (exchange.getRequestHeaders().contains(HEADER_X_SCOPE)) - scopes = exchange.getRequestHeaders().get(HEADER_X_SCOPE).toArray(new String[0]); - - TokenProvider.StoredToken token = session.tokens().getToken(scopes); + TokenProvider.StoredToken token = session.tokens().getToken(); HttpUrl.Builder url = BASE_API_URL.newBuilder() .addPathSegments(exchange.getRelativePath().substring(1)) diff --git a/lib/src/main/java/xyz/gianlu/librespot/ZeroconfServer.java b/lib/src/main/java/xyz/gianlu/librespot/ZeroconfServer.java index 91ba06b3..a5621d10 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/ZeroconfServer.java +++ b/lib/src/main/java/xyz/gianlu/librespot/ZeroconfServer.java @@ -27,8 +27,8 @@ import xyz.gianlu.librespot.common.NameThreadFactory; import xyz.gianlu.librespot.common.Utils; import xyz.gianlu.librespot.core.Session; +import xyz.gianlu.librespot.core.TokenProvider; import xyz.gianlu.librespot.crypto.DiffieHellman; -import xyz.gianlu.librespot.mercury.MercuryClient; import xyz.gianlu.zeroconf.Service; import xyz.gianlu.zeroconf.Zeroconf; @@ -371,7 +371,7 @@ private void handleAddUser(OutputStream out, Map params, String } sessionListeners.forEach(l -> l.sessionChanged(session)); - } catch (Session.SpotifyAuthenticationException | MercuryClient.MercuryException | IOException | GeneralSecurityException ex) { + } catch (Session.SpotifyAuthenticationException | TokenProvider.TokenException | IOException | GeneralSecurityException ex) { LOGGER.error("Couldn't establish a new session.", ex); synchronized (connectionLock) { diff --git a/lib/src/main/java/xyz/gianlu/librespot/audio/PlayableContentFeeder.java b/lib/src/main/java/xyz/gianlu/librespot/audio/PlayableContentFeeder.java index 46406cba..e9c7fc95 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/audio/PlayableContentFeeder.java +++ b/lib/src/main/java/xyz/gianlu/librespot/audio/PlayableContentFeeder.java @@ -36,7 +36,7 @@ import xyz.gianlu.librespot.common.NameThreadFactory; import xyz.gianlu.librespot.common.Utils; import xyz.gianlu.librespot.core.Session; -import xyz.gianlu.librespot.mercury.MercuryClient; +import xyz.gianlu.librespot.core.TokenProvider; import xyz.gianlu.librespot.metadata.EpisodeId; import xyz.gianlu.librespot.metadata.LocalId; import xyz.gianlu.librespot.metadata.PlayableId; @@ -81,7 +81,7 @@ private static Metadata.Track pickAlternativeIfNecessary(@NotNull Metadata.Track } @NotNull - public final LoadedStream load(@NotNull PlayableId id, @NotNull AudioQualityPicker audioQualityPicker, boolean preload, @Nullable HaltListener haltListener) throws CdnManager.CdnException, ContentRestrictedException, MercuryClient.MercuryException, IOException { + public final LoadedStream load(@NotNull PlayableId id, @NotNull AudioQualityPicker audioQualityPicker, boolean preload, @Nullable HaltListener haltListener) throws CdnManager.CdnException, ContentRestrictedException, TokenProvider.TokenException, IOException { if (id instanceof TrackId) return loadTrack((TrackId) id, audioQualityPicker, preload, haltListener); else if (id instanceof EpisodeId) @@ -91,7 +91,7 @@ else if (id instanceof EpisodeId) } @NotNull - private StorageResolveResponse resolveStorageInteractive(@NotNull ByteString fileId, boolean preload) throws IOException, MercuryClient.MercuryException { + private StorageResolveResponse resolveStorageInteractive(@NotNull ByteString fileId, boolean preload) throws IOException, TokenProvider.TokenException { try (Response resp = session.api().send("GET", String.format(preload ? STORAGE_RESOLVE_INTERACTIVE_PREFETCH : STORAGE_RESOLVE_INTERACTIVE, Utils.bytesToHex(fileId)), null, null)) { if (resp.code() != 200) throw new IOException(resp.code() + ": " + resp.message()); @@ -102,7 +102,7 @@ private StorageResolveResponse resolveStorageInteractive(@NotNull ByteString fil } } - private @NotNull LoadedStream loadTrack(@NotNull TrackId id, @NotNull AudioQualityPicker audioQualityPicker, boolean preload, @Nullable HaltListener haltListener) throws IOException, MercuryClient.MercuryException, ContentRestrictedException, CdnManager.CdnException { + private @NotNull LoadedStream loadTrack(@NotNull TrackId id, @NotNull AudioQualityPicker audioQualityPicker, boolean preload, @Nullable HaltListener haltListener) throws IOException, TokenProvider.TokenException, ContentRestrictedException, CdnManager.CdnException { Metadata.Track original = session.api().getMetadata4Track(id); Metadata.Track track = pickAlternativeIfNecessary(original); if (track == null) { @@ -129,7 +129,7 @@ private LoadedStream loadCdnStream(@NotNull Metadata.AudioFile file, @Nullable M @NotNull @Contract("_, null, null, _, _ -> fail") - private LoadedStream loadStream(@NotNull Metadata.AudioFile file, @Nullable Metadata.Track track, @Nullable Metadata.Episode episode, boolean preload, @Nullable HaltListener haltListener) throws IOException, MercuryClient.MercuryException, CdnManager.CdnException { + private LoadedStream loadStream(@NotNull Metadata.AudioFile file, @Nullable Metadata.Track track, @Nullable Metadata.Episode episode, boolean preload, @Nullable HaltListener haltListener) throws IOException, TokenProvider.TokenException, CdnManager.CdnException { if (track == null && episode == null) throw new IllegalStateException(); @@ -156,7 +156,7 @@ private LoadedStream loadStream(@NotNull Metadata.AudioFile file, @Nullable Meta } @NotNull - private LoadedStream loadTrack(@NotNull Metadata.Track track, @NotNull AudioQualityPicker audioQualityPicker, boolean preload, @Nullable HaltListener haltListener) throws IOException, CdnManager.CdnException, MercuryClient.MercuryException { + private LoadedStream loadTrack(@NotNull Metadata.Track track, @NotNull AudioQualityPicker audioQualityPicker, boolean preload, @Nullable HaltListener haltListener) throws IOException, CdnManager.CdnException, TokenProvider.TokenException { Metadata.AudioFile file = audioQualityPicker.getFile(track.getFileList()); if (file == null) { LOGGER.error("Couldn't find any suitable audio file, available: {}", Utils.formatsToString(track.getFileList())); @@ -167,7 +167,7 @@ private LoadedStream loadTrack(@NotNull Metadata.Track track, @NotNull AudioQual } @NotNull - private LoadedStream loadEpisode(@NotNull EpisodeId id, @NotNull AudioQualityPicker audioQualityPicker, boolean preload, @Nullable HaltListener haltListener) throws IOException, MercuryClient.MercuryException, CdnManager.CdnException { + private LoadedStream loadEpisode(@NotNull EpisodeId id, @NotNull AudioQualityPicker audioQualityPicker, boolean preload, @Nullable HaltListener haltListener) throws IOException, TokenProvider.TokenException, CdnManager.CdnException { Metadata.Episode episode = session.api().getMetadata4Episode(id); if (episode.hasExternalUrl()) { diff --git a/lib/src/main/java/xyz/gianlu/librespot/audio/cdn/CdnManager.java b/lib/src/main/java/xyz/gianlu/librespot/audio/cdn/CdnManager.java index 4904b3c6..76a879bb 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/audio/cdn/CdnManager.java +++ b/lib/src/main/java/xyz/gianlu/librespot/audio/cdn/CdnManager.java @@ -34,7 +34,7 @@ import xyz.gianlu.librespot.common.NameThreadFactory; import xyz.gianlu.librespot.common.Utils; import xyz.gianlu.librespot.core.Session; -import xyz.gianlu.librespot.mercury.MercuryClient; +import xyz.gianlu.librespot.core.TokenProvider; import java.io.IOException; import java.io.InputStream; @@ -88,7 +88,7 @@ public Streamer streamFile(@NotNull Metadata.AudioFile file, @NotNull byte[] key * This is used only to RENEW the url if needed. */ @NotNull - private HttpUrl getAudioUrl(@NotNull ByteString fileId) throws IOException, CdnException, MercuryClient.MercuryException { + private HttpUrl getAudioUrl(@NotNull ByteString fileId) throws IOException, CdnException, TokenProvider.TokenException { try (Response resp = session.api().send("GET", String.format("/storage-resolve/files/audio/interactive/%s", Utils.bytesToHex(fileId)), null, null)) { if (resp.code() != 200) throw new IOException(resp.code() + ": " + resp.message()); @@ -145,7 +145,7 @@ HttpUrl url() throws CdnException { if (expiration <= System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5)) { try { url = getAudioUrl(fileId); - } catch (IOException | MercuryClient.MercuryException ex) { + } catch (IOException | TokenProvider.TokenException ex) { throw new CdnException(ex); } } diff --git a/lib/src/main/java/xyz/gianlu/librespot/core/ApResolver.java b/lib/src/main/java/xyz/gianlu/librespot/core/ApResolver.java index 0740bd66..7c5850b2 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/core/ApResolver.java +++ b/lib/src/main/java/xyz/gianlu/librespot/core/ApResolver.java @@ -29,7 +29,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.io.Reader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; diff --git a/lib/src/main/java/xyz/gianlu/librespot/core/OAuth.java b/lib/src/main/java/xyz/gianlu/librespot/core/OAuth.java index 5c69eda9..ac16a436 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/core/OAuth.java +++ b/lib/src/main/java/xyz/gianlu/librespot/core/OAuth.java @@ -9,7 +9,10 @@ import org.slf4j.LoggerFactory; import java.io.*; -import java.net.*; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; diff --git a/lib/src/main/java/xyz/gianlu/librespot/core/Session.java b/lib/src/main/java/xyz/gianlu/librespot/core/Session.java index 7e1d265d..55dffd4b 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/core/Session.java +++ b/lib/src/main/java/xyz/gianlu/librespot/core/Session.java @@ -24,8 +24,8 @@ import com.spotify.connectstate.Connect; import com.spotify.explicit.ExplicitContentPubsub; import com.spotify.explicit.ExplicitContentPubsub.UserAttributesUpdate; -import okhttp3.Authenticator; import okhttp3.*; +import okhttp3.Authenticator; import okio.BufferedSink; import okio.GzipSink; import okio.Okio; @@ -340,7 +340,7 @@ private void connect() throws IOException, GeneralSecurityException, SpotifyAuth * Authenticates with the server and creates all the necessary components. * All of them should be initialized inside the synchronized block and MUST NOT call any method on this {@link Session} object. */ - private void authenticate(@NotNull Authentication.LoginCredentials credentials) throws IOException, GeneralSecurityException, SpotifyAuthenticationException, MercuryClient.MercuryException { + private void authenticate(@NotNull Authentication.LoginCredentials credentials) throws IOException, GeneralSecurityException, SpotifyAuthenticationException, TokenProvider.TokenException { authenticatePartial(credentials, false); if (credentials.getTyp() == Authentication.AuthenticationType.AUTHENTICATION_SPOTIFY_TOKEN) @@ -1069,7 +1069,7 @@ public Builder userPass(@NotNull String username, @NotNull String password) { * Creates a connected and fully authenticated {@link Session} object. */ @NotNull - public Session create() throws IOException, GeneralSecurityException, SpotifyAuthenticationException, MercuryClient.MercuryException { + public Session create() throws IOException, GeneralSecurityException, SpotifyAuthenticationException, TokenProvider.TokenException { if (loginCredentials == null) throw new IllegalStateException("You must select an authentication method."); diff --git a/lib/src/main/java/xyz/gianlu/librespot/core/TimeProvider.java b/lib/src/main/java/xyz/gianlu/librespot/core/TimeProvider.java index a06684bb..d3586d43 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/core/TimeProvider.java +++ b/lib/src/main/java/xyz/gianlu/librespot/core/TimeProvider.java @@ -25,7 +25,6 @@ import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import xyz.gianlu.librespot.mercury.MercuryClient; import java.io.IOException; import java.net.InetAddress; @@ -101,7 +100,7 @@ private static void updateMelody(@NotNull Session session) { LOGGER.error("Failed notifying server of time request! {code: {}, msg: {}}", resp.code(), resp.message()); return; } - } catch (IOException | MercuryClient.MercuryException ex) { + } catch (IOException | TokenProvider.TokenException ex) { LOGGER.error("Failed notifying server of time request!", ex); return; } @@ -122,7 +121,7 @@ private static void updateMelody(@NotNull Session session) { } LOGGER.info("Loaded time offset from melody: {}ms", diff); - } catch (IOException | MercuryClient.MercuryException ex) { + } catch (IOException | TokenProvider.TokenException ex) { LOGGER.error("Failed requesting time!", ex); } } diff --git a/lib/src/main/java/xyz/gianlu/librespot/core/TokenProvider.java b/lib/src/main/java/xyz/gianlu/librespot/core/TokenProvider.java index b20a2527..9ecd5170 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/core/TokenProvider.java +++ b/lib/src/main/java/xyz/gianlu/librespot/core/TokenProvider.java @@ -16,22 +16,17 @@ package xyz.gianlu.librespot.core; -import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.google.protobuf.ByteString; +import com.spotify.login5v3.Credentials; +import com.spotify.login5v3.Login5; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import xyz.gianlu.librespot.common.Utils; -import xyz.gianlu.librespot.json.GenericJson; -import xyz.gianlu.librespot.mercury.MercuryClient; -import xyz.gianlu.librespot.mercury.MercuryRequests; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; +import java.security.NoSuchAlgorithmException; /** * @author Gianlu @@ -40,61 +35,76 @@ public final class TokenProvider { private final static Logger LOGGER = LoggerFactory.getLogger(TokenProvider.class); private final static int TOKEN_EXPIRE_THRESHOLD = 10; private final Session session; - private final List tokens = new ArrayList<>(); + private StoredToken token = null; TokenProvider(@NotNull Session session) { this.session = session; } - @Nullable - private StoredToken findTokenWithAllScopes(String[] scopes) { - for (StoredToken token : tokens) - if (token.hasScopes(scopes)) - return token; - - return null; - } - @NotNull - public synchronized StoredToken getToken(@NotNull String... scopes) throws IOException, MercuryClient.MercuryException { - if (scopes.length == 0) throw new IllegalArgumentException(); + public synchronized StoredToken getToken() throws IOException, TokenException { + if (this.token != null) { + if (this.token.expired()) this.token = null; + else return this.token; + } - StoredToken token = findTokenWithAllScopes(scopes); - if (token != null) { - if (token.expired()) tokens.remove(token); - else return token; + LOGGER.debug("Token expired or not suitable, requesting again. {oldToken: {}}", this.token); + + try { + Login5Api api = new Login5Api(session); + Login5.LoginResponse resp = api.login5( + Login5.LoginRequest.newBuilder() + .setStoredCredential(Credentials.StoredCredential.newBuilder() + .setUsername(session.username()) + .setData(ByteString.copyFrom(session.apWelcome().getReusableAuthCredentials().toByteArray())) + .build()) + .build() + ); + if (!resp.hasOk()) throw new TokenException(resp.getError().getNumber()); + Login5.LoginOk okResponse = resp.getOk(); + + JsonObject tokenBuilder = new JsonObject(); + tokenBuilder.addProperty("accessToken", okResponse.getAccessToken()); + tokenBuilder.addProperty("expiresIn", okResponse.getAccessTokenExpiresIn()); + tokenBuilder.addProperty("tokenType", "Bearer"); + + this.token = new StoredToken(tokenBuilder); + + LOGGER.debug("Updated token successfully! {newToken: {}}", this.token); + + return this.token; + }catch (NoSuchAlgorithmException e) { + throw new IOException(e); } + } - LOGGER.debug("Token expired or not suitable, requesting again. {scopes: {}, oldToken: {}}", Arrays.asList(scopes), token); - GenericJson resp = session.mercury().sendSync(MercuryRequests.requestToken(session.deviceId(), String.join(",", scopes))); - token = new StoredToken(resp.obj); + @NotNull + public String get() throws IOException, TokenException { + return getToken().accessToken; + } - LOGGER.debug("Updated token successfully! {scopes: {}, newToken: {}}", Arrays.asList(scopes), token); - tokens.add(token); + public static class TokenException extends Exception { + private final int code; - return token; - } + private TokenException(int code) { + super("Error while requesting token! Code: " + code); + this.code = code; + } - @NotNull - public String get(@NotNull String scope) throws IOException, MercuryClient.MercuryException { - return getToken(scope).accessToken; + public int getCode() { + return code; + } } public static class StoredToken { public final int expiresIn; public final String accessToken; - public final String[] scopes; public final long timestamp; private StoredToken(@NotNull JsonObject obj) { timestamp = TimeProvider.currentTimeMillis(); expiresIn = obj.get("expiresIn").getAsInt(); accessToken = obj.get("accessToken").getAsString(); - - JsonArray scopesArray = obj.getAsJsonArray("scope"); - scopes = new String[scopesArray.size()]; - for (int i = 0; i < scopesArray.size(); i++) - scopes[i] = scopesArray.get(i).getAsString(); } public boolean expired() { @@ -106,25 +116,8 @@ public String toString() { return "StoredToken{" + "expiresIn=" + expiresIn + ", accessToken='" + Utils.truncateMiddle(accessToken, 12) + - "', scopes=" + Arrays.toString(scopes) + ", timestamp=" + timestamp + '}'; } - - public boolean hasScope(@NotNull String scope) { - for (String s : scopes) - if (Objects.equals(s, scope)) - return true; - - return false; - } - - public boolean hasScopes(String[] sc) { - for (String s : sc) - if (!hasScope(s)) - return false; - - return true; - } } } diff --git a/lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java b/lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java index 9b0a5ff0..430bd8f8 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java +++ b/lib/src/main/java/xyz/gianlu/librespot/dealer/ApiClient.java @@ -33,8 +33,8 @@ import org.slf4j.LoggerFactory; import xyz.gianlu.librespot.Version; import xyz.gianlu.librespot.core.Session; +import xyz.gianlu.librespot.core.TokenProvider; import xyz.gianlu.librespot.json.StationsWrapper; -import xyz.gianlu.librespot.mercury.MercuryClient; import xyz.gianlu.librespot.mercury.MercuryRequests; import xyz.gianlu.librespot.metadata.*; @@ -74,7 +74,7 @@ public void writeTo(@NotNull BufferedSink sink) throws IOException { } @NotNull - private Request buildRequest(@NotNull String method, @NotNull String suffix, @Nullable Headers headers, @Nullable RequestBody body) throws IOException, MercuryClient.MercuryException { + private Request buildRequest(@NotNull String method, @NotNull String suffix, @Nullable Headers headers, @Nullable RequestBody body) throws IOException, TokenProvider.TokenException { if (clientToken == null) { ClientToken.ClientTokenResponse resp = clientToken(); clientToken = resp.getGrantedToken().getToken(); @@ -84,13 +84,13 @@ private Request buildRequest(@NotNull String method, @NotNull String suffix, @Nu Request.Builder request = new Request.Builder(); request.method(method, body); if (headers != null) request.headers(headers); - request.addHeader("Authorization", "Bearer " + session.tokens().get("playlist-read")); + request.addHeader("Authorization", "Bearer " + session.tokens().get()); request.addHeader("client-token", clientToken); request.url(baseUrl + suffix); return request.build(); } - public void sendAsync(@NotNull String method, @NotNull String suffix, @Nullable Headers headers, @Nullable RequestBody body, @NotNull Callback callback) throws IOException, MercuryClient.MercuryException { + public void sendAsync(@NotNull String method, @NotNull String suffix, @Nullable Headers headers, @Nullable RequestBody body, @NotNull Callback callback) throws IOException, TokenProvider.TokenException { session.client().newCall(buildRequest(method, suffix, headers, body)).enqueue(callback); } @@ -104,10 +104,10 @@ public void sendAsync(@NotNull String method, @NotNull String suffix, @Nullable * @param tries How many times the request should be reattempted (0 = none) * @return The response * @throws IOException The last {@link IOException} thrown by {@link Call#execute()} - * @throws MercuryClient.MercuryException If the API token couldn't be requested + * @throws TokenProvider.TokenException If the API token couldn't be requested */ @NotNull - public Response send(@NotNull String method, @NotNull String suffix, @Nullable Headers headers, @Nullable RequestBody body, int tries) throws IOException, MercuryClient.MercuryException { + public Response send(@NotNull String method, @NotNull String suffix, @Nullable Headers headers, @Nullable RequestBody body, int tries) throws IOException, TokenProvider.TokenException { IOException lastEx; do { try { @@ -127,11 +127,11 @@ public Response send(@NotNull String method, @NotNull String suffix, @Nullable H } @NotNull - public Response send(@NotNull String method, @NotNull String suffix, @Nullable Headers headers, @Nullable RequestBody body) throws IOException, MercuryClient.MercuryException { + public Response send(@NotNull String method, @NotNull String suffix, @Nullable Headers headers, @Nullable RequestBody body) throws IOException, TokenProvider.TokenException { return send(method, suffix, headers, body, 1); } - public void putConnectState(@NotNull String connectionId, @NotNull Connect.PutStateRequest proto) throws IOException, MercuryClient.MercuryException { + public void putConnectState(@NotNull String connectionId, @NotNull Connect.PutStateRequest proto) throws IOException, TokenProvider.TokenException { try (Response resp = send("PUT", "/connect-state/v1/devices/" + session.deviceId(), new Headers.Builder() .add("X-Spotify-Connection-Id", connectionId).build(), protoBody(proto), 5 /* We want this to succeed */)) { if (resp.code() == 413) @@ -142,7 +142,7 @@ else if (resp.code() != 200) } @NotNull - public Metadata.Track getMetadata4Track(@NotNull TrackId track) throws IOException, MercuryClient.MercuryException { + public Metadata.Track getMetadata4Track(@NotNull TrackId track) throws IOException, TokenProvider.TokenException { try (Response resp = send("GET", "/metadata/4/track/" + track.hexId(), null, null)) { StatusCodeException.checkStatus(resp); @@ -153,7 +153,7 @@ public Metadata.Track getMetadata4Track(@NotNull TrackId track) throws IOExcepti } @NotNull - public Metadata.Episode getMetadata4Episode(@NotNull EpisodeId episode) throws IOException, MercuryClient.MercuryException { + public Metadata.Episode getMetadata4Episode(@NotNull EpisodeId episode) throws IOException, TokenProvider.TokenException { try (Response resp = send("GET", "/metadata/4/episode/" + episode.hexId(), null, null)) { StatusCodeException.checkStatus(resp); @@ -164,7 +164,7 @@ public Metadata.Episode getMetadata4Episode(@NotNull EpisodeId episode) throws I } @NotNull - public Metadata.Album getMetadata4Album(@NotNull AlbumId album) throws IOException, MercuryClient.MercuryException { + public Metadata.Album getMetadata4Album(@NotNull AlbumId album) throws IOException, TokenProvider.TokenException { try (Response resp = send("GET", "/metadata/4/album/" + album.hexId(), null, null)) { StatusCodeException.checkStatus(resp); @@ -175,7 +175,7 @@ public Metadata.Album getMetadata4Album(@NotNull AlbumId album) throws IOExcepti } @NotNull - public Metadata.Artist getMetadata4Artist(@NotNull ArtistId artist) throws IOException, MercuryClient.MercuryException { + public Metadata.Artist getMetadata4Artist(@NotNull ArtistId artist) throws IOException, TokenProvider.TokenException { try (Response resp = send("GET", "/metadata/4/artist/" + artist.hexId(), null, null)) { StatusCodeException.checkStatus(resp); @@ -186,7 +186,7 @@ public Metadata.Artist getMetadata4Artist(@NotNull ArtistId artist) throws IOExc } @NotNull - public Metadata.Show getMetadata4Show(@NotNull ShowId show) throws IOException, MercuryClient.MercuryException { + public Metadata.Show getMetadata4Show(@NotNull ShowId show) throws IOException, TokenProvider.TokenException { try (Response resp = send("GET", "/metadata/4/show/" + show.hexId(), null, null)) { StatusCodeException.checkStatus(resp); @@ -197,7 +197,7 @@ public Metadata.Show getMetadata4Show(@NotNull ShowId show) throws IOException, } @NotNull - public EntityCanvazResponse getCanvases(@NotNull EntityCanvazRequest req) throws IOException, MercuryClient.MercuryException { + public EntityCanvazResponse getCanvases(@NotNull EntityCanvazRequest req) throws IOException, TokenProvider.TokenException { try (Response resp = send("POST", "/canvaz-cache/v0/canvases", null, protoBody(req))) { StatusCodeException.checkStatus(resp); @@ -208,7 +208,7 @@ public EntityCanvazResponse getCanvases(@NotNull EntityCanvazRequest req) throws } @NotNull - public ExtendedMetadata.BatchedExtensionResponse getExtendedMetadata(@NotNull ExtendedMetadata.BatchedEntityRequest req) throws IOException, MercuryClient.MercuryException { + public ExtendedMetadata.BatchedExtensionResponse getExtendedMetadata(@NotNull ExtendedMetadata.BatchedEntityRequest req) throws IOException, TokenProvider.TokenException { try (Response resp = send("POST", "/extended-metadata/v0/extended-metadata", null, protoBody(req))) { StatusCodeException.checkStatus(resp); @@ -219,7 +219,7 @@ public ExtendedMetadata.BatchedExtensionResponse getExtendedMetadata(@NotNull Ex } @NotNull - public Playlist4ApiProto.SelectedListContent getPlaylist(@NotNull PlaylistId id) throws IOException, MercuryClient.MercuryException { + public Playlist4ApiProto.SelectedListContent getPlaylist(@NotNull PlaylistId id) throws IOException, TokenProvider.TokenException { try (Response resp = send("GET", "/playlist/v2/playlist/" + id.id(), null, null)) { StatusCodeException.checkStatus(resp); @@ -231,7 +231,7 @@ public Playlist4ApiProto.SelectedListContent getPlaylist(@NotNull PlaylistId id) } @NotNull - public JsonObject getUserProfile(@NotNull String id, @Nullable Integer playlistLimit, @Nullable Integer artistLimit) throws IOException, MercuryClient.MercuryException { + public JsonObject getUserProfile(@NotNull String id, @Nullable Integer playlistLimit, @Nullable Integer artistLimit) throws IOException, TokenProvider.TokenException { StringBuilder url = new StringBuilder(); url.append("/user-profile-view/v3/profile/"); url.append(id); @@ -262,7 +262,7 @@ public JsonObject getUserProfile(@NotNull String id, @Nullable Integer playlistL } @NotNull - public JsonObject getUserFollowers(@NotNull String id) throws IOException, MercuryClient.MercuryException { + public JsonObject getUserFollowers(@NotNull String id) throws IOException, TokenProvider.TokenException { try (Response resp = send("GET", "/user-profile-view/v3/profile/" + id + "/followers", null, null)) { StatusCodeException.checkStatus(resp); @@ -273,7 +273,7 @@ public JsonObject getUserFollowers(@NotNull String id) throws IOException, Mercu } @NotNull - public JsonObject getUserFollowing(@NotNull String id) throws IOException, MercuryClient.MercuryException { + public JsonObject getUserFollowing(@NotNull String id) throws IOException, TokenProvider.TokenException { try (Response resp = send("GET", "/user-profile-view/v3/profile/" + id + "/following", null, null)) { StatusCodeException.checkStatus(resp); @@ -284,7 +284,7 @@ public JsonObject getUserFollowing(@NotNull String id) throws IOException, Mercu } @NotNull - public JsonObject getRadioForTrack(@NotNull PlayableId id) throws IOException, MercuryClient.MercuryException { + public JsonObject getRadioForTrack(@NotNull PlayableId id) throws IOException, TokenProvider.TokenException { try (Response resp = send("GET", "/inspiredby-mix/v2/seed_to_playlist/" + id.toSpotifyUri() + "?response-format=json", null, null)) { StatusCodeException.checkStatus(resp); @@ -295,7 +295,7 @@ public JsonObject getRadioForTrack(@NotNull PlayableId id) throws IOException, M } @NotNull - public StationsWrapper getApolloStation(@NotNull String context, @NotNull List prevTracks, int count, boolean autoplay) throws IOException, MercuryClient.MercuryException { + public StationsWrapper getApolloStation(@NotNull String context, @NotNull List prevTracks, int count, boolean autoplay) throws IOException, TokenProvider.TokenException { StringBuilder prevTracksStr = new StringBuilder(); for (int i = 0; i < prevTracks.size(); i++) { if (i != 0) prevTracksStr.append(","); diff --git a/lib/src/main/java/xyz/gianlu/librespot/dealer/DealerClient.java b/lib/src/main/java/xyz/gianlu/librespot/dealer/DealerClient.java index 887b62c1..982227a4 100644 --- a/lib/src/main/java/xyz/gianlu/librespot/dealer/DealerClient.java +++ b/lib/src/main/java/xyz/gianlu/librespot/dealer/DealerClient.java @@ -32,7 +32,7 @@ import xyz.gianlu.librespot.common.NameThreadFactory; import xyz.gianlu.librespot.common.Utils; import xyz.gianlu.librespot.core.Session; -import xyz.gianlu.librespot.mercury.MercuryClient; +import xyz.gianlu.librespot.core.TokenProvider; import java.io.*; import java.util.*; @@ -73,9 +73,9 @@ private static Map getHeaders(@NotNull JsonObject obj) { /** * Creates a new WebSocket client. Intended for internal use only! */ - public synchronized void connect() throws IOException, MercuryClient.MercuryException { + public synchronized void connect() throws IOException, TokenProvider.TokenException { conn = new ConnectionHolder(session, new Request.Builder() - .url(String.format("wss://%s/?access_token=%s", session.apResolver().getRandomDealer(), session.tokens().get("playlist-read"))) + .url(String.format("wss://%s/?access_token=%s", session.apResolver().getRandomDealer(), session.tokens().get())) .build()); } @@ -273,7 +273,7 @@ private synchronized void connectionInvalided() { try { connect(); - } catch (IOException | MercuryClient.MercuryException ex) { + } catch (IOException | TokenProvider.TokenException ex) { LOGGER.error("Failed reconnecting, retrying...", ex); connectionInvalided(); } diff --git a/player/src/main/java/xyz/gianlu/librespot/player/Main.java b/player/src/main/java/xyz/gianlu/librespot/player/Main.java index f8bcca49..64ef1ed1 100644 --- a/player/src/main/java/xyz/gianlu/librespot/player/Main.java +++ b/player/src/main/java/xyz/gianlu/librespot/player/Main.java @@ -22,7 +22,7 @@ import xyz.gianlu.librespot.ZeroconfServer; import xyz.gianlu.librespot.common.Log4JUncaughtExceptionHandler; import xyz.gianlu.librespot.core.Session; -import xyz.gianlu.librespot.mercury.MercuryClient; +import xyz.gianlu.librespot.core.TokenProvider; import java.io.IOException; import java.security.GeneralSecurityException; @@ -32,7 +32,7 @@ */ public class Main { - public static void main(String[] args) throws IOException, GeneralSecurityException, Session.SpotifyAuthenticationException, MercuryClient.MercuryException { + public static void main(String[] args) throws IOException, GeneralSecurityException, Session.SpotifyAuthenticationException, TokenProvider.TokenException { FileConfiguration conf = new FileConfiguration(args); Configurator.setRootLevel(conf.loggingLevel()); Thread.setDefaultUncaughtExceptionHandler(new Log4JUncaughtExceptionHandler()); diff --git a/player/src/main/java/xyz/gianlu/librespot/player/StateWrapper.java b/player/src/main/java/xyz/gianlu/librespot/player/StateWrapper.java index 4f072251..ca2e170f 100644 --- a/player/src/main/java/xyz/gianlu/librespot/player/StateWrapper.java +++ b/player/src/main/java/xyz/gianlu/librespot/player/StateWrapper.java @@ -45,6 +45,7 @@ import xyz.gianlu.librespot.common.Utils; import xyz.gianlu.librespot.core.Session; import xyz.gianlu.librespot.core.TimeProvider; +import xyz.gianlu.librespot.core.TokenProvider; import xyz.gianlu.librespot.dealer.DealerClient; import xyz.gianlu.librespot.mercury.MercuryClient; import xyz.gianlu.librespot.metadata.*; @@ -254,7 +255,7 @@ private void loadTransforming() { else throw new IllegalArgumentException(); LOGGER.debug("Updated context with transforming information!"); - } catch (MercuryClient.MercuryException | IOException ex) { + } catch (TokenProvider.TokenException | IOException ex) { LOGGER.warn("Failed loading cuepoints!", ex); } } diff --git a/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java b/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java index e8af70ee..7a8be8c5 100644 --- a/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java +++ b/player/src/main/java/xyz/gianlu/librespot/player/playback/PlayerQueueEntry.java @@ -29,7 +29,7 @@ import xyz.gianlu.librespot.audio.decoders.VorbisOnlyAudioQuality; import xyz.gianlu.librespot.common.Utils; import xyz.gianlu.librespot.core.Session; -import xyz.gianlu.librespot.mercury.MercuryClient; +import xyz.gianlu.librespot.core.TokenProvider; import xyz.gianlu.librespot.metadata.LocalId; import xyz.gianlu.librespot.metadata.PlayableId; import xyz.gianlu.librespot.player.PlayerConfiguration; @@ -106,7 +106,7 @@ PlayerQueueEntry retrySelf(boolean preloaded) { * * @throws PlayableContentFeeder.ContentRestrictedException If the content cannot be retrieved because of restrictions (this condition won't change with a retry). */ - private void load(boolean preload) throws IOException, Decoder.DecoderException, MercuryClient.MercuryException, CdnManager.CdnException, PlayableContentFeeder.ContentRestrictedException { + private void load(boolean preload) throws IOException, Decoder.DecoderException, TokenProvider.TokenException, CdnManager.CdnException, PlayableContentFeeder.ContentRestrictedException { PlayableContentFeeder.LoadedStream stream; if (playable instanceof LocalId) stream = PlayableContentFeeder.LoadedStream.forLocalFile((LocalId) playable, @@ -274,7 +274,8 @@ public void run() { try { load(preloaded); - } catch (IOException | PlayableContentFeeder.ContentRestrictedException | CdnManager.CdnException | MercuryClient.MercuryException | Decoder.DecoderException ex) { + } catch (IOException | PlayableContentFeeder.ContentRestrictedException | CdnManager.CdnException | + TokenProvider.TokenException | Decoder.DecoderException ex) { close(); listener.loadingError(this, ex, retried); LOGGER.trace("{} terminated at loading.", this, ex); diff --git a/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java b/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java index cbf52144..e2d8d5a6 100644 --- a/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java +++ b/player/src/main/java/xyz/gianlu/librespot/player/state/DeviceStateHandler.java @@ -34,9 +34,9 @@ import xyz.gianlu.librespot.common.Utils; import xyz.gianlu.librespot.core.Session; import xyz.gianlu.librespot.core.TimeProvider; +import xyz.gianlu.librespot.core.TokenProvider; import xyz.gianlu.librespot.dealer.DealerClient; import xyz.gianlu.librespot.dealer.DealerClient.RequestResult; -import xyz.gianlu.librespot.mercury.MercuryClient; import xyz.gianlu.librespot.mercury.MercuryRequests; import xyz.gianlu.librespot.player.PlayerConfiguration; @@ -276,7 +276,7 @@ private void putConnectState(@NotNull Connect.PutStateRequest req) { LOGGER.info("Put state. {ts: {}, connId: {}, reason: {}}", req.getClientSideTimestamp(), Utils.truncateMiddle(connectionId, 10), req.getPutStateReason()); } - } catch (IOException | MercuryClient.MercuryException ex) { + } catch (IOException | TokenProvider.TokenException ex) { LOGGER.error("Failed updating state.", ex); } }