From 6028da6210659d79fd0e098fc9b40c0a55a0c31e Mon Sep 17 00:00:00 2001 From: eric <3024947@stud.hs-mannheim.de> Date: Sun, 10 Aug 2025 16:32:16 +0200 Subject: [PATCH] Es gab kein problem ich wurde nur 24h von der API geratelimited lol --- .../Roullette/controller/GameController.java | 6 +- .../eric/Roullette/service/GameService.java | 2 +- .../Roullette/service/SpotifyAuthService.java | 350 +++++------------- 3 files changed, 98 insertions(+), 260 deletions(-) diff --git a/src/main/java/eric/Roullette/controller/GameController.java b/src/main/java/eric/Roullette/controller/GameController.java index 394135c..80a9abe 100644 --- a/src/main/java/eric/Roullette/controller/GameController.java +++ b/src/main/java/eric/Roullette/controller/GameController.java @@ -56,7 +56,7 @@ public class GameController { return; } setToken(accessToken); - var devices = authService.getDevices(username, accessToken); + var devices = authService.getDevices(accessToken); ctx.json(devices); }); @@ -66,7 +66,7 @@ public class GameController { this.accessToken = accessToken; } - private void createGame(Context ctx) { + private void createGame(Context ctx) throws InterruptedException { Map body = ctx.bodyAsClass(Map.class); String user = (String) body.get("username"); if (user == null || user.isBlank()) { @@ -85,7 +85,7 @@ public class GameController { ctx.json(Map.of("status", "ok", "gameId", gameId)); } - private void joinGame(Context ctx) { + private void joinGame(Context ctx) throws InterruptedException { Map body = ctx.bodyAsClass(Map.class); String user = body.get("username"); String gameId = body.get("gameId"); diff --git a/src/main/java/eric/Roullette/service/GameService.java b/src/main/java/eric/Roullette/service/GameService.java index 8370d1f..dd7f08c 100644 --- a/src/main/java/eric/Roullette/service/GameService.java +++ b/src/main/java/eric/Roullette/service/GameService.java @@ -29,7 +29,7 @@ package eric.Roullette.service; return games.computeIfAbsent(gameId, Game::create); } - public void addPlayer(String gameId, String user) { + public void addPlayer(String gameId, String user) throws InterruptedException { Game g = getOrCreateGame(gameId); if (user != null && !g.players().contains(user)) { g.players().add(user); diff --git a/src/main/java/eric/Roullette/service/SpotifyAuthService.java b/src/main/java/eric/Roullette/service/SpotifyAuthService.java index 1cf970b..b94d986 100644 --- a/src/main/java/eric/Roullette/service/SpotifyAuthService.java +++ b/src/main/java/eric/Roullette/service/SpotifyAuthService.java @@ -1,28 +1,26 @@ package eric.Roullette.service; - import com.fasterxml.jackson.core.type.TypeReference; - import com.fasterxml.jackson.databind.ObjectMapper; - import com.neovisionaries.i18n.CountryCode; - import okhttp3.OkHttpClient; - import okhttp3.Request; - import okhttp3.Response; - import org.apache.hc.core5.http.ParseException; - import se.michaelthelin.spotify.SpotifyApi; - import se.michaelthelin.spotify.SpotifyHttpManager; - import se.michaelthelin.spotify.exceptions.SpotifyWebApiException; - import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials; - import se.michaelthelin.spotify.model_objects.specification.*; - import se.michaelthelin.spotify.requests.data.player.GetCurrentUsersRecentlyPlayedTracksRequest; - import se.michaelthelin.spotify.requests.data.library.GetUsersSavedTracksRequest; - import java.time.Instant; - import java.io.IOException; - import java.net.URI; - import java.util.*; - import java.util.concurrent.ConcurrentHashMap; - import java.util.concurrent.ThreadLocalRandom; - import java.util.stream.Collectors; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.neovisionaries.i18n.CountryCode; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.apache.hc.core5.http.ParseException; +import se.michaelthelin.spotify.SpotifyApi; +import se.michaelthelin.spotify.SpotifyHttpManager; +import se.michaelthelin.spotify.exceptions.SpotifyWebApiException; +import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials; +import se.michaelthelin.spotify.model_objects.specification.*; +import se.michaelthelin.spotify.requests.data.player.GetCurrentUsersRecentlyPlayedTracksRequest; +import se.michaelthelin.spotify.requests.data.library.GetUsersSavedTracksRequest; - import static com.neovisionaries.i18n.CountryCode.DE; +import java.io.IOException; +import java.net.URI; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static com.neovisionaries.i18n.CountryCode.DE; public class SpotifyAuthService { private final String clientId; @@ -30,39 +28,6 @@ public class SpotifyAuthService { private final URI redirectUri; // Speichert für jeden Benutzer eine eigene, authentifizierte SpotifyApi-Instanz private final Map userApis = new ConcurrentHashMap<>(); - private final Map userAuths = new ConcurrentHashMap<>(); - - private final OkHttpClient client = new OkHttpClient.Builder() - .addInterceptor(chain -> { - Request req = chain.request(); - Response res = chain.proceed(req); - - String retryAfter = res.header("Retry-After"); - String rateLimit = res.header("x-rate-limit-limit"); - String remaining = res.header("x-rate-limit-remaining"); - String reset = res.header("x-rate-limit-reset"); - - System.out.printf( - "SPOTIFY → %s %s → %d | limit=%s remaining=%s reset=%s retry-after=%s%n", - req.method(), req.url(), res.code(), - rateLimit, remaining, reset, retryAfter - ); - - if (res.code() == 429) { - long waitSec = retryAfter != null - ? Long.parseLong(retryAfter) - : 1; - try { - Thread.sleep((long)((waitSec + ThreadLocalRandom.current().nextDouble()) * 1000)); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - res.close(); - res = chain.proceed(req); - } - return res; - }) - .build(); public SpotifyAuthService(String clientId, String clientSecret, String redirectUri) { this.clientId = clientId; @@ -79,7 +44,7 @@ public class SpotifyAuthService { .build(); return tempApi.authorizationCodeUri() - .scope("user-read-recently-played user-library-read user-modify-playback-state user-read-playback-state") + .scope("user-read-recently-played user-library-read user-read-playback-state user-modify-playback-state streaming") .state(user) // Der Benutzername wird im State mitgegeben .build() .execute(); @@ -97,65 +62,13 @@ public class SpotifyAuthService { AuthorizationCodeCredentials creds = userApi.authorizationCode(code).build().execute(); userApi.setAccessToken(creds.getAccessToken()); userApi.setRefreshToken(creds.getRefreshToken()); - userAuths.put(user, new SpotifyAuth(userApi, creds.getAccessToken(), creds.getRefreshToken(), creds.getExpiresIn())); // Speichert die fertig konfigurierte API-Instanz für den Benutzer userApis.put(user, userApi); } - // Einfaches Cache für Tracks pro User - private final Map> recentTracksCache = new ConcurrentHashMap<>(); - private final Map> savedTracksCache = new ConcurrentHashMap<>(); - - // Hilfsmethode für Retry bei 429 - private T executeWithRetry(CallableWithException callable) throws IOException, SpotifyWebApiException, ParseException { - int maxRetries = 3; - int attempt = 0; - while (true) { - try { - return callable.call(); - } catch (SpotifyWebApiException e) { - // Prüfe auf Rate Limit (429) - if (e.getMessage() != null && e.getMessage().contains("429")) { - // Die SpotifyWebApiException bietet keinen direkten Zugriff auf den Header. - // Fallback: Warte 2 Sekunden, oder extrahiere aus der Fehlermeldung, falls vorhanden. - int waitSec = 2; - String msg = e.getMessage(); - if (msg != null) { - // Versuche "Retry-After" aus der Fehlermeldung zu extrahieren - String marker = "\"Retry-After\":"; - int idx = msg.indexOf(marker); - if (idx >= 0) { - int start = idx + marker.length(); - int end = msg.indexOf(",", start); - if (end == -1) end = msg.length(); - try { - waitSec = Integer.parseInt(msg.substring(start, end).replaceAll("[^0-9]", "")); - } catch (Exception ignore) { - } - } - } - System.out.println("Rate limit erreicht, warte " + waitSec + " Sekunden und versuche erneut..."); - try { - Thread.sleep(waitSec * 1000L); - } catch (InterruptedException ignored) { - } - if (++attempt > maxRetries) throw e; - } else { - throw e; - } - } - } - } - - @FunctionalInterface - private interface CallableWithException { - T call() throws IOException, SpotifyWebApiException, ParseException; - } - public List getRecentTracks(String user) { - System.out.println("Hole kürzlich gespielte Tracks für Benutzer: " + user); - int limit = 1; + int limit = 2; SpotifyApi userApi = userApis.get(user); if (userApi == null) { @@ -163,42 +76,35 @@ public class SpotifyAuthService { return Collections.emptyList(); } - // Cache nutzen - if (recentTracksCache.containsKey(user)) { - return recentTracksCache.get(user); - } - try { - List recentTracks = executeWithRetry(() -> { - GetCurrentUsersRecentlyPlayedTracksRequest request = userApi.getCurrentUsersRecentlyPlayedTracks() - .limit(limit) - .build(); - PagingCursorbased history = request.execute(); - if (history == null || history.getItems() == null) { - return Collections.emptyList(); - } - return Arrays.stream(history.getItems()) - .map(item -> item.getTrack().getUri()) - .distinct() - .toList(); - }); + GetCurrentUsersRecentlyPlayedTracksRequest request = userApi.getCurrentUsersRecentlyPlayedTracks() + .limit(limit) + .build(); + PagingCursorbased history = request.execute(); + if (history == null || history.getItems() == null) { + return Collections.emptyList(); + } + List recentTracks = Arrays.stream(history.getItems()) + .map(item -> item.getTrack().getUri()) + .distinct() + .toList(); if (recentTracks.size() < limit) { int newLimit = limit - recentTracks.size(); + // restliche songs mit kürzlich gespeicherten Tracks auffüllen List savedTracks = getSavedTracks(user, newLimit, 0); + // Nur Tracks hinzufügen, die noch nicht in recentTracks sind savedTracks.removeAll(recentTracks); recentTracks = new java.util.ArrayList<>(recentTracks); recentTracks.addAll(savedTracks.subList(0, Math.min(newLimit, savedTracks.size()))); - if (recentTracks.size() < limit) { + if(recentTracks.size() < limit){ newLimit = limit - recentTracks.size(); List savedTracks2 = getSavedTracks(user, newLimit, 50); savedTracks2.removeAll(recentTracks); recentTracks.addAll(savedTracks2.subList(0, Math.min(newLimit, savedTracks2.size()))); } } - List result = recentTracks.subList(0, Math.min(limit, recentTracks.size())); - recentTracksCache.put(user, result); // Cache speichern - return result; + return recentTracks.subList(0, Math.min(limit, recentTracks.size())); } catch (IOException | SpotifyWebApiException | ParseException e) { e.printStackTrace(); return Collections.emptyList(); @@ -206,162 +112,94 @@ public class SpotifyAuthService { } private List getSavedTracks(String user, int limit, int offset) { - System.out.println("Hole gespeicherte Tracks für Benutzer: " + user + ", Limit: " + limit + ", Offset: " + offset); SpotifyApi userApi = userApis.get(user); if (userApi == null) { System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + user); return Collections.emptyList(); } - - // Cache nutzen (nur für offset==0) - if (offset == 0 && savedTracksCache.containsKey(user)) { - return savedTracksCache.get(user); - } - try { - List saved = executeWithRetry(() -> { - List result = new ArrayList<>(); - int localOffset = offset; - while (result.size() < limit) { - GetUsersSavedTracksRequest req = userApi.getUsersSavedTracks() - .limit(limit) - .offset(localOffset) - .market(CountryCode.DE) - .build(); - Paging page = req.execute(); - if (page == null || page.getItems().length == 0) { - System.out.println("Keine weiteren gespeicherten Tracks gefunden."); - break; - } - for (SavedTrack st : page.getItems()) { - result.add(st.getTrack().getUri()); - if (result.size() == limit) break; - } - localOffset += limit; + List saved = new ArrayList<>(); + while (saved.size() < limit) { + GetUsersSavedTracksRequest req = userApi.getUsersSavedTracks() + .limit(limit) + .offset(offset) + .market(CountryCode.DE) + .build(); + Paging page = req.execute(); + if (page == null || page.getItems().length == 0){ + System.out.println("Keine weiteren gespeicherten Tracks gefunden."); + break; } - return result; - }); - if (offset == 0) savedTracksCache.put(user, saved); // Cache speichern + for (SavedTrack st : page.getItems()) { + saved.add(st.getTrack().getUri()); + if (saved.size() == limit) break; + } + offset += limit; + } return saved; } catch (IOException | SpotifyWebApiException | ParseException e) { e.printStackTrace(); return Collections.emptyList(); } } + public List getTrackInfos(List allTracks) { + //für jede URI den titel holen + List trackInfos = new ArrayList<>(); + for (String uri : allTracks) { + SpotifyApi userApi = userApis.values().stream().findFirst().orElse(null); + if (userApi == null) { + System.err.println("Kein SpotifyApi-Client gefunden."); + return Collections.emptyList(); + } + try { + String trackId = uri.startsWith("spotify:track:") ? uri.substring("spotify:track:".length()) : uri; + var track = userApi.getTrack(trackId) + .build() + .execute(); + if (track != null) { + String info = track.getName() + " - " + Arrays.stream(track.getArtists()) + .map(ArtistSimplified::getName) + .reduce((a, b) -> a + ", " + b) + .orElse("Unbekannt"); + trackInfos.add(info); -// public List getTrackInfos(List allTracks) { -// //für jede URI den titel holen -// List trackInfos = new ArrayList<>(); -// for (String uri : allTracks) { -// SpotifyApi userApi = userApis.values().stream().findFirst().orElse(null); -// if (userApi == null) { -// System.err.println("Kein SpotifyApi-Client gefunden."); -// return Collections.emptyList(); -// } -// try { -// String trackId = uri.startsWith("spotify:track:") ? uri.substring("spotify:track:".length()) : uri; -// var track = userApi.getTrack(trackId) -// .build() -// .execute(); -// if (track != null) { -// String info = track.getName() + " - " + Arrays.stream(track.getArtists()) -// .map(ArtistSimplified::getName) -// .reduce((a, b) -> a + ", " + b) -// .orElse("Unbekannt"); -// trackInfos.add(info); -// -// } else { -// System.err.println("Track nicht gefunden: " + uri); -// } -// } catch (IOException | SpotifyWebApiException | ParseException e) { -// System.err.println("Fehler beim Abrufen des Tracks: " + uri); -// e.printStackTrace(); -// } -// } return trackInfos; -// } + } else { + System.err.println("Track nicht gefunden: " + uri); + } + } catch (IOException | SpotifyWebApiException | ParseException e) { + System.err.println("Fehler beim Abrufen des Tracks: " + uri); + e.printStackTrace(); + } + } return trackInfos; + } public String getAccessTokenForUser(String username) { - System.out.println("Hole Access Token für Benutzer: " + username); - - SpotifyAuth auth = userAuths.get(username); - if (auth == null) return null; - try { - return auth.getToken(); - } catch (Exception e) { - e.printStackTrace(); + SpotifyApi userApi = userApis.get(username); + if (userApi == null) { + System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + username); return null; } -// SpotifyApi userApi = userApis.get(username); -// if (userApi == null) { -// System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + username); -// System.out.println("Kein SpotifyApi-Client für Benutzer gefunden: " + username); -// return null; -// } -// return userApi.getAccessToken(); + return userApi.getAccessToken(); } - - public List> getDevices(String username, String accessToken) { - System.out.println("Hole Geräte für Benutzer: " + username); - //String accessToken = getAccessTokenForUser(username); - if (accessToken == null) { - System.err.println("Kein gültiges Access Token für Benutzer: " + username); - return List.of(); - } - OkHttpClient okClient = client; + public List> getDevices(String accessToken) { + System.out.println("Hole Geräte für AccessToken: " + accessToken); + OkHttpClient client = new OkHttpClient(); Request req = new Request.Builder() .url("https://api.spotify.com/v1/me/player/devices") .addHeader("Authorization", "Bearer " + accessToken) .build(); - try (Response resp = okClient.newCall(req).execute()) { - if (!resp.isSuccessful()) { - System.out.println("Fehler beim Abrufen der Geräte: " + resp.code() + " - " + resp.message()); - return List.of(); - } - if (resp.body() == null) { - System.err.println("Antwort ohne Body erhalten."); - return List.of(); - } + try (Response resp = client.newCall(req).execute()) { + if (!resp.isSuccessful()) return List.of(); String body = resp.body().string(); - System.out.println("Response Code: " + resp.code()); - System.out.println("Response Body: " + body); // Parsen, z.B. mit Jackson var node = new ObjectMapper().readTree(body); var devices = node.get("devices"); - return new ObjectMapper().convertValue(devices, new TypeReference>>() { - }); + return new ObjectMapper().convertValue(devices, new TypeReference>>(){}); } catch (Exception e) { - e.printStackTrace(); return List.of(); } } - - private static class SpotifyAuth { - private final SpotifyApi api; - private String accessToken; - private String refreshToken; - private Instant expiresAt; - - public SpotifyAuth(SpotifyApi api, String accessToken, String refreshToken, int expiresIn) { - this.api = api; - this.accessToken = accessToken; - this.refreshToken = refreshToken; - this.expiresAt = Instant.now().plusSeconds(expiresIn); - } - - public synchronized String getToken() throws IOException, SpotifyWebApiException, ParseException { - if (Instant.now().isAfter(expiresAt.minusSeconds(60))) { - refreshToken(); - } - return accessToken; - } - - private void refreshToken() throws IOException, SpotifyWebApiException, ParseException { - var creds = api.authorizationCodeRefresh().build().execute(); - this.accessToken = creds.getAccessToken(); - this.expiresAt = Instant.now().plusSeconds(creds.getExpiresIn()); - api.setAccessToken(accessToken); - } - } } +