auswählen von geräten für den player

pull/24/head
eric 2025-08-08 00:08:09 +02:00
parent 2d473ebdd9
commit 1d901c5222
9 changed files with 166 additions and 20 deletions

View File

@ -78,12 +78,6 @@
<artifactId>javalin-testtools</artifactId> <artifactId>javalin-testtools</artifactId>
<version>6.7.0</version> <version>6.7.0</version>
<scope>test</scope> <scope>test</scope>
<exclusions>
<exclusion>
<artifactId>okhttp</artifactId>
<groupId>com.squareup.okhttp3</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
</dependencies> </dependencies>
<properties> <properties>

10
pom.xml
View File

@ -67,6 +67,16 @@
<version>${javalin.version}</version> <version>${javalin.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>1.9.22</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -1,22 +1,36 @@
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.ObjectNode;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context; import io.javalin.http.Context;
import eric.Roullette.service.GameService; import eric.Roullette.service.GameService;
import eric.Roullette.service.SpotifyAuthService; import eric.Roullette.service.SpotifyAuthService;
import eric.Roullette.websocket.GameWebSocketHandler; import eric.Roullette.websocket.GameWebSocketHandler;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.MediaType;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
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;
public class GameController { 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 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) {
this.gameService = gs; this.gameService = gs;
@ -27,6 +41,7 @@ package eric.Roullette.controller;
app.get("/api/game/{gameId}/players", this::getPlayers); app.get("/api/game/{gameId}/players", this::getPlayers);
app.post("/api/game/{gameId}/start-round", this::startRound); app.post("/api/game/{gameId}/start-round", this::startRound);
app.post("/api/game/{gameId}/guess", this::guess); app.post("/api/game/{gameId}/guess", this::guess);
app.get("/api/spotify/devices", this::getDevices);
} }
private void createGame(Context ctx) { private void createGame(Context ctx) {
@ -94,4 +109,18 @@ package eric.Roullette.controller;
"scores", game.scores() "scores", game.scores()
)); ));
} }
// Backend: Devices abrufen
private void getDevices(Context ctx) {
String username = ctx.queryParam("username");
if (username == null) {
ctx.status(400).result("username fehlt");
return;
}
List<Map<String, Object>> devices = authService.getDevices(username); // Implementiere diese Methode im SpotifyAuthService
ctx.json(Map.of("devices", devices));
}
} }

View File

@ -1,17 +1,20 @@
package eric.Roullette.service; package eric.Roullette.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.neovisionaries.i18n.CountryCode; import com.neovisionaries.i18n.CountryCode;
import okhttp3.OkHttpClient;
import okhttp3.Request;
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.Paging; import se.michaelthelin.spotify.model_objects.specification.*;
import se.michaelthelin.spotify.model_objects.specification.PagingCursorbased;
import se.michaelthelin.spotify.model_objects.specification.PlayHistory;
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 se.michaelthelin.spotify.model_objects.specification.SavedTrack;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.*; import java.util.*;
@ -41,7 +44,7 @@ public class SpotifyAuthService {
.build(); .build();
return tempApi.authorizationCodeUri() return tempApi.authorizationCodeUri()
.scope("user-read-recently-played user-library-read") .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();
@ -140,5 +143,63 @@ public class SpotifyAuthService {
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 {
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) {
SpotifyApi userApi = userApis.get(username);
if (userApi == null) {
System.err.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);
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 = client.newCall(req).execute()) {
if (!resp.isSuccessful()) return List.of();
String body = resp.body().string();
// 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>>>(){});
} catch (Exception e) {
return List.of();
}
}
}

View File

@ -102,11 +102,13 @@ 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);
String msg = JsonUtil.toJson(Map.of( String msg = JsonUtil.toJson(Map.of(
"type", "round-start", "type", "round-start",
"ownerOptions", opts, "ownerOptions", opts,
"songUri", songUri, "songUri", songUri,
"allTracks", allTracks "allTracks", allTracks,
"trackInfos", trackInfos
)); ));
broadcastToAll(gameId, msg); broadcastToAll(gameId, msg);
} }

View File

@ -36,7 +36,8 @@
<h2>Scoreboard</h2> <h2>Scoreboard</h2>
<ul id="scoreboard"></ul> <ul id="scoreboard"></ul>
<script type="module" src="/js/game.js"></script> <script type="module" src="/js/game.js"></script>
<div id="deviceSelectArea"></div>
<script type="module" src="/js/device-select.js"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,45 @@
// public/js/device-select.js
import { getParam, fetchJson } from "./utils.js";
const username = getParam("username");
const area = document.getElementById("deviceSelectArea") || document.body;
const label = document.createElement("label");
label.textContent = "Wiedergabegerät wählen: ";
label.htmlFor = "deviceSelect";
const select = document.createElement("select");
select.id = "deviceSelect";
select.style.margin = "0.5rem";
area.appendChild(label);
area.appendChild(select);
async function loadDevices() {
select.innerHTML = "";
const { devices } = await fetchJson(`/api/spotify/devices?username=${encodeURIComponent(username)}`);
if (!devices.length) {
const opt = document.createElement("option");
opt.textContent = "Keine Geräte gefunden";
opt.disabled = true;
select.appendChild(opt);
select.disabled = true;
return;
}
devices.forEach(d => {
const opt = document.createElement("option");
opt.value = d.id;
opt.textContent = `${d.name} (${d.type})${d.is_active ? " [aktiv]" : ""}`;
select.appendChild(opt);
});
select.disabled = false;
}
loadDevices();
select.addEventListener("change", () => {
const deviceId = select.value;
// Hier kannst du deviceId speichern oder an dein Backend schicken
console.log("Ausgewähltes Device:", deviceId);
// window.selectedDeviceId = deviceId; // z.B. global speichern
});

View File

@ -97,7 +97,7 @@ const optionsDiv = document.getElementById("options");
const resultP = document.getElementById("result"); const resultP = document.getElementById("result");
const scoreboard = document.getElementById("scoreboard"); const scoreboard = document.getElementById("scoreboard");
function handleRoundStart({ ownerOptions, songUri, allTracks }) { function handleRoundStart({ ownerOptions, songUri, allTracks, trackInfos }) {
// UI zurücksetzen // UI zurücksetzen
resultP.textContent = ""; resultP.textContent = "";
optionsDiv.innerHTML = ""; optionsDiv.innerHTML = "";
@ -111,6 +111,10 @@ function handleRoundStart({ ownerOptions, songUri, allTracks }) {
width="100%" height="80" frameborder="0" width="100%" height="80" frameborder="0"
allow="encrypted-media"> allow="encrypted-media">
</iframe>`; </iframe>`;
// Song automatisch abspielen (sofern deviceId bereit und Username vorhanden)
if (window.playOnSpotify && typeof window.playOnSpotify === "function") {
window.playOnSpotify(songUri, username);
}
// Tipp-Buttons erzeugen // Tipp-Buttons erzeugen
ownerOptions.forEach(user => { ownerOptions.forEach(user => {
@ -133,10 +137,10 @@ function handleRoundStart({ ownerOptions, songUri, allTracks }) {
roundArea.hidden = false; roundArea.hidden = false;
const songList = document.getElementById("songList"); const songList = document.getElementById("songList");
songList.innerHTML = ""; songList.innerHTML = "";
if (Array.isArray(allTracks)) { if (Array.isArray(trackInfos)) {
allTracks.forEach(uri => { trackInfos.forEach(trackInfo => {
const li = document.createElement("li"); const li = document.createElement("li");
li.textContent = uri; li.textContent = trackInfo
songList.appendChild(li); songList.appendChild(li);
}); });
} }