Liam
4821e2a4fa
All checks were successful
Deploy to Dokku / docker (ubuntu-latest) (push) Successful in 44s
206 lines
6.7 KiB
Java
206 lines
6.7 KiB
Java
package cc.fascinated.bat.service;
|
|
|
|
import cc.fascinated.bat.common.StringUtils;
|
|
import cc.fascinated.bat.exception.spotify.SpotifyTokenRefreshException;
|
|
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.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
|
|
public class SpotifyService {
|
|
/**
|
|
* The access token map.
|
|
*/
|
|
private final Map<String, AuthorizationCodeCredentials> accessToken = ExpiringMap.builder()
|
|
.expiration(30, TimeUnit.MINUTES)
|
|
.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();
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
return getSpotifyApi(user).getUsersCurrentlyPlayingTrack().build().execute();
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @return if the playback was paused
|
|
*/
|
|
@SneakyThrows
|
|
public boolean pausePlayback(BatUser user) {
|
|
getSpotifyApi(user).pauseUsersPlayback().build().execute();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Pauses playback for the user.
|
|
*
|
|
* @param user the user to start playback for
|
|
* @return if the playback was paused
|
|
*/
|
|
@SneakyThrows
|
|
public boolean resumePlayback(BatUser user) {
|
|
getSpotifyApi(user).startResumeUsersPlayback().build().execute();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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));
|
|
userService.saveUser(user);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param user the user to get the token for
|
|
*/
|
|
@SneakyThrows
|
|
public void ensureValidToken(SpotifyProfile profile, BatUser user) {
|
|
if (profile.getExpiresAt() == null || 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));
|
|
userService.saveUser(user);
|
|
} catch (SpotifyWebApiException ex) {
|
|
log.error("Failed to refresh Spotify token", ex);
|
|
throw new SpotifyTokenRefreshException("Failed to refresh Spotify token", ex);
|
|
}
|
|
}
|
|
}
|