Bat/src/main/java/cc/fascinated/bat/service/SpotifyService.java
Liam 6de1e8b2b1
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 52s
fix access token refreshing?!??!
2024-07-06 04:18:10 +01:00

240 lines
7.9 KiB
Java

package cc.fascinated.bat.service;
import cc.fascinated.bat.common.StringUtils;
import cc.fascinated.bat.features.spotify.profile.SpotifyProfile;
import cc.fascinated.bat.model.BatUser;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service;
import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.enums.AuthorizationScope;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials;
import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@Service
@Getter
@Log4j2(topic = "Spotify Service")
@DependsOn("discordService")
public class SpotifyService {
/**
* The access token map.
*/
private final Map<String, AuthorizationCodeCredentials> accessToken = ExpiringMap.builder()
.expiration(30, TimeUnit.MINUTES)
.build();
/**
* A cache of the currently playing track for each user.
*/
private final Map<BatUser, CurrentlyPlaying> currentlyPlayingCache = ExpiringMap.builder()
.expiration(30, TimeUnit.SECONDS)
.build();
/**
* The client ID.
*/
private final String clientId;
/**
* The client secret.
*/
private final String clientSecret;
/**
* The Spotify API instance.
*/
private final SpotifyApi spotifyApi;
/**
* The user service.
*/
private final UserService userService;
/**
* The authorization URL.
*/
private final String authorizationUrl;
public SpotifyService(@Value("${spotify.client-id}") String clientId, @Value("${spotify.client-secret}") String clientSecret,
@Value("${spotify.redirect-uri}") String redirectUri, @NonNull UserService userService) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.userService = userService;
this.spotifyApi = new SpotifyApi.Builder()
.setClientId(clientId)
.setClientSecret(clientSecret)
.setRedirectUri(URI.create(redirectUri))
.build();
this.authorizationUrl = spotifyApi.authorizationCodeUri()
.response_type("code")
.scope(AuthorizationScope.values())
.build().execute().toString();
}
/**
* Starts playback for the user.
*
* @param user the user to start playback for
*/
@SneakyThrows
public void skipTrack(BatUser user) {
getSpotifyApi(user).skipUsersPlaybackToNextTrack().build().execute();
}
/**
* Gets the currently playing track for the user.
*
* @param user the user to check
* @return the currently playing track
*/
@SneakyThrows
public CurrentlyPlaying getCurrentlyPlayingTrack(BatUser user) {
CurrentlyPlaying currentlyPlaying = currentlyPlayingCache.get(user);
// If the track is still playing return the cache otherwise fetch the track
if (currentlyPlaying != null && currentlyPlaying.getTimestamp() + currentlyPlaying.getProgress_ms() < System.currentTimeMillis()) {
return currentlyPlaying;
}
currentlyPlaying = getSpotifyApi(user).getUsersCurrentlyPlayingTrack().build().execute();
currentlyPlayingCache.put(user, currentlyPlaying);
return currentlyPlaying;
}
/**
* Checks if the user has a track playing.
*
* @param user the user to check
* @return whether a track is playing
*/
public boolean hasTrackPlaying(BatUser user) {
return getCurrentlyPlayingTrack(user) != null;
}
/**
* Pauses playback for the user.
*
* @param user the user to start playback for
*/
@SneakyThrows
public void pausePlayback(BatUser user) {
getSpotifyApi(user).pauseUsersPlayback().build().execute();
}
/**
* Pauses playback for the user.
*
* @param user the user to start playback for
*/
@SneakyThrows
public void resumePlayback(BatUser user) {
getSpotifyApi(user).startResumeUsersPlayback().build().execute();
}
/**
* Gets the authorization key to link the user's
* Spotify account with their Discord account.
*
* @param code the code to authorize with
* @return the authorization details
*/
@SneakyThrows
public String authorize(String code) {
if (code == null) {
return """
<a href="%s">Click here to authorize your Spotify account</a>
""".formatted(authorizationUrl);
}
AuthorizationCodeCredentials credentials = spotifyApi.authorizationCode(code).build().execute();
String key = StringUtils.randomString(16);
accessToken.put(key, credentials);
return """
<p>Successfully authorized your Spotify account!</p>
<p>Your key is: <strong>%s</strong></p>
""".formatted(key);
}
/**
* Gets a new Spotify API instance.
*
* @return the Spotify API
*/
@SneakyThrows
public SpotifyApi getSpotifyApi(BatUser user) {
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
ensureValidToken(profile, user);
return new SpotifyApi.Builder().setAccessToken(profile.getAccessToken()).build();
}
/**
* Ensures the user has a valid Spotify access token.
* <p>
* If the token is expired, it will be refreshed.
* </p>
*
* @param user the user to get the token for
*/
@SneakyThrows
public void ensureValidToken(SpotifyProfile profile, BatUser user) {
// If the token is still valid, return
if (profile.getExpiresAt() > System.currentTimeMillis()) {
return;
}
SpotifyApi api = new SpotifyApi.Builder()
.setClientId(clientId)
.setClientSecret(clientSecret)
.setAccessToken(profile.getAccessToken())
.setRefreshToken(profile.getRefreshToken())
.build();
try {
AuthorizationCodeCredentials credentials = api.authorizationCodeRefresh().build().execute();
profile.setAccessToken(credentials.getAccessToken());
profile.setExpiresAt(System.currentTimeMillis() + (credentials.getExpiresIn() * 1000));
log.info("Refreshed Spotify token for user \"{}\"", user.getName());
} catch (SpotifyWebApiException ex) {
log.error("Failed to refresh Spotify token", ex);
}
}
/**
* Links the user's Spotify account with their Discord account.
*
* @param user the user to link the account with
* @param key the key to link the account with
*/
public void linkAccount(BatUser user, String key) {
AuthorizationCodeCredentials credentials = accessToken.get(key);
if (credentials == null) {
return;
}
// Link the user's Spotify account
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
profile.setAccessToken(credentials.getAccessToken());
profile.setRefreshToken(credentials.getRefreshToken());
profile.setExpiresAt(System.currentTimeMillis() + (credentials.getExpiresIn() * 1000));
log.info("Linked Spotify account for user {}", user.getName());
}
/**
* Checks if the link code is valid.
*
* @param code the code to check
* @return if the code is valid
*/
public boolean isValidLinkCode(String code) {
return accessToken.containsKey(code);
}
}