auswählen von geräten für den player
parent
2d473ebdd9
commit
1d901c5222
|
|
@ -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
10
pom.xml
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue