add jukebox feature

This commit is contained in:
_Bastler 2021-11-28 17:30:02 +01:00
parent 9ad604d012
commit 40a2d1da52
8 changed files with 786 additions and 8 deletions

View File

@ -42,6 +42,11 @@
<artifactId>webstly-jitsi</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>de.bstly.we</groupId>
<artifactId>webstly-jukebox</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>de.bstly.we</groupId>
<artifactId>webstly-membership</artifactId>

View File

@ -42,12 +42,13 @@ public class SystemPropertyManager {
/**
* Gets the.
*
* @param key the key
* @param key the key
* @param defaultValue the default value
* @return the string
*/
public String get(String key, String defaultValue) {
return systemPropertyRepository.findById(key).orElse(new SystemProperty(key, defaultValue)).getValue();
return systemPropertyRepository.findById(key).orElse(new SystemProperty(key, defaultValue))
.getValue();
}
/**
@ -63,7 +64,7 @@ public class SystemPropertyManager {
/**
* Gets the boolean.
*
* @param key the key
* @param key the key
* @param defaultValue the default value
* @return the boolean
*/
@ -84,7 +85,7 @@ public class SystemPropertyManager {
/**
* Gets the integer.
*
* @param key the key
* @param key the key
* @param defaultValue the default value
* @return the integer
*/
@ -105,7 +106,7 @@ public class SystemPropertyManager {
/**
* Gets the long.
*
* @param key the key
* @param key the key
* @param defaultValue the default value
* @return the long
*/
@ -116,7 +117,7 @@ public class SystemPropertyManager {
/**
* Adds the.
*
* @param key the key
* @param key the key
* @param value the value
*/
public void add(String key, String value) {
@ -128,7 +129,7 @@ public class SystemPropertyManager {
/**
* Update.
*
* @param key the key
* @param key the key
* @param value the value
*/
public void update(String key, String value) {
@ -139,4 +140,12 @@ public class SystemPropertyManager {
systemPropertyRepository.save(systemProperty);
}
public void set(String key, String value) {
if (systemPropertyRepository.existsById(key)) {
update(key, value);
} else {
add(key, value);
}
}
}

21
jukebox/pom.xml Normal file
View File

@ -0,0 +1,21 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.bstly.we</groupId>
<artifactId>webstly-main</artifactId>
<version>${revision}</version>
</parent>
<name>jukebox</name>
<artifactId>webstly-jukebox</artifactId>
<dependencies>
<dependency>
<groupId>de.bstly.we</groupId>
<artifactId>webstly-partey</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,280 @@
/**
*
*/
package de.bstly.we.jukebox.businesslogic;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.client.WebClient;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.bstly.we.businesslogic.SystemPropertyManager;
import de.bstly.we.jukebox.businesslogic.model.JukeboxConfig;
import reactor.core.publisher.Mono;
/**
* The Class JukeboxManager.
*/
@Component
public class JukeboxManager implements SmartInitializingSingleton {
public static final String CLIENT_ID = "jukebox.client_id";
public static final String CLIENT_SECRET = "jukebox.client_secret";
public static final String REDIRECT_URL = "jukebox.redirect_url";
public static final String ACCOUNT_URL = "jukebox.account_url";
public static final String API_URL = "jukebox.api_url";
public static final String ACTIVE = "jukebox.active";
public static final String ACCESS_TOKEN = "jukebox.access_token";
public static final String REFRESH_TOKEN = "jukebox.refresh_token";
public static final String EXPIRES = "jukebox.expires";
@Autowired
private SystemPropertyManager systemPropertyManager;
protected WebClient webClient;
private JukeboxConfig config;
/*
* @see org.springframework.beans.factory.SmartInitializingSingleton#
* afterSingletonsInstantiated()
*/
/*
* @see org.springframework.beans.factory.SmartInitializingSingleton#
* afterSingletonsInstantiated()
*/
/*
* @see org.springframework.beans.factory.SmartInitializingSingleton#
* afterSingletonsInstantiated()
*/
@Override
public void afterSingletonsInstantiated() {
getConfig();
if (StringUtils.hasText(config.getApiUrl())) {
webClient = WebClient.builder().baseUrl(config.getApiUrl())
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json").build();
}
}
/**
* Gets the config.
*
* @return the config
*/
public JukeboxConfig getConfig() {
if (config == null) {
config = new JukeboxConfig();
config.setClientId(systemPropertyManager.get(CLIENT_ID));
config.setClientSecret(systemPropertyManager.get(CLIENT_SECRET));
config.setRedirectUrl(systemPropertyManager.get(REDIRECT_URL));
config.setApiUrl(systemPropertyManager.get(API_URL));
config.setAccountUrl(systemPropertyManager.get(ACCOUNT_URL));
config.setActive(systemPropertyManager.getBoolean(ACTIVE, false));
config.setAccessToken(systemPropertyManager.get(ACCESS_TOKEN));
config.setRefreshToken(systemPropertyManager.get(REFRESH_TOKEN));
if (StringUtils.hasText(systemPropertyManager.get(EXPIRES))) {
config.setExpires(Instant.parse(systemPropertyManager.get(EXPIRES)));
}
}
return config;
}
/**
* Sets the config.
*
* @param config the config
* @return the jukebox config
*/
public JukeboxConfig setConfig(JukeboxConfig config) {
this.config = config;
systemPropertyManager.set(CLIENT_ID, config.getClientId());
systemPropertyManager.set(CLIENT_SECRET, config.getClientSecret());
systemPropertyManager.set(REDIRECT_URL, config.getRedirectUrl());
systemPropertyManager.set(API_URL, config.getApiUrl());
systemPropertyManager.set(ACCOUNT_URL, config.getAccountUrl());
systemPropertyManager.set(ACTIVE, String.valueOf(config.isActive()));
systemPropertyManager.set(ACCESS_TOKEN, config.getAccessToken());
systemPropertyManager.set(REFRESH_TOKEN, config.getRefreshToken());
if (config.getExpires() != null) {
systemPropertyManager.set(EXPIRES, config.getExpires().toString());
}
if (StringUtils.hasText(config.getApiUrl())) {
webClient = WebClient.builder().baseUrl(config.getApiUrl())
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json").build();
}
return config;
}
/**
* Check token.
*
* @return true, if successful
*/
protected boolean checkToken() {
if (webClient == null || !StringUtils.hasText(config.getRefreshToken())) {
return false;
}
if (!StringUtils.hasText(config.getAccessToken()) || config.getExpires() == null
|| config.getExpires().isBefore(Instant.now())) {
String authHeader = "Basic "
+ new String(Base64.getEncoder()
.encode(String
.format("%s:%s", config.getClientId(), config.getClientSecret())
.getBytes()));
WebClient.RequestBodySpec request = WebClient.builder().baseUrl(config.getAccountUrl())
.build().method(HttpMethod.POST)
.uri(uriBuilder -> uriBuilder.path("/api/token").build())
.header(HttpHeaders.AUTHORIZATION, authHeader)
.header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded");
request.bodyValue("grant_type=refresh_token&refresh_token="
+ config.getRefreshToken());
String jsonString = request.retrieve().bodyToMono(String.class).block();
if (StringUtils.hasText(jsonString)) {
JsonObject response = JsonParser.parseString(jsonString).getAsJsonObject();
config.setAccessToken(response.get("access_token").getAsString());
config.setExpires(Instant.now().plus(response.get("expires_in").getAsLong(),
ChronoUnit.SECONDS));
if (response.has("refresh_token")) {
config.setRefreshToken(response.get("refresh_token").getAsString());
}
return true;
}
}
return true;
}
/**
* Gets the status.
*
* @return the status
*/
public JsonElement getStatus() {
if (!checkToken()) {
return null;
}
WebClient.RequestBodySpec request = webClient.method(HttpMethod.GET)
.uri(uriBuilder -> uriBuilder.path("/v1/me/player").build())
.header(HttpHeaders.AUTHORIZATION, "Bearer "
+ config.getAccessToken());
String jsonString = request.retrieve().bodyToMono(String.class).block();
if (StringUtils.hasText(jsonString)) {
JsonElement status = JsonParser.parseString(jsonString);
if (config.isActive()) {
boolean disable = false;
if (status == null || status.isJsonNull() || !status.isJsonObject()) {
disable = true;
} else {
JsonObject statusObject = status.getAsJsonObject();
if (!statusObject.has("device")) {
disable = true;
}
JsonObject device = statusObject.getAsJsonObject("device");
if (!device.has("is_private_session")
|| !device.get("is_private_session").getAsBoolean()) {
disable = true;
}
}
if (disable) {
config.setActive(false);
systemPropertyManager.set(ACTIVE, String.valueOf(config.isActive()));
}
}
return status;
}
return JsonNull.INSTANCE;
}
/**
* Search track.
*
* @param query the query
* @param offset the offset
* @return the json element
*/
public JsonElement searchTrack(String query, Long offset) {
if (!checkToken()) {
return null;
}
MultiValueMap<String, String> queryParameters = new LinkedMultiValueMap<String, String>();
queryParameters.add("q", query);
queryParameters.add("type", "track");
if (offset != null) {
queryParameters.add("offset", String.valueOf(offset));
}
WebClient.RequestBodySpec request = webClient.method(HttpMethod.GET).uri(
uriBuilder -> uriBuilder.path("/v1/search").queryParams(queryParameters).build())
.header(HttpHeaders.AUTHORIZATION, "Bearer "
+ config.getAccessToken());
String jsonString = request.retrieve().bodyToMono(String.class)
.onErrorResume(e -> Mono.just(e.getMessage())).block();
if (StringUtils.hasText(jsonString)) {
return JsonParser.parseString(jsonString);
}
return null;
}
/**
* Adds the to queue.
*
* @param uri the uri
*/
public void addToQueue(String uri) {
if (!checkToken()) {
return;
}
MultiValueMap<String, String> queryParameters = new LinkedMultiValueMap<String, String>();
queryParameters.add("uri", uri);
WebClient.RequestBodySpec request = webClient.method(HttpMethod.POST)
.uri(uriBuilder -> uriBuilder.path("/v1/me/player/queue")
.queryParams(queryParameters).build())
.header(HttpHeaders.AUTHORIZATION, "Bearer "
+ config.getAccessToken());
request.retrieve().bodyToMono(String.class).block();
}
/**
* Check active.
*/
@Scheduled(cron = "${we.bstly.jukebox.cron:0 */5 0 * * * }")
public void checkActive() {
getStatus();
}
}

View File

@ -0,0 +1,181 @@
/**
*
*/
package de.bstly.we.jukebox.businesslogic.model;
import java.time.Instant;
/**
* The Class JukeboxConfig.
*/
public class JukeboxConfig {
private String clientId;
private String clientSecret;
private String redirectUrl;
private String apiUrl;
private String accountUrl;
private boolean active;
private String accessToken;
private String refreshToken;
private Instant expires;
/**
* Gets the client id.
*
* @return the client id
*/
public String getClientId() {
return clientId;
}
/**
* Sets the client id.
*
* @param clientId the new client id
*/
public void setClientId(String clientId) {
this.clientId = clientId;
}
/**
* Gets the client secret.
*
* @return the client secret
*/
public String getClientSecret() {
return clientSecret;
}
/**
* Sets the client secret.
*
* @param clientSecret the new client secret
*/
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
/**
* Gets the redirect url.
*
* @return the redirect url
*/
public String getRedirectUrl() {
return redirectUrl;
}
/**
* Sets the redirect url.
*
* @param redirectUrl the new redirect url
*/
public void setRedirectUrl(String redirectUrl) {
this.redirectUrl = redirectUrl;
}
/**
* Gets the api url.
*
* @return the api url
*/
public String getApiUrl() {
return apiUrl;
}
/**
* Sets the api url.
*
* @param apiUrl the new api url
*/
public void setApiUrl(String apiUrl) {
this.apiUrl = apiUrl;
}
/**
* @return the accountUrl
*/
public String getAccountUrl() {
return accountUrl;
}
/**
* @param accountUrl the accountUrl to set
*/
public void setAccountUrl(String accountUrl) {
this.accountUrl = accountUrl;
}
/**
* Checks if is active.
*
* @return true, if is active
*/
public boolean isActive() {
return active;
}
/**
* Sets the active.
*
* @param active the new active
*/
public void setActive(boolean active) {
this.active = active;
}
/**
* Gets the access token.
*
* @return the access token
*/
public String getAccessToken() {
return accessToken;
}
/**
* Sets the access token.
*
* @param accessToken the new access token
*/
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
/**
* Gets the refresh token.
*
* @return the refresh token
*/
public String getRefreshToken() {
return refreshToken;
}
/**
* Sets the refresh token.
*
* @param refreshToken the new refresh token
*/
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
/**
* Gets the expires.
*
* @return the expires
*/
public Instant getExpires() {
return expires;
}
/**
* Sets the expires.
*
* @param expires the new expires
*/
public void setExpires(Instant expires) {
this.expires = expires;
}
}

View File

@ -0,0 +1,143 @@
/**
*
*/
package de.bstly.we.jukebox.controller;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.Optional;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.beust.jcommander.internal.Maps;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import de.bstly.we.businesslogic.PermissionManager;
import de.bstly.we.controller.BaseController;
import de.bstly.we.controller.support.EntityResponseStatusException;
import de.bstly.we.jukebox.businesslogic.JukeboxManager;
import de.bstly.we.partey.businesslogic.ParteyPermissions;
/**
* The Class JukeboxController.
*/
@RestController
@RequestMapping("/jukebox")
public class JukeboxController extends BaseController {
@Autowired
private PermissionManager permissionManager;
@Autowired
private JukeboxManager jukeboxManager;
private Gson gson = new Gson();
private Map<Long, Instant> queueList = Maps.newHashMap();
private Map<Long, Instant> searchList = Maps.newHashMap();
/**
* Check queue permission.
*/
protected void checkQueuePermission() {
if (!permissionManager.hasPermission(getCurrentUserId(), ParteyPermissions.PARTEY)) {
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
if (!jukeboxManager.getConfig().isActive()) {
throw new EntityResponseStatusException(HttpStatus.GONE);
}
jukeboxManager.getStatus();
if (!jukeboxManager.getConfig().isActive()) {
throw new EntityResponseStatusException(HttpStatus.GONE);
}
if (queueList.containsKey(getCurrentUserId()) && queueList.get(getCurrentUserId())
.isAfter(Instant.now().minus(1, ChronoUnit.MINUTES))) {
throw new EntityResponseStatusException(
Duration.between(queueList.get(getCurrentUserId()), Instant.now()).getSeconds(),
HttpStatus.PAYMENT_REQUIRED);
}
}
/**
* Check search permission.
*/
protected void checkSearchPermission() {
if (!permissionManager.hasPermission(getCurrentUserId(), ParteyPermissions.PARTEY)) {
throw new EntityResponseStatusException(HttpStatus.FORBIDDEN);
}
if (!jukeboxManager.getConfig().isActive()) {
throw new EntityResponseStatusException(HttpStatus.GONE);
}
if (searchList.containsKey(getCurrentUserId()) && searchList.get(getCurrentUserId())
.isAfter(Instant.now().minus(3, ChronoUnit.SECONDS))) {
throw new EntityResponseStatusException(Duration
.between(searchList.get(getCurrentUserId()), Instant.now()).getSeconds(),
HttpStatus.PAYMENT_REQUIRED);
}
}
/**
* Check.
*
* @throws JsonIOException the json IO exception
* @throws IOException Signals that an I/O exception has occurred.
*/
@PreAuthorize("isAuthenticated()")
@GetMapping
public void check() throws JsonIOException, IOException {
checkQueuePermission();
}
/**
* Search.
*
* @param query the query
* @param offset the offset
* @param response the response
* @throws JsonIOException the json IO exception
* @throws IOException Signals that an I/O exception has occurred.
*/
@PreAuthorize("isAuthenticated()")
@GetMapping("/search")
public void search(@RequestParam("q") String query,
@RequestParam("offset") Optional<Long> offset, HttpServletResponse response)
throws JsonIOException, IOException {
checkSearchPermission();
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
gson.toJson(jukeboxManager.searchTrack(query, offset.orElse(null)), response.getWriter());
searchList.put(getCurrentUserId(), Instant.now());
}
/**
* Queue.
*
* @param uri the uri
*/
@PreAuthorize("isAuthenticated()")
@PostMapping("/queue")
public void queue(@RequestParam("uri") String uri) {
checkQueuePermission();
jukeboxManager.addToQueue(uri);
queueList.put(getCurrentUserId(), Instant.now());
}
}

View File

@ -0,0 +1,138 @@
/**
*
*/
package de.bstly.we.jukebox.controller;
import java.io.IOException;
import java.util.Optional;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import de.bstly.we.controller.BaseController;
import de.bstly.we.jukebox.businesslogic.JukeboxManager;
import de.bstly.we.jukebox.businesslogic.model.JukeboxConfig;
/**
* The Class JukeboxManagementController.
*/
@RestController
@RequestMapping("/jukebox/manage")
public class JukeboxManagementController extends BaseController {
@Autowired
private JukeboxManager jukeboxManager;
private Gson gson = new Gson();
/**
* Gets the config.
*
* @return the config
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/config")
public JukeboxConfig getConfig() {
return jukeboxManager.getConfig();
}
/**
* Sets the config.
*
* @param config the config
* @return the jukebox config
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/config")
public JukeboxConfig setConfig(@RequestBody JukeboxConfig config) {
return jukeboxManager.setConfig(config);
}
/**
* Sets the config.
*
* @param config the config
* @return the jukebox config
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PutMapping
public void setActive() {
JukeboxConfig config = getConfig();
config.setActive(true);
jukeboxManager.setConfig(config);
}
/**
* Sets the config.
*
* @param config the config
* @return the jukebox config
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@DeleteMapping
public void disable() {
JukeboxConfig config = getConfig();
config.setActive(false);
jukeboxManager.setConfig(config);
}
/**
* Gets the status.
*
* @param response the response
* @return the status
* @throws JsonIOException the json IO exception
* @throws IOException Signals that an I/O exception has occurred.
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping()
public void getStatus(HttpServletResponse response) throws JsonIOException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
gson.toJson(jukeboxManager.getStatus(), response.getWriter());
}
/**
* Search.
*
* @param query the query
* @param offset the offset
* @param response the response
* @throws JsonIOException the json IO exception
* @throws IOException Signals that an I/O exception has occurred.
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/search")
public void search(@RequestParam("q") String query,
@RequestParam("offset") Optional<Long> offset, HttpServletResponse response)
throws JsonIOException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
gson.toJson(jukeboxManager.searchTrack(query, offset.orElse(null)), response.getWriter());
}
/**
* Queue.
*
* @param uri the uri
*/
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/queue")
public void queue(@RequestParam("uri") String uri) {
jukeboxManager.addToQueue(uri);
}
}

View File

@ -12,7 +12,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>11</java.version>
<revision>1.4.2-SNAPSHOT</revision>
<revision>1.4.3-SNAPSHOT</revision>
</properties>
<parent>
@ -30,6 +30,7 @@
<module>i18n</module>
<module>invite</module>
<module>jitsi</module>
<module>jukebox</module>
<module>jwt</module>
<module>membership</module>
<module>minetest</module>