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"