mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +00:00
Implemented Protobuf endpoints
This commit is contained in:
@@ -3,7 +3,9 @@ package de.grimsi.gameyfin.config;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||||
|
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
|
||||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
@@ -17,9 +19,24 @@ public class WebClientConfig implements WebClientCustomizer {
|
|||||||
public void customize(WebClient.Builder webClientBuilder) {
|
public void customize(WebClient.Builder webClientBuilder) {
|
||||||
webClientBuilder.filter(logResponse());
|
webClientBuilder.filter(logResponse());
|
||||||
webClientBuilder.filter(logRequest());
|
webClientBuilder.filter(logRequest());
|
||||||
|
|
||||||
|
// Enable use of system proxy
|
||||||
webClientBuilder.clientConnector(new ReactorClientHttpConnector(HttpClient.create().proxyWithSystemProperties()));
|
webClientBuilder.clientConnector(new ReactorClientHttpConnector(HttpClient.create().proxyWithSystemProperties()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This fixes the wrong Content-Type in reponses of the IGDB API by overwriting it so the WebClient is able to parse it automatically
|
||||||
|
* They return "application/protobuf", correct would be "application/x-protobuf"
|
||||||
|
* @return the filter function
|
||||||
|
*/
|
||||||
|
public static ExchangeFilterFunction fixProtobufContentTypeInterceptor() {
|
||||||
|
return ExchangeFilterFunction.ofResponseProcessor(clientResponse ->
|
||||||
|
Mono.just(clientResponse.mutate()
|
||||||
|
.headers(headers -> headers.remove(HttpHeaders.CONTENT_TYPE))
|
||||||
|
.header(HttpHeaders.CONTENT_TYPE, String.valueOf(ProtobufHttpMessageConverter.PROTOBUF))
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
private ExchangeFilterFunction logResponse() {
|
private ExchangeFilterFunction logResponse() {
|
||||||
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
|
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
|
||||||
log.debug("Response: {}", clientResponse.statusCode());
|
log.debug("Response: {}", clientResponse.statusCode());
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
package de.grimsi.gameyfin.igdb;
|
package de.grimsi.gameyfin.igdb;
|
||||||
|
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
import com.igdb.proto.Igdb;
|
||||||
import com.igdb.proto.Game;
|
import de.grimsi.gameyfin.config.WebClientConfig;
|
||||||
import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto;
|
import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -67,23 +64,21 @@ public class IgdbWrapper {
|
|||||||
log.info("Successfully authenticated.");
|
log.info("Successfully authenticated.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Game findGameByTitle(String title) {
|
public Igdb.Game findGameByTitle(String title) {
|
||||||
return searchForGameByTitle(title).orElseThrow(() -> new RuntimeException("Could not find game with title: \"%s\"".formatted(title)));
|
return searchForGameByTitle(title).orElseThrow(() -> new RuntimeException("Could not find game with title: \"%s\"".formatted(title)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Game> getGameById(Long id) {
|
public Optional<Igdb.Game> getGameById(Long id) {
|
||||||
byte[] gameBytes = igdbApiClient.post()
|
Igdb.GameResult gameResult = igdbApiClient.post()
|
||||||
.uri("games.pb")
|
.uri("games.pb")
|
||||||
.bodyValue("fields *; where id = %d;".formatted(id))
|
.bodyValue("fields *; where id = %d; limit 1;".formatted(id))
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToMono(byte[].class)
|
.bodyToMono(Igdb.GameResult.class)
|
||||||
.block();
|
.block();
|
||||||
|
|
||||||
try {
|
if (gameResult == null) return Optional.empty();
|
||||||
return Optional.ofNullable(Game.parseFrom(gameBytes));
|
|
||||||
} catch (InvalidProtocolBufferException e) {
|
return Optional.of(gameResult.getGames(0));
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initIgdbClient() {
|
private void initIgdbClient() {
|
||||||
@@ -95,21 +90,21 @@ public class IgdbWrapper {
|
|||||||
.baseUrl("https://api.igdb.com/v4/")
|
.baseUrl("https://api.igdb.com/v4/")
|
||||||
.defaultHeader("Client-ID", clientId)
|
.defaultHeader("Client-ID", clientId)
|
||||||
.defaultHeader("Authorization", "Bearer %s".formatted(accessToken.getAccessToken()))
|
.defaultHeader("Authorization", "Bearer %s".formatted(accessToken.getAccessToken()))
|
||||||
|
.filter(WebClientConfig.fixProtobufContentTypeInterceptor())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Game> searchForGameByTitle(String searchTerm) {
|
private Optional<Igdb.Game> searchForGameByTitle(String searchTerm) {
|
||||||
List<Game> games = new ArrayList<>();
|
Igdb.GameResult gameResult = igdbApiClient.post()
|
||||||
|
|
||||||
igdbApiClient.post()
|
|
||||||
.uri("games.pb")
|
.uri("games.pb")
|
||||||
.bodyValue("fields *; search \"%s\";".formatted(searchTerm))
|
.bodyValue("fields *; search \"%s\";".formatted(searchTerm))
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToFlux(Game.class)
|
.bodyToMono(Igdb.GameResult.class)
|
||||||
.doOnNext(games::add)
|
.block();
|
||||||
.blockLast();
|
|
||||||
|
|
||||||
if (games.isEmpty()) return Optional.empty();
|
if (gameResult == null) return Optional.empty();
|
||||||
|
|
||||||
|
List<Igdb.Game> games = gameResult.getGamesList();
|
||||||
|
|
||||||
// First check if there are any matches with the exact search term
|
// First check if there are any matches with the exact search term
|
||||||
// If no exact match has been found, check if there are matches where the name ends with the search term
|
// If no exact match has been found, check if there are matches where the name ends with the search term
|
||||||
@@ -119,10 +114,10 @@ public class IgdbWrapper {
|
|||||||
// Example: Searching for "Rainbow Six Siege" will result in returning "Tom Clancy's Rainbow Six Siege" (the game we want)
|
// Example: Searching for "Rainbow Six Siege" will result in returning "Tom Clancy's Rainbow Six Siege" (the game we want)
|
||||||
// If we just used the first result from IGDB we would get something like "Tom Clancy's Rainbow Six Siege Demon Veil" as a result
|
// If we just used the first result from IGDB we would get something like "Tom Clancy's Rainbow Six Siege Demon Veil" as a result
|
||||||
|
|
||||||
Optional<Game> srExactTitleMatch = games.stream().filter(s -> s.getName().equals(searchTerm)).findFirst();
|
Optional<Igdb.Game> srExactTitleMatch = games.stream().filter(s -> s.getName().equals(searchTerm)).findFirst();
|
||||||
if (srExactTitleMatch.isPresent()) return srExactTitleMatch;
|
if (srExactTitleMatch.isPresent()) return srExactTitleMatch;
|
||||||
|
|
||||||
Optional<Game> srTitleEndsWithMatch = games.stream().filter(s -> s.getName().endsWith(searchTerm)).findFirst();
|
Optional<Igdb.Game> srTitleEndsWithMatch = games.stream().filter(s -> s.getName().endsWith(searchTerm)).findFirst();
|
||||||
if (srTitleEndsWithMatch.isPresent()) return srTitleEndsWithMatch;
|
if (srTitleEndsWithMatch.isPresent()) return srTitleEndsWithMatch;
|
||||||
|
|
||||||
return Optional.of(games.get(0));
|
return Optional.of(games.get(0));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package de.grimsi.gameyfin.rest;
|
package de.grimsi.gameyfin.rest;
|
||||||
|
|
||||||
import com.igdb.proto.Game;
|
import com.igdb.proto.Igdb;
|
||||||
import de.grimsi.gameyfin.dto.GameDto;
|
import de.grimsi.gameyfin.dto.GameDto;
|
||||||
import de.grimsi.gameyfin.igdb.IgdbWrapper;
|
import de.grimsi.gameyfin.igdb.IgdbWrapper;
|
||||||
import de.grimsi.gameyfin.service.FilesystemService;
|
import de.grimsi.gameyfin.service.FilesystemService;
|
||||||
@@ -15,6 +15,7 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class GameyfinDevController {
|
public class GameyfinDevController {
|
||||||
@@ -27,7 +28,7 @@ public class GameyfinDevController {
|
|||||||
|
|
||||||
@GetMapping(value = "/dev/findGameByTitle/{title}", produces = MediaType.APPLICATION_JSON_VALUE)
|
@GetMapping(value = "/dev/findGameByTitle/{title}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public GameDto findGameByTitle(@PathVariable("title") String title) {
|
public GameDto findGameByTitle(@PathVariable("title") String title) {
|
||||||
Game game;
|
Igdb.Game game;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
game = igdbWrapper.findGameByTitle(title);
|
game = igdbWrapper.findGameByTitle(title);
|
||||||
@@ -41,6 +42,24 @@ public class GameyfinDevController {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/dev/getGameById/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public GameDto findGameByTitle(@PathVariable("id") Long id) {
|
||||||
|
Optional<Igdb.Game> gameOptional;
|
||||||
|
|
||||||
|
try {
|
||||||
|
gameOptional = igdbWrapper.getGameById(id);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
Igdb.Game game = gameOptional.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with id %d not found".formatted(id)));
|
||||||
|
|
||||||
|
return GameDto.builder()
|
||||||
|
.name(game.getName())
|
||||||
|
.releaseDate(ProtobufUtils.toInstant(game.getFirstReleaseDate()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/dev/gameFiles", produces = MediaType.APPLICATION_JSON_VALUE)
|
@GetMapping(value = "/dev/gameFiles", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public List<String> getAllGameFiles() {
|
public List<String> getAllGameFiles() {
|
||||||
return filesystemService.getGameFiles().stream().map(Path::toString).toList();
|
return filesystemService.getGameFiles().stream().map(Path::toString).toList();
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ package com.igdb.proto;
|
|||||||
|
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
//option java_outer_classname = "Igdb";
|
option java_outer_classname = "Igdb";
|
||||||
option java_multiple_files = true; // Must be true because of private access in files.
|
//option java_multiple_files = true; // Must be true because of private access in files.
|
||||||
option optimize_for = CODE_SIZE;
|
option optimize_for = CODE_SIZE;
|
||||||
|
|
||||||
message Count {
|
message Count {
|
||||||
|
|||||||
Reference in New Issue
Block a user