alles kaputt
parent
e9ea337ef9
commit
c2c5d570c8
|
|
@ -36,6 +36,18 @@ public class App {
|
||||||
config.staticFiles.add("/public", Location.CLASSPATH);
|
config.staticFiles.add("/public", Location.CLASSPATH);
|
||||||
}).start(cfg.port);
|
}).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) -> {
|
app.exception(Exception.class, (e, ctx) -> {
|
||||||
log.error("Unhandled error", e);
|
log.error("Unhandled error", e);
|
||||||
ctx.status(500).json(Map.of("error", e.getMessage()));
|
ctx.status(500).json(Map.of("error", e.getMessage()));
|
||||||
|
|
@ -58,7 +70,7 @@ public class App {
|
||||||
});
|
});
|
||||||
|
|
||||||
// WS-Handler
|
// WS-Handler
|
||||||
GameWebSocketHandler wsHandler = new GameWebSocketHandler(gs, sas);
|
GameWebSocketHandler wsHandler = new GameWebSocketHandler(gs);
|
||||||
|
|
||||||
// HTTP-Controller
|
// HTTP-Controller
|
||||||
new GameController(app, gs, sas, wsHandler);
|
new GameController(app, gs, sas, wsHandler);
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ public class GameController {
|
||||||
private final GameService gameService;
|
private final GameService gameService;
|
||||||
private final SpotifyAuthService authService;
|
private final SpotifyAuthService authService;
|
||||||
private final GameWebSocketHandler webSocketHandler;
|
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);
|
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GameController.class);
|
||||||
|
|
||||||
public GameController(Javalin app, GameService gs, SpotifyAuthService sas, GameWebSocketHandler wsHandler) {
|
public GameController(Javalin app, GameService gs, SpotifyAuthService sas, GameWebSocketHandler wsHandler) {
|
||||||
|
|
@ -48,11 +50,22 @@ public class GameController {
|
||||||
ctx.status(400).result("username fehlt");
|
ctx.status(400).result("username fehlt");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var devices = authService.getDevices(username); // Diese Methode muss es im SpotifyAuthService geben
|
var accessToken = authService.getAccessTokenForUser(username);
|
||||||
ctx.json(Map.of("devices", devices));
|
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 setToken(String accessToken) {
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
private void createGame(Context ctx) {
|
private void createGame(Context ctx) {
|
||||||
Map<String, Object> body = ctx.bodyAsClass(Map.class);
|
Map<String, Object> body = ctx.bodyAsClass(Map.class);
|
||||||
String user = (String) body.get("username");
|
String user = (String) body.get("username");
|
||||||
|
|
@ -118,6 +131,7 @@ public class GameController {
|
||||||
"scores", game.scores()
|
"scores", game.scores()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
// Java
|
||||||
private void playTrack(Context ctx) {
|
private void playTrack(Context ctx) {
|
||||||
Map<String, String> body = ctx.bodyAsClass(Map.class);
|
Map<String, String> body = ctx.bodyAsClass(Map.class);
|
||||||
String username = body.get("username");
|
String username = body.get("username");
|
||||||
|
|
@ -129,10 +143,8 @@ public class GameController {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String accessToken = authService.getAccessTokenForUser(username);
|
//String accessToken = authService.getAccessTokenForUser(username);
|
||||||
OkHttpClient client = new OkHttpClient();
|
OkHttpClient client = httpClient;
|
||||||
|
|
||||||
// 1. Track-Details holen
|
|
||||||
String trackId = trackUri.split(":")[2];
|
String trackId = trackUri.split(":")[2];
|
||||||
Request getTrack = new Request.Builder()
|
Request getTrack = new Request.Builder()
|
||||||
.url("https://api.spotify.com/v1/tracks/" + trackId)
|
.url("https://api.spotify.com/v1/tracks/" + trackId)
|
||||||
|
|
@ -140,14 +152,13 @@ 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(500).result("Fehler beim Laden der Track-Details");
|
ctx.status(trackResp.code()).result("Fehler beim Laden der Track-Details: " + trackResp.body().string());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
|
||||||
// 2. Play-Request mit position_ms
|
|
||||||
ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
|
ObjectNode jsonNode = JsonNodeFactory.instance.objectNode();
|
||||||
jsonNode.putArray("uris").add(trackUri);
|
jsonNode.putArray("uris").add(trackUri);
|
||||||
jsonNode.put("position_ms", startOffset);
|
jsonNode.put("position_ms", startOffset);
|
||||||
|
|
@ -160,16 +171,22 @@ public class GameController {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
try (Response playResp = client.newCall(playReq).execute()) {
|
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()) {
|
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: " + playResp.body().string());
|
ctx.status(playResp.code()).result("Fehler beim Abspielen: " + playResp.body().string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Fehler beim Abspielen des Tracks", e);
|
log.error("Fehler beim Abspielen des Tracks", e);
|
||||||
ctx.status(500).result("Interner Fehler");
|
ctx.status(500).result("Interner Fehler: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ package eric.Roullette.service;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Game getOrCreateGame(String gameId) {
|
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) {
|
public void addPlayer(String gameId, String user) {
|
||||||
|
|
@ -82,4 +82,10 @@ package eric.Roullette.service;
|
||||||
public Set<WsContext> getSessions(String gameId) {
|
public Set<WsContext> getSessions(String gameId) {
|
||||||
return sessions.getOrDefault(gameId, Collections.emptySet());
|
return sessions.getOrDefault(gameId, Collections.emptySet());
|
||||||
}
|
}
|
||||||
|
// Map<Player, List<Track>>
|
||||||
|
public Map<String, List<String>> getPlayerTracks(String gameId) {
|
||||||
|
Game game = games.get(gameId);
|
||||||
|
if (game == null) return Collections.emptyMap();
|
||||||
|
return game.playerTracks();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,11 +14,13 @@ package eric.Roullette.service;
|
||||||
import se.michaelthelin.spotify.model_objects.specification.*;
|
import se.michaelthelin.spotify.model_objects.specification.*;
|
||||||
import se.michaelthelin.spotify.requests.data.player.GetCurrentUsersRecentlyPlayedTracksRequest;
|
import se.michaelthelin.spotify.requests.data.player.GetCurrentUsersRecentlyPlayedTracksRequest;
|
||||||
import se.michaelthelin.spotify.requests.data.library.GetUsersSavedTracksRequest;
|
import se.michaelthelin.spotify.requests.data.library.GetUsersSavedTracksRequest;
|
||||||
|
import java.time.Instant;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static com.neovisionaries.i18n.CountryCode.DE;
|
import static com.neovisionaries.i18n.CountryCode.DE;
|
||||||
|
|
||||||
|
|
@ -28,6 +30,39 @@ public class SpotifyAuthService {
|
||||||
private final URI redirectUri;
|
private final URI redirectUri;
|
||||||
// Speichert für jeden Benutzer eine eigene, authentifizierte SpotifyApi-Instanz
|
// Speichert für jeden Benutzer eine eigene, authentifizierte SpotifyApi-Instanz
|
||||||
private final Map<String, SpotifyApi> userApis = new ConcurrentHashMap<>();
|
private final Map<String, SpotifyApi> userApis = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, SpotifyAuth> 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) {
|
public SpotifyAuthService(String clientId, String clientSecret, String redirectUri) {
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
|
|
@ -44,7 +79,7 @@ public class SpotifyAuthService {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return tempApi.authorizationCodeUri()
|
return tempApi.authorizationCodeUri()
|
||||||
.scope("user-read-recently-played user-library-read user-read-playback-state user-modify-playback-state streaming")
|
.scope("user-read-recently-played user-library-read user-modify-playback-state user-read-playback-state")
|
||||||
.state(user) // Der Benutzername wird im State mitgegeben
|
.state(user) // Der Benutzername wird im State mitgegeben
|
||||||
.build()
|
.build()
|
||||||
.execute();
|
.execute();
|
||||||
|
|
@ -62,13 +97,65 @@ public class SpotifyAuthService {
|
||||||
AuthorizationCodeCredentials creds = userApi.authorizationCode(code).build().execute();
|
AuthorizationCodeCredentials creds = userApi.authorizationCode(code).build().execute();
|
||||||
userApi.setAccessToken(creds.getAccessToken());
|
userApi.setAccessToken(creds.getAccessToken());
|
||||||
userApi.setRefreshToken(creds.getRefreshToken());
|
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
|
// Speichert die fertig konfigurierte API-Instanz für den Benutzer
|
||||||
userApis.put(user, userApi);
|
userApis.put(user, userApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Einfaches Cache für Tracks pro User
|
||||||
|
private final Map<String, List<String>> recentTracksCache = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, List<String>> savedTracksCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Hilfsmethode für Retry bei 429
|
||||||
|
private <T> T executeWithRetry(CallableWithException<T> 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> {
|
||||||
|
T call() throws IOException, SpotifyWebApiException, ParseException;
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getRecentTracks(String user) {
|
public List<String> getRecentTracks(String user) {
|
||||||
int limit = 50;
|
System.out.println("Hole kürzlich gespielte Tracks für Benutzer: " + user);
|
||||||
|
int limit = 1;
|
||||||
SpotifyApi userApi = userApis.get(user);
|
SpotifyApi userApi = userApis.get(user);
|
||||||
|
|
||||||
if (userApi == null) {
|
if (userApi == null) {
|
||||||
|
|
@ -76,7 +163,13 @@ public class SpotifyAuthService {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache nutzen
|
||||||
|
if (recentTracksCache.containsKey(user)) {
|
||||||
|
return recentTracksCache.get(user);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
List<String> recentTracks = executeWithRetry(() -> {
|
||||||
GetCurrentUsersRecentlyPlayedTracksRequest request = userApi.getCurrentUsersRecentlyPlayedTracks()
|
GetCurrentUsersRecentlyPlayedTracksRequest request = userApi.getCurrentUsersRecentlyPlayedTracks()
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.build();
|
.build();
|
||||||
|
|
@ -84,27 +177,28 @@ public class SpotifyAuthService {
|
||||||
if (history == null || history.getItems() == null) {
|
if (history == null || history.getItems() == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
List<String> recentTracks = Arrays.stream(history.getItems())
|
return Arrays.stream(history.getItems())
|
||||||
.map(item -> item.getTrack().getUri())
|
.map(item -> item.getTrack().getUri())
|
||||||
.distinct()
|
.distinct()
|
||||||
.toList();
|
.toList();
|
||||||
|
});
|
||||||
|
|
||||||
if (recentTracks.size() < limit) {
|
if (recentTracks.size() < limit) {
|
||||||
int newLimit = limit - recentTracks.size();
|
int newLimit = limit - recentTracks.size();
|
||||||
// restliche songs mit kürzlich gespeicherten Tracks auffüllen
|
|
||||||
List<String> savedTracks = getSavedTracks(user, newLimit, 0);
|
List<String> savedTracks = getSavedTracks(user, newLimit, 0);
|
||||||
// Nur Tracks hinzufügen, die noch nicht in recentTracks sind
|
|
||||||
savedTracks.removeAll(recentTracks);
|
savedTracks.removeAll(recentTracks);
|
||||||
recentTracks = new java.util.ArrayList<>(recentTracks);
|
recentTracks = new java.util.ArrayList<>(recentTracks);
|
||||||
recentTracks.addAll(savedTracks.subList(0, Math.min(newLimit, savedTracks.size())));
|
recentTracks.addAll(savedTracks.subList(0, Math.min(newLimit, savedTracks.size())));
|
||||||
if(recentTracks.size() < limit){
|
if (recentTracks.size() < limit) {
|
||||||
newLimit = limit - recentTracks.size();
|
newLimit = limit - recentTracks.size();
|
||||||
List<String> savedTracks2 = getSavedTracks(user, newLimit, 50);
|
List<String> savedTracks2 = getSavedTracks(user, newLimit, 50);
|
||||||
savedTracks2.removeAll(recentTracks);
|
savedTracks2.removeAll(recentTracks);
|
||||||
recentTracks.addAll(savedTracks2.subList(0, Math.min(newLimit, savedTracks2.size())));
|
recentTracks.addAll(savedTracks2.subList(0, Math.min(newLimit, savedTracks2.size())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return recentTracks.subList(0, Math.min(limit, recentTracks.size()));
|
List<String> result = recentTracks.subList(0, Math.min(limit, recentTracks.size()));
|
||||||
|
recentTracksCache.put(user, result); // Cache speichern
|
||||||
|
return result;
|
||||||
} catch (IOException | SpotifyWebApiException | ParseException e) {
|
} catch (IOException | SpotifyWebApiException | ParseException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|
@ -112,94 +206,162 @@ 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) {
|
||||||
System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + user);
|
System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + user);
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache nutzen (nur für offset==0)
|
||||||
|
if (offset == 0 && savedTracksCache.containsKey(user)) {
|
||||||
|
return savedTracksCache.get(user);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<String> saved = new ArrayList<>();
|
List<String> saved = executeWithRetry(() -> {
|
||||||
while (saved.size() < limit) {
|
List<String> result = new ArrayList<>();
|
||||||
|
int localOffset = offset;
|
||||||
|
while (result.size() < limit) {
|
||||||
GetUsersSavedTracksRequest req = userApi.getUsersSavedTracks()
|
GetUsersSavedTracksRequest req = userApi.getUsersSavedTracks()
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(localOffset)
|
||||||
.market(CountryCode.DE)
|
.market(CountryCode.DE)
|
||||||
.build();
|
.build();
|
||||||
Paging<SavedTrack> page = req.execute();
|
Paging<SavedTrack> page = req.execute();
|
||||||
if (page == null || page.getItems().length == 0){
|
if (page == null || page.getItems().length == 0) {
|
||||||
System.out.println("Keine weiteren gespeicherten Tracks gefunden.");
|
System.out.println("Keine weiteren gespeicherten Tracks gefunden.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
for (SavedTrack st : page.getItems()) {
|
for (SavedTrack st : page.getItems()) {
|
||||||
saved.add(st.getTrack().getUri());
|
result.add(st.getTrack().getUri());
|
||||||
if (saved.size() == limit) break;
|
if (result.size() == limit) break;
|
||||||
}
|
}
|
||||||
offset += limit;
|
localOffset += limit;
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
if (offset == 0) savedTracksCache.put(user, saved); // Cache speichern
|
||||||
return saved;
|
return saved;
|
||||||
} catch (IOException | SpotifyWebApiException | ParseException e) {
|
} catch (IOException | SpotifyWebApiException | ParseException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public List<String> getTrackInfos(List<String> allTracks) {
|
|
||||||
//für jede URI den titel holen
|
|
||||||
List<String> 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 {
|
// public List<String> getTrackInfos(List<String> allTracks) {
|
||||||
System.err.println("Track nicht gefunden: " + uri);
|
// //für jede URI den titel holen
|
||||||
}
|
// List<String> trackInfos = new ArrayList<>();
|
||||||
} catch (IOException | SpotifyWebApiException | ParseException e) {
|
// for (String uri : allTracks) {
|
||||||
System.err.println("Fehler beim Abrufen des Tracks: " + uri);
|
// SpotifyApi userApi = userApis.values().stream().findFirst().orElse(null);
|
||||||
e.printStackTrace();
|
// if (userApi == null) {
|
||||||
}
|
// System.err.println("Kein SpotifyApi-Client gefunden.");
|
||||||
} return trackInfos;
|
// 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) {
|
public String getAccessTokenForUser(String username) {
|
||||||
SpotifyApi userApi = userApis.get(username);
|
System.out.println("Hole Access Token für Benutzer: " + username);
|
||||||
if (userApi == null) {
|
|
||||||
System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + username);
|
SpotifyAuth auth = userAuths.get(username);
|
||||||
|
if (auth == null) return null;
|
||||||
|
try {
|
||||||
|
return auth.getToken();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
return null;
|
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<Map<String, Object>> getDevices(String username) {
|
|
||||||
String accessToken = getAccessTokenForUser(username);
|
public List<Map<String, Object>> getDevices(String username, String accessToken) {
|
||||||
OkHttpClient client = new OkHttpClient();
|
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()
|
Request req = new Request.Builder()
|
||||||
.url("https://api.spotify.com/v1/me/player/devices")
|
.url("https://api.spotify.com/v1/me/player/devices")
|
||||||
.addHeader("Authorization", "Bearer " + accessToken)
|
.addHeader("Authorization", "Bearer " + accessToken)
|
||||||
.build();
|
.build();
|
||||||
try (Response resp = client.newCall(req).execute()) {
|
try (Response resp = okClient.newCall(req).execute()) {
|
||||||
if (!resp.isSuccessful()) return List.of();
|
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();
|
String body = resp.body().string();
|
||||||
|
System.out.println("Response Code: " + resp.code());
|
||||||
|
System.out.println("Response Body: " + body);
|
||||||
// Parsen, z.B. mit Jackson
|
// Parsen, z.B. mit Jackson
|
||||||
var node = new ObjectMapper().readTree(body);
|
var node = new ObjectMapper().readTree(body);
|
||||||
var devices = node.get("devices");
|
var devices = node.get("devices");
|
||||||
return new ObjectMapper().convertValue(devices, new TypeReference<List<Map<String, Object>>>(){});
|
return new ObjectMapper().convertValue(devices, new TypeReference<List<Map<String, Object>>>() {
|
||||||
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
return List.of();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package eric.Roullette.websocket;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import eric.Roullette.service.GameService;
|
import eric.Roullette.service.GameService;
|
||||||
import eric.Roullette.service.SpotifyAuthService;
|
//import eric.Roullette.service.SpotifyAuthService;
|
||||||
import eric.Roullette.util.JsonUtil;
|
import eric.Roullette.util.JsonUtil;
|
||||||
import io.javalin.websocket.WsConfig;
|
import io.javalin.websocket.WsConfig;
|
||||||
import io.javalin.websocket.WsContext;
|
import io.javalin.websocket.WsContext;
|
||||||
|
|
@ -17,14 +17,20 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
public class GameWebSocketHandler {
|
public class GameWebSocketHandler {
|
||||||
|
|
||||||
private final GameService service;
|
private final GameService service;
|
||||||
private final SpotifyAuthService authService;
|
//private final SpotifyAuthService authService;
|
||||||
|
|
||||||
// Spiel-ID → (Username → deren Guess)
|
// Spiel-ID → (Username → deren Guess)
|
||||||
private final Map<String, Map<String, String>> currentGuesses = new ConcurrentHashMap<>();
|
private final Map<String, Map<String, String>> currentGuesses = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public GameWebSocketHandler(GameService gameService, SpotifyAuthService authService) {
|
private final Map<String, List<String>> trackInfoCache = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, List<String>> allTracksCache = new ConcurrentHashMap<>();
|
||||||
|
//Map<gameId, Map<player, List<String>>
|
||||||
|
private final Map<String, Map<String, List<String>>> playerTracksCache = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Map<String, List<String>>> playerTrackInfoCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public GameWebSocketHandler(GameService gameService) {
|
||||||
this.service = gameService;
|
this.service = gameService;
|
||||||
this.authService = authService;
|
//this.authService = authService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -71,11 +77,33 @@ public class GameWebSocketHandler {
|
||||||
}
|
}
|
||||||
case "requestPlayers" -> service.broadcastPlayers(gameId);
|
case "requestPlayers" -> service.broadcastPlayers(gameId);
|
||||||
|
|
||||||
|
case "next-round" -> {
|
||||||
|
nextround(gameId);
|
||||||
|
}
|
||||||
case "start-round" -> {
|
case "start-round" -> {
|
||||||
|
var currentGame = service.getOrCreateGame(gameId);
|
||||||
|
if (currentGame.players().isEmpty()) return;
|
||||||
|
// Tracks pro Spieler sammeln
|
||||||
|
Map<String, List<String>> playerTracks = currentGame.playerTracks();
|
||||||
|
List<String> 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);
|
||||||
|
}
|
||||||
|
broadcastRoundStart(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()));
|
||||||
|
|
@ -90,21 +118,6 @@ public class GameWebSocketHandler {
|
||||||
// Jetzt Broadcast mit den aktuellen Daten
|
// Jetzt Broadcast mit den aktuellen Daten
|
||||||
broadcastRoundStart(gameId);
|
broadcastRoundStart(gameId);
|
||||||
}
|
}
|
||||||
case "next-round" -> {
|
|
||||||
var currentGame = service.getOrCreateGame(gameId);
|
|
||||||
List<String> allTracks = new ArrayList<>();
|
|
||||||
for (String player : currentGame.players()) {
|
|
||||||
allTracks.addAll(authService.getRecentTracks(player));
|
|
||||||
}
|
|
||||||
if (!allTracks.isEmpty()) {
|
|
||||||
service.startRound(gameId, allTracks);
|
|
||||||
}
|
|
||||||
broadcastRoundStart(gameId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----- Broadcast-Methoden -----
|
// ----- Broadcast-Methoden -----
|
||||||
|
|
||||||
/** Broadcastet den Runden-Start (Song + Optionen) an alle Clients. */
|
/** Broadcastet den Runden-Start (Song + Optionen) an alle Clients. */
|
||||||
|
|
@ -113,7 +126,18 @@ 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 = authService.getTrackInfos(allTracks);
|
List<String> trackInfos = game.allTracks();
|
||||||
|
// 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",
|
||||||
"ownerOptions", opts,
|
"ownerOptions", opts,
|
||||||
|
|
@ -167,18 +191,8 @@ private void broadcastRoundResult(String gameId) {
|
||||||
// nächste Runde starten
|
// nächste Runde starten
|
||||||
// ...
|
// ...
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try { Thread.sleep(4000); } catch (InterruptedException ignored) {}
|
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
|
||||||
// Songs erneut sammeln
|
nextround(gameId);
|
||||||
var currentGame = service.getOrCreateGame(gameId);
|
|
||||||
List<String> 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);
|
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ area.appendChild(select);
|
||||||
|
|
||||||
async function loadDevices() {
|
async function loadDevices() {
|
||||||
select.innerHTML = "";
|
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) {
|
if (!devices.length) {
|
||||||
const opt = document.createElement("option");
|
const opt = document.createElement("option");
|
||||||
opt.textContent = "Keine Geräte gefunden";
|
opt.textContent = "Keine Geräte gefunden";
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ function connectWebSocket() {
|
||||||
setupStartRound(socket);
|
setupStartRound(socket);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener("message", ({ data }) => {
|
socket.addEventListener("message", async ({ data }) => {
|
||||||
console.log("WS-Rohdaten:", data);
|
console.log("WS-Rohdaten:", data);
|
||||||
const msg = JSON.parse(data);
|
const msg = JSON.parse(data);
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ function connectWebSocket() {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
break;
|
break;
|
||||||
case "round-start":
|
case "round-start":
|
||||||
handleRoundStart(msg);
|
await handleRoundStart(msg);
|
||||||
break;
|
break;
|
||||||
case "round-result":
|
case "round-result":
|
||||||
handleRoundResult(msg);
|
handleRoundResult(msg);
|
||||||
|
|
@ -107,8 +107,12 @@ const resultP = document.getElementById("result");
|
||||||
const scoreboard = document.getElementById("scoreboard");
|
const scoreboard = document.getElementById("scoreboard");
|
||||||
|
|
||||||
// 8) Funktion zum Anzeigen einer neuen Runde
|
// 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
|
// UI zurücksetzen
|
||||||
|
//if (playLock) return; // Verhindert parallele Ausführung
|
||||||
|
//playLock = true;
|
||||||
resultP.textContent = "";
|
resultP.textContent = "";
|
||||||
optionsDiv.innerHTML = "";
|
optionsDiv.innerHTML = "";
|
||||||
songEmbed.innerHTML = "";
|
songEmbed.innerHTML = "";
|
||||||
|
|
@ -123,9 +127,13 @@ function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) {
|
||||||
</iframe>`;
|
</iframe>`;
|
||||||
|
|
||||||
if (window.playOnSpotify && typeof window.playOnSpotify === "function") {
|
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
|
// Dynamische Kreisverteilung der Buttons
|
||||||
// Warten, bis #options gerendert ist
|
// Warten, bis #options gerendert ist
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -162,6 +170,7 @@ function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) {
|
||||||
songList.appendChild(li);
|
songList.appendChild(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
//playLock = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9) Funktion zum Anzeigen des Ergebnisses
|
// 9) Funktion zum Anzeigen des Ergebnisses
|
||||||
|
|
@ -213,6 +222,7 @@ function handleGameEnd({winner}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spotify-Playback Funktion
|
// Spotify-Playback Funktion
|
||||||
|
// game.js
|
||||||
async function playOnSpotify(trackUri, username) {
|
async function playOnSpotify(trackUri, username) {
|
||||||
const deviceId = document.getElementById("deviceSelect")?.value;
|
const deviceId = document.getElementById("deviceSelect")?.value;
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue