Es gab kein problem ich wurde nur 24h von der API geratelimited lol

pull/24/head
eric 2025-08-10 16:32:16 +02:00
parent c2c5d570c8
commit 6028da6210
3 changed files with 98 additions and 260 deletions

View File

@ -56,7 +56,7 @@ public class GameController {
return; return;
} }
setToken(accessToken); setToken(accessToken);
var devices = authService.getDevices(username, accessToken); var devices = authService.getDevices(accessToken);
ctx.json(devices); ctx.json(devices);
}); });
@ -66,7 +66,7 @@ public class GameController {
this.accessToken = accessToken; this.accessToken = accessToken;
} }
private void createGame(Context ctx) { private void createGame(Context ctx) throws InterruptedException {
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");
if (user == null || user.isBlank()) { if (user == null || user.isBlank()) {
@ -85,7 +85,7 @@ public class GameController {
ctx.json(Map.of("status", "ok", "gameId", gameId)); ctx.json(Map.of("status", "ok", "gameId", gameId));
} }
private void joinGame(Context ctx) { private void joinGame(Context ctx) throws InterruptedException {
Map<String, String> body = ctx.bodyAsClass(Map.class); Map<String, String> body = ctx.bodyAsClass(Map.class);
String user = body.get("username"); String user = body.get("username");
String gameId = body.get("gameId"); String gameId = body.get("gameId");

View File

@ -29,7 +29,7 @@ package eric.Roullette.service;
return games.computeIfAbsent(gameId, Game::create); return games.computeIfAbsent(gameId, Game::create);
} }
public void addPlayer(String gameId, String user) { public void addPlayer(String gameId, String user) throws InterruptedException {
Game g = getOrCreateGame(gameId); Game g = getOrCreateGame(gameId);
if (user != null && !g.players().contains(user)) { if (user != null && !g.players().contains(user)) {
g.players().add(user); g.players().add(user);

View File

@ -1,28 +1,26 @@
package eric.Roullette.service; package eric.Roullette.service;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.neovisionaries.i18n.CountryCode; import com.neovisionaries.i18n.CountryCode;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.ParseException;
import se.michaelthelin.spotify.SpotifyApi; import se.michaelthelin.spotify.SpotifyApi;
import se.michaelthelin.spotify.SpotifyHttpManager; import se.michaelthelin.spotify.SpotifyHttpManager;
import se.michaelthelin.spotify.exceptions.SpotifyWebApiException; import se.michaelthelin.spotify.exceptions.SpotifyWebApiException;
import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials; import se.michaelthelin.spotify.model_objects.credentials.AuthorizationCodeCredentials;
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.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import static com.neovisionaries.i18n.CountryCode.DE; import java.io.IOException;
import java.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static com.neovisionaries.i18n.CountryCode.DE;
public class SpotifyAuthService { public class SpotifyAuthService {
private final String clientId; private final String clientId;
@ -30,39 +28,6 @@ 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;
@ -79,7 +44,7 @@ public class SpotifyAuthService {
.build(); .build();
return tempApi.authorizationCodeUri() return tempApi.authorizationCodeUri()
.scope("user-read-recently-played user-library-read user-modify-playback-state user-read-playback-state") .scope("user-read-recently-played user-library-read user-read-playback-state user-modify-playback-state streaming")
.state(user) // Der Benutzername wird im State mitgegeben .state(user) // Der Benutzername wird im State mitgegeben
.build() .build()
.execute(); .execute();
@ -97,65 +62,13 @@ 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) {
System.out.println("Hole kürzlich gespielte Tracks für Benutzer: " + user); int limit = 2;
int limit = 1;
SpotifyApi userApi = userApis.get(user); SpotifyApi userApi = userApis.get(user);
if (userApi == null) { if (userApi == null) {
@ -163,42 +76,35 @@ 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(); PagingCursorbased<PlayHistory> history = request.execute();
PagingCursorbased<PlayHistory> history = request.execute(); 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())));
} }
} }
List<String> result = recentTracks.subList(0, Math.min(limit, recentTracks.size())); return 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();
@ -206,162 +112,94 @@ 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 = executeWithRetry(() -> { List<String> saved = new ArrayList<>();
List<String> result = new ArrayList<>(); while (saved.size() < limit) {
int localOffset = offset; GetUsersSavedTracksRequest req = userApi.getUsersSavedTracks()
while (result.size() < limit) { .limit(limit)
GetUsersSavedTracksRequest req = userApi.getUsersSavedTracks() .offset(offset)
.limit(limit) .market(CountryCode.DE)
.offset(localOffset) .build();
.market(CountryCode.DE) Paging<SavedTrack> page = req.execute();
.build(); if (page == null || page.getItems().length == 0){
Paging<SavedTrack> page = req.execute(); System.out.println("Keine weiteren gespeicherten Tracks gefunden.");
if (page == null || page.getItems().length == 0) { break;
System.out.println("Keine weiteren gespeicherten Tracks gefunden.");
break;
}
for (SavedTrack st : page.getItems()) {
result.add(st.getTrack().getUri());
if (result.size() == limit) break;
}
localOffset += limit;
} }
return result; for (SavedTrack st : page.getItems()) {
}); saved.add(st.getTrack().getUri());
if (offset == 0) savedTracksCache.put(user, saved); // Cache speichern if (saved.size() == limit) break;
}
offset += limit;
}
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);
// public List<String> getTrackInfos(List<String> allTracks) { } else {
// //für jede URI den titel holen System.err.println("Track nicht gefunden: " + uri);
// List<String> trackInfos = new ArrayList<>(); }
// for (String uri : allTracks) { } catch (IOException | SpotifyWebApiException | ParseException e) {
// SpotifyApi userApi = userApis.values().stream().findFirst().orElse(null); System.err.println("Fehler beim Abrufen des Tracks: " + uri);
// if (userApi == null) { e.printStackTrace();
// System.err.println("Kein SpotifyApi-Client gefunden."); }
// return Collections.emptyList(); } return trackInfos;
// } }
// 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) {
System.out.println("Hole Access Token für Benutzer: " + username); SpotifyApi userApi = userApis.get(username);
if (userApi == null) {
SpotifyAuth auth = userAuths.get(username); System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + username);
if (auth == null) return null;
try {
return auth.getToken();
} catch (Exception e) {
e.printStackTrace();
return null; return null;
} }
// SpotifyApi userApi = userApis.get(username); return userApi.getAccessToken();
// 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 accessToken) {
public List<Map<String, Object>> getDevices(String username, String accessToken) { System.out.println("Hole Geräte für AccessToken: " + accessToken);
System.out.println("Hole Geräte für Benutzer: " + username); OkHttpClient client = new OkHttpClient();
//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 = okClient.newCall(req).execute()) { try (Response resp = client.newCall(req).execute()) {
if (!resp.isSuccessful()) { if (!resp.isSuccessful()) return List.of();
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);
}
}
} }