2024-06-28 03:01:21 +01:00
|
|
|
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;
|
2024-06-28 19:59:29 +01:00
|
|
|
import lombok.extern.log4j.Log4j2;
|
2024-06-28 03:01:21 +01:00
|
|
|
import net.jodah.expiringmap.ExpiringMap;
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
2024-07-05 19:49:16 +01:00
|
|
|
import org.springframework.context.annotation.DependsOn;
|
2024-06-28 03:01:21 +01:00
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
import se.michaelthelin.spotify.SpotifyApi;
|
|
|
|
import se.michaelthelin.spotify.enums.AuthorizationScope;
|
2024-06-29 16:54:39 +01:00
|
|
|
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
|
2024-06-28 03:01:21 +01:00
|
|
|
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
|
2024-07-01 01:12:32 +01:00
|
|
|
@Log4j2(topic = "Spotify Service")
|
2024-07-05 19:49:16 +01:00
|
|
|
@DependsOn("discordService")
|
2024-06-28 03:01:21 +01:00
|
|
|
public class SpotifyService {
|
|
|
|
/**
|
|
|
|
* The access token map.
|
|
|
|
*/
|
|
|
|
private final Map<String, AuthorizationCodeCredentials> accessToken = ExpiringMap.builder()
|
|
|
|
.expiration(30, TimeUnit.MINUTES)
|
|
|
|
.build();
|
|
|
|
|
2024-07-06 00:31:48 +01:00
|
|
|
/**
|
|
|
|
* A cache of the currently playing track for each user.
|
|
|
|
*/
|
|
|
|
private final Map<BatUser, CurrentlyPlaying> currentlyPlayingCache = ExpiringMap.builder()
|
|
|
|
.expiration(30, TimeUnit.SECONDS)
|
|
|
|
.build();
|
|
|
|
|
2024-06-28 03:01:21 +01:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
2024-06-28 03:05:36 +01:00
|
|
|
public SpotifyService(@Value("${spotify.client-id}") String clientId, @Value("${spotify.client-secret}") String clientSecret,
|
|
|
|
@Value("${spotify.redirect-uri}") String redirectUri, @NonNull UserService userService) {
|
2024-06-28 03:01:21 +01:00
|
|
|
this.clientId = clientId;
|
|
|
|
this.clientSecret = clientSecret;
|
|
|
|
this.userService = userService;
|
|
|
|
|
|
|
|
this.spotifyApi = new SpotifyApi.Builder()
|
|
|
|
.setClientId(clientId)
|
|
|
|
.setClientSecret(clientSecret)
|
2024-06-28 03:05:36 +01:00
|
|
|
.setRedirectUri(URI.create(redirectUri))
|
2024-06-28 03:01:21 +01:00
|
|
|
.build();
|
|
|
|
this.authorizationUrl = spotifyApi.authorizationCodeUri()
|
|
|
|
.response_type("code")
|
2024-06-28 03:56:34 +01:00
|
|
|
.scope(AuthorizationScope.values())
|
2024-06-28 03:01:21 +01:00
|
|
|
.build().execute().toString();
|
|
|
|
}
|
|
|
|
|
2024-06-29 21:36:45 +01:00
|
|
|
/**
|
|
|
|
* Starts playback for the user.
|
|
|
|
*
|
|
|
|
* @param user the user to start playback for
|
|
|
|
*/
|
|
|
|
@SneakyThrows
|
2024-07-06 00:41:31 +01:00
|
|
|
public void skipTrack(BatUser user) {
|
2024-06-29 21:36:45 +01:00
|
|
|
getSpotifyApi(user).skipUsersPlaybackToNextTrack().build().execute();
|
|
|
|
}
|
|
|
|
|
2024-06-28 03:01:21 +01:00
|
|
|
/**
|
|
|
|
* Gets the currently playing track for the user.
|
|
|
|
*
|
|
|
|
* @param user the user to check
|
|
|
|
* @return the currently playing track
|
|
|
|
*/
|
2024-06-29 16:54:39 +01:00
|
|
|
@SneakyThrows
|
2024-06-28 03:01:21 +01:00
|
|
|
public CurrentlyPlaying getCurrentlyPlayingTrack(BatUser user) {
|
2024-07-06 00:41:31 +01:00
|
|
|
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;
|
2024-07-06 00:31:48 +01:00
|
|
|
}
|
2024-07-06 00:41:31 +01:00
|
|
|
currentlyPlaying = getSpotifyApi(user).getUsersCurrentlyPlayingTrack().build().execute();
|
2024-07-06 00:31:48 +01:00
|
|
|
currentlyPlayingCache.put(user, currentlyPlaying);
|
|
|
|
return currentlyPlaying;
|
2024-06-28 03:01:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2024-07-05 23:59:27 +01:00
|
|
|
public void pausePlayback(BatUser user) {
|
2024-06-28 19:59:29 +01:00
|
|
|
getSpotifyApi(user).pauseUsersPlayback().build().execute();
|
2024-06-28 03:01:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pauses playback for the user.
|
|
|
|
*
|
|
|
|
* @param user the user to start playback for
|
|
|
|
*/
|
2024-06-29 16:54:39 +01:00
|
|
|
@SneakyThrows
|
2024-07-05 23:59:27 +01:00
|
|
|
public void resumePlayback(BatUser user) {
|
2024-06-29 16:54:39 +01:00
|
|
|
getSpotifyApi(user).startResumeUsersPlayback().build().execute();
|
2024-06-28 03:01:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2024-06-28 03:13:54 +01:00
|
|
|
if (code == null) {
|
2024-06-29 13:08:13 +01:00
|
|
|
return """
|
|
|
|
<a href="%s">Click here to authorize your Spotify account</a>
|
|
|
|
""".formatted(authorizationUrl);
|
2024-06-28 03:13:54 +01:00
|
|
|
}
|
2024-06-28 03:01:21 +01:00
|
|
|
AuthorizationCodeCredentials credentials = spotifyApi.authorizationCode(code).build().execute();
|
|
|
|
String key = StringUtils.randomString(16);
|
|
|
|
accessToken.put(key, credentials);
|
2024-06-29 13:08:13 +01:00
|
|
|
return """
|
|
|
|
<p>Successfully authorized your Spotify account!</p>
|
|
|
|
<p>Your key is: <strong>%s</strong></p>
|
|
|
|
""".formatted(key);
|
2024-06-28 03:01:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a new Spotify API instance.
|
|
|
|
*
|
|
|
|
* @return the Spotify API
|
|
|
|
*/
|
2024-06-28 03:50:38 +01:00
|
|
|
@SneakyThrows
|
2024-06-28 03:01:21 +01:00
|
|
|
public SpotifyApi getSpotifyApi(BatUser user) {
|
|
|
|
SpotifyProfile profile = user.getProfile(SpotifyProfile.class);
|
2024-06-28 19:59:29 +01:00
|
|
|
ensureValidToken(profile, user);
|
2024-06-29 16:54:39 +01:00
|
|
|
return new SpotifyApi.Builder().setAccessToken(profile.getAccessToken()).build();
|
2024-06-28 19:59:29 +01:00
|
|
|
}
|
2024-06-28 03:50:38 +01:00
|
|
|
|
2024-06-28 19:59:29 +01:00
|
|
|
/**
|
|
|
|
* Ensures the user has a valid Spotify access token.
|
2024-07-05 23:59:27 +01:00
|
|
|
* <p>
|
2024-07-06 00:41:31 +01:00
|
|
|
* If the token is expired, it will be refreshed.
|
2024-07-05 23:59:27 +01:00
|
|
|
* </p>
|
2024-06-28 19:59:29 +01:00
|
|
|
*
|
|
|
|
* @param user the user to get the token for
|
|
|
|
*/
|
|
|
|
@SneakyThrows
|
|
|
|
public void ensureValidToken(SpotifyProfile profile, BatUser user) {
|
2024-07-06 04:18:10 +01:00
|
|
|
// If the token is still valid, return
|
|
|
|
if (profile.getExpiresAt() > System.currentTimeMillis()) {
|
2024-06-28 19:59:29 +01:00
|
|
|
return;
|
2024-06-28 03:50:38 +01:00
|
|
|
}
|
2024-06-29 16:54:39 +01:00
|
|
|
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));
|
2024-07-06 00:31:48 +01:00
|
|
|
log.info("Refreshed Spotify token for user \"{}\"", user.getName());
|
2024-06-29 16:54:39 +01:00
|
|
|
} catch (SpotifyWebApiException ex) {
|
|
|
|
log.error("Failed to refresh Spotify token", ex);
|
|
|
|
}
|
2024-06-28 03:01:21 +01:00
|
|
|
}
|
2024-06-29 21:36:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
2024-06-28 03:01:21 +01:00
|
|
|
}
|