mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Start implementation of persistence layer
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package de.grimsi.gameyfin.config;
|
||||
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -10,6 +11,7 @@ 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;
|
||||
import reactor.netty.transport.logging.AdvancedByteBufFormat;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@@ -17,16 +19,17 @@ public class WebClientConfig implements WebClientCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(WebClient.Builder webClientBuilder) {
|
||||
webClientBuilder.filter(logResponse());
|
||||
webClientBuilder.filter(logRequest());
|
||||
HttpClient httpClient = HttpClient.create()
|
||||
.wiretap(this.getClass().getCanonicalName(), LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL) // Enable full request / response logging (active only in DEV profile)
|
||||
.proxyWithSystemProperties(); // Enable use of system proxy
|
||||
|
||||
// Enable use of system proxy
|
||||
webClientBuilder.clientConnector(new ReactorClientHttpConnector(HttpClient.create().proxyWithSystemProperties()));
|
||||
webClientBuilder.clientConnector(new ReactorClientHttpConnector(httpClient));
|
||||
}
|
||||
|
||||
/**
|
||||
* This fixes the wrong Content-Type in responses 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() {
|
||||
@@ -34,20 +37,7 @@ public class WebClientConfig implements WebClientCustomizer {
|
||||
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());
|
||||
return Mono.just(clientResponse);
|
||||
});
|
||||
}
|
||||
|
||||
private ExchangeFilterFunction logRequest() {
|
||||
return (clientRequest, next) -> {
|
||||
log.debug("Request: {} {}", clientRequest.method(), clientRequest.url());
|
||||
return next.exchange(clientRequest);
|
||||
};
|
||||
.build())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public class Category {
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
|
||||
Category category = (Category) o;
|
||||
return slug != null && Objects.equals(slug, category.slug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public class Company {
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
private Long logoId;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
|
||||
Company company = (Company) o;
|
||||
return slug != null && Objects.equals(slug, company.slug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public class DetectedGame {
|
||||
|
||||
// Game properties
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
private String summary;
|
||||
|
||||
private Instant releaseDate;
|
||||
|
||||
private Integer userRating;
|
||||
|
||||
private Integer criticsRating;
|
||||
|
||||
private Integer totalRating;
|
||||
|
||||
@ManyToOne
|
||||
private Category category;
|
||||
|
||||
private boolean offlineCoop;
|
||||
|
||||
private boolean onlineCoop;
|
||||
|
||||
private boolean lanSupport;
|
||||
|
||||
private int maxPlayers;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long coverId;
|
||||
|
||||
@ElementCollection
|
||||
private List<Long> screenshotIds;
|
||||
|
||||
@ElementCollection
|
||||
private List<Long> videoIds;
|
||||
|
||||
@ManyToMany
|
||||
@ToString.Exclude
|
||||
private List<Company> companies;
|
||||
|
||||
@ManyToMany
|
||||
@ToString.Exclude
|
||||
private List<Genre> genres;
|
||||
|
||||
@ManyToMany
|
||||
@ToString.Exclude
|
||||
private List<Keyword> keywords;
|
||||
|
||||
@ManyToMany
|
||||
@ToString.Exclude
|
||||
private List<Theme> themes;
|
||||
|
||||
@ManyToMany
|
||||
@ToString.Exclude
|
||||
private List<PlayerPerspective> playerPerspectives;
|
||||
|
||||
// Technical properties
|
||||
@Column(nullable = false)
|
||||
private String path;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean isFolder;
|
||||
|
||||
@Column(columnDefinition = "boolean default false")
|
||||
private boolean confirmedMatch;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
|
||||
DetectedGame that = (DetectedGame) o;
|
||||
return slug != null && Objects.equals(slug, that.slug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
public class Game {
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public class Genre {
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
|
||||
Genre genre = (Genre) o;
|
||||
return slug != null && Objects.equals(slug, genre.slug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public class Keyword {
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
|
||||
Keyword keyword = (Keyword) o;
|
||||
return slug != null && Objects.equals(slug, keyword.slug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public class PlayerPerspective {
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
|
||||
PlayerPerspective that = (PlayerPerspective) o;
|
||||
return slug != null && Objects.equals(slug, that.slug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public class Theme {
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
|
||||
Theme theme = (Theme) o;
|
||||
return slug != null && Objects.equals(slug, theme.slug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,16 @@ import java.util.Optional;
|
||||
@Service
|
||||
public class IgdbWrapper {
|
||||
|
||||
private static final int MAIN_GAME_CATEGORY_VALUE = 0;
|
||||
|
||||
@Value("${gameyfin.igdb.api.client-id}")
|
||||
private String clientId;
|
||||
|
||||
@Value("${gameyfin.igdb.api.client-secret}")
|
||||
private String clientSecret;
|
||||
|
||||
@Value("${gameyfin.igdb.config.preferred-platform}")
|
||||
private int preferredPlatform;
|
||||
@Value("${gameyfin.igdb.config.preferred-platforms:6}")
|
||||
private String preferredPlatforms;
|
||||
|
||||
@Autowired
|
||||
private WebClient.Builder webclientBuilder;
|
||||
@@ -64,14 +66,10 @@ public class IgdbWrapper {
|
||||
log.info("Successfully authenticated.");
|
||||
}
|
||||
|
||||
public Igdb.Game findGameByTitle(String title) {
|
||||
return searchForGameByTitle(title).orElseThrow(() -> new RuntimeException("Could not find game with title: \"%s\"".formatted(title)));
|
||||
}
|
||||
|
||||
public Optional<Igdb.Game> getGameById(Long id) {
|
||||
Igdb.GameResult gameResult = igdbApiClient.post()
|
||||
.uri("games.pb")
|
||||
.bodyValue("fields *; where id = %d; limit 1;".formatted(id))
|
||||
.bodyValue("fields *; where id = %d & category = %d; limit 1;".formatted(id, MAIN_GAME_CATEGORY_VALUE))
|
||||
.retrieve()
|
||||
.bodyToMono(Igdb.GameResult.class)
|
||||
.block();
|
||||
@@ -81,23 +79,10 @@ public class IgdbWrapper {
|
||||
return Optional.of(gameResult.getGames(0));
|
||||
}
|
||||
|
||||
private void initIgdbClient() {
|
||||
if (accessToken == null) {
|
||||
authenticate();
|
||||
}
|
||||
|
||||
igdbApiClient = webclientBuilder
|
||||
.baseUrl("https://api.igdb.com/v4/")
|
||||
.defaultHeader("Client-ID", clientId)
|
||||
.defaultHeader("Authorization", "Bearer %s".formatted(accessToken.getAccessToken()))
|
||||
.filter(WebClientConfig.fixProtobufContentTypeInterceptor())
|
||||
.build();
|
||||
}
|
||||
|
||||
private Optional<Igdb.Game> searchForGameByTitle(String searchTerm) {
|
||||
public Optional<Igdb.Game> searchForGameByTitle(String searchTerm) {
|
||||
Igdb.GameResult gameResult = igdbApiClient.post()
|
||||
.uri("games.pb")
|
||||
.bodyValue("fields *; search \"%s\";".formatted(searchTerm))
|
||||
.bodyValue("fields *; search \"%s\"; where platforms = (%s) & category = %d;".formatted(searchTerm, preferredPlatforms, MAIN_GAME_CATEGORY_VALUE))
|
||||
.retrieve()
|
||||
.bodyToMono(Igdb.GameResult.class)
|
||||
.block();
|
||||
@@ -106,6 +91,9 @@ public class IgdbWrapper {
|
||||
|
||||
List<Igdb.Game> games = gameResult.getGamesList();
|
||||
|
||||
// If we only get one game, we don't have to check for exact matches, so return it directly
|
||||
if(games.size() == 1) return Optional.ofNullable(games.get(0));
|
||||
|
||||
// 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
|
||||
// This will filter out most DLCs and similiar stuff, but will detect a game even when your search term is not exactly the title
|
||||
@@ -122,4 +110,17 @@ public class IgdbWrapper {
|
||||
|
||||
return Optional.of(games.get(0));
|
||||
}
|
||||
|
||||
private void initIgdbClient() {
|
||||
if (accessToken == null) {
|
||||
authenticate();
|
||||
}
|
||||
|
||||
igdbApiClient = webclientBuilder
|
||||
.baseUrl("https://api.igdb.com/v4/")
|
||||
.defaultHeader("Client-ID", clientId)
|
||||
.defaultHeader("Authorization", "Bearer %s".formatted(accessToken.getAccessToken()))
|
||||
.filter(WebClientConfig.fixProtobufContentTypeInterceptor())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.entities.Category;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.util.ProtobufUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class GameMapper {
|
||||
|
||||
public static DetectedGame toDetectedGame(Igdb.Game g, Path path) {
|
||||
List<Igdb.MultiplayerMode> multiplayerModes = g.getMultiplayerModesList();
|
||||
List<Long> screenshotIds = g.getScreenshotsList().stream().map(Igdb.Screenshot::getId).toList();
|
||||
List<Long> videoIds = g.getVideosList().stream().map(Igdb.GameVideo::getId).toList();
|
||||
|
||||
return DetectedGame.builder()
|
||||
.slug(g.getSlug())
|
||||
.title(g.getName())
|
||||
.summary(g.getSummary())
|
||||
.releaseDate(ProtobufUtils.toInstant(g.getFirstReleaseDate()))
|
||||
.userRating((int) g.getRating())
|
||||
.criticsRating((int) g.getAggregatedRating())
|
||||
.totalRating((int) g.getTotalRating())
|
||||
//.category()
|
||||
.offlineCoop(hasOfflineCoop(multiplayerModes))
|
||||
.onlineCoop(hasOnlineCoop(multiplayerModes))
|
||||
.lanSupport(hasLanSupport(multiplayerModes))
|
||||
.maxPlayers(getMaxPlayers(multiplayerModes))
|
||||
.coverId(g.getCover().getId())
|
||||
.screenshotIds(screenshotIds)
|
||||
.videoIds(videoIds)
|
||||
//.companies()
|
||||
//.genres()
|
||||
//.keywords()
|
||||
//.themes()
|
||||
//.playerPerspectives()
|
||||
.path(path.toString())
|
||||
.isFolder(path.toFile().isDirectory())
|
||||
.build();
|
||||
}
|
||||
|
||||
private static boolean hasOfflineCoop(List<Igdb.MultiplayerMode> modes) {
|
||||
return modes.stream().anyMatch(Igdb.MultiplayerMode::getOfflinecoop);
|
||||
}
|
||||
|
||||
private static boolean hasLanSupport(List<Igdb.MultiplayerMode> modes) {
|
||||
return modes.stream().anyMatch(Igdb.MultiplayerMode::getLancoop);
|
||||
}
|
||||
|
||||
private static boolean hasOnlineCoop(List<Igdb.MultiplayerMode> modes) {
|
||||
return modes.stream().anyMatch(Igdb.MultiplayerMode::getOnlinecoop);
|
||||
}
|
||||
|
||||
private static int getMaxPlayers(List<Igdb.MultiplayerMode> modes) {
|
||||
return modes.stream().mapToInt(Igdb.MultiplayerMode::getOnlinecoopmax).max().orElse(0);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
public class GameyfinDevController {
|
||||
@@ -28,13 +28,8 @@ public class GameyfinDevController {
|
||||
|
||||
@GetMapping(value = "/dev/findGameByTitle/{title}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public GameDto findGameByTitle(@PathVariable("title") String title) {
|
||||
Igdb.Game game;
|
||||
|
||||
try {
|
||||
game = igdbWrapper.findGameByTitle(title);
|
||||
} catch (Exception e) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
|
||||
}
|
||||
Igdb.Game game = igdbWrapper.searchForGameByTitle(title)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find game with title: \"%s\"".formatted(title)));
|
||||
|
||||
return GameDto.builder()
|
||||
.name(game.getName())
|
||||
@@ -44,15 +39,8 @@ public class GameyfinDevController {
|
||||
|
||||
@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)));
|
||||
Igdb.Game game = igdbWrapper.getGameById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find game with id: %d".formatted(id)));
|
||||
|
||||
return GameDto.builder()
|
||||
.name(game.getName())
|
||||
@@ -67,8 +55,9 @@ public class GameyfinDevController {
|
||||
|
||||
@GetMapping(value = "/dev/games", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public List<GameDto> getAllGames() {
|
||||
return filesystemService.getGameFileNames().stream()
|
||||
.map(t -> igdbWrapper.findGameByTitle(t))
|
||||
return filesystemService.getGameFileNames().parallelStream()
|
||||
.map(t -> igdbWrapper.searchForGameByTitle(t).orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.map(g -> GameDto.builder()
|
||||
.name(g.getName())
|
||||
.releaseDate(ProtobufUtils.toInstant(g.getFirstReleaseDate()))
|
||||
|
||||
@@ -6,7 +6,7 @@ gameyfin:
|
||||
file-extensions: iso, zip, rar, 7z, exe
|
||||
igdb:
|
||||
config:
|
||||
preferred-platform: 6
|
||||
preferred-platforms: 6
|
||||
api:
|
||||
client-id: ""
|
||||
client-secret: ""
|
||||
Reference in New Issue
Block a user