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.stereotype.Service; import se.michaelthelin.spotify.SpotifyApi; import se.michaelthelin.spotify.enums.AuthorizationScope; 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 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 */ public CurrentlyPlaying getCurrentlyPlayingTrack(BatUser user) { try { return getSpotifyApi(user).getUsersCurrentlyPlayingTrack().build().execute(); } catch (Exception e) { throw new RuntimeException("Failed to get currently playing track", e); } } /** * 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 "Missing code param"; } AuthorizationCodeCredentials credentials = spotifyApi.authorizationCode(code).build().execute(); String key = StringUtils.randomString(16); accessToken.put(key, credentials); return "Authorization Key: " + 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()) .setRefreshToken(profile.getRefreshToken()) .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; } AuthorizationCodeCredentials credentials = spotifyApi.authorizationCodeRefresh().build().execute(); profile.setAccessToken(credentials.getAccessToken()); profile.setRefreshToken(credentials.getRefreshToken()); profile.setExpiresAt(System.currentTimeMillis() + (credentials.getExpiresIn() * 1000)); userService.saveUser(user); } }