diff --git a/src/main/java/eric/Roullette/App.java b/src/main/java/eric/Roullette/App.java index 723777b..437b444 100644 --- a/src/main/java/eric/Roullette/App.java +++ b/src/main/java/eric/Roullette/App.java @@ -23,18 +23,31 @@ public class App { if (cfg.spotifyClientId == null || cfg.spotifyClientSecret == null || cfg.spotifyRedirectUri == null) { throw new IllegalStateException("Spotify-Konfiguration fehlt: Bitte stelle sicher, dass ClientId, ClientSecret und RedirectUri gesetzt sind."); } - GameService gs = new GameService(); + SpotifyAuthService sas = new SpotifyAuthService( cfg.spotifyClientId, cfg.spotifyClientSecret, cfg.spotifyRedirectUri ); + GameService gs = new GameService(sas); Javalin app = Javalin.create(config -> { config.showJavalinBanner = false; 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())); @@ -57,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..9fc42d9 100644 --- a/src/main/java/eric/Roullette/controller/GameController.java +++ b/src/main/java/eric/Roullette/controller/GameController.java @@ -1,6 +1,6 @@ package eric.Roullette.controller; - import com.fasterxml.jackson.core.type.TypeReference; +// import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import io.javalin.Javalin; @@ -13,23 +13,25 @@ package eric.Roullette.controller; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.MediaType; - import com.fasterxml.jackson.databind.node.JsonNodeFactory; - import com.fasterxml.jackson.databind.node.ObjectNode; - import org.slf4j.Logger; - import org.slf4j.LoggerFactory; +// import com.fasterxml.jackson.databind.node.JsonNodeFactory; +// import com.fasterxml.jackson.databind.node.ObjectNode; +// import org.slf4j.Logger; +// import org.slf4j.LoggerFactory; import java.awt.*; - import java.io.IOException; - import java.util.List; +// import java.io.IOException; +// import java.util.List; import java.util.Map; - import java.util.Objects; - import java.util.UUID; +// import java.util.Objects; +// import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; - import java.util.concurrent.TimeUnit; +// import java.util.concurrent.TimeUnit; 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,13 +50,24 @@ 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(accessToken); + ctx.json(devices); + }); } - private void createGame(Context ctx) { - Map body = ctx.bodyAsClass(Map.class); + private void setToken(String accessToken) { + this.accessToken = accessToken; + } + @SuppressWarnings("unchecked") + private void createGame(Context ctx) throws InterruptedException { + Map body = (Map) ctx.bodyAsClass(Map.class); String user = (String) body.get("username"); if (user == null || user.isBlank()) { ctx.status(400).result("username fehlt"); @@ -72,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"); @@ -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,14 @@ 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; } + assert trackResp.body() != null; 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 +172,23 @@ 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()); + System.out.println("Fehler beim Abspielen des Tracks"); } } } } 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 8fbd73b..2161e7d 100644 --- a/src/main/java/eric/Roullette/service/GameService.java +++ b/src/main/java/eric/Roullette/service/GameService.java @@ -1,78 +1,110 @@ package eric.Roullette.service; -import eric.Roullette.dto.PlayersMessage; -import eric.Roullette.util.JsonUtil; -import io.javalin.websocket.WsContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.*; -import java.util.concurrent.*; + import eric.Roullette.dto.PlayersMessage; + import eric.Roullette.util.JsonUtil; + import io.javalin.websocket.WsContext; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import java.util.*; + import java.util.concurrent.*; -public class GameService { - private static final Logger log = LoggerFactory.getLogger(GameService.class); - private final Map> sessions = new ConcurrentHashMap<>(); - private final Map games = new ConcurrentHashMap<>(); + public class GameService { + private static final Logger log = LoggerFactory.getLogger(GameService.class); + private final SpotifyAuthService authService; + private final Map> sessions = new ConcurrentHashMap<>(); + private final Map games = new ConcurrentHashMap<>(); - public record Game(String id, List players, Map scores,String currentOwner, - String currentSong,List allTracks) { - public static Game create(String id) { - return new Game(id, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>()); + public GameService(SpotifyAuthService authService) { // <-- Konstruktor + this.authService = authService; } - } - - public Game getOrCreateGame(String gameId) { - return games.computeIfAbsent(gameId, id -> Game.create(id)); - } - - public void addPlayer(String gameId, String user) { - Game g = getOrCreateGame(gameId); - if (user != null && !g.players().contains(user)) { - g.players().add(user); - g.scores().putIfAbsent(user, 0); + public Map> getTrackInfos(Map> allPlayerTracks) { + // für jeden String Spieler in allPlayerTracks die Liste der Tracks an authservice übergeben + Map> trackInfos = new ConcurrentHashMap<>(); + for (Map.Entry> entry : allPlayerTracks.entrySet()) { + String player = entry.getKey(); + List tracks = entry.getValue(); + if (tracks.isEmpty()) continue; // Keine Tracks, skip + try { + List trackInfo = authService.getTrackInfos(tracks); + trackInfos.put(player, trackInfo); + } catch (Exception e) { + log.error("Fehler beim Abrufen der Track-Infos für Spieler {}: {}", player, e.getMessage()); + } + } + return trackInfos; } - } - public void registerSession(String gameId, WsContext ctx) { - sessions - .computeIfAbsent(gameId, id -> ConcurrentHashMap.newKeySet()) - .add(ctx); - broadcastPlayers(gameId); - } - public void removeSession(String gameId, WsContext ctx) { - sessions.getOrDefault(gameId, Collections.emptySet()).remove(ctx); - broadcastPlayers(gameId); - } + public record Game(String id, List players, Map scores, String currentOwner, + String currentSong, List allTracks, Map> playerTracks) { + public static Game create(String id) { + return new Game(id, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>(), new ConcurrentHashMap<>()); + } - public void broadcastPlayers(String gameId) { - Game game = games.get(gameId); - if (game == null) return; - PlayersMessage msg = new PlayersMessage(new ArrayList<>(game.players())); - sessions.getOrDefault(gameId, Collections.emptySet()) - .forEach(ctx -> ctx.send(JsonUtil.toJson(msg))); - } + } - public void createGame(String gameId) { - Game game = new Game(gameId, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>()); - games.put(gameId, game); - } + public Game getOrCreateGame(String gameId) { + return games.computeIfAbsent(gameId, Game::create); + } - public boolean gameExists(String gameId) { - return games.containsKey(gameId); - } - public Game startRound(String gameId, List uris) { - Game g = getOrCreateGame(gameId); - if (g.players().isEmpty()) throw new IllegalStateException("No players"); - String owner = g.players().get(ThreadLocalRandom.current().nextInt(g.players().size())); - String song = uris.get(ThreadLocalRandom.current().nextInt(uris.size())); - Game updated = new Game(gameId, g.players(), g.scores(), owner, song, uris); - games.put(gameId, updated); - return updated; - } - // In GameService.java - public Set getSessions(String gameId) { - return sessions.getOrDefault(gameId, Collections.emptySet()); - } + public void addPlayer(String gameId, String user) throws InterruptedException { + Game g = getOrCreateGame(gameId); + if (user != null && !g.players().contains(user)) { + g.players().add(user); + g.scores().putIfAbsent(user, 0); + // Songs einmalig laden und speichern + List tracks = authService.getRecentTracks(user); + g.playerTracks().put(user, tracks); + } + } -} + public void registerSession(String gameId, WsContext ctx) { + sessions + .computeIfAbsent(gameId, id -> ConcurrentHashMap.newKeySet()) + .add(ctx); + broadcastPlayers(gameId); + } + + public void removeSession(String gameId, WsContext ctx) { + sessions.getOrDefault(gameId, Collections.emptySet()).remove(ctx); + broadcastPlayers(gameId); + } + + public void broadcastPlayers(String gameId) { + Game game = games.get(gameId); + if (game == null) return; + PlayersMessage msg = new PlayersMessage(new ArrayList<>(game.players())); + sessions.getOrDefault(gameId, Collections.emptySet()) + .forEach(ctx -> ctx.send(JsonUtil.toJson(msg))); + } + + public void createGame(String gameId) { + Game game = new Game(gameId, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>(), new ConcurrentHashMap<>()); + games.put(gameId, game); + } + + public boolean gameExists(String gameId) { + return games.containsKey(gameId); + } + + public Game startRound(String gameId, List uris) { + Game g = getOrCreateGame(gameId); + if (g.players().isEmpty()) throw new IllegalStateException("No players"); + String owner = g.players().get(ThreadLocalRandom.current().nextInt(g.players().size())); + String song = uris.get(ThreadLocalRandom.current().nextInt(uris.size())); + Game updated = new Game(gameId, g.players(), g.scores(), owner, song, uris, g.playerTracks()); + games.put(gameId, updated); + return updated; + } + + 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..17cd358 100644 --- a/src/main/java/eric/Roullette/service/SpotifyAuthService.java +++ b/src/main/java/eric/Roullette/service/SpotifyAuthService.java @@ -1,152 +1,156 @@ 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 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.io.IOException; - import java.net.URI; - import java.util.*; - import java.util.concurrent.ConcurrentHashMap; +import java.io.IOException; +import java.net.URI; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; - import static com.neovisionaries.i18n.CountryCode.DE; +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<>(); - public SpotifyAuthService(String clientId, String clientSecret, String redirectUri) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.redirectUri = SpotifyHttpManager.makeUri(redirectUri); - } + 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(); + public URI getAuthorizationUri(String user) { + System.out.println("Erstelle Auth-URL für Benutzer: " + 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-read-playback-state user-modify-playback-state streaming") - .state(user) // Der Benutzername wird im State mitgegeben - .build() - .execute(); - } + 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(); + } - 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(); + 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()); + // 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); - } + // 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); + public List getRecentTracks(String user) { + System.out.println("Hole kürzlich gespielte Tracks für Benutzer: " + user); + int limit = 2; + SpotifyApi userApi = userApis.get(user); - if (userApi == null) { - System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + user); - return Collections.emptyList(); - } + 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(); + 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(); - } - } + 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); + 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(); - } - 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) { + 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 tracks) { + System.out.println("Hole Track-Infos für " + tracks.size() + " Tracks."); //für jede URI den titel holen List trackInfos = new ArrayList<>(); - for (String uri : allTracks) { + for (String uri : tracks) { SpotifyApi userApi = userApis.values().stream().findFirst().orElse(null); if (userApi == null) { System.err.println("Kein SpotifyApi-Client gefunden."); @@ -176,6 +180,7 @@ public class SpotifyAuthService { public String getAccessTokenForUser(String username) { + System.out.println("Hole AccessToken für Benutzer: " + username); SpotifyApi userApi = userApis.get(username); if (userApi == null) { System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + username); @@ -183,8 +188,8 @@ public class SpotifyAuthService { } return userApi.getAccessToken(); } - public List> getDevices(String username) { - String accessToken = getAccessTokenForUser(username); + 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") diff --git a/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java b/src/main/java/eric/Roullette/websocket/GameWebSocketHandler.java index b98b534..ec4cdf8 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 Map>> playerTracksCache = new ConcurrentHashMap<>(); + private 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,38 +77,62 @@ public class GameWebSocketHandler { } case "requestPlayers" -> service.broadcastPlayers(gameId); + case "next-round" -> { + nextround(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(authService.getRecentTracks(player)); + var currentGame = service.getOrCreateGame(gameId); + if (currentGame.players().isEmpty()) return; + // Tracks pro Spieler sammeln + Map> allPlayerTracks = currentGame.playerTracks(); + // alle tracks sammeln + List allTracks = allPlayerTracks.values().stream() + .flatMap(List::stream) + .toList(); + System.out.println("AlltracksCache für Spiel " + gameId + " hat " + allTracks.size() + " Songs (rundenstart)"); + if (!allTracks.isEmpty()) { + service.startRound(gameId, allTracks); } - 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 + // Trackinfos für alle Spieler sammeln + Map> allTrackInfos = service.getTrackInfos(allPlayerTracks); + // Cache für Trackinfos pro Spiel-ID + playerTrackInfoCache.put(gameId, allTrackInfos); + System.out.println("TrackInfosCache für Spiel " + gameId + " hat " + allTrackInfos.size() + " Spieler (rundenstart)"); broadcastRoundStart(gameId); } } }); } + public void nextround(String gameId) { + 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; + } + // TODO funktionalität bei neu joinenden Spielern überprüfen + // 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); + Map> trackInfos = playerTrackInfoCache.get(gameId); + // Cache pro Spiel-ID nutzen + String msg = JsonUtil.toJson(Map.of( "type", "round-start", "ownerOptions", opts, @@ -156,18 +186,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/game.html b/src/main/resources/public/game.html index 0577766..e27eafc 100644 --- a/src/main/resources/public/game.html +++ b/src/main/resources/public/game.html @@ -1,43 +1,162 @@ - - - Spotify Roulette – Spiel - + + + Spotify Roulette – Spiel + -

Spotify Roulette

-

Spiel-Code:

- -
-

Geladene Songs

-
    -
    - -

    Teilnehmer

    -
      - -
      - -
      - - - -

      Scoreboard

      -
        - +
        +

        Spotify Roulette

        +
        Spiel-Code:
        +
        +
        +
        +
        Teilnehmer
        +
          +
          +
          + +
          + +
          +
          Scoreboard
          +
            +
            +
            + +
            Spotify Roulette – alles ausser arbeiten
            + + -
            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 a091ee6..a9e7505 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); @@ -98,7 +98,7 @@ function connectWebSocket() { } connectWebSocket(); -// 8) Funktion zum Anzeigen einer neuen Runde +// Zugriff auf DOM-Elemente const startBtn = document.getElementById("startRound"); const roundArea = document.getElementById("roundArea"); const songEmbed = document.getElementById("songEmbed"); @@ -106,8 +106,13 @@ const optionsDiv = document.getElementById("options"); const resultP = document.getElementById("result"); const scoreboard = document.getElementById("scoreboard"); -function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) { +// 8) Funktion zum Anzeigen einer neuen Runde +//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 = ""; @@ -120,39 +125,52 @@ function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) { width="100%" height="80" frameborder="0" allow="encrypted-media"> `; - // Song automatisch abspielen (sofern deviceId bereit und Username vorhanden) + if (window.playOnSpotify && typeof window.playOnSpotify === "function") { - window.playOnSpotify(songUri, username); + await window.playOnSpotify(songUri, username); // Warten bis fertig } - // Tipp-Buttons erzeugen - ownerOptions.forEach(user => { - const btn = document.createElement("button"); - btn.textContent = user; - btn.addEventListener("click", () => { - socket.send(JSON.stringify({ - type: "guess", - username: username, - guess: user - })); - // Nach Tipp alle Buttons deaktivieren - optionsDiv.querySelectorAll("button").forEach(b => b.disabled = true); - }); - optionsDiv.append(btn); - }); + //if (window.playOnSpotify && typeof window.playOnSpotify === "function") { + // window.playOnSpotify(songUri, username); + //} - // UI anzeigen - startBtn.hidden = true; + // Dynamische Kreisverteilung der Buttons + // Warten, bis #options gerendert ist + setTimeout(() => { + const optsRect = optionsDiv.getBoundingClientRect(); + const radius = Math.min(optsRect.width, optsRect.height) / 2 - 50; // 50px Abstand zum Rand + + ownerOptions.forEach((user, i) => { + const btn = document.createElement("button"); + btn.textContent = user; + btn.classList.add("player-option"); + const angle = 360 * i / ownerOptions.length; + btn.style.transform = `rotate(${angle}deg) translateY(-${radius}px) rotate(${-angle}deg)`; + btn.addEventListener("click", () => { + socket.send(JSON.stringify({ + type: "guess", + username: username, + guess: user + })); + optionsDiv.querySelectorAll("button").forEach(b => b.disabled = true); + }); + optionsDiv.appendChild(btn); + }); + }, 0); + + startBtn.hidden = true; roundArea.hidden = false; + const songList = document.getElementById("songList"); songList.innerHTML = ""; - if (Array.isArray(trackInfos)) { - trackInfos.forEach(trackInfo => { - const li = document.createElement("li"); - li.textContent = trackInfo - songList.appendChild(li); - }); - } + //trackinfos ist eine map bestehend aus aus Spielername und Liste von Track-Infos + const userTracks = trackInfos?.[username] ?? []; + userTracks.forEach(trackInfo => { + const li = document.createElement("li"); + li.textContent = trackInfo; + songList.appendChild(li); + }); + //playLock = false; } // 9) Funktion zum Anzeigen des Ergebnisses @@ -166,43 +184,45 @@ function renderScoreboard(scores) { } function handleRoundResult({ scores, guesses, owner }) { - // Scoreboard updaten renderScoreboard(scores); - // Ergebnis für alle Spieler anzeigen - resultP.innerHTML = ""; // Vorher leeren + resultP.innerHTML = ""; Object.entries(guesses).forEach(([user, guess]) => { const correct = guess === owner; const icon = correct ? "✅" : "❌"; - const msg = `${icon} ${user} hat auf ${guess} getippt${correct ? " (richtig!)" : " (falsch)"}`; + const delta = correct ? "+3" : "-1"; + const msg = `${icon} ${user} hat auf ${guess} getippt${correct ? " (richtig!)" : " (falsch)"} [${delta}]`; const p = document.createElement("p"); p.textContent = msg; resultP.appendChild(p); }); - // Nach kurzer Pause für die nächste Runde vorbereiten - setTimeout(() => { - resultP.textContent = ""; - startBtn.hidden = true; - startBtn.disabled = true; - roundArea.hidden = true; - }, 3000); - + const nextBtn = document.getElementById("nextRound"); + nextBtn.hidden = false; + nextBtn.disabled = false; + nextBtn.onclick = () => { + socket.send(JSON.stringify({ type: "next-round" })); + nextBtn.hidden = true; + nextBtn.disabled = true; + resultP.textContent = ""; + startBtn.hidden = true; + startBtn.disabled = true; + roundArea.hidden = true; + }; } function handleGameEnd({winner}) { resultP.textContent = `🎉 ${winner} hat gewonnen!`; setTimeout(() => { - startBtn.hidden = false; - roundArea.hidden = true; - startBtn.disabled = false; - // scoreboard leeren + startBtn.hidden = false; + roundArea.hidden = true; + startBtn.disabled = false; scoreboard.innerHTML = ""; }, 6000); } -// public/js/game.js - +// Spotify-Playback Funktion +// game.js async function playOnSpotify(trackUri, username) { const deviceId = document.getElementById("deviceSelect")?.value; if (!deviceId) { @@ -213,11 +233,7 @@ async function playOnSpotify(trackUri, username) { const response = await fetch("/api/spotify/play", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username, - device_id: deviceId, - track_uri: trackUri - }) + body: JSON.stringify({ username, device_id: deviceId, track_uri: trackUri }) }); if (!response.ok) { const error = await response.text(); @@ -227,4 +243,4 @@ async function playOnSpotify(trackUri, username) { alert(`Netzwerkfehler: ${err.message}`); } } -window.playOnSpotify = playOnSpotify; \ No newline at end of file +window.playOnSpotify = playOnSpotify; diff --git a/src/test/java/eric/Roullette/service/GameServiceTest.java b/src/test/java/eric/Roullette/service/GameServiceTest.java index 6fa2810..2a87ae0 100644 --- a/src/test/java/eric/Roullette/service/GameServiceTest.java +++ b/src/test/java/eric/Roullette/service/GameServiceTest.java @@ -1,4 +1,4 @@ -// src/test/java/eric/roulette/service/GameServiceTest.java +// src/test/java/eric/Roullette/service/GameServiceTest.java package eric.Roullette.service; import org.junit.jupiter.api.Test; @@ -9,7 +9,9 @@ class GameServiceTest { @Test void testGetOrCreateGame() { - GameService service = new GameService(); + // Dummy-Parameter für SpotifyAuthService + SpotifyAuthService sas = new SpotifyAuthService("dummy", "dummy", "http://localhost"); + GameService service = new GameService(sas); // Erstes Mal anlegen GameService.Game g1 = service.getOrCreateGame("g1"); assertNotNull(g1); @@ -18,18 +20,4 @@ class GameServiceTest { GameService.Game g2 = service.getOrCreateGame("g1"); assertSame(g1, g2); } - -// @Test -// void testAddPlayerAndScores() { -// GameService service = new GameService(); -// service.getOrCreateGame("g2"); // Spiel anlegen -// service.addPlayer("g2", "Alice"); // Spieler hinzufügen -// GameService.Game game = service.getOrCreateGame("g2"); -// // Spieler-Liste korrekt -// assertTrue(game.players().contains("Alice")); -// assertEquals(1, game.players().size()); -// // Score für neuen Spieler initial 0 -// assertEquals(0, game.scores().get("Alice").intValue()); -// // Duplikate vermeiden -// } -} +} \ No newline at end of file