Es gab kein problem ich wurde nur 24h von der API geratelimited lol
parent
c2c5d570c8
commit
6028da6210
|
|
@ -56,7 +56,7 @@ public class GameController {
|
|||
return;
|
||||
}
|
||||
setToken(accessToken);
|
||||
var devices = authService.getDevices(username, accessToken);
|
||||
var devices = authService.getDevices(accessToken);
|
||||
ctx.json(devices);
|
||||
|
||||
});
|
||||
|
|
@ -66,7 +66,7 @@ public class GameController {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
private void createGame(Context ctx) {
|
||||
private void createGame(Context ctx) throws InterruptedException {
|
||||
Map<String, Object> body = ctx.bodyAsClass(Map.class);
|
||||
String user = (String) body.get("username");
|
||||
if (user == null || user.isBlank()) {
|
||||
|
|
@ -85,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<String, String> body = ctx.bodyAsClass(Map.class);
|
||||
String user = body.get("username");
|
||||
String gameId = body.get("gameId");
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ package eric.Roullette.service;
|
|||
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);
|
||||
if (user != null && !g.players().contains(user)) {
|
||||
g.players().add(user);
|
||||
|
|
|
|||
|
|
@ -14,13 +14,11 @@ package eric.Roullette.service;
|
|||
import se.michaelthelin.spotify.model_objects.specification.*;
|
||||
import se.michaelthelin.spotify.requests.data.player.GetCurrentUsersRecentlyPlayedTracksRequest;
|
||||
import se.michaelthelin.spotify.requests.data.library.GetUsersSavedTracksRequest;
|
||||
import java.time.Instant;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.neovisionaries.i18n.CountryCode.DE;
|
||||
|
||||
|
|
@ -30,39 +28,6 @@ public class SpotifyAuthService {
|
|||
private final URI redirectUri;
|
||||
// Speichert für jeden Benutzer eine eigene, authentifizierte SpotifyApi-Instanz
|
||||
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) {
|
||||
this.clientId = clientId;
|
||||
|
|
@ -79,7 +44,7 @@ public class SpotifyAuthService {
|
|||
.build();
|
||||
|
||||
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
|
||||
.build()
|
||||
.execute();
|
||||
|
|
@ -97,65 +62,13 @@ public class SpotifyAuthService {
|
|||
AuthorizationCodeCredentials creds = userApi.authorizationCode(code).build().execute();
|
||||
userApi.setAccessToken(creds.getAccessToken());
|
||||
userApi.setRefreshToken(creds.getRefreshToken());
|
||||
userAuths.put(user, new SpotifyAuth(userApi, creds.getAccessToken(), creds.getRefreshToken(), creds.getExpiresIn()));
|
||||
|
||||
// Speichert die fertig konfigurierte API-Instanz für den Benutzer
|
||||
userApis.put(user, userApi);
|
||||
}
|
||||
|
||||
// Einfaches Cache für Tracks pro User
|
||||
private final Map<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) {
|
||||
System.out.println("Hole kürzlich gespielte Tracks für Benutzer: " + user);
|
||||
int limit = 1;
|
||||
int limit = 2;
|
||||
SpotifyApi userApi = userApis.get(user);
|
||||
|
||||
if (userApi == null) {
|
||||
|
|
@ -163,13 +76,7 @@ public class SpotifyAuthService {
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Cache nutzen
|
||||
if (recentTracksCache.containsKey(user)) {
|
||||
return recentTracksCache.get(user);
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> recentTracks = executeWithRetry(() -> {
|
||||
GetCurrentUsersRecentlyPlayedTracksRequest request = userApi.getCurrentUsersRecentlyPlayedTracks()
|
||||
.limit(limit)
|
||||
.build();
|
||||
|
|
@ -177,15 +84,16 @@ public class SpotifyAuthService {
|
|||
if (history == null || history.getItems() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(history.getItems())
|
||||
List<String> 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<String> 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())));
|
||||
|
|
@ -196,9 +104,7 @@ public class SpotifyAuthService {
|
|||
recentTracks.addAll(savedTracks2.subList(0, Math.min(newLimit, savedTracks2.size())));
|
||||
}
|
||||
}
|
||||
List<String> result = recentTracks.subList(0, Math.min(limit, recentTracks.size()));
|
||||
recentTracksCache.put(user, result); // Cache speichern
|
||||
return result;
|
||||
return recentTracks.subList(0, Math.min(limit, recentTracks.size()));
|
||||
} catch (IOException | SpotifyWebApiException | ParseException e) {
|
||||
e.printStackTrace();
|
||||
return Collections.emptyList();
|
||||
|
|
@ -206,27 +112,18 @@ public class SpotifyAuthService {
|
|||
}
|
||||
|
||||
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);
|
||||
|
||||
if (userApi == null) {
|
||||
System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + user);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Cache nutzen (nur für offset==0)
|
||||
if (offset == 0 && savedTracksCache.containsKey(user)) {
|
||||
return savedTracksCache.get(user);
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> saved = executeWithRetry(() -> {
|
||||
List<String> result = new ArrayList<>();
|
||||
int localOffset = offset;
|
||||
while (result.size() < limit) {
|
||||
List<String> saved = new ArrayList<>();
|
||||
while (saved.size() < limit) {
|
||||
GetUsersSavedTracksRequest req = userApi.getUsersSavedTracks()
|
||||
.limit(limit)
|
||||
.offset(localOffset)
|
||||
.offset(offset)
|
||||
.market(CountryCode.DE)
|
||||
.build();
|
||||
Paging<SavedTrack> page = req.execute();
|
||||
|
|
@ -235,133 +132,74 @@ public class SpotifyAuthService {
|
|||
break;
|
||||
}
|
||||
for (SavedTrack st : page.getItems()) {
|
||||
result.add(st.getTrack().getUri());
|
||||
if (result.size() == limit) break;
|
||||
saved.add(st.getTrack().getUri());
|
||||
if (saved.size() == limit) break;
|
||||
}
|
||||
localOffset += limit;
|
||||
offset += limit;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
if (offset == 0) savedTracksCache.put(user, saved); // Cache speichern
|
||||
return saved;
|
||||
} catch (IOException | SpotifyWebApiException | ParseException e) {
|
||||
e.printStackTrace();
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
public List<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) {
|
||||
// //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 {
|
||||
// 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;
|
||||
// }
|
||||
} 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) {
|
||||
System.out.println("Hole Access Token für Benutzer: " + username);
|
||||
|
||||
SpotifyAuth auth = userAuths.get(username);
|
||||
if (auth == null) return null;
|
||||
try {
|
||||
return auth.getToken();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
SpotifyApi userApi = userApis.get(username);
|
||||
if (userApi == null) {
|
||||
System.err.println("Kein SpotifyApi-Client für Benutzer gefunden: " + username);
|
||||
return null;
|
||||
}
|
||||
// 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();
|
||||
return userApi.getAccessToken();
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getDevices(String username, String accessToken) {
|
||||
System.out.println("Hole Geräte für Benutzer: " + username);
|
||||
//String accessToken = getAccessTokenForUser(username);
|
||||
if (accessToken == null) {
|
||||
System.err.println("Kein gültiges Access Token für Benutzer: " + username);
|
||||
return List.of();
|
||||
}
|
||||
OkHttpClient okClient = client;
|
||||
public List<Map<String, Object>> 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")
|
||||
.addHeader("Authorization", "Bearer " + accessToken)
|
||||
.build();
|
||||
try (Response resp = okClient.newCall(req).execute()) {
|
||||
if (!resp.isSuccessful()) {
|
||||
System.out.println("Fehler beim Abrufen der Geräte: " + resp.code() + " - " + resp.message());
|
||||
return List.of();
|
||||
}
|
||||
if (resp.body() == null) {
|
||||
System.err.println("Antwort ohne Body erhalten.");
|
||||
return List.of();
|
||||
}
|
||||
try (Response resp = client.newCall(req).execute()) {
|
||||
if (!resp.isSuccessful()) return List.of();
|
||||
String body = resp.body().string();
|
||||
System.out.println("Response Code: " + resp.code());
|
||||
System.out.println("Response Body: " + body);
|
||||
// Parsen, z.B. mit Jackson
|
||||
var node = new ObjectMapper().readTree(body);
|
||||
var devices = node.get("devices");
|
||||
return new ObjectMapper().convertValue(devices, new TypeReference<List<Map<String, Object>>>() {
|
||||
});
|
||||
return new ObjectMapper().convertValue(devices, new TypeReference<List<Map<String, Object>>>(){});
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private static class SpotifyAuth {
|
||||
private final SpotifyApi api;
|
||||
private String accessToken;
|
||||
private String refreshToken;
|
||||
private Instant expiresAt;
|
||||
|
||||
public SpotifyAuth(SpotifyApi api, String accessToken, String refreshToken, int expiresIn) {
|
||||
this.api = api;
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
this.expiresAt = Instant.now().plusSeconds(expiresIn);
|
||||
}
|
||||
|
||||
public synchronized String getToken() throws IOException, SpotifyWebApiException, ParseException {
|
||||
if (Instant.now().isAfter(expiresAt.minusSeconds(60))) {
|
||||
refreshToken();
|
||||
}
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
private void refreshToken() throws IOException, SpotifyWebApiException, ParseException {
|
||||
var creds = api.authorizationCodeRefresh().build().execute();
|
||||
this.accessToken = creds.getAccessToken();
|
||||
this.expiresAt = Instant.now().plusSeconds(creds.getExpiresIn());
|
||||
api.setAccessToken(accessToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue