diff --git a/pom.xml b/pom.xml index 2a61f67..93d845e 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,11 @@ org.springframework.boot spring-boot-starter-websocket + + io.sentry + sentry-spring-boot-starter-jakarta + 7.10.0 + diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/LinkSubCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/LinkSubCommand.java index 9998579..9e2a77f 100644 --- a/src/main/java/cc/fascinated/bat/features/spotify/command/LinkSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/LinkSubCommand.java @@ -99,6 +99,14 @@ public class LinkSubCommand extends BatSubCommand implements EventListener { return; } String code = codeMapping.getAsString(); + if (!spotifyService.isValidLinkCode(code)) { + event.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("%s The link code you provided is invalid.".formatted(Emojis.CROSS_MARK_EMOJI)) + .build()) + .queue(); + return; + } + spotifyService.linkAccount(user, code); event.replyEmbeds(EmbedUtils.successEmbed() .setDescription("%s You have linked your Spotify account!".formatted(Emojis.CHECK_MARK_EMOJI.getFormatted())) diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/PauseSubCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/PauseSubCommand.java index b43e87c..58efc55 100644 --- a/src/main/java/cc/fascinated/bat/features/spotify/command/PauseSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/PauseSubCommand.java @@ -15,6 +15,8 @@ import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying; +import se.michaelthelin.spotify.model_objects.specification.Track; /** * @author Fascinated (fascinated7) @@ -46,8 +48,14 @@ public class PauseSubCommand extends BatSubCommand { } boolean didPause = spotifyService.pausePlayback(user); + CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user); + Track track = (Track) currentlyPlaying.getItem(); interaction.replyEmbeds(EmbedUtils.successEmbed() - .setDescription(didPause ? ":pause_button: Paused the current track." + .setDescription(didPause ? ":pause_button: Paused the track **[%s | %s](%s)**".formatted( + track.getName(), + track.getArtists()[0].getName(), + "https://open.spotify.com/track/%s".formatted(track.getId()) + ) : "%s The current track is already paused.".formatted(Emojis.CROSS_MARK_EMOJI)) .build()) .queue(); diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/ResumeSubCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/ResumeSubCommand.java index 3234477..9599e1a 100644 --- a/src/main/java/cc/fascinated/bat/features/spotify/command/ResumeSubCommand.java +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/ResumeSubCommand.java @@ -15,6 +15,8 @@ import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying; +import se.michaelthelin.spotify.model_objects.specification.Track; /** * @author Fascinated (fascinated7) @@ -46,9 +48,16 @@ public class ResumeSubCommand extends BatSubCommand { } boolean didPause = spotifyService.resumePlayback(user); + CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user); + Track track = (Track) currentlyPlaying.getItem(); interaction.replyEmbeds(EmbedUtils.successEmbed() - .setDescription(didPause ? "%s Resumed the current track.".formatted(Emojis.CHECK_MARK_EMOJI) : - "%s The current track is already playing.".formatted(Emojis.CROSS_MARK_EMOJI)) + .setDescription(didPause ? "%s Resumed the track **[%s | %s](%s)**".formatted( + Emojis.CHECK_MARK_EMOJI, + track.getName(), + track.getArtists()[0].getName(), + "https://open.spotify.com/track/%s".formatted(track.getId()) + ) + : "%s The current track is already playing.".formatted(Emojis.CROSS_MARK_EMOJI)) .build()) .queue(); } diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/SkipSubCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/SkipSubCommand.java new file mode 100644 index 0000000..9a38b67 --- /dev/null +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/SkipSubCommand.java @@ -0,0 +1,103 @@ +package cc.fascinated.bat.features.spotify.command; + +import cc.fascinated.bat.Emojis; +import cc.fascinated.bat.command.BatSubCommand; +import cc.fascinated.bat.command.CommandInfo; +import cc.fascinated.bat.common.EmbedUtils; +import cc.fascinated.bat.features.spotify.SpotifyFeature; +import cc.fascinated.bat.features.spotify.profile.SpotifyProfile; +import cc.fascinated.bat.model.BatGuild; +import cc.fascinated.bat.model.BatUser; +import cc.fascinated.bat.service.SpotifyService; +import lombok.NonNull; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel; +import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying; +import se.michaelthelin.spotify.model_objects.specification.Track; + +/** + * @author Fascinated (fascinated7) + */ +@Component +@CommandInfo(name = "skip", description = "Skip the current Spotify track") +public class SkipSubCommand extends BatSubCommand { + private static final Logger log = LoggerFactory.getLogger(SkipSubCommand.class); + private final SpotifyService spotifyService; + + @Autowired + public SkipSubCommand(@NonNull SpotifyService spotifyService) { + this.spotifyService = spotifyService; + } + + @Override + public void execute(BatGuild guild, @NonNull BatUser user, @NonNull MessageChannel channel, Member member, @NonNull SlashCommandInteraction interaction) { + SpotifyProfile profile = user.getProfile(SpotifyProfile.class); + if (!profile.hasLinkedAccount()) { + interaction.replyEmbeds(SpotifyFeature.linkAccountEmbed()).queue(); + return; + } + + if (!spotifyService.hasTrackPlaying(user)) { + interaction.replyEmbeds(EmbedUtils.errorEmbed() + .setDescription("%s You need to be playing a track to skip a track.".formatted(Emojis.CROSS_MARK_EMOJI)) + .build()) + .queue(); + return; + } + + CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user); + Track track = (Track) currentlyPlaying.getItem(); + String trackName = track.getName(); + + spotifyService.skipTrack(user); + Track newTrack = getNewTrack(user, trackName); + interaction.replyEmbeds(EmbedUtils.successEmbed() + .setDescription(""" + :track_next: Skipped the track: **[%s | %s](%s)** + %s New Track: **[%s | %s](%s)** + """.formatted( + trackName, + track.getArtists()[0].getName(), + "https://open.spotify.com/track/" + track.getId(), + Emojis.CHECK_MARK_EMOJI, + newTrack.getName(), + newTrack.getArtists()[0].getName(), + "https://open.spotify.com/track/" + newTrack.getId() + )).build()) + .queue(); + } + + /** + * Get the next track that is playing + * + * @param user The user to get the track for + * @param oldName The name of the old track + * @return The new track + */ + public Track getNewTrack(BatUser user, String oldName) { + int checks = 0; + + try { + Thread.sleep(150); + while (checks < 10) { + CurrentlyPlaying currentlyPlaying = spotifyService.getCurrentlyPlayingTrack(user); + Track track = (Track) currentlyPlaying.getItem(); + if (track.getName().equals(oldName)) { + Thread.sleep(250); + checks++; + } else { + log.info("Found new track {} in {} checks", track.getName(), checks); + return (Track) currentlyPlaying.getItem(); + } + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/src/main/java/cc/fascinated/bat/features/spotify/command/SpotifyCommand.java b/src/main/java/cc/fascinated/bat/features/spotify/command/SpotifyCommand.java index 1a16ff4..9b39f85 100644 --- a/src/main/java/cc/fascinated/bat/features/spotify/command/SpotifyCommand.java +++ b/src/main/java/cc/fascinated/bat/features/spotify/command/SpotifyCommand.java @@ -21,5 +21,6 @@ public class SpotifyCommand extends BatCommand { super.addSubCommand(context.getBean(PauseSubCommand.class)); super.addSubCommand(context.getBean(ResumeSubCommand.class)); super.addSubCommand(context.getBean(CurrentSubCommand.class)); + super.addSubCommand(context.getBean(SkipSubCommand.class)); } } diff --git a/src/main/java/cc/fascinated/bat/service/SpotifyService.java b/src/main/java/cc/fascinated/bat/service/SpotifyService.java index 839e9a9..a17318a 100644 --- a/src/main/java/cc/fascinated/bat/service/SpotifyService.java +++ b/src/main/java/cc/fascinated/bat/service/SpotifyService.java @@ -9,6 +9,7 @@ import lombok.NonNull; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; import net.jodah.expiringmap.ExpiringMap; +import org.apache.hc.core5.http.ParseException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import se.michaelthelin.spotify.SpotifyApi; @@ -17,6 +18,7 @@ import se.michaelthelin.spotify.exceptions.SpotifyWebApiException; import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials; import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying; +import java.io.IOException; import java.net.URI; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -77,6 +79,18 @@ public class SpotifyService { .build().execute().toString(); } + /** + * Starts playback for the user. + * + * @param user the user to start playback for + * @return if the playback was started + */ + @SneakyThrows + public boolean skipTrack(BatUser user) { + getSpotifyApi(user).skipUsersPlaybackToNextTrack().build().execute(); + return true; + } + /** * Gets the currently playing track for the user. * @@ -145,25 +159,6 @@ public class SpotifyService { """.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. * @@ -197,9 +192,40 @@ public class SpotifyService { profile.setAccessToken(credentials.getAccessToken()); profile.setExpiresAt(System.currentTimeMillis() + (credentials.getExpiresIn() * 1000)); userService.saveUser(user); + log.info("Refreshed Spotify token for user {}", user.getName()); } catch (SpotifyWebApiException ex) { log.error("Failed to refresh Spotify token", ex); throw new SpotifyTokenRefreshException("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)); + userService.saveUser(user); + 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); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0333029..0bc0ac0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,12 @@ discord: token: "oh my goodnesssssssssss" +# Sentry Configuration +sentry: + dsn: "CHANGE_ME" + tracesSampleRate: 1.0 + environment: "development" + # Spotify Configuration spotify: redirect-uri: "http://localhost:8080/spotify/callback"