From c2c5d570c8c824ef7508fb43328420cd8cfd9420 Mon Sep 17 00:00:00 2001 From: eric <3024947@stud.hs-mannheim.de> Date: Fri, 8 Aug 2025 16:40:22 +0200 Subject: [PATCH] alles kaputt --- src/main/java/eric/Roullette/App.java | 14 +- .../Roullette/controller/GameController.java | 39 +- .../eric/Roullette/service/GameService.java | 8 +- .../Roullette/service/SpotifyAuthService.java | 474 ++++++++++++------ .../websocket/GameWebSocketHandler.java | 104 ++-- src/main/resources/public/js/device-select.js | 2 +- src/main/resources/public/js/game.js | 24 +- 7 files changed, 443 insertions(+), 222 deletions(-) diff --git a/src/main/java/eric/Roullette/App.java b/src/main/java/eric/Roullette/App.java index 282d686..663a0ca 100644 --- a/src/main/java/eric/Roullette/App.java +++ b/src/main/java/eric/Roullette/App.java @@ -36,6 +36,18 @@ public class App { config.staticFiles.add("/public", Location.CLASSPATH); }).start(cfg.port); + app.before(ctx -> { + System.out.println("→ " + ctx.method() + " " + ctx.fullUrl()); + }); + app.after(ctx -> { + String limit = ctx.header("x-rate-limit-limit"); + String remaining = ctx.header("x-rate-limit-remaining"); + String reset = ctx.header("x-rate-limit-reset"); + String retryAfter = ctx.header("Retry-After"); + System.out.printf("← %d | limit=%s remaining=%s reset=%s retry-after=%s%n", + ctx.status().getCode(), limit, remaining, reset, retryAfter); + }); + app.exception(Exception.class, (e, ctx) -> { log.error("Unhandled error", e); ctx.status(500).json(Map.of("error", e.getMessage())); @@ -58,7 +70,7 @@ public class App { }); // WS-Handler - GameWebSocketHandler wsHandler = new GameWebSocketHandler(gs, sas); + GameWebSocketHandler wsHandler = new GameWebSocketHandler(gs); // HTTP-Controller new GameController(app, gs, sas, wsHandler); diff --git a/src/main/java/eric/Roullette/controller/GameController.java b/src/main/java/eric/Roullette/controller/GameController.java index 6a0eafe..394135c 100644 --- a/src/main/java/eric/Roullette/controller/GameController.java +++ b/src/main/java/eric/Roullette/controller/GameController.java @@ -30,6 +30,8 @@ public class GameController { private final GameService gameService; private final SpotifyAuthService authService; private final GameWebSocketHandler webSocketHandler; + private final OkHttpClient httpClient = new OkHttpClient(); + private String accessToken = ""; private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GameController.class); public GameController(Javalin app, GameService gs, SpotifyAuthService sas, GameWebSocketHandler wsHandler) { @@ -48,12 +50,23 @@ public class GameController { ctx.status(400).result("username fehlt"); return; } - var devices = authService.getDevices(username); // Diese Methode muss es im SpotifyAuthService geben - ctx.json(Map.of("devices", devices)); + var accessToken = authService.getAccessTokenForUser(username); + if (accessToken == null) { + ctx.status(401).result("Zugriffstoken fehlt oder ist ungültig"); + return; + } + setToken(accessToken); + var devices = authService.getDevices(username, accessToken); + ctx.json(devices); + }); } - private void createGame(Context ctx) { + private void setToken(String accessToken) { + this.accessToken = accessToken; + } + + private void createGame(Context ctx) { Map body = ctx.bodyAsClass(Map.class); String user = (String) body.get("username"); if (user == null || user.isBlank()) { @@ -118,6 +131,7 @@ public class GameController { "scores", game.scores() )); } + // Java private void playTrack(Context ctx) { Map body = ctx.bodyAsClass(Map.class); String username = body.get("username"); @@ -129,10 +143,8 @@ public class GameController { } try { - String accessToken = authService.getAccessTokenForUser(username); - OkHttpClient client = new OkHttpClient(); - - // 1. Track-Details holen + //String accessToken = authService.getAccessTokenForUser(username); + OkHttpClient client = httpClient; String trackId = trackUri.split(":")[2]; Request getTrack = new Request.Builder() .url("https://api.spotify.com/v1/tracks/" + trackId) @@ -140,14 +152,13 @@ public class GameController { .build(); try (Response trackResp = client.newCall(getTrack).execute()) { if (!trackResp.isSuccessful()) { - ctx.status(500).result("Fehler beim Laden der Track-Details"); + ctx.status(trackResp.code()).result("Fehler beim Laden der Track-Details: " + trackResp.body().string()); return; } var node = new com.fasterxml.jackson.databind.ObjectMapper().readTree(trackResp.body().string()); long durationMs = node.get("duration_ms").asLong(); long startOffset = durationMs / 2; - // 2. Play-Request mit position_ms ObjectNode jsonNode = JsonNodeFactory.instance.objectNode(); jsonNode.putArray("uris").add(trackUri); jsonNode.put("position_ms", startOffset); @@ -160,16 +171,22 @@ public class GameController { .build(); try (Response playResp = client.newCall(playReq).execute()) { + ctx.status(playResp.code()); + ctx.header("Retry-After", playResp.header("Retry-After") != null ? playResp.header("Retry-After") : ""); + ctx.header("x-rate-limit-limit", playResp.header("x-rate-limit-limit")); + ctx.header("x-rate-limit-remaining", playResp.header("x-rate-limit-remaining")); + ctx.header("x-rate-limit-reset", playResp.header("x-rate-limit-reset")); + ctx.result(playResp.body().string()); if (playResp.isSuccessful()) { ctx.status(204).result("Track erfolgreich abgespielt"); } else { - ctx.status(playResp.code()).result("Fehler: " + playResp.body().string()); + ctx.status(playResp.code()).result("Fehler beim Abspielen: " + playResp.body().string()); } } } } catch (Exception e) { log.error("Fehler beim Abspielen des Tracks", e); - ctx.status(500).result("Interner Fehler"); + ctx.status(500).result("Interner Fehler: " + e.getMessage()); } } diff --git a/src/main/java/eric/Roullette/service/GameService.java b/src/main/java/eric/Roullette/service/GameService.java index b1aea36..8370d1f 100644 --- a/src/main/java/eric/Roullette/service/GameService.java +++ b/src/main/java/eric/Roullette/service/GameService.java @@ -26,7 +26,7 @@ package eric.Roullette.service; } public Game getOrCreateGame(String gameId) { - return games.computeIfAbsent(gameId, id -> Game.create(id)); + return games.computeIfAbsent(gameId, Game::create); } public void addPlayer(String gameId, String user) { @@ -82,4 +82,10 @@ package eric.Roullette.service; public Set getSessions(String gameId) { return sessions.getOrDefault(gameId, Collections.emptySet()); } + // Map> + public Map> getPlayerTracks(String gameId) { + Game game = games.get(gameId); + if (game == null) return Collections.emptyMap(); + return game.playerTracks(); + } } \ No newline at end of file diff --git a/src/main/java/eric/Roullette/service/SpotifyAuthService.java b/src/main/java/eric/Roullette/service/SpotifyAuthService.java index 9349393..1cf970b 100644 --- a/src/main/java/eric/Roullette/service/SpotifyAuthService.java +++ b/src/main/java/eric/Roullette/service/SpotifyAuthService.java @@ -14,192 +14,354 @@ package eric.Roullette.service; 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 static com.neovisionaries.i18n.CountryCode.DE; public class SpotifyAuthService { - private final String clientId; - private final String clientSecret; - private final URI redirectUri; - // Speichert für jeden Benutzer eine eigene, authentifizierte SpotifyApi-Instanz - private final Map userApis = new ConcurrentHashMap<>(); + private final String clientId; + private final String clientSecret; + 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<>(); - public SpotifyAuthService(String clientId, String clientSecret, String redirectUri) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.redirectUri = SpotifyHttpManager.makeUri(redirectUri); - } + private final OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(chain -> { + Request req = chain.request(); + Response res = chain.proceed(req); - public URI getAuthorizationUri(String user) { - // Temporäre API-Instanz nur für die Erstellung der Auth-URL - SpotifyApi tempApi = new SpotifyApi.Builder() - .setClientId(clientId) - .setClientSecret(clientSecret) - .setRedirectUri(redirectUri) - .build(); + 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"); - return tempApi.authorizationCodeUri() - .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(); - } + 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 + ); - public void exchangeCode(String code, String user) throws IOException, ParseException, SpotifyWebApiException { - // Erstellt eine neue, dedizierte API-Instanz für diesen Benutzer - SpotifyApi userApi = new SpotifyApi.Builder() - .setClientId(clientId) - .setClientSecret(clientSecret) - .setRedirectUri(redirectUri) - .build(); + 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(); - // Tauscht den Code gegen Tokens und konfiguriert die Instanz - AuthorizationCodeCredentials creds = userApi.authorizationCode(code).build().execute(); - userApi.setAccessToken(creds.getAccessToken()); - userApi.setRefreshToken(creds.getRefreshToken()); - - // Speichert die fertig konfigurierte API-Instanz für den Benutzer - userApis.put(user, userApi); - } - - public List getRecentTracks(String user) { - int limit = 50; - SpotifyApi userApi = userApis.get(user); - - if (userApi == null) { - System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + user); - return Collections.emptyList(); - } - - try { - 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){ - newLimit = limit - recentTracks.size(); - List savedTracks2 = getSavedTracks(user, newLimit, 50); - savedTracks2.removeAll(recentTracks); - recentTracks.addAll(savedTracks2.subList(0, Math.min(newLimit, savedTracks2.size()))); - } - } - return recentTracks.subList(0, Math.min(limit, recentTracks.size())); - } catch (IOException | SpotifyWebApiException | ParseException e) { - e.printStackTrace(); - return Collections.emptyList(); - } - } - - private List getSavedTracks(String user, int limit, int offset) { - SpotifyApi userApi = userApis.get(user); - - if (userApi == null) { - System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + user); - return Collections.emptyList(); - } - try { - 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; - } - 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); - - } 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 SpotifyAuthService(String clientId, String clientSecret, String redirectUri) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.redirectUri = SpotifyHttpManager.makeUri(redirectUri); } + public URI getAuthorizationUri(String user) { + // Temporäre API-Instanz nur für die Erstellung der Auth-URL + SpotifyApi tempApi = new SpotifyApi.Builder() + .setClientId(clientId) + .setClientSecret(clientSecret) + .setRedirectUri(redirectUri) + .build(); + + return tempApi.authorizationCodeUri() + .scope("user-read-recently-played user-library-read user-modify-playback-state user-read-playback-state") + .state(user) // Der Benutzername wird im State mitgegeben + .build() + .execute(); + } + + public void exchangeCode(String code, String user) throws IOException, ParseException, SpotifyWebApiException { + // Erstellt eine neue, dedizierte API-Instanz für diesen Benutzer + SpotifyApi userApi = new SpotifyApi.Builder() + .setClientId(clientId) + .setClientSecret(clientSecret) + .setRedirectUri(redirectUri) + .build(); + + // Tauscht den Code gegen Tokens und konfiguriert die Instanz + 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; + SpotifyApi userApi = userApis.get(user); + + if (userApi == null) { + System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + user); + 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(); + }); + + if (recentTracks.size() < limit) { + int newLimit = limit - recentTracks.size(); + List savedTracks = getSavedTracks(user, newLimit, 0); + savedTracks.removeAll(recentTracks); + recentTracks = new java.util.ArrayList<>(recentTracks); + recentTracks.addAll(savedTracks.subList(0, Math.min(newLimit, savedTracks.size()))); + 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; + } catch (IOException | SpotifyWebApiException | ParseException e) { + e.printStackTrace(); + return Collections.emptyList(); + } + } + + 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; + } + return result; + }); + if (offset == 0) savedTracksCache.put(user, saved); // Cache speichern + 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); +// +// } 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) { - SpotifyApi userApi = userApis.get(username); - if (userApi == null) { - System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + 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(); return null; } - return userApi.getAccessToken(); +// 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(); } - public List> getDevices(String username) { - String accessToken = getAccessTokenForUser(username); - OkHttpClient client = new OkHttpClient(); + + 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; Request req = new Request.Builder() .url("https://api.spotify.com/v1/me/player/devices") .addHeader("Authorization", "Bearer " + accessToken) .build(); - try (Response resp = client.newCall(req).execute()) { - if (!resp.isSuccessful()) return List.of(); + 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(); + } 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); + } + } +} diff --git a/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java b/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java index 32d1fd1..20b6022 100644 --- a/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java +++ b/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java @@ -2,7 +2,7 @@ package eric.Roullette.websocket; import com.fasterxml.jackson.databind.JsonNode; import eric.Roullette.service.GameService; -import eric.Roullette.service.SpotifyAuthService; +//import eric.Roullette.service.SpotifyAuthService; import eric.Roullette.util.JsonUtil; import io.javalin.websocket.WsConfig; import io.javalin.websocket.WsContext; @@ -17,14 +17,20 @@ import java.util.concurrent.ConcurrentHashMap; public class GameWebSocketHandler { private final GameService service; - private final SpotifyAuthService authService; + //private final SpotifyAuthService authService; // Spiel-ID → (Username → deren Guess) private final Map> currentGuesses = new ConcurrentHashMap<>(); - public GameWebSocketHandler(GameService gameService, SpotifyAuthService authService) { + private final Map> trackInfoCache = new ConcurrentHashMap<>(); + private final Map> allTracksCache = new ConcurrentHashMap<>(); + //Map> + private final Map>> playerTracksCache = new ConcurrentHashMap<>(); + private final Map>> playerTrackInfoCache = new ConcurrentHashMap<>(); + + public GameWebSocketHandler(GameService gameService) { this.service = gameService; - this.authService = authService; + //this.authService = authService; } /** @@ -57,7 +63,7 @@ public class GameWebSocketHandler { switch (type) { case "guess" -> { - String user = node.get("username").asText(); + String user = node.get("username").asText(); String guess = node.get("guess").asText(); // Guess speichern currentGuesses @@ -71,31 +77,18 @@ public class GameWebSocketHandler { } case "requestPlayers" -> service.broadcastPlayers(gameId); - case "start-round" -> { - var game = service.getOrCreateGame(gameId); - if (game.players().isEmpty()) return; - - // Songs von allen Spielern sammeln - List allTracks = new ArrayList<>(); - for (String player : game.players()) { - allTracks.addAll(game.playerTracks().getOrDefault(player, List.of())); - } - if (allTracks.isEmpty()) { - // TODO: Fehler an Client senden, dass keine Songs da sind - return; - } - - // Runde im Service starten, um Song und Owner zu setzen - service.startRound(gameId, allTracks); - // Jetzt Broadcast mit den aktuellen Daten - broadcastRoundStart(gameId); - } case "next-round" -> { + nextround(gameId); + } + case "start-round" -> { var currentGame = service.getOrCreateGame(gameId); - List allTracks = new ArrayList<>(); - for (String player : currentGame.players()) { - allTracks.addAll(authService.getRecentTracks(player)); - } + if (currentGame.players().isEmpty()) return; + // Tracks pro Spieler sammeln + Map> playerTracks = currentGame.playerTracks(); + List allTracks = playerTracks.values().stream() + .flatMap(List::stream) + .toList(); + System.out.println("AlltracksCache für Spiel " + gameId + " hat " + allTracks.size() + " Songs (rundesnstart)"); if (!allTracks.isEmpty()) { service.startRound(gameId, allTracks); } @@ -105,15 +98,46 @@ public class GameWebSocketHandler { }); } + public void nextround(String gameId) { + var game = service.getOrCreateGame(gameId); + if (game.players().isEmpty()) return; + + // Songs von allen Spielern sammeln + Map> playerTracks = game.playerTracks(); + List allTracks = new ArrayList<>(); + for (String player : game.players()) { + allTracks.addAll(game.playerTracks().getOrDefault(player, List.of())); + } + if (allTracks.isEmpty()) { + // TODO: Fehler an Client senden, dass keine Songs da sind + return; + } + + // Runde im Service starten, um Song und Owner zu setzen + service.startRound(gameId, allTracks); + // Jetzt Broadcast mit den aktuellen Daten + broadcastRoundStart(gameId); + } // ----- Broadcast-Methoden ----- /** Broadcastet den Runden-Start (Song + Optionen) an alle Clients. */ - public void broadcastRoundStart(String gameId) { - var game = service.getOrCreateGame(gameId); - List opts = game.players(); - String songUri = game.currentSong(); + public void broadcastRoundStart(String gameId) { + var game = service.getOrCreateGame(gameId); + List opts = game.players(); + String songUri = game.currentSong(); List allTracks = game.allTracks(); - List trackInfos = authService.getTrackInfos(allTracks); + List trackInfos = game.allTracks(); + // Cache pro Spiel-ID nutzen +// List trackInfos = trackInfoCache.get(gameId); +// if (trackInfos == null || trackInfos.isEmpty()) { +// System.out.println("TrackInfoCache ist leer, hole Infos von Spotify"); +// trackInfos = authService.getTrackInfos(allTracks); +// trackInfoCache.put(gameId, trackInfos); +// System.out.println("TrackInfoCache für Spiel " + gameId + " hat " + trackInfos.size() + " Infos"); +// } else { +// System.out.println("TrackInfoCache ist nicht leer, nutze gecachte Infos"); +// } + String msg = JsonUtil.toJson(Map.of( "type", "round-start", "ownerOptions", opts, @@ -167,18 +191,8 @@ private void broadcastRoundResult(String gameId) { // nächste Runde starten // ... new Thread(() -> { - try { Thread.sleep(4000); } catch (InterruptedException ignored) {} - // Songs erneut sammeln - var currentGame = service.getOrCreateGame(gameId); - List allTracks = new ArrayList<>(); - for (String player : currentGame.players()) { - allTracks.addAll(authService.getRecentTracks(player)); - } - if (!allTracks.isEmpty()) { - // Neue Runde starten - service.startRound(gameId, allTracks); - } - broadcastRoundStart(gameId); + try { Thread.sleep(2000); } catch (InterruptedException ignored) {} + nextround(gameId); }).start(); } } diff --git a/src/main/resources/public/js/device-select.js b/src/main/resources/public/js/device-select.js index 64ac2d8..487fe5d 100644 --- a/src/main/resources/public/js/device-select.js +++ b/src/main/resources/public/js/device-select.js @@ -18,7 +18,7 @@ area.appendChild(select); async function loadDevices() { select.innerHTML = ""; - const { devices } = await fetchJson(`/api/spotify/devices?username=${encodeURIComponent(username)}`); + const devices = await fetchJson(`/api/spotify/devices?username=${encodeURIComponent(username)}`); if (!devices.length) { const opt = document.createElement("option"); opt.textContent = "Keine Geräte gefunden"; diff --git a/src/main/resources/public/js/game.js b/src/main/resources/public/js/game.js index 5d8593e..e98fafb 100644 --- a/src/main/resources/public/js/game.js +++ b/src/main/resources/public/js/game.js @@ -60,7 +60,7 @@ function connectWebSocket() { setupStartRound(socket); }); - socket.addEventListener("message", ({ data }) => { + socket.addEventListener("message", async ({ data }) => { console.log("WS-Rohdaten:", data); const msg = JSON.parse(data); @@ -73,7 +73,7 @@ function connectWebSocket() { window.location.reload(); break; case "round-start": - handleRoundStart(msg); + await handleRoundStart(msg); break; case "round-result": handleRoundResult(msg); @@ -107,8 +107,12 @@ const resultP = document.getElementById("result"); const scoreboard = document.getElementById("scoreboard"); // 8) Funktion zum Anzeigen einer neuen Runde -function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) { +//let playLock = false; + +async function handleRoundStart({ownerOptions, songUri, allTracks, trackInfos}) { // UI zurücksetzen + //if (playLock) return; // Verhindert parallele Ausführung + //playLock = true; resultP.textContent = ""; optionsDiv.innerHTML = ""; songEmbed.innerHTML = ""; @@ -123,9 +127,13 @@ function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) { `; if (window.playOnSpotify && typeof window.playOnSpotify === "function") { - window.playOnSpotify(songUri, username); + await window.playOnSpotify(songUri, username); // Warten bis fertig } + //if (window.playOnSpotify && typeof window.playOnSpotify === "function") { + // window.playOnSpotify(songUri, username); + //} + // Dynamische Kreisverteilung der Buttons // Warten, bis #options gerendert ist setTimeout(() => { @@ -140,9 +148,9 @@ function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) { btn.style.transform = `rotate(${angle}deg) translateY(-${radius}px) rotate(${-angle}deg)`; btn.addEventListener("click", () => { socket.send(JSON.stringify({ - type: "guess", + type: "guess", username: username, - guess: user + guess: user })); optionsDiv.querySelectorAll("button").forEach(b => b.disabled = true); }); @@ -150,7 +158,7 @@ function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) { }); }, 0); - startBtn.hidden = true; + startBtn.hidden = true; roundArea.hidden = false; const songList = document.getElementById("songList"); @@ -162,6 +170,7 @@ function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) { songList.appendChild(li); }); } + //playLock = false; } // 9) Funktion zum Anzeigen des Ergebnisses @@ -213,6 +222,7 @@ function handleGameEnd({winner}) { } // Spotify-Playback Funktion +// game.js async function playOnSpotify(trackUri, username) { const deviceId = document.getElementById("deviceSelect")?.value; if (!deviceId) {