From f46f82d4f3fa9a41c91aa521a4d780ffd101b2bc Mon Sep 17 00:00:00 2001 From: Simon Grimme Date: Fri, 15 Jul 2022 10:20:01 +0200 Subject: [PATCH] Implemented Protobuf endpoints --- .../gameyfin/config/WebClientConfig.java | 17 +++++++ .../de/grimsi/gameyfin/igdb/IgdbWrapper.java | 51 +++++++++---------- .../gameyfin/rest/GameyfinDevController.java | 23 ++++++++- src/main/resources/proto/igdbapi.proto | 4 +- 4 files changed, 63 insertions(+), 32 deletions(-) diff --git a/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java b/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java index ae3e1aa..7b269a9 100644 --- a/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java +++ b/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java @@ -3,7 +3,9 @@ package de.grimsi.gameyfin.config; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; 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.WebClient; import reactor.core.publisher.Mono; @@ -17,9 +19,24 @@ public class WebClientConfig implements WebClientCustomizer { public void customize(WebClient.Builder webClientBuilder) { webClientBuilder.filter(logResponse()); webClientBuilder.filter(logRequest()); + + // Enable use of system proxy 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() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { log.debug("Response: {}", clientResponse.statusCode()); diff --git a/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java b/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java index b105b98..2c0ecde 100644 --- a/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java +++ b/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java @@ -1,21 +1,18 @@ package de.grimsi.gameyfin.igdb; -import com.google.protobuf.InvalidProtocolBufferException; -import com.igdb.proto.Game; +import com.igdb.proto.Igdb; +import de.grimsi.gameyfin.config.WebClientConfig; import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; import javax.annotation.PostConstruct; import java.net.URI; -import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -67,23 +64,21 @@ public class IgdbWrapper { 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))); } - public Optional getGameById(Long id) { - byte[] gameBytes = igdbApiClient.post() - .uri("games.pb") - .bodyValue("fields *; where id = %d;".formatted(id)) - .retrieve() - .bodyToMono(byte[].class) - .block(); + public Optional getGameById(Long id) { + Igdb.GameResult gameResult = igdbApiClient.post() + .uri("games.pb") + .bodyValue("fields *; where id = %d; limit 1;".formatted(id)) + .retrieve() + .bodyToMono(Igdb.GameResult.class) + .block(); - try { - return Optional.ofNullable(Game.parseFrom(gameBytes)); - } catch (InvalidProtocolBufferException e) { - return Optional.empty(); - } + if (gameResult == null) return Optional.empty(); + + return Optional.of(gameResult.getGames(0)); } private void initIgdbClient() { @@ -95,21 +90,21 @@ public class IgdbWrapper { .baseUrl("https://api.igdb.com/v4/") .defaultHeader("Client-ID", clientId) .defaultHeader("Authorization", "Bearer %s".formatted(accessToken.getAccessToken())) + .filter(WebClientConfig.fixProtobufContentTypeInterceptor()) .build(); } - private Optional searchForGameByTitle(String searchTerm) { - List games = new ArrayList<>(); - - igdbApiClient.post() + private Optional searchForGameByTitle(String searchTerm) { + Igdb.GameResult gameResult = igdbApiClient.post() .uri("games.pb") .bodyValue("fields *; search \"%s\";".formatted(searchTerm)) .retrieve() - .bodyToFlux(Game.class) - .doOnNext(games::add) - .blockLast(); + .bodyToMono(Igdb.GameResult.class) + .block(); - if (games.isEmpty()) return Optional.empty(); + if (gameResult == null) return Optional.empty(); + + List games = gameResult.getGamesList(); // 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 @@ -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) // 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 srExactTitleMatch = games.stream().filter(s -> s.getName().equals(searchTerm)).findFirst(); + Optional srExactTitleMatch = games.stream().filter(s -> s.getName().equals(searchTerm)).findFirst(); if (srExactTitleMatch.isPresent()) return srExactTitleMatch; - Optional srTitleEndsWithMatch = games.stream().filter(s -> s.getName().endsWith(searchTerm)).findFirst(); + Optional srTitleEndsWithMatch = games.stream().filter(s -> s.getName().endsWith(searchTerm)).findFirst(); if (srTitleEndsWithMatch.isPresent()) return srTitleEndsWithMatch; return Optional.of(games.get(0)); diff --git a/src/main/java/de/grimsi/gameyfin/rest/GameyfinDevController.java b/src/main/java/de/grimsi/gameyfin/rest/GameyfinDevController.java index dc2ebcf..4443ce4 100644 --- a/src/main/java/de/grimsi/gameyfin/rest/GameyfinDevController.java +++ b/src/main/java/de/grimsi/gameyfin/rest/GameyfinDevController.java @@ -1,6 +1,6 @@ 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.igdb.IgdbWrapper; import de.grimsi.gameyfin.service.FilesystemService; @@ -15,6 +15,7 @@ import org.springframework.web.server.ResponseStatusException; import java.nio.file.Path; import java.util.List; +import java.util.Optional; @RestController public class GameyfinDevController { @@ -27,7 +28,7 @@ public class GameyfinDevController { @GetMapping(value = "/dev/findGameByTitle/{title}", produces = MediaType.APPLICATION_JSON_VALUE) public GameDto findGameByTitle(@PathVariable("title") String title) { - Game game; + Igdb.Game game; try { game = igdbWrapper.findGameByTitle(title); @@ -41,6 +42,24 @@ public class GameyfinDevController { .build(); } + @GetMapping(value = "/dev/getGameById/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public GameDto findGameByTitle(@PathVariable("id") Long id) { + Optional 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) public List getAllGameFiles() { return filesystemService.getGameFiles().stream().map(Path::toString).toList(); diff --git a/src/main/resources/proto/igdbapi.proto b/src/main/resources/proto/igdbapi.proto index 91d0b2a..7e6078a 100644 --- a/src/main/resources/proto/igdbapi.proto +++ b/src/main/resources/proto/igdbapi.proto @@ -4,8 +4,8 @@ package com.igdb.proto; import "google/protobuf/timestamp.proto"; -//option java_outer_classname = "Igdb"; -option java_multiple_files = true; // Must be true because of private access in files. +option java_outer_classname = "Igdb"; +//option java_multiple_files = true; // Must be true because of private access in files. option optimize_for = CODE_SIZE; message Count {