diff --git a/pom.xml b/pom.xml
index 64faf69..173f962 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,6 +18,7 @@
18
+ 3.11.4
@@ -49,6 +50,19 @@
2.11.0
+
+
+ com.google.protobuf
+ protobuf-java
+ 3.21.2
+
+
+ com.google.protobuf
+ protobuf-java-util
+ 3.21.1
+
+
+
org.springframework.boot
@@ -89,6 +103,34 @@
+
+
+
+ com.github.os72
+ protoc-jar-maven-plugin
+ ${protoc.plugin.version}
+
+
+ generate-sources
+
+ run
+
+
+ true
+
+ ${project.basedir}/src/main/resources/proto
+
+
+
+ java
+ ${project.build.directory}/generated-sources/protobuf
+
+
+
+
+
+
+
diff --git a/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java b/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java
new file mode 100644
index 0000000..ae3e1aa
--- /dev/null
+++ b/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java
@@ -0,0 +1,36 @@
+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.client.reactive.ReactorClientHttpConnector;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+import reactor.netty.http.client.HttpClient;
+
+@Slf4j
+@Configuration
+public class WebClientConfig implements WebClientCustomizer {
+
+ @Override
+ public void customize(WebClient.Builder webClientBuilder) {
+ webClientBuilder.filter(logResponse());
+ webClientBuilder.filter(logRequest());
+ webClientBuilder.clientConnector(new ReactorClientHttpConnector(HttpClient.create().proxyWithSystemProperties()));
+ }
+
+ private ExchangeFilterFunction logResponse() {
+ return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
+ log.debug("Response: {}", clientResponse.statusCode());
+ return Mono.just(clientResponse);
+ });
+ }
+
+ private ExchangeFilterFunction logRequest() {
+ return (clientRequest, next) -> {
+ log.debug("Request: {} {}", clientRequest.method(), clientRequest.url());
+ return next.exchange(clientRequest);
+ };
+ }
+}
diff --git a/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java b/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java
index 939d488..b105b98 100644
--- a/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java
+++ b/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java
@@ -1,15 +1,17 @@
package de.grimsi.gameyfin.igdb;
-import de.grimsi.gameyfin.igdb.dto.IgdbAccessToken;
-import de.grimsi.gameyfin.igdb.dto.IgdbGame;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.igdb.proto.Game;
+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.client.reactive.ReactorClientHttpConnector;
+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 reactor.netty.http.client.HttpClient;
import javax.annotation.PostConstruct;
import java.net.URI;
@@ -30,16 +32,18 @@ public class IgdbWrapper {
@Value("${gameyfin.igdb.config.preferred-platform}")
private int preferredPlatform;
- private final WebClient twitchApiClient = WebClient.builder()
- .clientConnector(new ReactorClientHttpConnector(HttpClient.create().proxyWithSystemProperties()))
- .build();
+ @Autowired
+ private WebClient.Builder webclientBuilder;
+
+ private WebClient twitchApiClient;
private WebClient igdbApiClient;
- private IgdbAccessToken accessToken;
+ private TwitchOAuthTokenDto accessToken;
@PostConstruct
public void init() {
+ twitchApiClient = webclientBuilder.build();
authenticate();
initIgdbClient();
}
@@ -57,25 +61,29 @@ public class IgdbWrapper {
.uri(url)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
- .bodyToMono(IgdbAccessToken.class)
+ .bodyToMono(TwitchOAuthTokenDto.class)
.block();
log.info("Successfully authenticated.");
}
- public IgdbGame findGameByTitle(String title) {
+ public Game findGameByTitle(String title) {
return searchForGameByTitle(title).orElseThrow(() -> new RuntimeException("Could not find game with title: \"%s\"".formatted(title)));
}
- private Optional getGameById(Long id) {
- return Optional.ofNullable(
- igdbApiClient.post()
- .uri("games")
+ public Optional getGameById(Long id) {
+ byte[] gameBytes = igdbApiClient.post()
+ .uri("games.pb")
.bodyValue("fields *; where id = %d;".formatted(id))
.retrieve()
- .bodyToMono(IgdbGame.class)
- .block()
- );
+ .bodyToMono(byte[].class)
+ .block();
+
+ try {
+ return Optional.ofNullable(Game.parseFrom(gameBytes));
+ } catch (InvalidProtocolBufferException e) {
+ return Optional.empty();
+ }
}
private void initIgdbClient() {
@@ -83,22 +91,21 @@ public class IgdbWrapper {
authenticate();
}
- igdbApiClient = WebClient.builder()
- .clientConnector(new ReactorClientHttpConnector(HttpClient.create().proxyWithSystemProperties()))
+ igdbApiClient = webclientBuilder
.baseUrl("https://api.igdb.com/v4/")
.defaultHeader("Client-ID", clientId)
.defaultHeader("Authorization", "Bearer %s".formatted(accessToken.getAccessToken()))
.build();
}
- private Optional searchForGameByTitle(String searchTerm) {
- List games = new ArrayList<>();
+ private Optional searchForGameByTitle(String searchTerm) {
+ List games = new ArrayList<>();
igdbApiClient.post()
- .uri("games")
+ .uri("games.pb")
.bodyValue("fields *; search \"%s\";".formatted(searchTerm))
.retrieve()
- .bodyToFlux(IgdbGame.class)
+ .bodyToFlux(Game.class)
.doOnNext(games::add)
.blockLast();
@@ -112,10 +119,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/igdb/dto/IgdbAccessToken.java b/src/main/java/de/grimsi/gameyfin/igdb/dto/TwitchOAuthTokenDto.java
similarity index 91%
rename from src/main/java/de/grimsi/gameyfin/igdb/dto/IgdbAccessToken.java
rename to src/main/java/de/grimsi/gameyfin/igdb/dto/TwitchOAuthTokenDto.java
index 70fce65..b971883 100644
--- a/src/main/java/de/grimsi/gameyfin/igdb/dto/IgdbAccessToken.java
+++ b/src/main/java/de/grimsi/gameyfin/igdb/dto/TwitchOAuthTokenDto.java
@@ -8,7 +8,7 @@ import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
-public class IgdbAccessToken {
+public class TwitchOAuthTokenDto {
private String accessToken;
private Long expiresIn;
private String tokenType;
diff --git a/src/main/java/de/grimsi/gameyfin/rest/GameyfinDevController.java b/src/main/java/de/grimsi/gameyfin/rest/GameyfinDevController.java
index d4cc758..dc2ebcf 100644
--- a/src/main/java/de/grimsi/gameyfin/rest/GameyfinDevController.java
+++ b/src/main/java/de/grimsi/gameyfin/rest/GameyfinDevController.java
@@ -1,10 +1,10 @@
package de.grimsi.gameyfin.rest;
+import com.igdb.proto.Game;
import de.grimsi.gameyfin.dto.GameDto;
import de.grimsi.gameyfin.igdb.IgdbWrapper;
-import de.grimsi.gameyfin.igdb.dto.IgdbGame;
import de.grimsi.gameyfin.service.FilesystemService;
-import org.apache.commons.io.FilenameUtils;
+import de.grimsi.gameyfin.util.ProtobufUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -15,7 +15,6 @@ import org.springframework.web.server.ResponseStatusException;
import java.nio.file.Path;
import java.util.List;
-import java.util.stream.Collectors;
@RestController
public class GameyfinDevController {
@@ -28,7 +27,7 @@ public class GameyfinDevController {
@GetMapping(value = "/dev/findGameByTitle/{title}", produces = MediaType.APPLICATION_JSON_VALUE)
public GameDto findGameByTitle(@PathVariable("title") String title) {
- IgdbGame game;
+ Game game;
try {
game = igdbWrapper.findGameByTitle(title);
@@ -36,7 +35,10 @@ public class GameyfinDevController {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
}
- return GameDto.builder().name(game.getName()).releaseDate(game.getFirstReleaseDate()).build();
+ return GameDto.builder()
+ .name(game.getName())
+ .releaseDate(ProtobufUtils.toInstant(game.getFirstReleaseDate()))
+ .build();
}
@GetMapping(value = "/dev/gameFiles", produces = MediaType.APPLICATION_JSON_VALUE)
@@ -48,7 +50,10 @@ public class GameyfinDevController {
public List getAllGames() {
return filesystemService.getGameFileNames().stream()
.map(t -> igdbWrapper.findGameByTitle(t))
- .map(g -> GameDto.builder().name(g.getName()).releaseDate(g.getFirstReleaseDate()).build())
+ .map(g -> GameDto.builder()
+ .name(g.getName())
+ .releaseDate(ProtobufUtils.toInstant(g.getFirstReleaseDate()))
+ .build())
.toList();
}
}
diff --git a/src/main/java/de/grimsi/gameyfin/util/ProtobufUtils.java b/src/main/java/de/grimsi/gameyfin/util/ProtobufUtils.java
new file mode 100644
index 0000000..faac3b9
--- /dev/null
+++ b/src/main/java/de/grimsi/gameyfin/util/ProtobufUtils.java
@@ -0,0 +1,11 @@
+package de.grimsi.gameyfin.util;
+
+import com.google.protobuf.Timestamp;
+
+import java.time.Instant;
+
+public class ProtobufUtils {
+ public static Instant toInstant(Timestamp t) {
+ return Instant.ofEpochSecond(t.getSeconds(), t.getNanos());
+ }
+}
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index f0f8dcf..b735051 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -1,5 +1,7 @@
gameyfin:
root: C:\Projects\privat\gameyfin-library
+ cache: C:\Projects\privat\gameyfin-library\.gameyfin
+ db: C:\Projects\privat\gameyfin-library\.gameyfin
igdb:
api:
client-id: 23l3l5qshx4dwjuao6yb8jyf1qrd08
diff --git a/src/main/resources/proto/igdbapi.proto b/src/main/resources/proto/igdbapi.proto
new file mode 100644
index 0000000..91d0b2a
--- /dev/null
+++ b/src/main/resources/proto/igdbapi.proto
@@ -0,0 +1,907 @@
+syntax = "proto3";
+
+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 optimize_for = CODE_SIZE;
+
+message Count {
+ int64 count = 1;
+}
+
+message MultiQueryResult {
+ string name = 1;
+ repeated bytes results = 2;
+ int64 count = 3;
+}
+
+message MultiQueryResultArray {
+ repeated MultiQueryResult result = 1;
+}
+
+message AgeRatingResult {
+ repeated AgeRating ageratings = 1;
+}
+
+message AgeRating {
+ uint64 id = 1;
+ AgeRatingCategoryEnum category = 2;
+ repeated AgeRatingContentDescription content_descriptions = 3;
+ AgeRatingRatingEnum rating = 4;
+ string rating_cover_url = 5;
+ string synopsis = 6;
+ string checksum = 7;
+}
+
+
+enum AgeRatingCategoryEnum {
+ AGERATING_CATEGORY_NULL = 0;
+ ESRB = 1;
+ PEGI = 2;
+ CERO = 3;
+ USK = 4;
+ GRAC = 5;
+ CLASS_IND = 6;
+ ACB = 7;
+}
+
+
+enum AgeRatingRatingEnum {
+ AGERATING_RATING_NULL = 0;
+ THREE = 1;
+ SEVEN = 2;
+ TWELVE = 3;
+ SIXTEEN = 4;
+ EIGHTEEN = 5;
+ RP = 6;
+ EC = 7;
+ E = 8;
+ E10 = 9;
+ T = 10;
+ M = 11;
+ AO = 12;
+ CERO_A = 13;
+ CERO_B = 14;
+ CERO_C = 15;
+ CERO_D = 16;
+ CERO_Z = 17;
+ USK_0 = 18;
+ USK_6 = 19;
+ USK_12 = 20;
+ USK_18 = 21;
+ GRAC_ALL = 22;
+ GRAC_TWELVE = 23;
+ GRAC_FIFTEEN = 24;
+ GRAC_EIGHTEEN = 25;
+ GRAC_TESTING = 26;
+ CLASS_IND_L = 27;
+ CLASS_IND_TEN = 28;
+ CLASS_IND_TWELVE = 29;
+ CLASS_IND_FOURTEEN = 30;
+ CLASS_IND_SIXTEEN = 31;
+ CLASS_IND_EIGHTEEN = 32;
+ ACB_G = 33;
+ ACB_PG = 34;
+ ACB_M = 35;
+ ACB_MA15 = 36;
+ ACB_R18 = 37;
+ ACB_RC = 38;
+}
+
+message AgeRatingContentDescriptionResult {
+ repeated AgeRatingContentDescription ageratingcontentdescriptions = 1;
+}
+
+message AgeRatingContentDescription {
+ uint64 id = 1;
+ AgeRatingRatingEnum category = 2;
+ string description = 3;
+ string checksum = 4;
+}
+
+message AlternativeNameResult {
+ repeated AlternativeName alternativenames = 1;
+}
+
+message AlternativeName {
+ uint64 id = 1;
+ string comment = 2;
+ Game game = 3;
+ string name = 4;
+ string checksum = 5;
+}
+
+message ArtworkResult {
+ repeated Artwork artworks = 1;
+}
+
+message Artwork {
+ uint64 id = 1;
+ bool alpha_channel = 2;
+ bool animated = 3;
+ Game game = 4;
+ int32 height = 5;
+ string image_id = 6;
+ string url = 7;
+ int32 width = 8;
+ string checksum = 9;
+}
+
+message CharacterResult {
+ repeated Character characters = 1;
+}
+
+message Character {
+ uint64 id = 1;
+ repeated string akas = 2;
+ string country_name = 3;
+ google.protobuf.Timestamp created_at = 4;
+ string description = 5;
+ repeated Game games = 6;
+ GenderGenderEnum gender = 7;
+ CharacterMugShot mug_shot = 8;
+ string name = 9;
+ string slug = 10;
+ CharacterSpeciesEnum species = 11;
+ google.protobuf.Timestamp updated_at = 12;
+ string url = 13;
+ string checksum = 14;
+}
+
+
+enum GenderGenderEnum {
+ MALE = 0;
+ FEMALE = 1;
+ OTHER = 2;
+}
+
+
+enum CharacterSpeciesEnum {
+ CHARACTER_SPECIES_NULL = 0;
+ HUMAN = 1;
+ ALIEN = 2;
+ ANIMAL = 3;
+ ANDROID = 4;
+ UNKNOWN = 5;
+}
+
+message CharacterMugShotResult {
+ repeated CharacterMugShot charactermugshots = 1;
+}
+
+message CharacterMugShot {
+ uint64 id = 1;
+ bool alpha_channel = 2;
+ bool animated = 3;
+ int32 height = 4;
+ string image_id = 5;
+ string url = 6;
+ int32 width = 7;
+ string checksum = 8;
+}
+
+message CollectionResult {
+ repeated Collection collections = 1;
+}
+
+message Collection {
+ uint64 id = 1;
+ google.protobuf.Timestamp created_at = 2;
+ repeated Game games = 3;
+ string name = 4;
+ string slug = 5;
+ google.protobuf.Timestamp updated_at = 6;
+ string url = 7;
+ string checksum = 8;
+}
+
+message CompanyResult {
+ repeated Company companies = 1;
+}
+
+message Company {
+ uint64 id = 1;
+ google.protobuf.Timestamp change_date = 2;
+ DateFormatChangeDateCategoryEnum change_date_category = 3;
+ Company changed_company_id = 4;
+ int32 country = 5;
+ google.protobuf.Timestamp created_at = 6;
+ string description = 7;
+ repeated Game developed = 8;
+ CompanyLogo logo = 9;
+ string name = 10;
+ Company parent = 11;
+ repeated Game published = 12;
+ string slug = 13;
+ google.protobuf.Timestamp start_date = 14;
+ DateFormatChangeDateCategoryEnum start_date_category = 15;
+ google.protobuf.Timestamp updated_at = 16;
+ string url = 17;
+ repeated CompanyWebsite websites = 18;
+ string checksum = 19;
+}
+
+
+enum DateFormatChangeDateCategoryEnum {
+ YYYYMMMMDD = 0;
+ YYYYMMMM = 1;
+ YYYY = 2;
+ YYYYQ1 = 3;
+ YYYYQ2 = 4;
+ YYYYQ3 = 5;
+ YYYYQ4 = 6;
+ TBD = 7;
+}
+
+message CompanyLogoResult {
+ repeated CompanyLogo companylogos = 1;
+}
+
+message CompanyLogo {
+ uint64 id = 1;
+ bool alpha_channel = 2;
+ bool animated = 3;
+ int32 height = 4;
+ string image_id = 5;
+ string url = 6;
+ int32 width = 7;
+ string checksum = 8;
+}
+
+message CompanyWebsiteResult {
+ repeated CompanyWebsite companywebsites = 1;
+}
+
+message CompanyWebsite {
+ uint64 id = 1;
+ WebsiteCategoryEnum category = 2;
+ bool trusted = 3;
+ string url = 4;
+ string checksum = 5;
+}
+
+
+enum WebsiteCategoryEnum {
+ WEBSITE_CATEGORY_NULL = 0;
+ WEBSITE_OFFICIAL = 1;
+ WEBSITE_WIKIA = 2;
+ WEBSITE_WIKIPEDIA = 3;
+ WEBSITE_FACEBOOK = 4;
+ WEBSITE_TWITTER = 5;
+ WEBSITE_TWITCH = 6;
+ WEBSITE_INSTAGRAM = 8;
+ WEBSITE_YOUTUBE = 9;
+ WEBSITE_IPHONE = 10;
+ WEBSITE_IPAD = 11;
+ WEBSITE_ANDROID = 12;
+ WEBSITE_STEAM = 13;
+ WEBSITE_REDDIT = 14;
+ WEBSITE_ITCH = 15;
+ WEBSITE_EPICGAMES = 16;
+ WEBSITE_GOG = 17;
+ WEBSITE_DISCORD = 18;
+}
+
+message CoverResult {
+ repeated Cover covers = 1;
+}
+
+message Cover {
+ uint64 id = 1;
+ bool alpha_channel = 2;
+ bool animated = 3;
+ Game game = 4;
+ int32 height = 5;
+ string image_id = 6;
+ string url = 7;
+ int32 width = 8;
+ string checksum = 9;
+}
+
+message ExternalGameResult {
+ repeated ExternalGame externalgames = 1;
+}
+
+message ExternalGame {
+ uint64 id = 1;
+ ExternalGameCategoryEnum category = 2;
+ google.protobuf.Timestamp created_at = 3;
+ Game game = 4;
+ string name = 5;
+ string uid = 6;
+ google.protobuf.Timestamp updated_at = 7;
+ string url = 8;
+ int32 year = 9;
+ ExternalGameMediaEnum media = 10;
+ Platform platform = 11;
+ repeated int32 countries = 12;
+ string checksum = 13;
+}
+
+
+enum ExternalGameCategoryEnum {
+ EXTERNALGAME_CATEGORY_NULL = 0;
+ EXTERNALGAME_STEAM = 1;
+ EXTERNALGAME_GOG = 5;
+ EXTERNALGAME_YOUTUBE = 10;
+ EXTERNALGAME_MICROSOFT = 11;
+ EXTERNALGAME_APPLE = 13;
+ EXTERNALGAME_TWITCH = 14;
+ EXTERNALGAME_ANDROID = 15;
+ EXTERNALGAME_AMAZON_ASIN = 20;
+ EXTERNALGAME_AMAZON_LUNA = 22;
+ EXTERNALGAME_AMAZON_ADG = 23;
+ EXTERNALGAME_EPIC_GAME_STORE = 26;
+ EXTERNALGAME_OCULUS = 28;
+}
+
+
+enum ExternalGameMediaEnum {
+ EXTERNALGAME_MEDIA_NULL = 0;
+ EXTERNALGAME_DIGITAL = 1;
+ EXTERNALGAME_PHYSICAL = 2;
+}
+
+message FranchiseResult {
+ repeated Franchise franchises = 1;
+}
+
+message Franchise {
+ uint64 id = 1;
+ google.protobuf.Timestamp created_at = 2;
+ repeated Game games = 3;
+ string name = 4;
+ string slug = 5;
+ google.protobuf.Timestamp updated_at = 6;
+ string url = 7;
+ string checksum = 8;
+}
+
+message GameResult {
+ repeated Game games = 1;
+}
+
+message Game {
+ uint64 id = 1;
+ repeated AgeRating age_ratings = 2;
+ double aggregated_rating = 3;
+ int32 aggregated_rating_count = 4;
+ repeated AlternativeName alternative_names = 5;
+ repeated Artwork artworks = 6;
+ repeated Game bundles = 7;
+ GameCategoryEnum category = 8;
+ Collection collection = 9;
+ Cover cover = 10;
+ google.protobuf.Timestamp created_at = 11;
+ repeated Game dlcs = 12;
+ repeated Game expansions = 13;
+ repeated ExternalGame external_games = 14;
+ google.protobuf.Timestamp first_release_date = 15;
+ int32 follows = 16;
+ Franchise franchise = 17;
+ repeated Franchise franchises = 18;
+ repeated GameEngine game_engines = 19;
+ repeated GameMode game_modes = 20;
+ repeated Genre genres = 21;
+ int32 hypes = 22;
+ repeated InvolvedCompany involved_companies = 23;
+ repeated Keyword keywords = 24;
+ repeated MultiplayerMode multiplayer_modes = 25;
+ string name = 26;
+ Game parent_game = 27;
+ repeated Platform platforms = 28;
+ repeated PlayerPerspective player_perspectives = 29;
+ double rating = 30;
+ int32 rating_count = 31;
+ repeated ReleaseDate release_dates = 32;
+ repeated Screenshot screenshots = 33;
+ repeated Game similar_games = 34;
+ string slug = 35;
+ repeated Game standalone_expansions = 36;
+ GameStatusEnum status = 37;
+ string storyline = 38;
+ string summary = 39;
+ repeated int32 tags = 40;
+ repeated Theme themes = 41;
+ double total_rating = 42;
+ int32 total_rating_count = 43;
+ google.protobuf.Timestamp updated_at = 44;
+ string url = 45;
+ Game version_parent = 46;
+ string version_title = 47;
+ repeated GameVideo videos = 48;
+ repeated Website websites = 49;
+ string checksum = 50;
+ repeated Game remakes = 51;
+ repeated Game remasters = 52;
+ repeated Game expanded_games = 53;
+ repeated Game ports = 54;
+ repeated Game forks = 55;
+}
+
+
+enum GameCategoryEnum {
+ MAIN_GAME = 0;
+ DLC_ADDON = 1;
+ EXPANSION = 2;
+ BUNDLE = 3;
+ STANDALONE_EXPANSION = 4;
+ MOD = 5;
+ EPISODE = 6;
+ SEASON = 7;
+ REMAKE = 8;
+ REMASTER = 9;
+ EXPANDED_GAME = 10;
+ PORT = 11;
+ FORK = 12;
+}
+
+
+enum GameStatusEnum {
+ RELEASED = 0;
+ ALPHA = 2;
+ BETA = 3;
+ EARLY_ACCESS = 4;
+ OFFLINE = 5;
+ CANCELLED = 6;
+ RUMORED = 7;
+ DELISTED = 8;
+}
+
+message GameEngineResult {
+ repeated GameEngine gameengines = 1;
+}
+
+message GameEngine {
+ uint64 id = 1;
+ repeated Company companies = 2;
+ google.protobuf.Timestamp created_at = 3;
+ string description = 4;
+ GameEngineLogo logo = 5;
+ string name = 6;
+ repeated Platform platforms = 7;
+ string slug = 8;
+ google.protobuf.Timestamp updated_at = 9;
+ string url = 10;
+ string checksum = 11;
+}
+
+message GameEngineLogoResult {
+ repeated GameEngineLogo gameenginelogos = 1;
+}
+
+message GameEngineLogo {
+ uint64 id = 1;
+ bool alpha_channel = 2;
+ bool animated = 3;
+ int32 height = 4;
+ string image_id = 5;
+ string url = 6;
+ int32 width = 7;
+ string checksum = 8;
+}
+
+message GameModeResult {
+ repeated GameMode gamemodes = 1;
+}
+
+message GameMode {
+ uint64 id = 1;
+ google.protobuf.Timestamp created_at = 2;
+ string name = 3;
+ string slug = 4;
+ google.protobuf.Timestamp updated_at = 5;
+ string url = 6;
+ string checksum = 7;
+}
+
+message GameVersionResult {
+ repeated GameVersion gameversions = 1;
+}
+
+message GameVersion {
+ uint64 id = 1;
+ google.protobuf.Timestamp created_at = 2;
+ repeated GameVersionFeature features = 3;
+ Game game = 4;
+ repeated Game games = 5;
+ google.protobuf.Timestamp updated_at = 6;
+ string url = 7;
+ string checksum = 8;
+}
+
+message GameVersionFeatureResult {
+ repeated GameVersionFeature gameversionfeatures = 1;
+}
+
+message GameVersionFeature {
+ uint64 id = 1;
+ GameVersionFeatureCategoryEnum category = 2;
+ string description = 3;
+ int32 position = 4;
+ string title = 5;
+ repeated GameVersionFeatureValue values = 6;
+ string checksum = 7;
+}
+
+
+enum GameVersionFeatureCategoryEnum {
+ BOOLEAN = 0;
+ DESCRIPTION = 1;
+}
+
+message GameVersionFeatureValueResult {
+ repeated GameVersionFeatureValue gameversionfeaturevalues = 1;
+}
+
+message GameVersionFeatureValue {
+ uint64 id = 1;
+ Game game = 2;
+ GameVersionFeature game_feature = 3;
+ GameVersionFeatureValueIncludedFeatureEnum included_feature = 4;
+ string note = 5;
+ string checksum = 6;
+}
+
+
+enum GameVersionFeatureValueIncludedFeatureEnum {
+ NOT_INCLUDED = 0;
+ INCLUDED = 1;
+ PRE_ORDER_ONLY = 2;
+}
+
+message GameVideoResult {
+ repeated GameVideo gamevideos = 1;
+}
+
+message GameVideo {
+ uint64 id = 1;
+ Game game = 2;
+ string name = 3;
+ string video_id = 4;
+ string checksum = 5;
+}
+
+message GenreResult {
+ repeated Genre genres = 1;
+}
+
+message Genre {
+ uint64 id = 1;
+ google.protobuf.Timestamp created_at = 2;
+ string name = 3;
+ string slug = 4;
+ google.protobuf.Timestamp updated_at = 5;
+ string url = 6;
+ string checksum = 7;
+}
+
+message InvolvedCompanyResult {
+ repeated InvolvedCompany involvedcompanies = 1;
+}
+
+message InvolvedCompany {
+ uint64 id = 1;
+ Company company = 2;
+ google.protobuf.Timestamp created_at = 3;
+ bool developer = 4;
+ Game game = 5;
+ bool porting = 6;
+ bool publisher = 7;
+ bool supporting = 8;
+ google.protobuf.Timestamp updated_at = 9;
+ string checksum = 10;
+}
+
+message KeywordResult {
+ repeated Keyword keywords = 1;
+}
+
+message Keyword {
+ uint64 id = 1;
+ google.protobuf.Timestamp created_at = 2;
+ string name = 3;
+ string slug = 4;
+ google.protobuf.Timestamp updated_at = 5;
+ string url = 6;
+ string checksum = 7;
+}
+
+message MultiplayerModeResult {
+ repeated MultiplayerMode multiplayermodes = 1;
+}
+
+message MultiplayerMode {
+ uint64 id = 1;
+ bool campaigncoop = 2;
+ bool dropin = 3;
+ Game game = 4;
+ bool lancoop = 5;
+ bool offlinecoop = 6;
+ int32 offlinecoopmax = 7;
+ int32 offlinemax = 8;
+ bool onlinecoop = 9;
+ int32 onlinecoopmax = 10;
+ int32 onlinemax = 11;
+ Platform platform = 12;
+ bool splitscreen = 13;
+ bool splitscreenonline = 14;
+ string checksum = 15;
+}
+
+message PlatformResult {
+ repeated Platform platforms = 1;
+}
+
+message Platform {
+ uint64 id = 1;
+ string abbreviation = 2;
+ string alternative_name = 3;
+ PlatformCategoryEnum category = 4;
+ google.protobuf.Timestamp created_at = 5;
+ int32 generation = 6;
+ string name = 7;
+ PlatformLogo platform_logo = 8;
+ PlatformFamily platform_family = 9;
+ string slug = 10;
+ string summary = 11;
+ google.protobuf.Timestamp updated_at = 12;
+ string url = 13;
+ repeated PlatformVersion versions = 14;
+ repeated PlatformWebsite websites = 15;
+ string checksum = 16;
+}
+
+
+enum PlatformCategoryEnum {
+ PLATFORM_CATEGORY_NULL = 0;
+ CONSOLE = 1;
+ ARCADE = 2;
+ PLATFORM = 3;
+ OPERATING_SYSTEM = 4;
+ PORTABLE_CONSOLE = 5;
+ COMPUTER = 6;
+}
+
+message PlatformFamilyResult {
+ repeated PlatformFamily platformfamilies = 1;
+}
+
+message PlatformFamily {
+ uint64 id = 1;
+ string name = 2;
+ string slug = 3;
+ string checksum = 4;
+}
+
+message PlatformLogoResult {
+ repeated PlatformLogo platformlogos = 1;
+}
+
+message PlatformLogo {
+ uint64 id = 1;
+ bool alpha_channel = 2;
+ bool animated = 3;
+ int32 height = 4;
+ string image_id = 5;
+ string url = 6;
+ int32 width = 7;
+ string checksum = 8;
+}
+
+message PlatformVersionResult {
+ repeated PlatformVersion platformversions = 1;
+}
+
+message PlatformVersion {
+ uint64 id = 1;
+ repeated PlatformVersionCompany companies = 2;
+ string connectivity = 3;
+ string cpu = 4;
+ string graphics = 5;
+ PlatformVersionCompany main_manufacturer = 6;
+ string media = 7;
+ string memory = 8;
+ string name = 9;
+ string online = 10;
+ string os = 11;
+ string output = 12;
+ PlatformLogo platform_logo = 13;
+ repeated PlatformVersionReleaseDate platform_version_release_dates = 14;
+ string resolutions = 15;
+ string slug = 16;
+ string sound = 17;
+ string storage = 18;
+ string summary = 19;
+ string url = 20;
+ string checksum = 21;
+}
+
+message PlatformVersionCompanyResult {
+ repeated PlatformVersionCompany platformversioncompanies = 1;
+}
+
+message PlatformVersionCompany {
+ uint64 id = 1;
+ string comment = 2;
+ Company company = 3;
+ bool developer = 4;
+ bool manufacturer = 5;
+ string checksum = 6;
+}
+
+message PlatformVersionReleaseDateResult {
+ repeated PlatformVersionReleaseDate platformversionreleasedates = 1;
+}
+
+message PlatformVersionReleaseDate {
+ uint64 id = 1;
+ DateFormatChangeDateCategoryEnum category = 2;
+ google.protobuf.Timestamp created_at = 3;
+ google.protobuf.Timestamp date = 4;
+ string human = 5;
+ int32 m = 6;
+ PlatformVersion platform_version = 7;
+ RegionRegionEnum region = 8;
+ google.protobuf.Timestamp updated_at = 9;
+ int32 y = 10;
+ string checksum = 11;
+}
+
+
+enum RegionRegionEnum {
+ REGION_REGION_NULL = 0;
+ EUROPE = 1;
+ NORTH_AMERICA = 2;
+ AUSTRALIA = 3;
+ NEW_ZEALAND = 4;
+ JAPAN = 5;
+ CHINA = 6;
+ ASIA = 7;
+ WORLDWIDE = 8;
+ KOREA = 9;
+ BRAZIL = 10;
+}
+
+message PlatformWebsiteResult {
+ repeated PlatformWebsite platformwebsites = 1;
+}
+
+message PlatformWebsite {
+ uint64 id = 1;
+ WebsiteCategoryEnum category = 2;
+ bool trusted = 3;
+ string url = 4;
+ string checksum = 5;
+}
+
+message PlayerPerspectiveResult {
+ repeated PlayerPerspective playerperspectives = 1;
+}
+
+message PlayerPerspective {
+ uint64 id = 1;
+ google.protobuf.Timestamp created_at = 2;
+ string name = 3;
+ string slug = 4;
+ google.protobuf.Timestamp updated_at = 5;
+ string url = 6;
+ string checksum = 7;
+}
+
+message ReleaseDateResult {
+ repeated ReleaseDate releasedates = 1;
+}
+
+message ReleaseDate {
+ uint64 id = 1;
+ DateFormatChangeDateCategoryEnum category = 2;
+ google.protobuf.Timestamp created_at = 3;
+ google.protobuf.Timestamp date = 4;
+ Game game = 5;
+ string human = 6;
+ int32 m = 7;
+ Platform platform = 8;
+ RegionRegionEnum region = 9;
+ google.protobuf.Timestamp updated_at = 10;
+ int32 y = 11;
+ string checksum = 12;
+}
+
+message ScreenshotResult {
+ repeated Screenshot screenshots = 1;
+}
+
+message Screenshot {
+ uint64 id = 1;
+ bool alpha_channel = 2;
+ bool animated = 3;
+ Game game = 4;
+ int32 height = 5;
+ string image_id = 6;
+ string url = 7;
+ int32 width = 8;
+ string checksum = 9;
+}
+
+message SearchResult {
+ repeated Search searches = 1;
+}
+
+message Search {
+ uint64 id = 1;
+ string alternative_name = 2;
+ Character character = 3;
+ Collection collection = 4;
+ Company company = 5;
+ string description = 6;
+ Game game = 7;
+ string name = 8;
+ Platform platform = 9;
+ google.protobuf.Timestamp published_at = 10;
+ TestDummy test_dummy = 11;
+ Theme theme = 12;
+ string checksum = 13;
+}
+
+message TestDummyResult {
+ repeated TestDummy testdummies = 1;
+}
+
+message TestDummy {
+ uint64 id = 1;
+ bool bool_value = 2;
+ google.protobuf.Timestamp created_at = 3;
+ TestDummyEnumTestEnum enum_test = 4;
+ double float_value = 5;
+ Game game = 6;
+ repeated int32 integer_array = 7;
+ int32 integer_value = 8;
+ string name = 9;
+ int32 new_integer_value = 10;
+ bool private = 11;
+ string slug = 12;
+ repeated string string_array = 13;
+ repeated TestDummy test_dummies = 14;
+ TestDummy test_dummy = 15;
+ google.protobuf.Timestamp updated_at = 16;
+ string url = 17;
+ string checksum = 18;
+}
+
+
+enum TestDummyEnumTestEnum {
+ TESTDUMMY_ENUM_TEST_NULL = 0;
+ ENUM1 = 1;
+ ENUM2 = 2;
+}
+
+message ThemeResult {
+ repeated Theme themes = 1;
+}
+
+message Theme {
+ uint64 id = 1;
+ google.protobuf.Timestamp created_at = 2;
+ string name = 3;
+ string slug = 4;
+ google.protobuf.Timestamp updated_at = 5;
+ string url = 6;
+ string checksum = 7;
+}
+
+message WebsiteResult {
+ repeated Website websites = 1;
+}
+
+message Website {
+ uint64 id = 1;
+ WebsiteCategoryEnum category = 2;
+ Game game = 3;
+ bool trusted = 4;
+ string url = 5;
+ string checksum = 6;
+}