trackinfos optimiert laden ohne zu viele api aufrufe

pull/24/head
eric 2025-08-10 17:35:39 +02:00
parent 6028da6210
commit 3e0822df3b
6 changed files with 79 additions and 58 deletions

View File

@ -36,17 +36,17 @@ public class App {
config.staticFiles.add("/public", Location.CLASSPATH); config.staticFiles.add("/public", Location.CLASSPATH);
}).start(cfg.port); }).start(cfg.port);
app.before(ctx -> { // app.before(ctx -> {
System.out.println("→ " + ctx.method() + " " + ctx.fullUrl()); // System.out.println("→ " + ctx.method() + " " + ctx.fullUrl());
}); // });
app.after(ctx -> { // app.after(ctx -> {
String limit = ctx.header("x-rate-limit-limit"); // String limit = ctx.header("x-rate-limit-limit");
String remaining = ctx.header("x-rate-limit-remaining"); // String remaining = ctx.header("x-rate-limit-remaining");
String reset = ctx.header("x-rate-limit-reset"); // String reset = ctx.header("x-rate-limit-reset");
String retryAfter = ctx.header("Retry-After"); // String retryAfter = ctx.header("Retry-After");
System.out.printf("← %d | limit=%s remaining=%s reset=%s retry-after=%s%n", // System.out.printf("← %d | limit=%s remaining=%s reset=%s retry-after=%s%n",
ctx.status().getCode(), limit, remaining, reset, retryAfter); // ctx.status().getCode(), limit, remaining, reset, retryAfter);
}); // });
app.exception(Exception.class, (e, ctx) -> { app.exception(Exception.class, (e, ctx) -> {
log.error("Unhandled error", e); log.error("Unhandled error", e);

View File

@ -1,6 +1,6 @@
package eric.Roullette.controller; 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.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import io.javalin.Javalin; import io.javalin.Javalin;
@ -13,18 +13,18 @@ package eric.Roullette.controller;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.MediaType; import okhttp3.MediaType;
import com.fasterxml.jackson.databind.node.JsonNodeFactory; // import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode; // import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger; // import org.slf4j.Logger;
import org.slf4j.LoggerFactory; // import org.slf4j.LoggerFactory;
import java.awt.*; import java.awt.*;
import java.io.IOException; // import java.io.IOException;
import java.util.List; // import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; // import java.util.Objects;
import java.util.UUID; // import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit; // import java.util.concurrent.TimeUnit;
public class GameController { public class GameController {
private final GameService gameService; private final GameService gameService;
@ -65,9 +65,9 @@ public class GameController {
private void setToken(String accessToken) { private void setToken(String accessToken) {
this.accessToken = accessToken; this.accessToken = accessToken;
} }
@SuppressWarnings("unchecked")
private void createGame(Context ctx) throws InterruptedException { private void createGame(Context ctx) throws InterruptedException {
Map<String, Object> body = ctx.bodyAsClass(Map.class); Map<String, Object> body = (Map<String, Object>) ctx.bodyAsClass(Map.class);
String user = (String) body.get("username"); String user = (String) body.get("username");
if (user == null || user.isBlank()) { if (user == null || user.isBlank()) {
ctx.status(400).result("username fehlt"); ctx.status(400).result("username fehlt");
@ -152,9 +152,10 @@ public class GameController {
.build(); .build();
try (Response trackResp = client.newCall(getTrack).execute()) { try (Response trackResp = client.newCall(getTrack).execute()) {
if (!trackResp.isSuccessful()) { if (!trackResp.isSuccessful()) {
ctx.status(trackResp.code()).result("Fehler beim Laden der Track-Details: " + trackResp.body().string()); //ctx.status(trackResp.code()).result("Fehler beim Laden der Track-Details: " + trackResp.body().string());
return; return;
} }
assert trackResp.body() != null;
var node = new com.fasterxml.jackson.databind.ObjectMapper().readTree(trackResp.body().string()); var node = new com.fasterxml.jackson.databind.ObjectMapper().readTree(trackResp.body().string());
long durationMs = node.get("duration_ms").asLong(); long durationMs = node.get("duration_ms").asLong();
long startOffset = durationMs / 2; long startOffset = durationMs / 2;
@ -171,16 +172,17 @@ public class GameController {
.build(); .build();
try (Response playResp = client.newCall(playReq).execute()) { try (Response playResp = client.newCall(playReq).execute()) {
ctx.status(playResp.code()); // ctx.status(playResp.code());
ctx.header("Retry-After", playResp.header("Retry-After") != null ? playResp.header("Retry-After") : ""); // 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-limit", playResp.header("x-rate-limit-limit"));
ctx.header("x-rate-limit-remaining", playResp.header("x-rate-limit-remaining")); // 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.header("x-rate-limit-reset", playResp.header("x-rate-limit-reset"));
ctx.result(playResp.body().string()); // ctx.result(playResp.body().string());
if (playResp.isSuccessful()) { if (playResp.isSuccessful()) {
ctx.status(204).result("Track erfolgreich abgespielt"); ctx.status(204).result("Track erfolgreich abgespielt");
} else { } else {
ctx.status(playResp.code()).result("Fehler beim Abspielen: " + playResp.body().string()); //ctx.status(playResp.code()).result("Fehler beim Abspielen: " + playResp.body().string());
System.out.println("Fehler beim Abspielen des Tracks");
} }
} }
} }

View File

@ -18,11 +18,30 @@ package eric.Roullette.service;
this.authService = authService; this.authService = authService;
} }
public Map<String, List<String>> getTrackInfos(Map<String, List<String>> allPlayerTracks) {
// für jeden String Spieler in allPlayerTracks die Liste der Tracks an authservice übergeben
Map<String, List<String>> trackInfos = new ConcurrentHashMap<>();
for (Map.Entry<String, List<String>> entry : allPlayerTracks.entrySet()) {
String player = entry.getKey();
List<String> tracks = entry.getValue();
if (tracks.isEmpty()) continue; // Keine Tracks, skip
try {
List<String> 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 record Game(String id, List<String> players, Map<String,Integer> scores, String currentOwner, public record Game(String id, List<String> players, Map<String,Integer> scores, String currentOwner,
String currentSong, List<String> allTracks, Map<String, List<String>> playerTracks) { String currentSong, List<String> allTracks, Map<String, List<String>> playerTracks) {
public static Game create(String id) { public static Game create(String id) {
return new Game(id, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>(), new ConcurrentHashMap<>()); return new Game(id, new CopyOnWriteArrayList<>(), new ConcurrentHashMap<>(), null, null, new ArrayList<>(), new ConcurrentHashMap<>());
} }
} }
public Game getOrCreateGame(String gameId) { public Game getOrCreateGame(String gameId) {

View File

@ -36,6 +36,7 @@ public class SpotifyAuthService {
} }
public URI getAuthorizationUri(String user) { 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 // Temporäre API-Instanz nur für die Erstellung der Auth-URL
SpotifyApi tempApi = new SpotifyApi.Builder() SpotifyApi tempApi = new SpotifyApi.Builder()
.setClientId(clientId) .setClientId(clientId)
@ -68,6 +69,7 @@ public class SpotifyAuthService {
} }
public List<String> getRecentTracks(String user) { public List<String> getRecentTracks(String user) {
System.out.println("Hole kürzlich gespielte Tracks für Benutzer: " + user);
int limit = 2; int limit = 2;
SpotifyApi userApi = userApis.get(user); SpotifyApi userApi = userApis.get(user);
@ -112,6 +114,7 @@ public class SpotifyAuthService {
} }
private List<String> getSavedTracks(String user, int limit, int offset) { private List<String> 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); SpotifyApi userApi = userApis.get(user);
if (userApi == null) { if (userApi == null) {
@ -143,10 +146,11 @@ public class SpotifyAuthService {
return Collections.emptyList(); return Collections.emptyList();
} }
} }
public List<String> getTrackInfos(List<String> allTracks) { public List<String> getTrackInfos(List<String> tracks) {
System.out.println("Hole Track-Infos für " + tracks.size() + " Tracks.");
//für jede URI den titel holen //für jede URI den titel holen
List<String> trackInfos = new ArrayList<>(); List<String> trackInfos = new ArrayList<>();
for (String uri : allTracks) { for (String uri : tracks) {
SpotifyApi userApi = userApis.values().stream().findFirst().orElse(null); SpotifyApi userApi = userApis.values().stream().findFirst().orElse(null);
if (userApi == null) { if (userApi == null) {
System.err.println("Kein SpotifyApi-Client gefunden."); System.err.println("Kein SpotifyApi-Client gefunden.");
@ -176,6 +180,7 @@ public class SpotifyAuthService {
public String getAccessTokenForUser(String username) { public String getAccessTokenForUser(String username) {
System.out.println("Hole AccessToken für Benutzer: " + username);
SpotifyApi userApi = userApis.get(username); SpotifyApi userApi = userApis.get(username);
if (userApi == null) { if (userApi == null) {
System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + username); System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + username);

View File

@ -25,8 +25,8 @@ public class GameWebSocketHandler {
private final Map<String, List<String>> trackInfoCache = new ConcurrentHashMap<>(); private final Map<String, List<String>> trackInfoCache = new ConcurrentHashMap<>();
private final Map<String, List<String>> allTracksCache = new ConcurrentHashMap<>(); private final Map<String, List<String>> allTracksCache = new ConcurrentHashMap<>();
//Map<gameId, Map<player, List<String>> //Map<gameId, Map<player, List<String>>
private final Map<String, Map<String, List<String>>> playerTracksCache = new ConcurrentHashMap<>(); private Map<String, Map<String, List<String>>> playerTracksCache = new ConcurrentHashMap<>();
private final Map<String, Map<String, List<String>>> playerTrackInfoCache = new ConcurrentHashMap<>(); private Map<String, Map<String, List<String>>> playerTrackInfoCache = new ConcurrentHashMap<>();
public GameWebSocketHandler(GameService gameService) { public GameWebSocketHandler(GameService gameService) {
this.service = gameService; this.service = gameService;
@ -84,14 +84,20 @@ public class GameWebSocketHandler {
var currentGame = service.getOrCreateGame(gameId); var currentGame = service.getOrCreateGame(gameId);
if (currentGame.players().isEmpty()) return; if (currentGame.players().isEmpty()) return;
// Tracks pro Spieler sammeln // Tracks pro Spieler sammeln
Map<String, List<String>> playerTracks = currentGame.playerTracks(); Map<String, List<String>> allPlayerTracks = currentGame.playerTracks();
List<String> allTracks = playerTracks.values().stream() // alle tracks sammeln
List<String> allTracks = allPlayerTracks.values().stream()
.flatMap(List::stream) .flatMap(List::stream)
.toList(); .toList();
System.out.println("AlltracksCache für Spiel " + gameId + " hat " + allTracks.size() + " Songs (rundesnstart)"); System.out.println("AlltracksCache für Spiel " + gameId + " hat " + allTracks.size() + " Songs (rundenstart)");
if (!allTracks.isEmpty()) { if (!allTracks.isEmpty()) {
service.startRound(gameId, allTracks); service.startRound(gameId, allTracks);
} }
// Trackinfos für alle Spieler sammeln
Map<String, List<String>> 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); broadcastRoundStart(gameId);
} }
} }
@ -101,9 +107,7 @@ public class GameWebSocketHandler {
public void nextround(String gameId) { public void nextround(String gameId) {
var game = service.getOrCreateGame(gameId); var game = service.getOrCreateGame(gameId);
if (game.players().isEmpty()) return; if (game.players().isEmpty()) return;
// Songs von allen Spielern sammeln // Songs von allen Spielern sammeln
Map<String, List<String>> playerTracks = game.playerTracks();
List<String> allTracks = new ArrayList<>(); List<String> allTracks = new ArrayList<>();
for (String player : game.players()) { for (String player : game.players()) {
allTracks.addAll(game.playerTracks().getOrDefault(player, List.of())); allTracks.addAll(game.playerTracks().getOrDefault(player, List.of()));
@ -112,7 +116,7 @@ public class GameWebSocketHandler {
// TODO: Fehler an Client senden, dass keine Songs da sind // TODO: Fehler an Client senden, dass keine Songs da sind
return; return;
} }
// TODO funktionalität bei neu joinenden Spielern überprüfen
// Runde im Service starten, um Song und Owner zu setzen // Runde im Service starten, um Song und Owner zu setzen
service.startRound(gameId, allTracks); service.startRound(gameId, allTracks);
// Jetzt Broadcast mit den aktuellen Daten // Jetzt Broadcast mit den aktuellen Daten
@ -126,17 +130,8 @@ public class GameWebSocketHandler {
List<String> opts = game.players(); List<String> opts = game.players();
String songUri = game.currentSong(); String songUri = game.currentSong();
List<String> allTracks = game.allTracks(); List<String> allTracks = game.allTracks();
List<String> trackInfos = game.allTracks(); Map<String, List<String>> trackInfos = playerTrackInfoCache.get(gameId);
// Cache pro Spiel-ID nutzen // Cache pro Spiel-ID nutzen
// List<String> 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( String msg = JsonUtil.toJson(Map.of(
"type", "round-start", "type", "round-start",

View File

@ -163,13 +163,13 @@ async function handleRoundStart({ownerOptions, songUri, allTracks, trackInfos})
const songList = document.getElementById("songList"); const songList = document.getElementById("songList");
songList.innerHTML = ""; songList.innerHTML = "";
if (Array.isArray(trackInfos)) { //trackinfos ist eine map bestehend aus aus Spielername und Liste von Track-Infos
trackInfos.forEach(trackInfo => { const userTracks = trackInfos?.[username] ?? [];
userTracks.forEach(trackInfo => {
const li = document.createElement("li"); const li = document.createElement("li");
li.textContent = trackInfo; li.textContent = trackInfo;
songList.appendChild(li); songList.appendChild(li);
}); });
}
//playLock = false; //playLock = false;
} }