mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Start development of v2
This commit is contained in:
+16
-9
@@ -1,8 +1,10 @@
|
||||
node_modules
|
||||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
@@ -12,12 +14,18 @@ target/
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
@@ -25,16 +33,15 @@ target/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
/.mvn/
|
||||
|
||||
### Kotlin ###
|
||||
.kotlin
|
||||
|
||||
### Custom ###
|
||||
/data/
|
||||
/backend/src/main/resources/static/
|
||||
/docker/docker-compose.yml
|
||||
/.gameyfin/
|
||||
/.gameyfin/
|
||||
@@ -1,5 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Angular Application" type="JavascriptDebugType" uri="http://localhost:4200" useFirstLineBreakpoints="true">
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1,12 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Angular CLI Server" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/frontend/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="start" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1,46 +1,43 @@
|
||||
<div align="center">
|
||||
<h2>Note: v2 is in a very early stage</h2>
|
||||
<h3>Most features are missing, existing features will probably break!</h3>
|
||||
|
||||
</br></br></br>
|
||||
<img src="assets/Gameyfin_Logo_White_Border.svg" height="128px" width="auto" alt="Gameyfin Logo">
|
||||
<h1>Gameyfin</h1>
|
||||
<p align="center">A simple game library manager.</p>
|
||||
<p align="center">A simple game library manager. Now even better</p>
|
||||
</div>
|
||||
|
||||
# Overview
|
||||
|
||||
Name and functionality inspired by [Jellyfin](https://jellyfin.org/).
|
||||
|
||||
## Video
|
||||
|
||||
Click [this link](https://youtu.be/BSaccEm0tpo) to watch how to install and set up Gameyfin on your machine.
|
||||
|
||||
## Features
|
||||
|
||||
* Automatically scans your game library folder and downloads additional metadata from IGDB
|
||||
* Access your library via your Web-Browser
|
||||
* Download games directly from your browser
|
||||
* LAN-friendly (everything is cached locally)
|
||||
* Native Docker support (alternatively it's only one .jar file to run on bare metal)
|
||||
* Light and dark theme
|
||||
✨ Automatically scans and indexes your game libraries
|
||||
⬇️ Access your library via your web browser & download games directly from there
|
||||
👥 Share your library with friends & family
|
||||
⚛️ LAN-friendly (everything is cached locally)
|
||||
🐋 Runs in a container or as single <binary file / JAR file> on bare metal
|
||||
🌈 Themes (including light and dark mode)
|
||||
🔌 Easily expandable with plugins
|
||||
🔒 Integrates into your SSO solution via OAuth2 / OpenID Connect
|
||||
|
||||
## Preview
|
||||
## Screenshots
|
||||
|
||||
https://user-images.githubusercontent.com/9295182/197277953-d69464a4-d280-407b-9274-ae62e6917981.mp4
|
||||
`Work in progress`
|
||||
|
||||
## Installation
|
||||
|
||||
### General
|
||||
`Work in progress`
|
||||
|
||||
Since Gameyfin loads information from IGDB, you need to register yourself there. Follow [this guide](https://api-docs.igdb.com/#account-creation).
|
||||
## Contribute to Gameyfin
|
||||
|
||||
### Docker
|
||||
`Work in progress`
|
||||
|
||||
1. Download the `docker-compose.example.yml` file from this repository and rename it to just `docker-compose.yml`
|
||||
2. Edit the configuration values to your liking
|
||||
3. Run `docker-compose up -d`
|
||||
|
||||
### Bare metal
|
||||
|
||||
1. Make sure you have a JRE or JDK with version 18 or greater installed
|
||||
2. Download the latest `gameyfin.jar` and `gameyfin.properties` file from the releases page
|
||||
3. Edit the config options in the `gameyfin.properties` file
|
||||
4. Use the following command to start Gameyfin: `java -jar gameyfin.jar`
|
||||
5. Open the address of your Gameyfin host in your browser, Gameyfin runs under port 8080 by default
|
||||
Gameyfin v2 is written in Kotlin and uses the following libraries/frameworks:
|
||||
* Spring Boot 3 for the backend
|
||||
* Vaadin for the frontend
|
||||
* PF4J for the plugin system
|
||||
* H2 database for persistence
|
||||
* Micrometer and Prometheus for monitoring
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 110 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
-288
@@ -1,288 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<artifactId>gameyfin</artifactId>
|
||||
<groupId>de.grimsi</groupId>
|
||||
<version>1.4.6-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>gameyfin-backend</artifactId>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
|
||||
<springdoc-openapi-ui.version>1.7.0</springdoc-openapi-ui.version>
|
||||
<resilience4j.version>2.2.0</resilience4j.version>
|
||||
<commons-io.version>2.15.1</commons-io.version>
|
||||
<commons-compress.version>1.21</commons-compress.version>
|
||||
<protoc.plugin.version>3.11.4</protoc.plugin.version>
|
||||
<protobuf-java.version>3.25.2</protobuf-java.version>
|
||||
<easy-random.version>5.0.0</easy-random.version>
|
||||
<easy-random-protobuf.version>0.4.0</easy-random-protobuf.version>
|
||||
<jacoco-maven-plugin.version>0.8.11</jacoco-maven-plugin.version>
|
||||
|
||||
<!-- Use older version because the newer versions have problems with non-ASCII characters in properties files (umlauts etc.) -->
|
||||
<maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Frontend -->
|
||||
<dependency>
|
||||
<groupId>de.grimsi</groupId>
|
||||
<artifactId>gameyfin-frontend</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenAPI -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-ui</artifactId>
|
||||
<version>${springdoc-openapi-ui.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Webclient Rate limiter -->
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-reactor</artifactId>
|
||||
<version>${resilience4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-ratelimiter</artifactId>
|
||||
<version>${resilience4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.resilience4j</groupId>
|
||||
<artifactId>resilience4j-bulkhead</artifactId>
|
||||
<version>${resilience4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Persistence -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- File handling -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>${commons-io.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Protobuf dependencies -->
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf-java.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java-util</artifactId>
|
||||
<version>${protobuf-java.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jeasy</groupId>
|
||||
<artifactId>easy-random-core</artifactId>
|
||||
<version>${easy-random.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- Temporarily disabled due to a StackOverflow bug: https://github.com/murdos/easy-random-protobuf/issues/136
|
||||
<dependency>
|
||||
<groupId>io.github.murdos</groupId>
|
||||
<artifactId>easy-random-protobuf</artifactId>
|
||||
<version>${easy-random-protobuf.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>-->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.jimfs</groupId>
|
||||
<artifactId>jimfs</artifactId>
|
||||
<version>1.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Dev -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>gameyfin-${project.parent.version}</finalName>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${basedir}/src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>**/*.properties</include>
|
||||
<include>**/*.yml</include>
|
||||
<include>**/*.yaml</include>
|
||||
<include>**/*.sql</include>
|
||||
<include>**/*.txt</include>
|
||||
<include>**/*.json</include>
|
||||
<include>**/*.js</include>
|
||||
<include>**/*.css</include>
|
||||
<include>**/*.html</include>
|
||||
<include>**/*.ico</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Import the compiled frontend -->
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>${maven-resources-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-resources</id>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${basedir}/src/main/resources/static</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${project.parent.basedir}/frontend/dist/frontend/</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Protobuf source generation plugin -->
|
||||
<plugin>
|
||||
<groupId>com.github.os72</groupId>
|
||||
<artifactId>protoc-jar-maven-plugin</artifactId>
|
||||
<version>${protoc.plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>generate-sources</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<includeStdTypes>true</includeStdTypes>
|
||||
<optimizeCodegen>false</optimizeCodegen>
|
||||
<inputDirectories>
|
||||
<include>${project.basedir}/src/main/resources/proto</include>
|
||||
</inputDirectories>
|
||||
<outputTargets>
|
||||
<outputTarget>
|
||||
<type>java</type>
|
||||
<outputDirectory>${project.build.directory}/generated-sources/protobuf
|
||||
</outputDirectory>
|
||||
</outputTarget>
|
||||
</outputTargets>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- JaCoCo coverage report -->
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>${jacoco-maven-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>jacoco-initialize</id>
|
||||
<goals>
|
||||
<goal>prepare-agent</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>jacoco-site</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>report</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -1,18 +0,0 @@
|
||||
package de.grimsi.gameyfin;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
public class GameyfinApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
new SpringApplicationBuilder(GameyfinApplication.class)
|
||||
.properties( "file.encoding=UTF-8", "spring.config.name=application,gameyfin,database,secure")
|
||||
.build()
|
||||
.run(args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package de.grimsi.gameyfin.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
|
||||
/**
|
||||
* This class holds configuration for the default {@link java.nio.file.spi.FileSystemProvider} used by Gameyfin.
|
||||
*/
|
||||
@Configuration
|
||||
public class FileSystemProviderConfig {
|
||||
/**
|
||||
* Configures the default {@link FileSystem} to be used.
|
||||
* This makes it easier to mock certain classes in unit tests.
|
||||
* @return the default FileSystem
|
||||
*/
|
||||
@Bean
|
||||
public FileSystem defaultFileSystem() {
|
||||
return FileSystems.getDefault();
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package de.grimsi.gameyfin.config;
|
||||
|
||||
import de.grimsi.gameyfin.service.FilesystemService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class contains logic to the configuration of the filesystem which Gameyfin works on.
|
||||
* It handles creating required folders, setting up paths for Gameyfin and setting the correct properties of those folders.
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class FilesystemConfig {
|
||||
|
||||
@Value("${gameyfin.folders.data}")
|
||||
private String dataFolderPath;
|
||||
|
||||
private final FilesystemService filesystemService;
|
||||
|
||||
/**
|
||||
* This will create the cache folder for Gameyfin.
|
||||
* The path of this folder is specified in the "gameyfin.cache" parameter which is either:
|
||||
* 1. Derived from the first configured library folder path or
|
||||
* 2. Explicitly set by the user
|
||||
* <p>
|
||||
* For more details see {@link GameyfinFolderConfig#setConfigurableEnvironment(ConfigurableEnvironment)}
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@Order(1)
|
||||
public void createCacheFolder() {
|
||||
filesystemService.createCacheFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* This will make sure that the internal folder (".gameyfin") is marked as hidden on DOS/Windows-based systems.
|
||||
* On UNIX-based systems files and folders starting with a dot are hidden
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
@Order(2)
|
||||
public void hideInternalFolderOnDOS() throws IOException {
|
||||
if (!isRunningOnWindows()) return;
|
||||
|
||||
Path internalFolder = filesystemService.getPath(dataFolderPath);
|
||||
|
||||
if (!Files.exists(internalFolder) || !Files.isDirectory(internalFolder)) return;
|
||||
|
||||
Files.setAttribute(internalFolder, "dos:hidden", Boolean.TRUE, LinkOption.NOFOLLOW_LINKS);
|
||||
}
|
||||
|
||||
private boolean isRunningOnWindows() {
|
||||
return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows");
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package de.grimsi.gameyfin.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Configuration
|
||||
public class FrontendConfig implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("classpath:/static/")
|
||||
.resourceChain(true)
|
||||
.addResolver(new PathResourceResolver() {
|
||||
@Override
|
||||
protected Resource getResource(@NonNull String resourcePath, @NonNull Resource location) throws IOException {
|
||||
Resource requestedResource = location.createRelative(resourcePath);
|
||||
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource : new ClassPathResource("/static/index.html");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package de.grimsi.gameyfin.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.*;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Arrays;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* This class handles the creation of all folders used by Gameyfin.
|
||||
* It also stores their paths in Spring environment variables.
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class GameyfinFolderConfig {
|
||||
|
||||
|
||||
@Value("${gameyfin.internal-folder}")
|
||||
private String INTERNAL_FOLDER_NAME;
|
||||
|
||||
/**
|
||||
* The following SpEL expression will:
|
||||
* 1. Split the comma-seperated string contained in "gameyfin.sources" into elements
|
||||
* 2. Take the first element
|
||||
* 3. Assign its value to the variable
|
||||
*/
|
||||
@Value("#{'${gameyfin.sources}'.split(',')[0]}")
|
||||
private String firstLibraryPath;
|
||||
|
||||
@Value("${gameyfin.folders.data}")
|
||||
private String dataFolderPath;
|
||||
|
||||
private final Environment env;
|
||||
|
||||
/**
|
||||
* Dynamically sets the "gameyfin.db" and "gameyfin.cache" properties
|
||||
* if the "gameyfin.folders.data" property is *not* set by the user (default).
|
||||
*
|
||||
* @param env - The application environment, provided by the Spring container
|
||||
*/
|
||||
@Autowired
|
||||
public void setConfigurableEnvironment(ConfigurableEnvironment env) {
|
||||
Properties props = new Properties();
|
||||
|
||||
if (!StringUtils.hasText(dataFolderPath)) {
|
||||
|
||||
//set the data folder property, so it can be referenced at runtime
|
||||
props.setProperty("gameyfin.folders.data", "%s/%s".formatted(firstLibraryPath, INTERNAL_FOLDER_NAME));
|
||||
|
||||
props.setProperty("gameyfin.db", "%s/%s/db".formatted(firstLibraryPath, INTERNAL_FOLDER_NAME));
|
||||
props.setProperty("gameyfin.cache", "%s/%s/cache".formatted(firstLibraryPath, INTERNAL_FOLDER_NAME));
|
||||
} else {
|
||||
|
||||
props.setProperty("gameyfin.db", "%s/%s/db".formatted(dataFolderPath, INTERNAL_FOLDER_NAME));
|
||||
props.setProperty("gameyfin.cache", "%s/%s/cache".formatted(dataFolderPath, INTERNAL_FOLDER_NAME));
|
||||
}
|
||||
|
||||
env.getPropertySources().addFirst(new PropertiesPropertySource("gameyfinFilesystemProperties", props));
|
||||
}
|
||||
|
||||
/**
|
||||
* This bean is needed so Spring initializes the data source after we are done messing with the configuration environment
|
||||
*
|
||||
* @return DataSource
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.datasource")
|
||||
@Bean
|
||||
@Primary
|
||||
public DataSource getDataSource() {
|
||||
Properties properties = loadAllProperties();
|
||||
|
||||
return DataSourceBuilder
|
||||
.create()
|
||||
.url(properties.getProperty("spring.datasource.url"))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Properties loadAllProperties() {
|
||||
Properties props = new Properties();
|
||||
|
||||
MutablePropertySources propSrcs = ((AbstractEnvironment) env).getPropertySources();
|
||||
|
||||
StreamSupport.stream(propSrcs.spliterator(), false)
|
||||
.filter(EnumerablePropertySource.class::isInstance)
|
||||
.map(ps -> ((EnumerablePropertySource<?>) ps).getPropertyNames())
|
||||
.flatMap(Arrays::stream)
|
||||
.forEach(propName -> props.setProperty(propName, env.getProperty(propName)));
|
||||
|
||||
return props;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package de.grimsi.gameyfin.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PropertiesLoaderUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Configuration
|
||||
public class SecureProperties {
|
||||
|
||||
@Autowired
|
||||
public void setConfigurableEnvironment(ConfigurableEnvironment env) {
|
||||
try {
|
||||
Resource resource = new ClassPathResource("/config/secure.yml");
|
||||
env.getPropertySources().addFirst(new PropertiesPropertySource(Objects.requireNonNull(resource.getFilename()), PropertiesLoaderUtils.loadProperties(resource)));
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package de.grimsi.gameyfin.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Value("${gameyfin.user}")
|
||||
private String username;
|
||||
|
||||
@Value("${gameyfin.password}")
|
||||
private String password;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.csrf(AbstractHttpConfigurer::disable);
|
||||
http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable));
|
||||
http.httpBasic(Customizer.withDefaults());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public InMemoryUserDetailsManager userDetailsService() {
|
||||
UserDetails user = User
|
||||
.builder()
|
||||
.username(username)
|
||||
.password("{noop}" + password) // FIXME: not very secure
|
||||
.authorities("ADMIN_API_ACCESS")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
|
||||
@Bean
|
||||
CorsConfigurationSource corsConfigurationSource() {
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
|
||||
return source;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package de.grimsi.gameyfin.config;
|
||||
|
||||
import io.github.resilience4j.bulkhead.Bulkhead;
|
||||
import io.github.resilience4j.bulkhead.BulkheadConfig;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiter;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.transport.logging.AdvancedByteBufFormat;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Slf4j
|
||||
@Getter
|
||||
@Configuration
|
||||
public class WebClientConfig implements WebClientCustomizer {
|
||||
|
||||
private final RateLimiter igdbRateLimiter;
|
||||
private final Bulkhead igdbConcurrencyLimiter;
|
||||
|
||||
|
||||
public WebClientConfig(@Value("${gameyfin.igdb.api.max-concurrent-requests}") int maxConcurrentRequestsToIgdb,
|
||||
@Value("${gameyfin.igdb.api.max-requests-per-second}") int maxRequestsPerSecondToIgdb) {
|
||||
|
||||
log.info("IGDB API connection properties: max. {} req/s, max. {} concurrent requests", maxRequestsPerSecondToIgdb, maxConcurrentRequestsToIgdb);
|
||||
|
||||
igdbRateLimiter = RateLimiter.of("igdb-rate-limiter",
|
||||
RateLimiterConfig.custom()
|
||||
.limitForPeriod(maxRequestsPerSecondToIgdb)
|
||||
.limitRefreshPeriod(Duration.ofSeconds(1))
|
||||
.timeoutDuration(Duration.ofMinutes(1))
|
||||
.build());
|
||||
|
||||
igdbConcurrencyLimiter = Bulkhead.of("igdb-concurrency-limiter",
|
||||
BulkheadConfig.custom()
|
||||
.maxConcurrentCalls(maxConcurrentRequestsToIgdb)
|
||||
.maxWaitDuration(Duration.ofMinutes(1))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(WebClient.Builder webClientBuilder) {
|
||||
HttpClient httpClient = HttpClient.create()
|
||||
.wiretap(this.getClass().getCanonicalName(), LogLevel.TRACE, AdvancedByteBufFormat.TEXTUAL) // Enable full request / response logging in TRACE
|
||||
.proxyWithSystemProperties(); // Enable use of system proxy
|
||||
|
||||
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() {
|
||||
return ExchangeFilterFunction.ofResponseProcessor(clientResponse ->
|
||||
Mono.just(clientResponse.mutate()
|
||||
.headers(headers -> headers.remove(HttpHeaders.CONTENT_TYPE))
|
||||
.header(HttpHeaders.CONTENT_TYPE, String.valueOf(ProtobufHttpMessageConverter.PROTOBUF))
|
||||
.build())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package de.grimsi.gameyfin.config.properties;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// see https://stackoverflow.com/questions/26699385/spring-boot-yaml-configuration-for-a-list-of-strings
|
||||
@ConfigurationProperties("gameyfin")
|
||||
// Technically SonarQube is correct, but I like to keep the lowercase method names since it correlates better with the config keys
|
||||
@SuppressWarnings("java:S3010")
|
||||
public record GameyfinProperties(
|
||||
folders folders,
|
||||
List<String> fileExtensions,
|
||||
List<String> fileSuffixes,
|
||||
igdb igdb) {
|
||||
|
||||
public record folders(String data) {}
|
||||
|
||||
|
||||
public record igdb(config config) {
|
||||
public record config(List<Integer> preferredPlatforms) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package de.grimsi.gameyfin.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AutocompleteSuggestionDto {
|
||||
private String slug;
|
||||
private String title;
|
||||
private Instant releaseDate;
|
||||
private List<String> platforms;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package de.grimsi.gameyfin.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class GameOverviewDto {
|
||||
private String slug;
|
||||
private String title;
|
||||
private String coverId;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package de.grimsi.gameyfin.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ImageDownloadResultDto {
|
||||
private int coverDownloads;
|
||||
private int screenshotDownloads;
|
||||
private int companyLogoDownloads;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package de.grimsi.gameyfin.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LibraryScanRequestDto {
|
||||
private String path;
|
||||
private boolean downloadImages;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package de.grimsi.gameyfin.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class LibraryScanResult {
|
||||
private int newGames;
|
||||
private int deletedGames;
|
||||
private int newUnmappableFiles;
|
||||
private int totalGames;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package de.grimsi.gameyfin.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LibraryScanResultDto {
|
||||
private int newGames;
|
||||
private int deletedGames;
|
||||
private int newUnmappableFiles;
|
||||
private int totalGames;
|
||||
private int coverDownloads;
|
||||
private int screenshotDownloads;
|
||||
private int companyLogoDownloads;
|
||||
private int scanDuration;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.grimsi.gameyfin.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PathToSlugDto {
|
||||
private String path;
|
||||
private String slug;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
public class Company {
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
private String 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();
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
public class DetectedGame {
|
||||
|
||||
// Game properties
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "CLOB")
|
||||
private String summary;
|
||||
|
||||
private Instant releaseDate;
|
||||
|
||||
private Integer userRating;
|
||||
|
||||
private Integer criticsRating;
|
||||
|
||||
private Integer totalRating;
|
||||
|
||||
private String category;
|
||||
|
||||
private boolean offlineCoop;
|
||||
|
||||
private boolean onlineCoop;
|
||||
|
||||
private boolean lanSupport;
|
||||
|
||||
private int maxPlayers;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String coverId;
|
||||
|
||||
@ElementCollection
|
||||
private List<String> screenshotIds;
|
||||
|
||||
@ElementCollection
|
||||
private List<String> videoIds;
|
||||
|
||||
@ManyToMany(cascade = CascadeType.MERGE)
|
||||
@ToString.Exclude
|
||||
private List<Company> companies;
|
||||
|
||||
@ManyToMany(cascade = CascadeType.MERGE)
|
||||
@ToString.Exclude
|
||||
private List<Genre> genres;
|
||||
|
||||
@ManyToMany(cascade = CascadeType.MERGE)
|
||||
@ToString.Exclude
|
||||
private List<Keyword> keywords;
|
||||
|
||||
@ManyToMany(cascade = CascadeType.MERGE)
|
||||
@ToString.Exclude
|
||||
private List<Theme> themes;
|
||||
|
||||
@ManyToMany(cascade = CascadeType.MERGE)
|
||||
@ToString.Exclude
|
||||
private List<PlayerPerspective> playerPerspectives;
|
||||
|
||||
@ManyToMany(cascade = CascadeType.MERGE)
|
||||
@ToString.Exclude
|
||||
private List<Platform> platforms;
|
||||
|
||||
@ManyToOne(cascade = CascadeType.MERGE)
|
||||
@JoinColumn(name = "library")
|
||||
@ToString.Exclude
|
||||
private Library library;
|
||||
|
||||
// Technical properties
|
||||
@Column(nullable = false)
|
||||
private String path;
|
||||
|
||||
@Column(nullable = false)
|
||||
private long diskSize;
|
||||
|
||||
@Column(columnDefinition = "boolean default false")
|
||||
private boolean confirmedMatch;
|
||||
|
||||
@CreationTimestamp
|
||||
private Instant addedToLibrary;
|
||||
|
||||
@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,35 +0,0 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@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();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@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();
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
public class Library {
|
||||
|
||||
@Id
|
||||
private String path;
|
||||
|
||||
@ManyToMany(cascade = CascadeType.MERGE)
|
||||
@ToString.Exclude
|
||||
private List<Platform> platforms;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
|
||||
Library library = (Library) o;
|
||||
return path != null && Objects.equals(path, library.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
public class Platform {
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
private String logoId;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
|
||||
Platform platform = (Platform) o;
|
||||
return slug != null && Objects.equals(slug, platform.slug);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@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();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
@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();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package de.grimsi.gameyfin.entities;
|
||||
|
||||
import lombok.*;
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
public class UnmappableFile {
|
||||
|
||||
public UnmappableFile(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "HIBERNATE_SEQUENCE")
|
||||
@SequenceGenerator(name = "HIBERNATE_SEQUENCE", allocationSize = 1)
|
||||
private Long id;
|
||||
|
||||
private String path;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
|
||||
UnmappableFile that = (UnmappableFile) o;
|
||||
return path != null && Objects.equals(path, that.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getClass().hashCode();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package de.grimsi.gameyfin.exceptions;
|
||||
|
||||
public class DownloadAbortedException extends RuntimeException {
|
||||
public DownloadAbortedException() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package de.grimsi.gameyfin.igdb;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class IgdbApiProperties {
|
||||
public static final String ENDPOINT_GAMES_PROTOBUF = "games.pb";
|
||||
public static final String ENDPOINT_PLATFORMS_PROTOBUF = "platforms.pb";
|
||||
|
||||
private static final List<String> GAME_QUERY_FIELDS = List.of(
|
||||
"slug", "name", "summary", "first_release_date", "rating", "aggregated_rating", "total_rating", "category",
|
||||
"multiplayer_modes.lancoop", "multiplayer_modes.onlinecoop", "multiplayer_modes.offlinecoop", "multiplayer_modes.onlinemax",
|
||||
"cover.image_id", "screenshots.image_id", "videos.video_id",
|
||||
"involved_companies.company.slug", "involved_companies.company.name", "involved_companies.company.logo.image_id",
|
||||
"genres.slug", "genres.name",
|
||||
"keywords.slug", "keywords.name",
|
||||
"themes.slug", "themes.name",
|
||||
"player_perspectives.slug", "player_perspectives.name",
|
||||
"platforms.slug", "platforms.name", "platforms.platform_logo.image_id"
|
||||
);
|
||||
|
||||
public static final String GAME_QUERY_FIELDS_STRING = String.join(",", GAME_QUERY_FIELDS);
|
||||
|
||||
public static final String IMAGES_BASE_URL = "https://images.igdb.com/igdb/image/upload/";
|
||||
|
||||
public static final String COVER_IMAGE_SIZE = "cover_big";
|
||||
public static final String SCREENSHOT_IMAGE_SIZE = "screenshot_med";
|
||||
public static final String LOGO_IMAGE_SIZE = "logo_med";
|
||||
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
package de.grimsi.gameyfin.igdb;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Builder for fluent building of igdb api queries.
|
||||
*/
|
||||
public class IgdbApiQueryBuilder {
|
||||
private final StringBuilder stringBuilder;
|
||||
private String search;
|
||||
private String fields;
|
||||
private String limit;
|
||||
private String where;
|
||||
private String sort;
|
||||
|
||||
public IgdbApiQueryBuilder() {
|
||||
this.stringBuilder = new StringBuilder();
|
||||
this.fields = "fields *;";
|
||||
this.search = "";
|
||||
this.limit = "";
|
||||
this.where = "";
|
||||
this.sort = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} that are concatenated through the `&` operator.
|
||||
* This condition produces `(condition & condition & condition)`.
|
||||
*
|
||||
* @param conditions multiple conditions
|
||||
* @return an {@link AndCondition}
|
||||
*/
|
||||
public static Condition and(Condition... conditions) {
|
||||
return new AndCondition(conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} that are concatenated through the `|` operator.
|
||||
* This condition produces `(condition | condition | condition)`.
|
||||
*
|
||||
* @param conditions multiple conditions
|
||||
* @return an {@link OrCondition}
|
||||
*/
|
||||
public static Condition or(Condition... conditions) {
|
||||
return new OrCondition(conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to look for string values in a list.
|
||||
* This condition produces `field = ("val1","val2")`.
|
||||
*
|
||||
* @param field a field to search through
|
||||
* @param values the string values
|
||||
* @return an {@link InCondition}
|
||||
*/
|
||||
public static Condition in(String field, String... values) {
|
||||
return new InCondition(field, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to look for number values in a list.
|
||||
* This condition produces `field = (1,2,3)`.
|
||||
*
|
||||
* @param field a field to search through
|
||||
* @param values the number values
|
||||
* @return an {@link InCondition}
|
||||
*/
|
||||
public static Condition in(String field, Number... values) {
|
||||
return new InCondition(field, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to look for values in a collection.
|
||||
* This condition produces `field = ("val1","val2")` if a collection of strings is passed.
|
||||
* This condition produces `field = (1,2,3)` if a collection of numbers is passed.
|
||||
*
|
||||
* @param field a field to search through.
|
||||
* @param values a collection of values.
|
||||
* @return an {@link InCondition}.
|
||||
*/
|
||||
public static Condition in(String field, Collection<?> values) {
|
||||
return new InCondition(field, values.toArray(new Object[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to filter for matching string values.
|
||||
* This condition produces `field = "value"`.
|
||||
*
|
||||
* @param field a field to search through.
|
||||
* @param value a value for comparison.
|
||||
* @return an {@link InCondition}.
|
||||
*/
|
||||
public static Condition equal(String field, String value) {
|
||||
return new EqualsCondition(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to filter for matching number values.
|
||||
* This condition produces `field = 123`.
|
||||
*
|
||||
* @param field a field to search through.
|
||||
* @param value a value for comparison.
|
||||
* @return an {@link InCondition}.
|
||||
*/
|
||||
public static Condition equal(String field, Number value) {
|
||||
return new EqualsCondition(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to filter for non-matching string values.
|
||||
* This condition produces `field != "value"`.
|
||||
*
|
||||
* @param field a field to search through.
|
||||
* @param value a value for comparison.
|
||||
* @return an {@link InCondition}.
|
||||
*/
|
||||
public static Condition not(String field, String value) {
|
||||
return new NotCondition(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to filter for non-matching number values.
|
||||
* This condition produces `field != 123`.
|
||||
*
|
||||
* @param field a field to search through.
|
||||
* @param value a value for comparison.
|
||||
* @return an {@link InCondition}.
|
||||
*/
|
||||
public static Condition not(String field, Number value) {
|
||||
return new NotCondition(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to check if a value is bigger than the given value.
|
||||
* This condition produces `field > 123`.
|
||||
*
|
||||
* @param field a field to search through.
|
||||
* @param value a value for comparison.
|
||||
* @return an {@link InCondition}.
|
||||
*/
|
||||
public static Condition greater(String field, Number value) {
|
||||
return new GreaterThanCondition(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to check if a value is bigger or equal to the given value.
|
||||
* This condition produces `field >= 123`.
|
||||
*
|
||||
* @param field a field to search through.
|
||||
* @param value a value for comparison.
|
||||
* @return an {@link InCondition}.
|
||||
*/
|
||||
public static Condition greaterEquals(String field, Number value) {
|
||||
return new GreaterEqualsCondition(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to check if a value is smaller than the given value.
|
||||
* This condition produces `field < 123`.
|
||||
*
|
||||
* @param field a field to search through.
|
||||
* @param value a value for comparison.
|
||||
* @return an {@link InCondition}.
|
||||
*/
|
||||
public static Condition lesser(String field, Number value) {
|
||||
return new LessThanCondition(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Condition} to check if a value is smaller or equal to the given value.
|
||||
* This condition produces `field <= 123`.
|
||||
*
|
||||
* @param field a field to search through.
|
||||
* @param value a value for comparison.
|
||||
* @return an {@link InCondition}.
|
||||
*/
|
||||
public static Condition lesserEquals(String field, Number value) {
|
||||
return new LesserEqualsCondition(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the query string.
|
||||
*
|
||||
* @return an igdb compatible query
|
||||
*/
|
||||
public String build() {
|
||||
stringBuilder.append(search);
|
||||
stringBuilder.append(fields);
|
||||
stringBuilder.append(limit);
|
||||
stringBuilder.append(where);
|
||||
stringBuilder.append(sort);
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the `search "xyz";` query param.
|
||||
*
|
||||
* @param searchTerm a term to search for.
|
||||
* @return the builder
|
||||
*/
|
||||
public IgdbApiQueryBuilder search(String searchTerm) {
|
||||
this.search = "search \"%s\";".formatted(searchTerm);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the `fields abc,xyz;` query param.
|
||||
*
|
||||
* @param fields fields that should be returned (defaults to *).
|
||||
* @return the builder
|
||||
*/
|
||||
public IgdbApiQueryBuilder fields(String fields) {
|
||||
this.fields = "fields %s;".formatted(fields);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the `limit 1234;` query param.
|
||||
*
|
||||
* @param limit how many results should be returned.
|
||||
* @return the builder
|
||||
*/
|
||||
public IgdbApiQueryBuilder limit(int limit) {
|
||||
this.limit = "limit %d;".formatted(limit);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the `where xyz;` query param.
|
||||
*
|
||||
* @param condition a {@link Condition} object containing all conditions to filter the igdb db.
|
||||
* @return the builder
|
||||
*/
|
||||
public IgdbApiQueryBuilder where(Condition condition) {
|
||||
this.where = "where %s;".formatted(condition.build());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the `sort xyz asc;` query param.
|
||||
*
|
||||
* @param field a term to search for.
|
||||
* @param order the {@link SortOrder} (either ASC or DESC).
|
||||
* @return the builder
|
||||
*/
|
||||
public IgdbApiQueryBuilder sort(String field, SortOrder order) {
|
||||
this.sort = "sort %s %s;".formatted(field, order.value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort order enum for sorting query result.
|
||||
*/
|
||||
public enum SortOrder {
|
||||
ASC("asc"), DESC("desc");
|
||||
|
||||
public final String value;
|
||||
|
||||
SortOrder(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract condition object.
|
||||
*/
|
||||
public abstract static class Condition {
|
||||
protected static String wrap(String conditions) {
|
||||
return "(%s)".formatted(conditions);
|
||||
}
|
||||
|
||||
public abstract String build();
|
||||
}
|
||||
|
||||
/**
|
||||
* InCondition
|
||||
*/
|
||||
public static class InCondition extends Condition {
|
||||
private static final String PATTERN = "%s = (%s)";
|
||||
private final String field;
|
||||
private final String in;
|
||||
|
||||
public InCondition(String field, Object[] values) {
|
||||
this.field = field;
|
||||
if (Arrays.stream(values).anyMatch(String.class::isInstance))
|
||||
this.in = Arrays.stream(values).map("\"%s\""::formatted).collect(Collectors.joining(","));
|
||||
else if (Arrays.stream(values).anyMatch(Number.class::isInstance))
|
||||
this.in = Arrays.stream(values).map("%d"::formatted).collect(Collectors.joining(","));
|
||||
else this.in = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String build() {
|
||||
return PATTERN.formatted(field, in);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NotCondition
|
||||
*/
|
||||
public static class NotCondition extends OperatorCondition {
|
||||
private static final String OPERATOR = "!=";
|
||||
|
||||
public NotCondition(String field, String value) {
|
||||
super(field, OPERATOR, value);
|
||||
}
|
||||
|
||||
public NotCondition(String field, Number value) {
|
||||
super(field, OPERATOR, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EqualsCondition
|
||||
*/
|
||||
public static class EqualsCondition extends OperatorCondition {
|
||||
|
||||
private static final String OPERATOR = "=";
|
||||
|
||||
public EqualsCondition(String field, String value) {
|
||||
super(field, OPERATOR, value);
|
||||
}
|
||||
|
||||
public EqualsCondition(String field, Number value) {
|
||||
super(field, OPERATOR, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GreaterThanCondition
|
||||
*/
|
||||
public static class GreaterThanCondition extends OperatorCondition {
|
||||
|
||||
private static final String OPERATOR = ">";
|
||||
|
||||
public GreaterThanCondition(String field, Number value) {
|
||||
super(field, OPERATOR, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GreaterEqualsCondition
|
||||
*/
|
||||
public static class GreaterEqualsCondition extends OperatorCondition {
|
||||
|
||||
private static final String OPERATOR = ">=";
|
||||
|
||||
public GreaterEqualsCondition(String field, Number value) {
|
||||
super(field, OPERATOR, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LessThanCondition
|
||||
*/
|
||||
public static class LessThanCondition extends OperatorCondition {
|
||||
|
||||
private static final String OPERATOR = "<";
|
||||
|
||||
public LessThanCondition(String field, Number value) {
|
||||
super(field, OPERATOR, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LesserEqualsCondition
|
||||
*/
|
||||
public static class LesserEqualsCondition extends OperatorCondition {
|
||||
|
||||
private static final String OPERATOR = "<=";
|
||||
|
||||
public LesserEqualsCondition(String field, Number value) {
|
||||
super(field, OPERATOR, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OperatorCondition for inheritance
|
||||
*/
|
||||
public static class OperatorCondition extends Condition {
|
||||
private static final String PATTERN = "%s %s %s";
|
||||
private static final String ESCAPED_STRING = "\"%s\"";
|
||||
private static final String DIGITS = "%s";
|
||||
|
||||
private final String field;
|
||||
private final String value;
|
||||
private final String operator;
|
||||
|
||||
public OperatorCondition(String field, String operator, String value) {
|
||||
this.field = field;
|
||||
this.operator = operator;
|
||||
this.value = value != null ? ESCAPED_STRING.formatted(value) : null;
|
||||
}
|
||||
|
||||
public OperatorCondition(String field, String operator, Number value) {
|
||||
this.field = field;
|
||||
this.operator = operator;
|
||||
this.value = value != null ? DIGITS.formatted(value) : null;
|
||||
}
|
||||
|
||||
public OperatorCondition() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String build() {
|
||||
return PATTERN.formatted(field, operator, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AndCondition
|
||||
*/
|
||||
public static class AndCondition extends Condition {
|
||||
private static final String AND = " & ";
|
||||
|
||||
private final Condition[] conditions;
|
||||
|
||||
public AndCondition(Condition[] conditions) {
|
||||
this.conditions = conditions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String build() {
|
||||
return wrap(Arrays.stream(conditions).map(Condition::build).collect(Collectors.joining(AND)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OrCondition
|
||||
*/
|
||||
public static class OrCondition extends Condition {
|
||||
private static final String OR = " | ";
|
||||
|
||||
private final Condition[] conditions;
|
||||
|
||||
public OrCondition(Condition[] conditions) {
|
||||
this.conditions = conditions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String build() {
|
||||
return wrap(Arrays.stream(conditions).map(Condition::build).collect(Collectors.joining(OR)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
package de.grimsi.gameyfin.igdb;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.config.WebClientConfig;
|
||||
import de.grimsi.gameyfin.config.properties.GameyfinProperties;
|
||||
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
|
||||
import de.grimsi.gameyfin.entities.Platform;
|
||||
import de.grimsi.gameyfin.igdb.IgdbApiQueryBuilder.*;
|
||||
import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto;
|
||||
import de.grimsi.gameyfin.mapper.GameMapper;
|
||||
import de.grimsi.gameyfin.mapper.PlatformMapper;
|
||||
import io.github.resilience4j.reactor.bulkhead.operator.BulkheadOperator;
|
||||
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static de.grimsi.gameyfin.igdb.IgdbApiProperties.GAME_QUERY_FIELDS_STRING;
|
||||
import static de.grimsi.gameyfin.igdb.IgdbApiQueryBuilder.*;
|
||||
import static org.apache.commons.lang3.ObjectUtils.isNotEmpty;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class IgdbWrapper {
|
||||
private final WebClient.Builder webclientBuilder;
|
||||
private final WebClientConfig webClientConfig;
|
||||
private final GameMapper gameMapper;
|
||||
private final GameyfinProperties gameyfinProperties;
|
||||
|
||||
@Value("${gameyfin.igdb.api.client-id}")
|
||||
private String clientId;
|
||||
@Value("${gameyfin.igdb.api.client-secret}")
|
||||
private String clientSecret;
|
||||
@Value("${gameyfin.igdb.api.endpoints.base}")
|
||||
private String igdbApiBaseUrl;
|
||||
@Value("${gameyfin.igdb.api.endpoints.auth}")
|
||||
private String twitchAuthUrl;
|
||||
private WebClient twitchApiClient;
|
||||
|
||||
private WebClient igdbApiClient;
|
||||
|
||||
private TwitchOAuthTokenDto accessToken;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
twitchApiClient = webclientBuilder.build();
|
||||
authenticate();
|
||||
initIgdbClient();
|
||||
}
|
||||
|
||||
private void authenticate() {
|
||||
log.info("Authenticating on Twitch API...");
|
||||
|
||||
URI url = UriComponentsBuilder
|
||||
.fromHttpUrl(twitchAuthUrl)
|
||||
.query("client_id={client_id}").query("client_secret={client_secret}").query("grant_type=client_credentials")
|
||||
.buildAndExpand(clientId, clientSecret)
|
||||
.toUri();
|
||||
|
||||
this.accessToken = twitchApiClient
|
||||
.post()
|
||||
.uri(url)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(TwitchOAuthTokenDto.class)
|
||||
.block();
|
||||
|
||||
log.info("Successfully authenticated.");
|
||||
}
|
||||
|
||||
public Optional<Igdb.Game> getGameById(Long id) {
|
||||
IgdbApiQueryBuilder queryBuilder = new IgdbApiQueryBuilder();
|
||||
Igdb.GameResult gameResult = queryIgdbApi(
|
||||
IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF,
|
||||
queryBuilder.fields(GAME_QUERY_FIELDS_STRING)
|
||||
.where(equal("id", id))
|
||||
.limit(1)
|
||||
.build(),
|
||||
Igdb.GameResult.class
|
||||
);
|
||||
|
||||
if (gameResult == null) return Optional.empty();
|
||||
|
||||
return Optional.of(gameResult.getGames(0));
|
||||
}
|
||||
|
||||
public Optional<Igdb.Game> getGameBySlug(String slug) {
|
||||
IgdbApiQueryBuilder queryBuilder = new IgdbApiQueryBuilder();
|
||||
Igdb.GameResult gameResult = queryIgdbApi(
|
||||
IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF,
|
||||
queryBuilder.fields(GAME_QUERY_FIELDS_STRING)
|
||||
.where(equal("slug", slug))
|
||||
.limit(1)
|
||||
.build(),
|
||||
Igdb.GameResult.class
|
||||
);
|
||||
|
||||
if (gameResult == null) return Optional.empty();
|
||||
|
||||
return Optional.of(gameResult.getGames(0));
|
||||
}
|
||||
|
||||
public List<AutocompleteSuggestionDto> findPossibleMatchingTitles(String searchTerm, int limit) {
|
||||
IgdbApiQueryBuilder queryBuilder = new IgdbApiQueryBuilder();
|
||||
Igdb.GameResult gameResult = queryIgdbApi(
|
||||
IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF,
|
||||
queryBuilder.search(searchTerm)
|
||||
.fields("slug,name,first_release_date,platforms.name")
|
||||
.where(in("platforms", gameyfinProperties.igdb().config().preferredPlatforms()))
|
||||
.limit(limit)
|
||||
.build(),
|
||||
Igdb.GameResult.class
|
||||
);
|
||||
|
||||
if (gameResult == null) return Collections.emptyList();
|
||||
|
||||
return gameResult.getGamesList().stream().map(gameMapper::toAutocompleteSuggestionDto).toList();
|
||||
}
|
||||
|
||||
public Optional<Igdb.Game> searchForGameByTitle(String searchTerm) {
|
||||
return searchForGameByTitle(searchTerm, List.of());
|
||||
}
|
||||
|
||||
public Optional<Igdb.Game> searchForGameByTitle(String searchTerm, Collection<String> platformSlugs) {
|
||||
IgdbApiQueryBuilder queryBuilder = new IgdbApiQueryBuilder();
|
||||
Condition platforms = isNotEmpty(platformSlugs) ?
|
||||
and(in("platforms", gameyfinProperties.igdb().config().preferredPlatforms()), in("platforms.slug", platformSlugs)) :
|
||||
in("platforms", gameyfinProperties.igdb().config().preferredPlatforms());
|
||||
|
||||
Igdb.GameResult gameResult = queryIgdbApi(
|
||||
IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF,
|
||||
queryBuilder.search(searchTerm)
|
||||
.fields(GAME_QUERY_FIELDS_STRING)
|
||||
.where(platforms)
|
||||
.build(),
|
||||
Igdb.GameResult.class
|
||||
);
|
||||
|
||||
if (gameResult == null) {
|
||||
log.warn("Could not find game for title '{}'", searchTerm);
|
||||
|
||||
// Try to remove brackets (and their content) at the end of the search term and search again
|
||||
// Although this process is recursive, we will only end up with a maximum recursion depth of two
|
||||
Pattern brackets = Pattern.compile("[()<>{}\\[\\]]");
|
||||
Matcher hasBrackets = brackets.matcher(searchTerm);
|
||||
|
||||
if (hasBrackets.find()) {
|
||||
String searchTermWithoutBrackets = searchTerm.split(brackets.pattern())[0].trim();
|
||||
log.warn("Removed brackets, trying again with search term '{}'", searchTermWithoutBrackets);
|
||||
return searchForGameByTitle(searchTermWithoutBrackets, platformSlugs);
|
||||
} else if (searchTerm.contains("-") || searchTerm.contains(".") || searchTerm.contains("_") || searchTerm.contains("INTERNAL") || searchTerm.contains("internal") || searchTerm.contains("REPACK") || searchTerm.contains("PROPER") || searchTerm.contains("repack") || searchTerm.contains("proper")) {
|
||||
String searchTermWithoutDash = searchTerm;
|
||||
if (searchTermWithoutDash.contains("-")) {
|
||||
searchTermWithoutDash = searchTermWithoutDash.substring(0, searchTermWithoutDash.lastIndexOf('-')).trim();
|
||||
}
|
||||
searchTermWithoutDash = searchTermWithoutDash.replace(".", " ");
|
||||
searchTermWithoutDash = searchTermWithoutDash.replace("_", " ");
|
||||
searchTermWithoutDash = searchTermWithoutDash.replaceAll("(?i)repack", " ");
|
||||
searchTermWithoutDash = searchTermWithoutDash.replaceAll("(?i)internal", " ");
|
||||
searchTermWithoutDash = searchTermWithoutDash.replaceAll("(?i)proper", " ");
|
||||
log.warn("Removed release stuff, trying again with search term '{}'", searchTermWithoutDash);
|
||||
return searchForGameByTitle(searchTermWithoutDash, platformSlugs);
|
||||
}
|
||||
log.warn("Using slug as last resort for " + searchTerm + ": " + searchTerm.replace(" ", "-").toLowerCase());
|
||||
return getGameBySlug(searchTerm.replace(" ", "-").toLowerCase());
|
||||
}
|
||||
|
||||
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
|
||||
// If that also returns nothing, just return the first search result
|
||||
//
|
||||
// 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<Igdb.Game> srExactTitleMatch = games.stream().filter(s -> s.getName().equals(searchTerm)).findFirst();
|
||||
if (srExactTitleMatch.isPresent()) return srExactTitleMatch;
|
||||
|
||||
Optional<Igdb.Game> srTitleEndsWithMatch = games.stream().filter(s -> s.getName().endsWith(searchTerm)).findFirst();
|
||||
if (srTitleEndsWithMatch.isPresent()) return srTitleEndsWithMatch;
|
||||
|
||||
// Just return the first result and hope that IGDBs search algorithm is somewhat helpful this time
|
||||
return Optional.of(games.get(0));
|
||||
}
|
||||
|
||||
public List<Platform> findPlatforms(String searchTerm, int limit) {
|
||||
IgdbApiQueryBuilder queryBuilder = new IgdbApiQueryBuilder();
|
||||
Igdb.PlatformResult platformResult = queryIgdbApi(
|
||||
IgdbApiProperties.ENDPOINT_PLATFORMS_PROTOBUF,
|
||||
queryBuilder.search(searchTerm)
|
||||
.fields("slug,name")
|
||||
.limit(limit)
|
||||
.build(),
|
||||
Igdb.PlatformResult.class
|
||||
);
|
||||
|
||||
if (platformResult == null) return Collections.emptyList();
|
||||
|
||||
return platformResult.getPlatformsList().stream().map(PlatformMapper::toPlatform).toList();
|
||||
}
|
||||
|
||||
public Optional<Platform> getPlatformBySlug(String slug) {
|
||||
IgdbApiQueryBuilder queryBuilder = new IgdbApiQueryBuilder();
|
||||
Igdb.PlatformResult platformResult = queryIgdbApi(
|
||||
IgdbApiProperties.ENDPOINT_PLATFORMS_PROTOBUF,
|
||||
queryBuilder.fields("slug,name,platform_logo")
|
||||
.where(equal("slug", slug))
|
||||
.build(),
|
||||
Igdb.PlatformResult.class
|
||||
);
|
||||
|
||||
if (platformResult == null) return Optional.empty();
|
||||
|
||||
return platformResult.getPlatformsList().stream().map(PlatformMapper::toPlatform).findFirst();
|
||||
}
|
||||
|
||||
private void initIgdbClient() {
|
||||
if (accessToken == null) {
|
||||
authenticate();
|
||||
}
|
||||
|
||||
igdbApiClient = webclientBuilder
|
||||
.baseUrl(igdbApiBaseUrl)
|
||||
.defaultHeader("Client-ID", clientId)
|
||||
.defaultHeader("Authorization", "Bearer %s".formatted(accessToken.getAccessToken()))
|
||||
.filter(WebClientConfig.fixProtobufContentTypeInterceptor())
|
||||
.build();
|
||||
}
|
||||
|
||||
private <T> T queryIgdbApi(String endpoint, String query, Class<T> responseClass) {
|
||||
return igdbApiClient.post()
|
||||
.uri(endpoint)
|
||||
.bodyValue(query)
|
||||
.retrieve()
|
||||
.bodyToMono(responseClass)
|
||||
.transformDeferred(BulkheadOperator.of(webClientConfig.getIgdbConcurrencyLimiter()))
|
||||
.transformDeferred(RateLimiterOperator.of(webClientConfig.getIgdbRateLimiter()))
|
||||
.block();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package de.grimsi.gameyfin.igdb.dto;
|
||||
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
||||
public class TwitchOAuthTokenDto {
|
||||
private String accessToken;
|
||||
private Long expiresIn;
|
||||
private String tokenType;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.entities.Company;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CompanyMapper {
|
||||
|
||||
public static Company toCompany(Igdb.InvolvedCompany c) {
|
||||
return Company.builder()
|
||||
.slug(c.getCompany().getSlug())
|
||||
.name(c.getCompany().getName())
|
||||
.logoId(c.getCompany().getLogo().getImageId())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static List<Company> toCompanies(List<Igdb.InvolvedCompany> c) {
|
||||
return c.stream().map(CompanyMapper::toCompany).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
|
||||
import de.grimsi.gameyfin.dto.GameOverviewDto;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.entities.Library;
|
||||
import de.grimsi.gameyfin.service.FilesystemService;
|
||||
import de.grimsi.gameyfin.service.LibraryService;
|
||||
import de.grimsi.gameyfin.util.ProtobufUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class GameMapper {
|
||||
|
||||
private final FilesystemService filesystemService;
|
||||
|
||||
public DetectedGame toDetectedGame(Igdb.Game g, Path path, Library library) {
|
||||
List<Igdb.MultiplayerMode> multiplayerModes = g.getMultiplayerModesList();
|
||||
List<String> screenshotIds = g.getScreenshotsList().stream().map(Igdb.Screenshot::getImageId).toList();
|
||||
List<String> videoIds = g.getVideosList().stream().map(Igdb.GameVideo::getVideoId).toList();
|
||||
|
||||
return DetectedGame.builder()
|
||||
.slug(g.getSlug())
|
||||
.title(g.getName())
|
||||
.summary(g.getSummary())
|
||||
.releaseDate(ProtobufUtil.toInstant(g.getFirstReleaseDate()))
|
||||
.userRating((int) g.getRating())
|
||||
.criticsRating((int) g.getAggregatedRating())
|
||||
.totalRating((int) g.getTotalRating())
|
||||
.category(g.getCategory().name())
|
||||
.offlineCoop(hasOfflineCoop(multiplayerModes))
|
||||
.onlineCoop(hasOnlineCoop(multiplayerModes))
|
||||
.lanSupport(hasLanSupport(multiplayerModes))
|
||||
.maxPlayers(getMaxPlayers(multiplayerModes))
|
||||
.coverId(getCoverId(g))
|
||||
.screenshotIds(screenshotIds)
|
||||
.videoIds(videoIds)
|
||||
.companies(CompanyMapper.toCompanies(g.getInvolvedCompaniesList()))
|
||||
.genres(GenreMapper.toGenres(g.getGenresList()))
|
||||
.keywords(KeywordMapper.toKeywords(g.getKeywordsList()))
|
||||
.themes(ThemeMapper.toThemes(g.getThemesList()))
|
||||
.playerPerspectives(PlayerPerspectiveMapper.toPlayerPerspectives(g.getPlayerPerspectivesList()))
|
||||
.platforms(PlatformMapper.toPlatforms(g.getPlatformsList()))
|
||||
.path(path.toString())
|
||||
.library(library)
|
||||
.diskSize(calculateDiskSize(g, path))
|
||||
.addedToLibrary(Instant.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
public GameOverviewDto toGameOverviewDto(DetectedGame game) {
|
||||
return GameOverviewDto.builder()
|
||||
.slug(game.getSlug())
|
||||
.title(game.getTitle())
|
||||
.coverId(game.getCoverId())
|
||||
.build();
|
||||
}
|
||||
|
||||
public AutocompleteSuggestionDto toAutocompleteSuggestionDto(Igdb.Game game) {
|
||||
return AutocompleteSuggestionDto.builder()
|
||||
.slug(game.getSlug())
|
||||
.title(game.getName())
|
||||
.releaseDate(ProtobufUtil.toInstant(game.getFirstReleaseDate()))
|
||||
.platforms(game.getPlatformsList().stream().map(Igdb.Platform::getName).toList())
|
||||
.build();
|
||||
}
|
||||
|
||||
private String getCoverId(Igdb.Game g) {
|
||||
String coverId = g.getCover().getImageId();
|
||||
|
||||
if (StringUtils.hasText(coverId)) return coverId;
|
||||
|
||||
return "nocover";
|
||||
}
|
||||
|
||||
private boolean hasOfflineCoop(List<Igdb.MultiplayerMode> modes) {
|
||||
return modes.stream().anyMatch(Igdb.MultiplayerMode::getOfflinecoop);
|
||||
}
|
||||
|
||||
private boolean hasLanSupport(List<Igdb.MultiplayerMode> modes) {
|
||||
return modes.stream().anyMatch(Igdb.MultiplayerMode::getLancoop);
|
||||
}
|
||||
|
||||
private boolean hasOnlineCoop(List<Igdb.MultiplayerMode> modes) {
|
||||
return modes.stream().anyMatch(Igdb.MultiplayerMode::getOnlinecoop);
|
||||
}
|
||||
|
||||
private int getMaxPlayers(List<Igdb.MultiplayerMode> modes) {
|
||||
return modes.stream().mapToInt(Igdb.MultiplayerMode::getOnlinecoopmax).max().orElse(0);
|
||||
}
|
||||
|
||||
private long calculateDiskSize(Igdb.Game g, Path path) {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
log.info("Calculating disk size for game '{}'...", g.getName());
|
||||
|
||||
stopWatch.start();
|
||||
|
||||
long fileSize;
|
||||
|
||||
try {
|
||||
fileSize = filesystemService.getSizeOnDisk(path);
|
||||
} catch (IOException e) {
|
||||
log.error("Error while calculating disk size for game '{}'", g.getName());
|
||||
fileSize = -1L;
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
log.info("Calculated disk size for game '{}' in {} seconds", g.getName(), (int) stopWatch.getTotalTimeSeconds());
|
||||
return fileSize;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.entities.Genre;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GenreMapper {
|
||||
|
||||
public static Genre toGenre(Igdb.Genre g) {
|
||||
return Genre.builder()
|
||||
.slug(g.getSlug())
|
||||
.name(g.getName())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static List<Genre> toGenres(List<Igdb.Genre> g) {
|
||||
return g.stream().map(GenreMapper::toGenre).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.entities.Keyword;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class KeywordMapper {
|
||||
public static Keyword toKeyword(Igdb.Keyword g) {
|
||||
return Keyword.builder()
|
||||
.slug(g.getSlug())
|
||||
.name(g.getName())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static List<Keyword> toKeywords(List<Igdb.Keyword> g) {
|
||||
return g.stream().map(KeywordMapper::toKeyword).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.entities.Platform;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PlatformMapper {
|
||||
|
||||
public static Platform toPlatform(Igdb.Platform c) {
|
||||
return Platform.builder()
|
||||
.slug(c.getSlug())
|
||||
.name(c.getName())
|
||||
.logoId(c.getPlatformLogo().getImageId())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static List<Platform> toPlatforms(List<Igdb.Platform> c) {
|
||||
return c.stream().map(PlatformMapper::toPlatform).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.entities.PlayerPerspective;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PlayerPerspectiveMapper {
|
||||
|
||||
public static PlayerPerspective toPlayerPerspective(Igdb.PlayerPerspective g) {
|
||||
return PlayerPerspective.builder()
|
||||
.slug(g.getSlug())
|
||||
.name(g.getName())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static List<PlayerPerspective> toPlayerPerspectives(List<Igdb.PlayerPerspective> g) {
|
||||
return g.stream().map(PlayerPerspectiveMapper::toPlayerPerspective).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.entities.Theme;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ThemeMapper {
|
||||
public static Theme toTheme(Igdb.Theme g) {
|
||||
return Theme.builder()
|
||||
.slug(g.getSlug())
|
||||
.name(g.getName())
|
||||
.build();
|
||||
}
|
||||
|
||||
public static List<Theme> toThemes(List<Igdb.Theme> g) {
|
||||
return g.stream().map(ThemeMapper::toTheme).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.Company;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface CompanyRepository extends JpaRepository<Company, String> {
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
public interface DetectedGameRepository extends JpaRepository<DetectedGame, String> {
|
||||
|
||||
boolean existsByPath(String path);
|
||||
|
||||
boolean existsBySlug(String slug);
|
||||
|
||||
Optional<DetectedGame> findByPath(String path);
|
||||
|
||||
List<DetectedGame> findByPathStartsWithAndLibraryIsNull(String path);
|
||||
|
||||
List<DetectedGame> getAllByPathNotIn(Collection<String> paths);
|
||||
|
||||
List<DetectedGame> getAllByPathNotInAndPathStartsWith(Collection<String> paths, String libraryPath);
|
||||
|
||||
default List<DetectedGame> getAllByPathNotInAndPathStartsWith(List<Path> paths, String libraryPath) {
|
||||
List<String> pathStrings = paths.stream().map(Path::toString).toList();
|
||||
// get games that are not in the paths list but are starting with libraryPath if libraryPath is not empty
|
||||
return isBlank(libraryPath) ? getAllByPathNotIn(pathStrings) : getAllByPathNotInAndPathStartsWith(pathStrings, libraryPath);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.Genre;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface GenreRepository extends JpaRepository<Genre, String> {
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.Keyword;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface KeywordRepository extends JpaRepository<Keyword, String> {
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.Library;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface LibraryRepository extends JpaRepository<Library, String> {
|
||||
|
||||
boolean existsByPathIgnoreCase(String path);
|
||||
|
||||
Optional<Library> findByPath(String path);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.Platform;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PlatformRepository extends JpaRepository<Platform, String> {
|
||||
|
||||
boolean existsBySlug(String slug);
|
||||
|
||||
Optional<Platform> findBySlug(String slug);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.PlayerPerspective;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface PlayerPerspectiveRepository extends JpaRepository<PlayerPerspective, String> {
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.Keyword;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ThemeRepository extends JpaRepository<Keyword, String> {
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.UnmappableFile;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
public interface UnmappableFileRepository extends JpaRepository<UnmappableFile, Long> {
|
||||
|
||||
boolean existsByPath(String path);
|
||||
|
||||
List<UnmappableFile> getAllByPathNotIn(Collection<String> paths);
|
||||
|
||||
List<UnmappableFile> getAllByPathNotInAndPathStartsWith(Collection<String> paths, String libraryPath);
|
||||
|
||||
|
||||
Optional<UnmappableFile> findByPath(String path);
|
||||
|
||||
default List<UnmappableFile> getAllByPathNotInAndPathStartsWith(List<Path> paths, String libraryPath) {
|
||||
List<String> pathStrings = paths.stream().map(Path::toString).toList();
|
||||
// get unmapped files that are not in the paths list but are starting with libraryPath if libraryPath is not empty
|
||||
return isBlank(libraryPath) ? getAllByPathNotIn(pathStrings) : getAllByPathNotInAndPathStartsWith(pathStrings, libraryPath);
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package de.grimsi.gameyfin.rest;
|
||||
|
||||
import de.grimsi.gameyfin.dto.GameOverviewDto;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.service.DownloadService;
|
||||
import de.grimsi.gameyfin.service.GameService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This controller handles logic related to detected games.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/v1/games")
|
||||
@RequiredArgsConstructor
|
||||
public class GamesController {
|
||||
|
||||
private final GameService gameService;
|
||||
private final DownloadService downloadService;
|
||||
|
||||
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public List<DetectedGame> getAllGames() {
|
||||
return gameService.getAllDetectedGames();
|
||||
}
|
||||
|
||||
@GetMapping(value = "/game/{slug}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public DetectedGame getGame(@PathVariable String slug) {
|
||||
return gameService.getDetectedGame(slug);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/game-overviews", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public List<GameOverviewDto> getGameOverviews() {
|
||||
return gameService.getGameOverviews();
|
||||
}
|
||||
|
||||
@GetMapping(value = "/game-mappings", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, String> getGameMappings() {
|
||||
return gameService.getAllMappings();
|
||||
}
|
||||
|
||||
@GetMapping(value = "/game/{slug}/download")
|
||||
public ResponseEntity<StreamingResponseBody> downloadGameFiles(@PathVariable String slug) {
|
||||
|
||||
DetectedGame game = gameService.getDetectedGame(slug);
|
||||
|
||||
String downloadFileName = downloadService.getDownloadFileName(game);
|
||||
long downloadFileSize = downloadService.getDownloadFileSize(game);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"%s\"".formatted(downloadFileName));
|
||||
headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
|
||||
headers.add(HttpHeaders.PRAGMA, "no-cache");
|
||||
headers.add(HttpHeaders.EXPIRES, "0");
|
||||
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
||||
if (downloadFileSize > 0) {
|
||||
headers.setContentLength(downloadFileSize);
|
||||
}
|
||||
|
||||
return ResponseEntity
|
||||
.ok()
|
||||
.headers(headers)
|
||||
.body(out -> downloadService.sendGamefilesToClient(game, out));
|
||||
}
|
||||
|
||||
@GetMapping(value = "/game/{slug}/refresh", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public DetectedGame refreshGame(@PathVariable String slug) {
|
||||
return gameService.refreshGame(slug);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package de.grimsi.gameyfin.rest;
|
||||
|
||||
import de.grimsi.gameyfin.service.DownloadService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* This controller handles functionality for images.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/v1/images")
|
||||
@RequiredArgsConstructor
|
||||
public class ImageController {
|
||||
|
||||
private final DownloadService downloadService;
|
||||
|
||||
@GetMapping(value = "/{imageId}", produces = MediaType.IMAGE_PNG_VALUE)
|
||||
public ResponseEntity<Resource> getImage(@PathVariable String imageId) {
|
||||
return ResponseEntity.ok()
|
||||
.cacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic())
|
||||
.body(downloadService.sendImageToClient(imageId));
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package de.grimsi.gameyfin.rest;
|
||||
|
||||
import de.grimsi.gameyfin.dto.*;
|
||||
import de.grimsi.gameyfin.entities.Library;
|
||||
import de.grimsi.gameyfin.service.ImageService;
|
||||
import de.grimsi.gameyfin.service.LibraryService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
/**
|
||||
* This controller handles functionality of the library.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/v1/library")
|
||||
@PreAuthorize("hasAuthority('ADMIN_API_ACCESS')")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LibraryController {
|
||||
|
||||
private final LibraryService libraryService;
|
||||
private final ImageService imageService;
|
||||
|
||||
@PostMapping(value = "/scan", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public LibraryScanResultDto scanLibraries(@RequestBody LibraryScanRequestDto libraryScanRequest) {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
|
||||
LibraryScanResultDto lscDto = new LibraryScanResultDto();
|
||||
|
||||
String path = libraryScanRequest.getPath();
|
||||
List<Library> libraries = isNotBlank(path) ? List.of(libraryService.getLibrary(path)) : libraryService.getLibraries();
|
||||
List<LibraryScanResult> libraryScanResults = libraries.stream().map(libraryService::scanGameLibrary).toList();
|
||||
|
||||
lscDto.setNewGames(libraryScanResults.stream().map(LibraryScanResult::getNewGames).reduce(0, Integer::sum));
|
||||
lscDto.setDeletedGames(libraryScanResults.stream().map(LibraryScanResult::getDeletedGames).reduce(0, Integer::sum));
|
||||
lscDto.setNewUnmappableFiles(libraryScanResults.stream().map(LibraryScanResult::getNewUnmappableFiles).reduce(0, Integer::sum));
|
||||
lscDto.setTotalGames(libraryScanResults.stream().map(LibraryScanResult::getTotalGames).reduce(0, Integer::sum));
|
||||
|
||||
if (libraryScanRequest.isDownloadImages()) {
|
||||
ImageDownloadResultDto idrDto = downloadImages();
|
||||
|
||||
lscDto.setCoverDownloads(idrDto.getCoverDownloads());
|
||||
lscDto.setScreenshotDownloads(idrDto.getScreenshotDownloads());
|
||||
lscDto.setCompanyLogoDownloads(idrDto.getCompanyLogoDownloads());
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
lscDto.setScanDuration((int) stopWatch.getTotalTimeSeconds());
|
||||
|
||||
log.info("Library scan completed in {} seconds.", (int) stopWatch.getTotalTimeSeconds());
|
||||
|
||||
return lscDto;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/download-images")
|
||||
public ImageDownloadResultDto downloadImages() {
|
||||
ImageDownloadResultDto idrDto = new ImageDownloadResultDto();
|
||||
|
||||
idrDto.setCoverDownloads(imageService.downloadGameCoversFromIgdb());
|
||||
idrDto.setScreenshotDownloads(imageService.downloadGameScreenshotsFromIgdb());
|
||||
idrDto.setCompanyLogoDownloads(imageService.downloadCompanyLogosFromIgdb());
|
||||
|
||||
log.info("Downloading images completed.");
|
||||
|
||||
return idrDto;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/files", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public List<String> getAllFiles() {
|
||||
return libraryService.getGameFiles().stream().map(Path::toString).toList();
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package de.grimsi.gameyfin.rest;
|
||||
|
||||
|
||||
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
|
||||
import de.grimsi.gameyfin.dto.PathToSlugDto;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.entities.Library;
|
||||
import de.grimsi.gameyfin.entities.Platform;
|
||||
import de.grimsi.gameyfin.entities.UnmappableFile;
|
||||
import de.grimsi.gameyfin.service.GameService;
|
||||
import de.grimsi.gameyfin.service.ImageService;
|
||||
import de.grimsi.gameyfin.service.LibraryService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1/library-management")
|
||||
@PreAuthorize("hasAuthority('ADMIN_API_ACCESS')")
|
||||
@RequiredArgsConstructor
|
||||
public class LibraryManagementController {
|
||||
|
||||
private final GameService gameService;
|
||||
private final ImageService imageService;
|
||||
private final LibraryService libraryService;
|
||||
|
||||
@DeleteMapping(value = "/delete-game/{slug}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void deleteGame(@PathVariable String slug) {
|
||||
gameService.deleteGame(slug);
|
||||
}
|
||||
|
||||
@DeleteMapping(value = "/delete-unmapped-file/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public void deleteUnmappedFile(@PathVariable Long id) {
|
||||
gameService.deleteUnmappedFile(id);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/confirm-game/{slug}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public DetectedGame confirmMatch(@PathVariable String slug, @RequestParam(required = false, defaultValue = "true") boolean confirm) {
|
||||
return gameService.confirmGame(slug, confirm);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/map-path", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public DetectedGame manuallyMapPathToSlug(@RequestBody PathToSlugDto pathToSlugDto) {
|
||||
DetectedGame game = gameService.mapPathToGame(pathToSlugDto.getPath(), pathToSlugDto.getSlug());
|
||||
|
||||
imageService.downloadGameCoversFromIgdb();
|
||||
imageService.downloadGameScreenshotsFromIgdb();
|
||||
imageService.downloadCompanyLogosFromIgdb();
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
@GetMapping(value = "/unmapped-files", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public List<UnmappableFile> getUnmappedFiles() {
|
||||
return gameService.getAllUnmappedFiles();
|
||||
}
|
||||
|
||||
@GetMapping(value = "/autocomplete-suggestions", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public List<AutocompleteSuggestionDto> getAutocompleteSuggestions(@RequestParam String searchTerm, @RequestParam(required = false, defaultValue = "10") int limit) {
|
||||
return libraryService.getAutocompleteSuggestions(searchTerm, limit);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/platforms", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public List<Platform> getPlatforms(@RequestParam String searchTerm, @RequestParam(required = false, defaultValue = "10") int limit) {
|
||||
return libraryService.getPlatforms(searchTerm, limit);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/libraries", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public List<Library> getLibraries() {
|
||||
return libraryService.getOrCreateLibraries();
|
||||
}
|
||||
|
||||
@PostMapping(value = "/map-library", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Library mapPathToPlatform(@RequestBody PathToSlugDto pathToSlugDto) {
|
||||
return libraryService.mapPlatformsToLibrary(pathToSlugDto.getPath(), pathToSlugDto.getSlug());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package de.grimsi.gameyfin.service;
|
||||
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.exceptions.DownloadAbortedException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static de.grimsi.gameyfin.util.FilenameUtil.getFilenameWithExtension;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DownloadService {
|
||||
|
||||
private final FilesystemService filesystemService;
|
||||
|
||||
public String getDownloadFileName(DetectedGame g) {
|
||||
Path path = filesystemService.getPath(g.getPath());
|
||||
|
||||
if (!Files.isDirectory(path)) return getFilenameWithExtension(path);
|
||||
return getFilenameWithExtension(path) + ".zip";
|
||||
}
|
||||
|
||||
public long getDownloadFileSize(DetectedGame game) {
|
||||
Path path = filesystemService.getPath(game.getPath());
|
||||
|
||||
try {
|
||||
if (!Files.isDirectory(path)) {
|
||||
long fileSize = filesystemService.getSizeOnDisk(path);
|
||||
log.info("Calculated file size for {} ({} MB).", path, Math.divideExact(fileSize, 1000000L));
|
||||
return fileSize;
|
||||
} else {
|
||||
// return zero since we cannot set content length for ZipOutputStreams that are used to archive directories
|
||||
return 0;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DownloadAbortedException();
|
||||
}
|
||||
}
|
||||
|
||||
public Resource sendImageToClient(String imageId) {
|
||||
String filename = "%s.png".formatted(imageId);
|
||||
return filesystemService.getFileFromCache(filename);
|
||||
}
|
||||
|
||||
public void sendGamefilesToClient(DetectedGame game, OutputStream outputStream) {
|
||||
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
|
||||
log.info("Starting game file download for {}...", game.getTitle());
|
||||
|
||||
stopWatch.start();
|
||||
|
||||
Path path = filesystemService.getPath(game.getPath());
|
||||
|
||||
try {
|
||||
if (path.toFile().isDirectory()) {
|
||||
sendGamefilesAsZipToClient(path, outputStream);
|
||||
} else {
|
||||
sendGamefileToClient(path, outputStream);
|
||||
}
|
||||
} catch (DownloadAbortedException e) {
|
||||
stopWatch.stop();
|
||||
log.info("Download of game {} was aborted by client after {} seconds", game.getTitle(), (int) stopWatch.getTotalTimeSeconds());
|
||||
return;
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
log.info("Downloaded game files of {} in {} seconds.", game.getTitle(), (int) stopWatch.getTotalTimeSeconds());
|
||||
}
|
||||
|
||||
private void sendGamefileToClient(Path path, OutputStream outputStream) {
|
||||
try {
|
||||
Files.copy(path, outputStream);
|
||||
} catch (ClientAbortException e) {
|
||||
// Aborted downloads will be handled gracefully
|
||||
throw new DownloadAbortedException();
|
||||
} catch (IOException e) {
|
||||
log.error("Error while downloading file:", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendGamefilesAsZipToClient(Path path, OutputStream outputStream) {
|
||||
log.info("Archiving game path {} for download...", path);
|
||||
ZipOutputStream zos = new ZipOutputStream(outputStream) {{
|
||||
def.setLevel(Deflater.NO_COMPRESSION);
|
||||
}};
|
||||
|
||||
try {
|
||||
Files.walkFileTree(path, new SimpleFileVisitor<>() {
|
||||
@SneakyThrows
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||
zos.putNextEntry(new ZipEntry(path.relativize(file).toString()));
|
||||
log.debug("Adding file {} to archive...", file);
|
||||
Files.copy(file, zos);
|
||||
zos.closeEntry();
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
|
||||
zos.close();
|
||||
} catch (ClientAbortException e) {
|
||||
// Aborted downloads will be handled gracefully
|
||||
throw new DownloadAbortedException();
|
||||
} catch (IOException e) {
|
||||
log.error("Error while zipping files:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package de.grimsi.gameyfin.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.*;
|
||||
|
||||
/**
|
||||
* This class handles all filesystem operations for Gameyfin.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FilesystemService {
|
||||
|
||||
@Value("${gameyfin.cache}")
|
||||
private String cacheFolderPath;
|
||||
|
||||
private final FileSystem fileSystem;
|
||||
|
||||
/**
|
||||
* Returns the given path on the configured filesystem.
|
||||
* Basically just another way of doing {@link Path#of(String, String...)}, but easier to mock.
|
||||
* @return The path
|
||||
*/
|
||||
public Path getPath(String first, String... more) {
|
||||
return fileSystem.getPath(first, more);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will create the folder specified in the "gameyfin.cache" property.
|
||||
* If the folder already exists, nothing will happen.
|
||||
*/
|
||||
public void createCacheFolder() {
|
||||
log.debug("Creating cache folder...");
|
||||
|
||||
try {
|
||||
Files.createDirectories(getPath(cacheFolderPath));
|
||||
log.debug("Cache folder created.");
|
||||
} catch (IOException e) {
|
||||
log.error("Error while creating the cache folder.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveFileToCache(Flux<DataBuffer> dataBuffer, String filename) {
|
||||
DataBufferUtils.write(dataBuffer, getPath(cacheFolderPath).resolve(filename), StandardOpenOption.CREATE)
|
||||
.share().block();
|
||||
}
|
||||
|
||||
public ByteArrayResource getFileFromCache(String filename) {
|
||||
try {
|
||||
return new ByteArrayResource(Files.readAllBytes(getPath(cacheFolderPath, filename)));
|
||||
} catch (IOException e) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find image file %s".formatted(filename));
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteFileFromCache(String filename) {
|
||||
try {
|
||||
Files.delete(getPathFromFilename(filename));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCachedFileCorrupt(String filename) {
|
||||
try {
|
||||
return Files.size(getPathFromFilename(filename)) == 0L;
|
||||
} catch (IOException e) {
|
||||
log.error("Could not determine file size of '{}'", filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean doesCachedFileExist(String filename) {
|
||||
return Files.exists(getPathFromFilename(filename));
|
||||
}
|
||||
|
||||
public long getSizeOnDisk(Path path) throws IOException {
|
||||
if (Files.isDirectory(path)) {
|
||||
// Some benchmarks I did have shown that trying to parallelize this process makes it slower instead of faster
|
||||
return FileUtils.sizeOfDirectory(path.toFile());
|
||||
} else {
|
||||
try {
|
||||
return Files.size(path);
|
||||
} catch (IOException e) {
|
||||
log.error("Error while calculating size of file '{}'.", path);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Path getPathFromFilename(String filename) {
|
||||
return getPath(cacheFolderPath, filename);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package de.grimsi.gameyfin.service;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.dto.GameOverviewDto;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.entities.Library;
|
||||
import de.grimsi.gameyfin.entities.UnmappableFile;
|
||||
import de.grimsi.gameyfin.igdb.IgdbWrapper;
|
||||
import de.grimsi.gameyfin.mapper.GameMapper;
|
||||
import de.grimsi.gameyfin.repositories.DetectedGameRepository;
|
||||
import de.grimsi.gameyfin.repositories.LibraryRepository;
|
||||
import de.grimsi.gameyfin.repositories.UnmappableFileRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Service
|
||||
public class GameService {
|
||||
|
||||
private final IgdbWrapper igdbWrapper;
|
||||
private final GameMapper gameMapper;
|
||||
private final DetectedGameRepository detectedGameRepository;
|
||||
private final UnmappableFileRepository unmappableFileRepository;
|
||||
private final LibraryRepository libraryRepository;
|
||||
private final FilesystemService filesystemService;
|
||||
|
||||
public List<DetectedGame> getAllDetectedGames() {
|
||||
return detectedGameRepository.findAll();
|
||||
}
|
||||
|
||||
public DetectedGame getDetectedGame(String slug) {
|
||||
return detectedGameRepository.findById(slug)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with slug '%s' not found in library.".formatted(slug)));
|
||||
}
|
||||
|
||||
public List<UnmappableFile> getAllUnmappedFiles() {
|
||||
return unmappableFileRepository.findAll();
|
||||
}
|
||||
|
||||
public Map<String, String> getAllMappings() {
|
||||
return detectedGameRepository.findAll().stream().collect(Collectors.toMap(DetectedGame::getPath, DetectedGame::getTitle));
|
||||
}
|
||||
|
||||
public List<GameOverviewDto> getGameOverviews() {
|
||||
return detectedGameRepository.findAll().stream().map(gameMapper::toGameOverviewDto).toList();
|
||||
}
|
||||
|
||||
public void deleteGame(String slug) {
|
||||
DetectedGame gameToBeDeleted = getDetectedGame(slug);
|
||||
|
||||
// Add the path of the game to be deleted to the unmappable files,
|
||||
// so it doesn't get re-indexed on the next library scan
|
||||
unmappableFileRepository.save(new UnmappableFile(gameToBeDeleted.getPath()));
|
||||
|
||||
detectedGameRepository.delete(gameToBeDeleted);
|
||||
}
|
||||
|
||||
public void deleteUnmappedFile(Long id) {
|
||||
unmappableFileRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public DetectedGame confirmGame(String slug, boolean confirm) {
|
||||
DetectedGame g = getDetectedGame(slug);
|
||||
g.setConfirmedMatch(confirm);
|
||||
return detectedGameRepository.save(g);
|
||||
}
|
||||
|
||||
public DetectedGame mapPathToGame(String path, String slug) {
|
||||
|
||||
if (detectedGameRepository.existsBySlug(slug))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Game with slug '%s' already exists in database.".formatted(slug));
|
||||
|
||||
Optional<UnmappableFile> optionalUnmappableFile = unmappableFileRepository.findByPath(path);
|
||||
|
||||
if (optionalUnmappableFile.isPresent()) {
|
||||
return mapUnmappableFile(optionalUnmappableFile.get(), slug);
|
||||
}
|
||||
|
||||
Optional<DetectedGame> optionalDetectedGame = detectedGameRepository.findByPath(path);
|
||||
|
||||
if (optionalDetectedGame.isPresent()) {
|
||||
return mapDetectedGame(optionalDetectedGame.get(), slug);
|
||||
}
|
||||
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Path '%s' not in database".formatted(path));
|
||||
}
|
||||
|
||||
public DetectedGame refreshGame(String slug) {
|
||||
Optional<DetectedGame> optionalDetectedGame = detectedGameRepository.findById(slug);
|
||||
|
||||
if (optionalDetectedGame.isPresent()) {
|
||||
log.info("Refreshing game with slug '{}'", slug);
|
||||
return mapDetectedGame(optionalDetectedGame.get(), slug);
|
||||
}
|
||||
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with slug '%s' not found in database".formatted(slug));
|
||||
}
|
||||
|
||||
private DetectedGame mapUnmappableFile(UnmappableFile unmappableFile, String slug) {
|
||||
DetectedGame game = mapPathToGame(filesystemService.getPath(unmappableFile.getPath()), slug);
|
||||
unmappableFileRepository.delete(unmappableFile);
|
||||
return game;
|
||||
}
|
||||
|
||||
private DetectedGame mapDetectedGame(DetectedGame existingGame, String slug) {
|
||||
DetectedGame game = mapPathToGame(filesystemService.getPath(existingGame.getPath()), slug);
|
||||
detectedGameRepository.delete(existingGame);
|
||||
return game;
|
||||
}
|
||||
|
||||
private DetectedGame mapPathToGame(Path path, String slug) {
|
||||
Igdb.Game igdbGame = igdbWrapper.getGameBySlug(slug)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with slug '%s' does not exist on IGDB.".formatted(slug)));
|
||||
|
||||
// Parent folder should be the library
|
||||
Path libraryPath = path.getParent();
|
||||
Library library = libraryRepository.findByPath(libraryPath.toString()).orElse(null);
|
||||
DetectedGame game = gameMapper.toDetectedGame(igdbGame, path, library);
|
||||
game.setConfirmedMatch(true);
|
||||
|
||||
return detectedGameRepository.save(game);
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package de.grimsi.gameyfin.service;
|
||||
|
||||
import de.grimsi.gameyfin.entities.Company;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.igdb.IgdbApiProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ImageService {
|
||||
|
||||
private final FilesystemService filesystemService;
|
||||
private final GameService gameService;
|
||||
private final WebClient.Builder webclientBuilder;
|
||||
private WebClient igdbImageClient;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
igdbImageClient = webclientBuilder.baseUrl(IgdbApiProperties.IMAGES_BASE_URL).build();
|
||||
}
|
||||
|
||||
public int downloadGameCoversFromIgdb() {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
|
||||
log.info("Starting game cover download...");
|
||||
stopWatch.start();
|
||||
|
||||
MultiValueMap<String, String> gameToImageIds = new LinkedMultiValueMap<>(
|
||||
gameService.getAllDetectedGames().stream()
|
||||
.collect(Collectors.toMap(DetectedGame::getSlug, g -> Collections.singletonList(g.getCoverId()))));
|
||||
|
||||
int downloadCount = saveImagesIntoCache(gameToImageIds, IgdbApiProperties.COVER_IMAGE_SIZE, "cover", "game");
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
log.info("Downloaded {} covers in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
|
||||
return downloadCount;
|
||||
}
|
||||
|
||||
public int downloadGameScreenshotsFromIgdb() {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
|
||||
log.info("Starting game screenshot download...");
|
||||
stopWatch.start();
|
||||
|
||||
MultiValueMap<String, String> gamesToImageIds = new LinkedMultiValueMap<>(
|
||||
gameService.getAllDetectedGames().stream()
|
||||
.collect(Collectors.toMap(DetectedGame::getSlug, DetectedGame::getScreenshotIds)));
|
||||
|
||||
int downloadCount = saveImagesIntoCache(gamesToImageIds, IgdbApiProperties.SCREENSHOT_IMAGE_SIZE, "screenshot", "game");
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
log.info("Downloaded {} screenshots in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
|
||||
return downloadCount;
|
||||
}
|
||||
|
||||
public int downloadCompanyLogosFromIgdb() {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
|
||||
log.info("Starting company logo download...");
|
||||
stopWatch.start();
|
||||
|
||||
Map<String, List<String>> companyToLogoIdMap = gameService.getAllDetectedGames().stream()
|
||||
.flatMap(g -> g.getCompanies().stream())
|
||||
.collect(Collectors.toMap(Company::getSlug, c -> Collections.singletonList(c.getLogoId()), (c1, c2) -> c1));
|
||||
|
||||
MultiValueMap<String, String> companiesToLogoIds = new LinkedMultiValueMap<>(companyToLogoIdMap);
|
||||
|
||||
int downloadCount = saveImagesIntoCache(companiesToLogoIds, IgdbApiProperties.LOGO_IMAGE_SIZE, "logo", "company");
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
log.info("Downloaded {} company logos in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
|
||||
return downloadCount;
|
||||
}
|
||||
|
||||
private int saveImagesIntoCache(MultiValueMap<String, String> entityToImageIds, String imageSize, String imageType, String entityType) {
|
||||
AtomicInteger downloadCounter = new AtomicInteger();
|
||||
|
||||
entityToImageIds.forEach((key, value) -> value.forEach(imageId -> {
|
||||
|
||||
if (!StringUtils.hasText(imageId)) return;
|
||||
|
||||
String imgFileName = "%s.png".formatted(imageId);
|
||||
String imgUrl = "t_%s/%s".formatted(imageSize, imgFileName);
|
||||
|
||||
if (filesystemService.doesCachedFileExist(imgFileName)) {
|
||||
if (filesystemService.isCachedFileCorrupt(imgFileName)) {
|
||||
log.info("File '{}' is corrupt, retrying download...", imgFileName);
|
||||
filesystemService.deleteFileFromCache(imgFileName);
|
||||
} else {
|
||||
log.debug("{} for {} '{}' already downloaded ({}), skipping.",
|
||||
imageType.substring(0, 1).toUpperCase() + imageType.substring(1).toLowerCase(),
|
||||
entityType,
|
||||
key,
|
||||
imgFileName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Flux<DataBuffer> dataBuffer = igdbImageClient.get()
|
||||
.uri(imgUrl)
|
||||
.retrieve()
|
||||
.bodyToFlux(DataBuffer.class);
|
||||
|
||||
try {
|
||||
filesystemService.saveFileToCache(dataBuffer, imgFileName);
|
||||
} catch (WebClientResponseException e) {
|
||||
if (e.getStatusCode().is4xxClientError()) {
|
||||
log.error("Could not download {} for {} '{}' from {}: {}", imageType, entityType, key, IgdbApiProperties.IMAGES_BASE_URL + imgUrl, e.getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
downloadCounter.getAndIncrement();
|
||||
log.info("Downloaded {} for {} '{}' from {}", imageType, entityType, key, IgdbApiProperties.IMAGES_BASE_URL + imgUrl);
|
||||
}));
|
||||
|
||||
return downloadCounter.get();
|
||||
}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
package de.grimsi.gameyfin.service;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
|
||||
import de.grimsi.gameyfin.dto.LibraryScanResult;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.entities.Library;
|
||||
import de.grimsi.gameyfin.entities.Platform;
|
||||
import de.grimsi.gameyfin.entities.UnmappableFile;
|
||||
import de.grimsi.gameyfin.igdb.IgdbWrapper;
|
||||
import de.grimsi.gameyfin.mapper.GameMapper;
|
||||
import de.grimsi.gameyfin.repositories.DetectedGameRepository;
|
||||
import de.grimsi.gameyfin.repositories.LibraryRepository;
|
||||
import de.grimsi.gameyfin.repositories.PlatformRepository;
|
||||
import de.grimsi.gameyfin.repositories.UnmappableFileRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static de.grimsi.gameyfin.util.FilenameUtil.getFilenameWithoutAdditions;
|
||||
import static de.grimsi.gameyfin.util.FilenameUtil.hasGameArchiveExtension;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LibraryService {
|
||||
|
||||
@Value("${gameyfin.sources}")
|
||||
private List<String> libraryFolders;
|
||||
|
||||
private final IgdbWrapper igdbWrapper;
|
||||
private final GameMapper gameMapper;
|
||||
private final DetectedGameRepository detectedGameRepository;
|
||||
private final UnmappableFileRepository unmappableFileRepository;
|
||||
private final LibraryRepository libraryRepository;
|
||||
private final PlatformRepository platformRepository;
|
||||
private final FilesystemService filesystemService;
|
||||
|
||||
public List<Path> getGameFiles() {
|
||||
return getGameFiles(null);
|
||||
}
|
||||
|
||||
public List<Path> getGameFiles(String path) {
|
||||
List<Path> gamefiles = new ArrayList<>();
|
||||
|
||||
libraryFolders.stream().map(Path::of).filter(allPathsOrSpecific(path)).forEach(
|
||||
folder -> {
|
||||
try (Stream<Path> stream = Files.list(folder)) {
|
||||
// return all sub-folders (non-recursive) and files that have an extension that indicates that they are a downloadable file
|
||||
List<Path> gameFilesFromThisFolder = stream
|
||||
.filter(p -> Files.isDirectory(p) || hasGameArchiveExtension(p))
|
||||
// filter out all hidden files and folders
|
||||
.filter(p -> {
|
||||
try {
|
||||
return !(Files.isHidden(p));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Error while checking if '%s' is hidden.".formatted(p), e);
|
||||
}
|
||||
})
|
||||
// filter out all empty directories
|
||||
.filter(p -> {
|
||||
if (!Files.isDirectory(p)) return true;
|
||||
|
||||
try (DirectoryStream<Path> s = Files.newDirectoryStream(p)) {
|
||||
return s.iterator().hasNext();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Error while checking if folder '%s' is empty.".formatted(p), e);
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
|
||||
gamefiles.addAll(gameFilesFromThisFolder);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Error while opening library folder '%s'".formatted(folder), e);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return gamefiles;
|
||||
}
|
||||
|
||||
public LibraryScanResult scanGameLibrary(Library library) {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
|
||||
log.info("Starting scan...");
|
||||
stopWatch.start();
|
||||
|
||||
AtomicInteger newUnmappedFilesCounter = new AtomicInteger();
|
||||
|
||||
String libraryPath = library.getPath();
|
||||
List<Path> gameFiles = getGameFiles(libraryPath);
|
||||
|
||||
// Check if any games that are in the library have been removed from the file system
|
||||
// This would include renamed files, but they will be re-detected by the next step
|
||||
List<DetectedGame> deletedGames = detectedGameRepository.getAllByPathNotInAndPathStartsWith(gameFiles, libraryPath);
|
||||
detectedGameRepository.deleteAll(deletedGames);
|
||||
deletedGames.forEach(g -> log.info("Game '{}' has been moved or deleted.", g.getPath()));
|
||||
|
||||
// Now check if there are any unmapped files that have been removed from the file system
|
||||
List<UnmappableFile> deletedUnmappableFiles = unmappableFileRepository.getAllByPathNotInAndPathStartsWith(gameFiles, libraryPath);
|
||||
unmappableFileRepository.deleteAll(deletedUnmappableFiles);
|
||||
deletedUnmappableFiles.forEach(g -> log.info("Unmapped file '{}' has been moved or deleted.", g.getPath()));
|
||||
|
||||
// Filter out the games we already know and the ones we already tried to map to a game without success
|
||||
gameFiles = gameFiles.stream()
|
||||
.filter(g -> !detectedGameRepository.existsByPath(g.toString()))
|
||||
.filter(g -> !unmappableFileRepository.existsByPath(g.toString()))
|
||||
.peek(p -> log.info("Found new potential game: {}", p))
|
||||
.toList();
|
||||
|
||||
// Check if library has assigned platforms, so we can search for matching games by specific platforms to get a more accurate match
|
||||
Set<String> platformsFilter = libraryRepository.findByPath(libraryPath).map(Library::getPlatforms)
|
||||
.map(platforms -> platforms.stream()
|
||||
.map(Platform::getSlug)
|
||||
.collect(Collectors.toSet()))
|
||||
.orElse(Set.of());
|
||||
|
||||
// For each new game, load the info from IGDB
|
||||
// If a game is not found on IGDB, add it to the list of unmapped files, so we won't query the API later on for the same path
|
||||
// If a game is not found on IGDB, blacklist the path, so we won't query the API later for the same path
|
||||
List<DetectedGame> newDetectedGames = gameFiles.stream()
|
||||
.map(p -> {
|
||||
Optional<Igdb.Game> optionalGame = igdbWrapper.searchForGameByTitle(getFilenameWithoutAdditions(p), platformsFilter);
|
||||
|
||||
if (optionalGame.isPresent() && detectedGameRepository.existsBySlug(optionalGame.get().getSlug())) {
|
||||
log.warn("Game with slug '{}' already exists in database", optionalGame.get().getSlug());
|
||||
optionalGame = Optional.empty();
|
||||
}
|
||||
|
||||
return optionalGame.map(game -> Map.entry(p, game)).or(() -> {
|
||||
unmappableFileRepository.save(new UnmappableFile(p.toString()));
|
||||
newUnmappedFilesCounter.getAndIncrement();
|
||||
log.info("Added path '{}' to list of unmapped files", p);
|
||||
return Optional.empty();
|
||||
});
|
||||
})
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.peek(e -> log.info("Mapped file '{}' to game '{}' (slug: {})", e.getKey(), e.getValue().getName(), e.getValue().getSlug()))
|
||||
.map(e -> gameMapper.toDetectedGame(e.getValue(), e.getKey(), library))
|
||||
.collect(toList());
|
||||
|
||||
List<DetectedGame> duplicateGames = getDuplicates(newDetectedGames);
|
||||
newUnmappedFilesCounter.getAndAdd(duplicateGames.size());
|
||||
newDetectedGames.removeAll(duplicateGames);
|
||||
|
||||
try {
|
||||
newDetectedGames = detectedGameRepository.saveAll(newDetectedGames);
|
||||
} catch (Exception e) {
|
||||
log.error("Could not save {} detected games!", newDetectedGames.size());
|
||||
List<UnmappableFile> unmappableFiles = newDetectedGames.stream()
|
||||
.map(game -> new UnmappableFile(game.getPath())).toList();
|
||||
unmappableFileRepository.saveAll(unmappableFiles);
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
log.info("Scan finished in {} seconds: Found {} new games, deleted {} games, could not map {} files/folders, {} games total.",
|
||||
(int) stopWatch.getTotalTimeSeconds(), newDetectedGames.size(), deletedGames.size() + deletedUnmappableFiles.size(), newUnmappedFilesCounter.get(), detectedGameRepository.count());
|
||||
|
||||
return LibraryScanResult.builder()
|
||||
.newGames(newDetectedGames.size())
|
||||
.deletedGames(deletedGames.size() + deletedUnmappableFiles.size())
|
||||
.newUnmappableFiles(newUnmappedFilesCounter.get())
|
||||
.totalGames((int) detectedGameRepository.count())
|
||||
.build();
|
||||
}
|
||||
|
||||
public List<AutocompleteSuggestionDto> getAutocompleteSuggestions(String searchTerm, int limit) {
|
||||
return igdbWrapper.findPossibleMatchingTitles(searchTerm, limit);
|
||||
}
|
||||
|
||||
private List<DetectedGame> getDuplicates(List<DetectedGame> gamesToFilter) {
|
||||
return gamesToFilter.stream().filter(g -> Collections.frequency(gamesToFilter, g) > 1)
|
||||
.peek(d -> {
|
||||
log.warn("Found duplicate for game '{}' under path '{}'. Mapping must be done manually.", d.getTitle(), d.getPath());
|
||||
unmappableFileRepository.save(new UnmappableFile(d.getPath()));
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<Library> getLibraries() {
|
||||
return libraryRepository.findAll();
|
||||
}
|
||||
|
||||
public Library getLibrary(String path) {
|
||||
return libraryRepository.findByPath(path).orElse(null);
|
||||
}
|
||||
|
||||
public List<Library> getOrCreateLibraries() {
|
||||
libraryFolders.stream().map(Path::of)
|
||||
.filter(path -> path.toFile().isDirectory()) // check if path is a valid directory
|
||||
.filter(path -> !libraryRepository.existsByPathIgnoreCase(path.toString()))
|
||||
.forEach(path -> {
|
||||
// save new paths as library without platforms
|
||||
Library library = new Library(path.toString(), List.of());
|
||||
libraryRepository.save(library);
|
||||
});
|
||||
|
||||
List<Library> libraries = libraryRepository.findAll();
|
||||
libraries.forEach(library -> {
|
||||
// remap existing games to this library as well
|
||||
List<DetectedGame> gamesWithoutLibraryAssignment =
|
||||
detectedGameRepository.findByPathStartsWithAndLibraryIsNull(library.getPath());
|
||||
gamesWithoutLibraryAssignment.forEach(game -> game.setLibrary(library));
|
||||
detectedGameRepository.saveAll(gamesWithoutLibraryAssignment);
|
||||
});
|
||||
return libraries;
|
||||
}
|
||||
|
||||
public List<Platform> getPlatforms(String searchTerm, int limit) {
|
||||
return igdbWrapper.findPlatforms(searchTerm, limit);
|
||||
}
|
||||
|
||||
public Library mapPlatformsToLibrary(String path, String slugs) {
|
||||
Library library = libraryRepository.findByPath(path)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find library for path %s".formatted(path)));
|
||||
|
||||
Set<String> platformSlugs = Arrays.stream(slugs.split(",")).collect(toSet());
|
||||
|
||||
List<Platform> platforms = platformSlugs.stream()
|
||||
.map(slug -> {
|
||||
Optional<Platform> p = platformRepository.findBySlug(slug);
|
||||
return p.isPresent() ? p : igdbWrapper.getPlatformBySlug(slug);
|
||||
})
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.collect(toList());
|
||||
|
||||
library.setPlatforms(platforms);
|
||||
libraryRepository.save(library);
|
||||
|
||||
return library;
|
||||
}
|
||||
|
||||
private Predicate<Path> allPathsOrSpecific(String path) {
|
||||
return p -> isBlank(path) || p.equals(filesystemService.getPath(path));
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package de.grimsi.gameyfin.util;
|
||||
|
||||
import de.grimsi.gameyfin.config.properties.GameyfinProperties;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
public class FilenameUtil {
|
||||
|
||||
private static List<String> possibleGameFileExtensions;
|
||||
private static List<String> possibleGameFileSuffixes;
|
||||
// matches v1.1.1 v1.1 v1 version numbers
|
||||
private static final Pattern versionPattern = Pattern.compile("v(\\d+\\.)?(\\d+\\.)?(\\d+)");
|
||||
|
||||
// Suppress SONAR detecting this as a potential stack overflow
|
||||
// SONAR is correct, but I honestly don't know how to fix it
|
||||
// Also it would require 6k+ character long filenames to really overflow the JVM stack
|
||||
@SuppressWarnings("java:S5998")
|
||||
private static final Pattern trailingNoisePattern = Pattern.compile("( |\\(\\)|\\[]|[-_.])+$");
|
||||
@SuppressWarnings("java:S5998")
|
||||
private static final Pattern headingNoisePattern = Pattern.compile("^( |\\(\\)|\\[]|[-_.])+");
|
||||
|
||||
public FilenameUtil(GameyfinProperties gameyfinProperties) {
|
||||
possibleGameFileExtensions = gameyfinProperties.fileExtensions();
|
||||
|
||||
// Sort in descending length, so for example "windows" gets checked before "win"
|
||||
FilenameUtil.possibleGameFileSuffixes = gameyfinProperties.fileSuffixes();
|
||||
possibleGameFileSuffixes.sort((s1,s2) -> Integer.compare(s2.length(), s1.length()));
|
||||
}
|
||||
|
||||
public static String getFilenameWithoutExtension(Path p) {
|
||||
|
||||
// If the path points to a folder, return the folder name
|
||||
// Folders like "Counter Strike 1.6" would otherwise be returned as "Counter Strike 1"
|
||||
if (Files.isDirectory(p)) return FilenameUtils.getName(p.toString());
|
||||
|
||||
return FilenameUtils.getBaseName(p.toString());
|
||||
}
|
||||
|
||||
public static String getFilenameWithExtension(Path p) {
|
||||
return FilenameUtils.getName(p.toString());
|
||||
}
|
||||
|
||||
public static boolean hasGameArchiveExtension(Path p) {
|
||||
return possibleGameFileExtensions.contains(FilenameUtils.getExtension(p.getFileName().toString()));
|
||||
}
|
||||
|
||||
public static String getFilenameWithoutAdditions(Path p) {
|
||||
String name = getFilenameWithoutExtension(p).toLowerCase();
|
||||
for(String suffix : possibleGameFileSuffixes) {
|
||||
name = name.replace(suffix, "");
|
||||
}
|
||||
name = removePattern(name, versionPattern);
|
||||
name = removePattern(name, trailingNoisePattern);
|
||||
name = removePattern(name, headingNoisePattern);
|
||||
|
||||
// sanity check to never return an empty name
|
||||
return name.isBlank() ? getFilenameWithoutExtension(p) : name;
|
||||
}
|
||||
|
||||
public static String removePattern(String string, Pattern pattern) {
|
||||
Matcher matcher = pattern.matcher(string);
|
||||
if (matcher.find()) {
|
||||
return matcher.replaceAll("");
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package de.grimsi.gameyfin.util;
|
||||
|
||||
import com.google.protobuf.Timestamp;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class ProtobufUtil {
|
||||
public static Instant toInstant(Timestamp t) {
|
||||
return Instant.ofEpochSecond(t.getSeconds(), t.getNanos());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"name": "gameyfin.sources",
|
||||
"type": "java.lang.String[]",
|
||||
"description": "List of directories Gameyfin should scan for games."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
gameyfin:
|
||||
user: admin
|
||||
password: password
|
||||
|
||||
logging:
|
||||
level:
|
||||
de.grimsi: debug
|
||||
# org.springframework.web.reactive.function.client.ExchangeFunctions: debug
|
||||
org.apache.catalina.core.ContainerBase: info
|
||||
|
||||
spring.mvc.log-request-details: true
|
||||
@@ -1,5 +0,0 @@
|
||||
# General
|
||||
logging.level:
|
||||
root: info
|
||||
# Hides an error log on the first aborted download
|
||||
org.apache.catalina.core.ContainerBase: off
|
||||
@@ -1,10 +0,0 @@
|
||||
${AnsiColor.GREEN}
|
||||
_____ ___ _
|
||||
/ ___/ ___ _ __ _ ___ __ __ / _/ (_) ___
|
||||
/ (_ / / _ `/ / ' \/ -_) / // / / _/ / / / _ \
|
||||
\___/ \_,_/ /_/_/_/\__/ \_, / /_/ /_/ /_//_/
|
||||
/___/
|
||||
${AnsiColor.WHITE}
|
||||
${application.name} ${application.version}
|
||||
Powered by Spring Boot ${spring-boot.version}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
spring:
|
||||
jpa:
|
||||
open-in-view: true
|
||||
properties:
|
||||
hibernate:
|
||||
enable_lazy_load_no_trans: true
|
||||
event:
|
||||
merge:
|
||||
entity_copy_observer: allow
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
flyway:
|
||||
baseline-on-migrate: true
|
||||
datasource:
|
||||
username: gfadmin
|
||||
password: gameyfin
|
||||
db-name: gameyfin_db
|
||||
url: jdbc:h2:file:${gameyfin.db}/${spring.datasource.db-name}
|
||||
driverClassName: org.h2.Driver
|
||||
@@ -1,301 +0,0 @@
|
||||
gameyfin:
|
||||
folders:
|
||||
data: ""
|
||||
file-extensions:
|
||||
- 'rar' # Compressed Archive -- Use WinRAR or 7-Zip to open and extract.
|
||||
- '7z' # Compressed Archive -- Use WinRAR or 7-Zip to open and extract.
|
||||
- '7zip' # Compressed Archive -- Use WinRAR or 7-Zip to open and extract.
|
||||
- 'zip' # Compressed Archive -- Use WinRAR or 7-Zip to open and extract.
|
||||
- 'tar.gz' # Compressed Archive -- Use WinRAR or 7-Zip to open and extract.
|
||||
- 'n.gz' # Compressed Archive -- Use WinRAR or 7-Zip to open and extract.
|
||||
- 'tar' # Archive -- Use 7-Zip to open and extract.
|
||||
- '001' # Split Archive -- Use WinRAR or 7-Zip to open and extract.
|
||||
- 'part1' # Split Archive -- Use WinRAR or 7-Zip to open and extract.
|
||||
- 'r01' # Split Archive -- Use WinRAR or 7-Zip to open and extract.
|
||||
- 'gzip' # Compressed Archive -- Use 7-Zip to open and extract. Can be loaded by PCSX2 directly.
|
||||
- 'iso' # Disk Image for Multiple systems
|
||||
- 'img' # Disk Image for Multiple systems
|
||||
- 'ccd' # CloneCD Control File for Multiple systems -- Usually comes with .img files.
|
||||
- 'bin' # Binary File for Multiple systems -- Usually accompanied with, and used by, a cue sheet.
|
||||
- 'cue' # Cue Sheet for Multiple systems -- Metadata file for .bin/.iso/.img tracks.
|
||||
- 'chd' # Compressed Hard Disk for Multiple systems -- Originally made for MAME, now used by multiple emulators
|
||||
- 'mdf' # Media Descriptor File for Multiple systems -- Disc image made from Alcohol 120%
|
||||
- 'mds' # Media Descriptor Sheet for Multiple systems -- Similar to .cue or .ccd, but for .mdf files.
|
||||
- 'ecm' # Error Code Modeler for PlayStation -- Compressed PS1 disc image, use Unecm to decompress it.
|
||||
- 'cso' # Compressed ISO for PS2/PSP/GC/Wii -- Compressed disc image, see here to convert to and from .iso
|
||||
- 'gcz' # Compressed Dolphin ISO for GC/Wii -- Compressed Wii/GC disc image, see here to convert to and from .iso or Compressed GameCube Disc Image for GameCube -- Can be either a compressed .gcm or .iso
|
||||
- 'rvz' # Modern Dolphin Format for GC/Wii -- A new compressed format developed by the Dolphin team able to efficiently compress junk/padding data, allowing both small file sizes and archival quality.
|
||||
- 'cdi' # CD Image for Dreamcast -- Typically used with Dreamcast, CDI is a compressed GDI
|
||||
- 'gdi' # CD Image for Dreamcast -- Typically used with Dreamcast, CDI is a compressed GDI
|
||||
- 'sbI' # CD Image -- Sub-channel data. Generally used in the case of PS1 for Digital Rights Management
|
||||
- 'fds' # Famicom Disk System for NES
|
||||
- 'ndd' # Nintendo 64DD for N64
|
||||
- 'wav' # Waveform Audio File Format for Tape Based Systems -- Raw data in audio format†
|
||||
- 'tap' # Tape File for Tape Based Systems -- Raw tape data usable by appropriate emulators
|
||||
- 'tzx' # ZX Spectrum Tape File for Compatible Tape Based Systems (ZXS, CPC, C64, etc.) -- Prefered file type for all emulators, perfect representation
|
||||
- 'cdc' # CPC Digital Tape for Amstrad CPC -- Identical to the .tzx file but useable only by Amstrad CPC emulators
|
||||
- 'cas' # PC-6000 and PC-6601 Cassette for NEC PC-6000 series
|
||||
- 'nes' # NES ROM for NES -- .unf and .unif correspond to the now deprecated Universal NES Image Format.
|
||||
- 'nez' # NES ROM for NES -- .unf and .unif correspond to the now deprecated Universal NES Image Format.
|
||||
- 'unf' # NES ROM for NES -- .unf and .unif correspond to the now deprecated Universal NES Image Format.
|
||||
- 'unif' # NES ROM for NES -- .unf and .unif correspond to the now deprecated Universal NES Image Format.
|
||||
- 'smc' # Super Magicom for SNES -- Headered ROMs dumped from a Super Magicom copier.
|
||||
- 'sfc' # Super Famicom ROM for SNES -- Headerless SNES ROM
|
||||
- 'md' # Multi Game Doctor for Genesis -- ROMs dumped from a Multi Game Doctor
|
||||
- 'smd' # Super Magic Drive for Genesis -- Headered ROMs dumped from a Super Magic Drive
|
||||
- 'gen' # Genesis ROM for Genesis -- Headerless Genesis ROM
|
||||
- 'gg' # Game Gear ROM for Game Gear
|
||||
- 'z64' # Zip Diskette N64 ROM for Nintendo 64 -- Headered N64 ROMS dumped with a Mr. Backup Z64
|
||||
- 'v64' # Doctor V64 for Nintendo 64 -- Headered N64 ROMS dumped with a Doctor V64
|
||||
- 'n64' # N64 ROM for Nintendo 64
|
||||
- 'gb' # Game Boy ROM for Game Boy
|
||||
- 'gbc' # Game Boy Color ROM for Game Boy Color
|
||||
- 'gba' # Game Boy Advance ROM for Game Boy Advance
|
||||
- 'gcm' # GameCube Master Image for GameCube -- GC disc dumped the way it's read
|
||||
- 'nds' # Nintendo DS ROM for Nintendo DS or Game Boy Advance ROM for Game Boy Advance
|
||||
- 'srl' # Nintendo DS ROM for Nintendo DS
|
||||
- 'dsi' # DSiWare ROM for DSiWare -- Different from regular DS ROMs.
|
||||
- 'app' # DSiWare ROM for DSiWare -- Different from regular DS ROMs.
|
||||
- 'ids' # iQue DS ROM for iQue/Nintendo DS -- The same thing as regular Nintendo DS ROMs, but for the Chinese iQue DS.
|
||||
- 'wbfs' # Wii Backup File System for Wii -- Use WBFS Manager to convert to and from .iso
|
||||
- 'wad' # Wii Application Data for Wii or "Where's all the data files?" for Doom -- Used for Wii channels or Doom
|
||||
- 'cia' # CTR Importable Archive for 3DS
|
||||
- '3ds' # Nintendo 3DS ROM for 3DS
|
||||
- 'nsp' # Nintendo Switch ROM for Nintendo Switch -- Dump of SD and NAND games, Updates
|
||||
- 'xci' # Nintendo Switch ROM for Nintendo Switch -- Dump of a game cartridge
|
||||
- 'ngp' # NGP/C ROM for Neo Geo Pocket/Color
|
||||
- 'ngc' # NGP/C ROM for Neo Geo Pocket/Color
|
||||
- 'pce' # PC Engine ROM for PC Engine
|
||||
- 'vpk' # PlayStation Vita ROM for PlayStation Vita -- Compressed files forming a PlayStation Vita ROM
|
||||
- 'vb' # Virtual Boy ROM for Virtual Boy -- Also used for Visual Basic source files
|
||||
- 'ws' # WonderSwan (Color) ROM for WonderSwan/Color
|
||||
- 'wsc' # WonderSwan (Color) ROM for WonderSwan/Color
|
||||
- 'ipa' # Apple iPhone App Data for iOS
|
||||
- 'apk' # Android App Data for Android
|
||||
- 'obb' # Android App Data Resources for Android
|
||||
- 'elf' # Executable and Linkable Format for PS2/PS3/GC/Wii -- Typically a homebrew/small application file
|
||||
- 'pbp' # Perl Builder File for PlayStation Portable -- Game and homebrew eboot file, also used for PS1 ISO storage (eboot.pbp)
|
||||
- 'dol' # Dolphin File for GameCube/Wii -- Executable
|
||||
- 'xbe' # Xbox Executable for Xbox/X360 -- Xbox Executable. Usually comes with a folder with game data. X360 ones can be loaded by Xenia.
|
||||
- 'xex' # Xbox Executable for Xbox/X360 -- Xbox Executable. Usually comes with a folder with game data. X360 ones can be loaded by Xenia.
|
||||
- 'cfg' # Configuration File for Multiple systems -- Use a text editor such as notepad/Notepad++ to edit
|
||||
- 'ini' # Configuration File for Multiple systems -- Use a text editor such as notepad/Notepad++ to edit
|
||||
- 'dll' # Dynamic Link Library for Multiple systems -- Typically used for plugins/emulation cores in emulators
|
||||
- 'so' # Shared Library for Multiple systems -- Typically used for plugins/emulation cores in emulators
|
||||
- 'xml' # Extensible Markup Language for MAME/MESS -- Contains various information about emulated systems and ROMs.
|
||||
- 'hsi' # XML-like for MESS -- Contains various information about ROMs.
|
||||
- 'lay' # Layout for MAME/MESS -- XML-like file used to describe the visual layout of artwork overlays and placement of emulation input/output.
|
||||
- 'nv' # Non-Volatile RAM for MAME/MESS -- Created by default to store nvram of emulated systems or software in the NVRAM folder.
|
||||
- 'm3u' # Playlist file for Multiple systems -- Used to play multiple discs back to back, automates disc swapping.
|
||||
file-suffixes: windows, win, english, win32, win64, opengl, stable
|
||||
igdb:
|
||||
api:
|
||||
endpoints:
|
||||
auth: https://id.twitch.tv/oauth2/token
|
||||
base: https://api.igdb.com/v4/
|
||||
client-id:
|
||||
client-secret:
|
||||
max-concurrent-requests: 2
|
||||
max-requests-per-second: 4
|
||||
config:
|
||||
preferred-platforms:
|
||||
- 3 # Linux or GNU/Linux
|
||||
- 4 # Nintendo 64 or N64
|
||||
- 5 # Wii or Revolution
|
||||
- 6 # PC (Microsoft Windows) or mswin
|
||||
- 7 # PlayStation or PSX, PSOne, PS
|
||||
- 8 # PlayStation 2 or PS2
|
||||
- 9 # PlayStation 3 or PS3
|
||||
- 11 # Xbox
|
||||
- 12 # Xbox 360 or X360
|
||||
# - 13 # DOS or PC DOS
|
||||
- 14 # Mac or Mac OS
|
||||
# - 15 # Commodore C64/128/MAX or C64/C128/MAX
|
||||
# - 16 # Amiga
|
||||
- 18 # Nintendo Entertainment System or NES
|
||||
- 19 # Super Nintendo Entertainment System or SNES, Super Nintendo
|
||||
- 20 # Nintendo DS or NDS
|
||||
- 21 # Nintendo GameCube or GCN
|
||||
- 22 # Game Boy Color or GBC
|
||||
- 23 # Dreamcast or DC
|
||||
- 24 # Game Boy Advance or GBA
|
||||
# - 25 # Amstrad CPC or Colour Personal Computer
|
||||
# - 26 # ZX Spectrum
|
||||
# - 27 # MSX
|
||||
- 29 # Sega Mega Drive/Genesis or Sega Genesis
|
||||
# - 30 # Sega 32X
|
||||
- 32 # Sega Saturn or JVC Saturn, Hi-Saturn, Samsung Saturn, V-Saturn
|
||||
- 33 # Game Boy or GB
|
||||
# - 34 # Android or Infocusa3
|
||||
- 35 # Sega Game Gear or GG
|
||||
- 37 # Nintendo 3DS or 3DS
|
||||
- 38 # PlayStation Portable or PSP
|
||||
# - 39 # iOS
|
||||
- 41 # Wii U
|
||||
# - 42 # N-Gage or NGage
|
||||
# - 44 # Tapwave Zodiac
|
||||
- 46 # PlayStation Vita or PS Vita
|
||||
- 47 # Virtual Console
|
||||
- 48 # PlayStation 4 or PS4
|
||||
# - 49 # Xbox One or XONE
|
||||
# - 50 # 3DO Interactive Multiplayer or 3DO
|
||||
# - 51 # Family Computer Disk System or Famicom Disk System, FDS
|
||||
# - 52 # Arcade
|
||||
# - 53 # MSX2
|
||||
# - 55 # Legacy Mobile Device or Legacy Cellphone
|
||||
# - 57 # WonderSwan or WS
|
||||
- 58 # Super Famicom or SFC
|
||||
# - 59 # Atari 2600 or Atari VCS
|
||||
# - 60 # Atari 7800 or Atari 7800 ProSystem
|
||||
# - 61 # Atari Lynx
|
||||
# - 62 # Atari Jaguar
|
||||
# - 63 # Atari ST/STE
|
||||
- 64 # Sega Master System/Mark III or SMS, Mark III
|
||||
# - 65 # Atari 8-bit
|
||||
# - 66 # Atari 5200 or Atari 5200 SuperSystem
|
||||
# - 67 # Intellivision
|
||||
# - 68 # ColecoVision
|
||||
# - 69 # BBC Microcomputer System or BBC Micro
|
||||
# - 70 # Vectrex
|
||||
# - 71 # Commodore VIC-20
|
||||
# - 72 # Ouya
|
||||
# - 73 # BlackBerry OS
|
||||
# - 74 # Windows Phone or WP
|
||||
# - 75 # Apple II or apple ][
|
||||
# - 77 # Sharp X1
|
||||
# - 78 # Sega CD or Mega CD
|
||||
# - 79 # Neo Geo MVS or Neo Geo Multi Video System
|
||||
# - 80 # Neo Geo AES or AES
|
||||
# - 82 # Web browser or Internet
|
||||
# - 84 # SG-1000 or Sega Game 1000
|
||||
# - 85 # Donner Model 30
|
||||
# - 86 # TurboGrafx-16/PC Engine
|
||||
# - 87 # Virtual Boy or VB
|
||||
# - 88 # Odyssey or Magnavox Odyssey; Odysee; Odisea; Odissea
|
||||
# - 89 # Microvision
|
||||
# - 90 # Commodore PET
|
||||
# - 91 # Bally Astrocade or Bally Arcade
|
||||
# - 93 # Commodore 16 or C16
|
||||
# - 94 # Commodore Plus/4
|
||||
# - 95 # PDP-1 or Programmed Data Processor-1
|
||||
# - 96 # PDP-10
|
||||
# - 97 # PDP-8
|
||||
# - 98 # DEC GT40
|
||||
# - 99 # Family Computer or Famicom
|
||||
# - 100 # Analogue electronics
|
||||
# - 101 # Ferranti Nimrod Computer
|
||||
# - 102 # EDSAC or Electronic Delay Storage Automatic Calculator
|
||||
# - 103 # PDP-7
|
||||
# - 104 # HP 2100
|
||||
# - 105 # HP 3000
|
||||
# - 106 # SDS Sigma 7
|
||||
# - 107 # Call-A-Computer time-shared mainframe computer system
|
||||
# - 108 # PDP-11
|
||||
# - 109 # CDC Cyber 70
|
||||
# - 110 # PLATO or Programmed Logic for Automatic Teaching Operations
|
||||
# - 111 # Imlac PDS-1
|
||||
# - 112 # Microcomputer
|
||||
# - 113 # OnLive Game System
|
||||
# - 114 # Amiga CD32
|
||||
# - 115 # Apple IIGS
|
||||
# - 116 # Acorn Archimedes
|
||||
# - 117 # Philips CD-i
|
||||
# - 118 # FM Towns
|
||||
# - 119 # Neo Geo Pocket or NGP
|
||||
# - 120 # Neo Geo Pocket Color or NGPC
|
||||
# - 121 # Sharp X68000
|
||||
# - 122 # Nuon
|
||||
# - 123 # WonderSwan Color or WSC
|
||||
# - 124 # SwanCrystal
|
||||
# - 125 # PC-8801
|
||||
# - 126 # TRS-80
|
||||
# - 127 # Fairchild Channel F
|
||||
# - 128 # PC Engine SuperGrafx
|
||||
# - 129 # Texas Instruments TI-99 or Texas Instruments TI-99/4A
|
||||
- 130 # Nintendo Switch or NX
|
||||
# - 131 # Nintendo PlayStation or Nintendo Super Disc
|
||||
# - 132 # Amazon Fire TV
|
||||
# - 133 # Odyssey 2 / Videopac G7000 or Magnavox Odyssey²
|
||||
# - 134 # Acorn Electron
|
||||
# - 135 # Hyper Neo Geo 64
|
||||
# - 136 # Neo Geo CD or NGCD
|
||||
- 137 # New Nintendo 3DS or n3DS
|
||||
# - 138 # VC 4000
|
||||
# - 139 # 1292 Advanced Programmable Video System
|
||||
# - 140 # AY-3-8500
|
||||
# - 141 # AY-3-8610
|
||||
# - 142 # PC-50X Family
|
||||
# - 143 # AY-3-8760
|
||||
# - 144 # AY-3-8710
|
||||
# - 145 # AY-3-8603
|
||||
# - 146 # AY-3-8605
|
||||
# - 147 # AY-3-8606
|
||||
# - 148 # AY-3-8607
|
||||
# - 149 # PC-98
|
||||
# - 150 # Turbografx-16/PC Engine CD or TG-16CD/PCECD
|
||||
# - 151 # TRS-80 Color Computer or Tandy Color Computer
|
||||
# - 152 # FM-7 or Fujitsu Micro 7
|
||||
# - 153 # Dragon 32/64
|
||||
# - 154 # Amstrad PCW
|
||||
# - 155 # Tatung Einstein
|
||||
# - 156 # Thomson MO5
|
||||
# - 157 # NEC PC-6000 Series
|
||||
# - 158 # Commodore CDTV or Commodore Dynamic Total Vision
|
||||
- 159 # Nintendo DSi
|
||||
# - 161 # Windows Mixed Reality or WMR
|
||||
# - 162 # Oculus VR
|
||||
# - 163 # SteamVR
|
||||
# - 164 # Daydream
|
||||
- 165 # PlayStation VR or PSVR
|
||||
# - 166 # Pokémon mini
|
||||
# - 167 # PlayStation 5 or PS5
|
||||
# - 169 # Xbox Series X|S or XSX
|
||||
# - 170 # Google Stadia or Stadia
|
||||
# - 203 # DUPLICATE Stadia
|
||||
# - 236 # Exidy Sorcerer
|
||||
# - 237 # Sol-20
|
||||
# - 238 # DVD Player or Digital Versatile Disc Player
|
||||
# - 239 # Blu-ray Player
|
||||
# - 240 # Zeebo
|
||||
# - 274 # PC-FX
|
||||
# - 306 # Satellaview
|
||||
# - 307 # Game & Watch or Tricotronic, GW, G&W
|
||||
# - 308 # Playdia
|
||||
# - 309 # Evercade
|
||||
# - 339 # Sega Pico or Kids Computer Pico
|
||||
# - 372 # OOParts
|
||||
# - 373 # Sinclair ZX81 or ZX81
|
||||
# - 374 # Sharp MZ-2200
|
||||
# - 375 # Epoch Cassette Vision
|
||||
# - 376 # Epoch Super Cassette Vision or YENO Super Cassette Vision
|
||||
# - 377 # Plug & Play or TV Game
|
||||
# - 378 # Gamate or Super Boy
|
||||
# - 379 # Game.com or Tiger Game.com
|
||||
# - 380 # Casio Loopy or Loopy
|
||||
# - 381 # Playdate
|
||||
# - 382 # Intellivision Amico
|
||||
# - 384 # Oculus Quest or Quest
|
||||
# - 385 # Oculus Rift or Rift
|
||||
# - 386 # Meta Quest 2 or Quest 2
|
||||
# - 387 # Oculus Go or Go
|
||||
# - 388 # Gear VR or Samsung Gear VR
|
||||
# - 389 # AirConsole
|
||||
# - 390 # PlayStation VR2 or PSVR2
|
||||
# - 405 # Windows Mobile or Pocket PC
|
||||
# - 406 # Sinclair QL or Sinclair Quantum Leap
|
||||
# - 407 # HyperScan
|
||||
# - 408 # Mega Duck/Cougar Boy or WG-108
|
||||
# - 409 # Legacy Computer
|
||||
# - 410 # Atari Jaguar CD or Jag CD
|
||||
# - 411 # Handheld Electronic LCD or Handheld LCD Game
|
||||
# - 412 # Leapster or Leapster Learning Game System
|
||||
# - 413 # Leapster Explorer/LeadPad Explorer
|
||||
# - 414 # LeapTV or LeapTV VCD
|
||||
# - 415 # Watara/QuickShot Supervision
|
||||
# - 416 # Nintendo 64DD or 64DD
|
||||
# - 417 # Palm OS or Garnet OS
|
||||
# - 438 # Arduboy
|
||||
# - 439 # V.Smile or V.SMILE TV LEARNING SYSTEM
|
||||
# - 440 # Visual Memory Unit / Visual Memory System or VMU / VMS
|
||||
# - 441 # PocketStation
|
||||
# - 471 # Meta Quest 3
|
||||
@@ -1,30 +0,0 @@
|
||||
application:
|
||||
version: '@project.version@'
|
||||
name: Gameyfin
|
||||
|
||||
server:
|
||||
servlet:
|
||||
context-path: /
|
||||
error:
|
||||
include-stacktrace: never
|
||||
|
||||
spring:
|
||||
jackson:
|
||||
deserialization:
|
||||
fail-on-unknown-properties: false
|
||||
default-property-inclusion: non_null
|
||||
mapper:
|
||||
accept-case-insensitive-enums: true
|
||||
mvc:
|
||||
async:
|
||||
request-timeout: -1
|
||||
|
||||
management:
|
||||
endpoint:
|
||||
health:
|
||||
enabled: true
|
||||
endpoints:
|
||||
enabled-by-default: false
|
||||
|
||||
gameyfin:
|
||||
internal-folder: .gameyfin
|
||||
@@ -1,148 +0,0 @@
|
||||
-- Automatically generated by JPA
|
||||
|
||||
-- Hibernate sequence
|
||||
create sequence HIBERNATE_SEQUENCE start with 1 increment by 1;
|
||||
|
||||
-- Detected Games
|
||||
create table DETECTED_GAME
|
||||
(
|
||||
slug varchar(255) not null,
|
||||
category varchar(255),
|
||||
confirmed_match boolean default false,
|
||||
cover_id varchar(255) not null,
|
||||
critics_rating integer,
|
||||
disk_size bigint not null,
|
||||
lan_support boolean not null,
|
||||
max_players integer not null,
|
||||
offline_coop boolean not null,
|
||||
online_coop boolean not null,
|
||||
path varchar(255) not null,
|
||||
release_date timestamp,
|
||||
summary CLOB,
|
||||
title varchar(255) not null,
|
||||
total_rating integer,
|
||||
user_rating integer,
|
||||
primary key (slug)
|
||||
);
|
||||
|
||||
-- Companies
|
||||
create table COMPANY
|
||||
(
|
||||
slug varchar(255) not null,
|
||||
logo_id varchar(255),
|
||||
name varchar(255) not null,
|
||||
primary key (slug)
|
||||
);
|
||||
|
||||
-- Genres
|
||||
create table GENRE
|
||||
(
|
||||
slug varchar(255) not null,
|
||||
name varchar(255),
|
||||
primary key (slug)
|
||||
);
|
||||
|
||||
-- Themes
|
||||
create table THEME
|
||||
(
|
||||
slug varchar(255) not null,
|
||||
name varchar(255),
|
||||
primary key (slug)
|
||||
);
|
||||
|
||||
-- Keywords
|
||||
create table KEYWORD
|
||||
(
|
||||
slug varchar(255) not null,
|
||||
name varchar(255),
|
||||
primary key (slug)
|
||||
);
|
||||
|
||||
-- Player Perspectives
|
||||
create table PLAYER_PERSPECTIVE
|
||||
(
|
||||
slug varchar(255) not null,
|
||||
name varchar(255),
|
||||
primary key (slug)
|
||||
);
|
||||
|
||||
-- Unmappable files
|
||||
create table UNMAPPABLE_FILE
|
||||
(
|
||||
id bigint not null,
|
||||
path varchar(255),
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
-- Game <-> Companies
|
||||
create table DETECTED_GAME_COMPANIES
|
||||
(
|
||||
detected_game_slug varchar(255) not null,
|
||||
companies_slug varchar(255) not null
|
||||
);
|
||||
alter table DETECTED_GAME_COMPANIES
|
||||
add constraint companies_company_slug foreign key (companies_slug) references company;
|
||||
alter table DETECTED_GAME_COMPANIES
|
||||
add constraint companies_detected_game_slug foreign key (detected_game_slug) references detected_game;
|
||||
|
||||
-- Game <-> Genres
|
||||
create table DETECTED_GAME_GENRES
|
||||
(
|
||||
detected_game_slug varchar(255) not null,
|
||||
genres_slug varchar(255) not null
|
||||
);
|
||||
alter table DETECTED_GAME_GENRES
|
||||
add constraint genres_genre_slug foreign key (genres_slug) references genre;
|
||||
alter table DETECTED_GAME_GENRES
|
||||
add constraint genres_detected_game_slug foreign key (detected_game_slug) references detected_game;
|
||||
|
||||
-- Game <-> Themes
|
||||
create table DETECTED_GAME_THEMES
|
||||
(
|
||||
detected_game_slug varchar(255) not null,
|
||||
themes_slug varchar(255) not null
|
||||
);
|
||||
alter table DETECTED_GAME_THEMES
|
||||
add constraint themes_theme_slug foreign key (themes_slug) references theme;
|
||||
alter table DETECTED_GAME_THEMES
|
||||
add constraint themes_detected_game_slug foreign key (detected_game_slug) references detected_game;
|
||||
|
||||
-- Game <-> Keywords
|
||||
create table DETECTED_GAME_KEYWORDS
|
||||
(
|
||||
detected_game_slug varchar(255) not null,
|
||||
keywords_slug varchar(255) not null
|
||||
);
|
||||
alter table DETECTED_GAME_KEYWORDS
|
||||
add constraint keywords_keyword_slug foreign key (keywords_slug) references keyword;
|
||||
alter table DETECTED_GAME_KEYWORDS
|
||||
add constraint keywords_detected_game_slug foreign key (detected_game_slug) references detected_game;
|
||||
|
||||
-- Game <-> Player Perspectives
|
||||
create table DETECTED_GAME_PLAYER_PERSPECTIVES
|
||||
(
|
||||
detected_game_slug varchar(255) not null,
|
||||
player_perspectives_slug varchar(255) not null
|
||||
);
|
||||
alter table DETECTED_GAME_PLAYER_PERSPECTIVES
|
||||
add constraint player_perspectives_player_perspective_slug foreign key (player_perspectives_slug) references player_perspective;
|
||||
alter table DETECTED_GAME_PLAYER_PERSPECTIVES
|
||||
add constraint player_perspectives_detected_game_slug foreign key (detected_game_slug) references detected_game;
|
||||
|
||||
-- Game <-> Videos
|
||||
create table DETECTED_GAME_VIDEO_IDS
|
||||
(
|
||||
detected_game_slug varchar(255) not null,
|
||||
video_ids varchar(255)
|
||||
);
|
||||
alter table DETECTED_GAME_VIDEO_IDS
|
||||
add constraint video_ids_detected_game_slug foreign key (detected_game_slug) references detected_game;
|
||||
|
||||
-- Game <-> Screenshots
|
||||
create table DETECTED_GAME_SCREENSHOT_IDS
|
||||
(
|
||||
detected_game_slug varchar(255) not null,
|
||||
screenshot_ids varchar(255)
|
||||
);
|
||||
alter table DETECTED_GAME_SCREENSHOT_IDS
|
||||
add constraint screenshot_ids_detected_game_slug foreign key (detected_game_slug) references detected_game;
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
-- Add field "addedToLibrary" to the "DetectedGame" table with the default value of CURRENT_TIMESTAMP()
|
||||
|
||||
alter table DETECTED_GAME
|
||||
add added_to_library timestamp not null default CURRENT_TIMESTAMP()
|
||||
@@ -1,45 +0,0 @@
|
||||
-- Add platforms
|
||||
|
||||
-- Platforms
|
||||
CREATE TABLE platform
|
||||
(
|
||||
slug VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255),
|
||||
logo_id VARCHAR(255),
|
||||
PRIMARY KEY (slug)
|
||||
);
|
||||
|
||||
-- Game <-> Platforms
|
||||
CREATE TABLE detected_game_platforms
|
||||
(
|
||||
detected_game_slug VARCHAR(255) NOT NULL,
|
||||
platforms_slug VARCHAR(255) NOT NULL
|
||||
);
|
||||
ALTER TABLE detected_game_platforms
|
||||
ADD CONSTRAINT platforms_platform_slug FOREIGN KEY (platforms_slug) REFERENCES platform;
|
||||
ALTER TABLE detected_game_platforms
|
||||
ADD CONSTRAINT platforms_detected_game_slug FOREIGN KEY (detected_game_slug) REFERENCES detected_game;
|
||||
|
||||
-- Add libraries
|
||||
|
||||
-- Libraries
|
||||
CREATE TABLE library
|
||||
(
|
||||
path VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (path)
|
||||
);
|
||||
|
||||
-- Library <-> Platforms
|
||||
CREATE TABLE library_platforms
|
||||
(
|
||||
library_path VARCHAR(255) NOT NULL,
|
||||
platforms_slug VARCHAR(255) NOT NULL
|
||||
);
|
||||
ALTER TABLE library_platforms
|
||||
ADD CONSTRAINT libraries_platform_slug FOREIGN KEY (platforms_slug) REFERENCES platform;
|
||||
ALTER TABLE library_platforms
|
||||
ADD CONSTRAINT libraries_library_path FOREIGN KEY (library_path) REFERENCES library;
|
||||
|
||||
-- Library <-> Game
|
||||
ALTER TABLE detected_game
|
||||
ADD library VARCHAR(255);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,373 +0,0 @@
|
||||
package de.grimsi.gameyfin.igdb;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.protobuf.Message;
|
||||
import com.google.protobuf.Timestamp;
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.config.WebClientConfig;
|
||||
import de.grimsi.gameyfin.config.properties.GameyfinProperties;
|
||||
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
|
||||
import de.grimsi.gameyfin.entities.Platform;
|
||||
import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto;
|
||||
import de.grimsi.gameyfin.mapper.GameMapper;
|
||||
import io.github.resilience4j.bulkhead.Bulkhead;
|
||||
import io.github.resilience4j.bulkhead.BulkheadConfig;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiter;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
import okio.Buffer;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static de.grimsi.gameyfin.igdb.IgdbApiQueryBuilder.equal;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IgdbWrapperTest {
|
||||
|
||||
private static final MockWebServer igdbApiMock = new MockWebServer();
|
||||
private static final MockWebServer twitchApiMock = new MockWebServer();
|
||||
private static final EasyRandom easyRandom = new EasyRandom();
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private static IgdbWrapper target;
|
||||
|
||||
@BeforeAll
|
||||
static void setup() throws IOException, InterruptedException {
|
||||
WebClientConfig webClientConfigMock = mock(WebClientConfig.class);
|
||||
GameMapper gameMapperMock = mock(GameMapper.class);
|
||||
GameyfinProperties gameyfinPropertiesMock = mock(GameyfinProperties.class, Mockito.RETURNS_DEEP_STUBS);
|
||||
|
||||
target = new IgdbWrapper(WebClient.builder(), webClientConfigMock, gameMapperMock, gameyfinPropertiesMock);
|
||||
|
||||
igdbApiMock.start();
|
||||
twitchApiMock.start();
|
||||
|
||||
ReflectionTestUtils.setField(target, "clientId", "client_id_value");
|
||||
ReflectionTestUtils.setField(target, "clientSecret", "client_secret_value");
|
||||
ReflectionTestUtils.setField(target, "igdbApiBaseUrl", "http://localhost:%s".formatted(igdbApiMock.getPort()));
|
||||
ReflectionTestUtils.setField(target, "twitchAuthUrl", "http://localhost:%s/oauth2/token".formatted(twitchApiMock.getPort()));
|
||||
|
||||
when(gameyfinPropertiesMock.igdb().config().preferredPlatforms()).thenReturn(List.of(6));
|
||||
|
||||
when(webClientConfigMock.getIgdbConcurrencyLimiter()).thenReturn(Bulkhead.of("test_bulkhead", BulkheadConfig.ofDefaults()));
|
||||
when(webClientConfigMock.getIgdbRateLimiter()).thenReturn(RateLimiter.of("test_ratelimiter", RateLimiterConfig.ofDefaults()));
|
||||
|
||||
TwitchOAuthTokenDto mockToken = easyRandom.nextObject(TwitchOAuthTokenDto.class);
|
||||
|
||||
twitchApiMock.enqueue(new MockResponse()
|
||||
.setBody(objectMapper.writeValueAsString(mockToken))
|
||||
.addHeader("Content-Type", "application/json"));
|
||||
|
||||
target.init();
|
||||
|
||||
RecordedRequest r = twitchApiMock.takeRequest();
|
||||
assertThat(r.getRequestUrl()).isNotNull();
|
||||
assertThat(r.getRequestUrl().encodedPath()).isEqualTo("/oauth2/token");
|
||||
assertThat(r.getRequestUrl().queryParameter("client_id")).isEqualTo("client_id_value");
|
||||
assertThat(r.getRequestUrl().queryParameter("client_secret")).isEqualTo("client_secret_value");
|
||||
assertThat(r.getRequestUrl().queryParameter("grant_type")).isEqualTo("client_credentials");
|
||||
|
||||
twitchApiMock.shutdown();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() throws IOException {
|
||||
igdbApiMock.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGameById() throws InterruptedException {
|
||||
//Igdb.GameResult gameResult = easyRandom.nextObject(Igdb.GameResult.class);
|
||||
Igdb.GameResult gameResult = Igdb.GameResult.newBuilder()
|
||||
.addAllGames(List.of(
|
||||
Igdb.Game.newBuilder().setId(easyRandom.nextLong()).build(),
|
||||
Igdb.Game.newBuilder().setId(easyRandom.nextLong()).build(),
|
||||
Igdb.Game.newBuilder().setId(easyRandom.nextLong()).build()))
|
||||
.build();
|
||||
|
||||
Long gameId = gameResult.getGames(0).getId();
|
||||
|
||||
igdbApiMock.enqueue(new MockResponse()
|
||||
.setBody(toBuffer(gameResult))
|
||||
.setHeader("Content-Type", "application/protobuf")
|
||||
);
|
||||
|
||||
Optional<Igdb.Game> gameOptional = target.getGameById(gameId);
|
||||
|
||||
assertThat(gameOptional).isPresent();
|
||||
|
||||
Igdb.Game game = gameOptional.get();
|
||||
|
||||
assertThat(game.getId()).isEqualTo(gameId);
|
||||
|
||||
RecordedRequest r = igdbApiMock.takeRequest();
|
||||
assertThat(r.getRequestUrl()).isNotNull();
|
||||
assertThat(r.getRequestUrl().encodedPath()).isEqualTo("/%s".formatted(IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF));
|
||||
|
||||
String expectedQuery = "fields %s;limit 1;where id = %d;".formatted(IgdbApiProperties.GAME_QUERY_FIELDS_STRING, gameId);
|
||||
|
||||
assertThat(r.getBody().readUtf8()).isEqualTo(expectedQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGameBySlug() throws InterruptedException {
|
||||
Igdb.GameResult gameResult = Igdb.GameResult.newBuilder()
|
||||
.addAllGames(List.of(
|
||||
Igdb.Game.newBuilder().setSlug("game_slug_1").build(),
|
||||
Igdb.Game.newBuilder().setSlug("game_slug_2").build(),
|
||||
Igdb.Game.newBuilder().setSlug("game_slug_3").build()))
|
||||
.build();
|
||||
|
||||
String gameSlug = gameResult.getGames(0).getSlug();
|
||||
|
||||
igdbApiMock.enqueue(new MockResponse()
|
||||
.setBody(toBuffer(gameResult))
|
||||
.setHeader("Content-Type", "application/protobuf")
|
||||
);
|
||||
|
||||
Optional<Igdb.Game> gameOptional = target.getGameBySlug("game_slug_1");
|
||||
|
||||
assertThat(gameOptional).isPresent();
|
||||
|
||||
Igdb.Game game = gameOptional.get();
|
||||
|
||||
assertThat(game.getSlug()).isEqualTo(gameSlug);
|
||||
|
||||
RecordedRequest r = igdbApiMock.takeRequest();
|
||||
assertThat(r.getRequestUrl()).isNotNull();
|
||||
assertThat(r.getRequestUrl().encodedPath()).isEqualTo("/%s".formatted(IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF));
|
||||
|
||||
String expectedQuery = "fields %s;limit 1;where slug = \"%s\";".formatted(IgdbApiProperties.GAME_QUERY_FIELDS_STRING, gameSlug);
|
||||
|
||||
assertThat(r.getBody().readUtf8()).isEqualTo(expectedQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findPossibleMatchingTitles() throws InterruptedException {
|
||||
Igdb.GameResult gameResult = Igdb.GameResult.newBuilder()
|
||||
.addAllGames(List.of(
|
||||
Igdb.Game.newBuilder().setName("title_1").build(),
|
||||
Igdb.Game.newBuilder().setName("title_2").build(),
|
||||
Igdb.Game.newBuilder().setName("title_3").build()))
|
||||
.build();
|
||||
|
||||
String gameTitle = gameResult.getGames(0).getName();
|
||||
|
||||
igdbApiMock.enqueue(new MockResponse()
|
||||
.setBody(toBuffer(gameResult))
|
||||
.setHeader("Content-Type", "application/protobuf")
|
||||
);
|
||||
|
||||
List<AutocompleteSuggestionDto> suggestions = target.findPossibleMatchingTitles(gameTitle, gameResult.getGamesCount());
|
||||
|
||||
assertThat(suggestions).hasSize(gameResult.getGamesCount());
|
||||
|
||||
RecordedRequest r = igdbApiMock.takeRequest();
|
||||
assertThat(r.getRequestUrl()).isNotNull();
|
||||
assertThat(r.getRequestUrl().encodedPath()).isEqualTo("/%s".formatted(IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF));
|
||||
|
||||
String expectedQuery = "search \"%s\";fields slug,name,first_release_date,platforms.name;limit %d;where platforms = (6);".formatted(gameTitle, gameResult.getGamesCount());
|
||||
|
||||
assertThat(r.getBody().readUtf8()).isEqualTo(expectedQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchForGameByTitle_exactMatch() throws InterruptedException {
|
||||
Igdb.GameResult gameResult = Igdb.GameResult.newBuilder()
|
||||
.addAllGames(List.of(
|
||||
Igdb.Game.newBuilder().setName("title_1").build(),
|
||||
Igdb.Game.newBuilder().setName("title_2").build(),
|
||||
Igdb.Game.newBuilder().setName("title_3").build()))
|
||||
.build();
|
||||
|
||||
String searchTerm = gameResult.getGames(0).getName();
|
||||
|
||||
igdbApiMock.enqueue(new MockResponse()
|
||||
.setBody(toBuffer(gameResult))
|
||||
.setHeader("Content-Type", "application/protobuf")
|
||||
);
|
||||
|
||||
Optional<Igdb.Game> gameOptional = target.searchForGameByTitle(searchTerm);
|
||||
|
||||
assertThat(gameOptional).isPresent();
|
||||
|
||||
Igdb.Game game = gameOptional.get();
|
||||
|
||||
assertThat(game.getName()).isEqualTo(searchTerm);
|
||||
|
||||
RecordedRequest r = igdbApiMock.takeRequest();
|
||||
assertThat(r.getRequestUrl()).isNotNull();
|
||||
assertThat(r.getRequestUrl().encodedPath()).isEqualTo("/%s".formatted(IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF));
|
||||
|
||||
String expectedQuery = "search \"%s\";fields %s;where platforms = (6);".formatted(searchTerm, IgdbApiProperties.GAME_QUERY_FIELDS_STRING);
|
||||
|
||||
assertThat(r.getBody().readUtf8()).isEqualTo(expectedQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchForGameByTitle_EndsWith() throws InterruptedException {
|
||||
Igdb.GameResult gameResult = Igdb.GameResult.newBuilder()
|
||||
.addAllGames(List.of(
|
||||
Igdb.Game.newBuilder().setName("some_prefix title_1").build(),
|
||||
Igdb.Game.newBuilder().setName("title_2)").build(),
|
||||
Igdb.Game.newBuilder().setName("title_3").build()))
|
||||
.build();
|
||||
|
||||
String searchTerm = "title_1";
|
||||
|
||||
igdbApiMock.enqueue(new MockResponse()
|
||||
.setBody(toBuffer(gameResult))
|
||||
.setHeader("Content-Type", "application/protobuf")
|
||||
);
|
||||
|
||||
Optional<Igdb.Game> gameOptional = target.searchForGameByTitle(searchTerm);
|
||||
|
||||
assertThat(gameOptional).isPresent();
|
||||
|
||||
Igdb.Game game = gameOptional.get();
|
||||
|
||||
assertThat(game.getName()).isEqualTo(gameResult.getGames(0).getName());
|
||||
|
||||
RecordedRequest r = igdbApiMock.takeRequest();
|
||||
assertThat(r.getRequestUrl()).isNotNull();
|
||||
assertThat(r.getRequestUrl().encodedPath()).isEqualTo("/%s".formatted(IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF));
|
||||
|
||||
String expectedQuery = "search \"%s\";fields %s;where platforms = (6);".formatted(searchTerm, IgdbApiProperties.GAME_QUERY_FIELDS_STRING);
|
||||
|
||||
assertThat(r.getBody().readUtf8()).isEqualTo(expectedQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchForGameByTitle_Brackets() throws InterruptedException {
|
||||
Igdb.GameResult gameResult = Igdb.GameResult.newBuilder()
|
||||
.addAllGames(List.of(
|
||||
Igdb.Game.newBuilder().setName("title_1").build(),
|
||||
Igdb.Game.newBuilder().setName("title_2").build(),
|
||||
Igdb.Game.newBuilder().setName("title_3").build()))
|
||||
.build();
|
||||
|
||||
String searchTerm = gameResult.getGames(0).getName() + " (Text in brackets should be ignored)";
|
||||
|
||||
// First request should result in an empty response
|
||||
igdbApiMock.enqueue(new MockResponse().setHeader("Content-Type", "application/protobuf"));
|
||||
|
||||
// Second request should contain the same query, but with brackets removed
|
||||
igdbApiMock.enqueue(new MockResponse()
|
||||
.setBody(toBuffer(gameResult))
|
||||
.setHeader("Content-Type", "application/protobuf")
|
||||
);
|
||||
|
||||
Optional<Igdb.Game> gameOptional = target.searchForGameByTitle(searchTerm);
|
||||
|
||||
assertThat(gameOptional).isPresent();
|
||||
|
||||
Igdb.Game game = gameOptional.get();
|
||||
|
||||
// Result should be game with title equal to search term with brackets removed
|
||||
assertThat(game.getName()).isEqualTo(gameResult.getGames(0).getName());
|
||||
|
||||
// First query (should contain brackets)
|
||||
RecordedRequest r1 = igdbApiMock.takeRequest();
|
||||
assertThat(r1.getRequestUrl()).isNotNull();
|
||||
assertThat(r1.getRequestUrl().encodedPath()).isEqualTo("/%s".formatted(IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF));
|
||||
|
||||
String r1_expectedQuery = "search \"%s\";fields %s;where platforms = (6);".formatted(searchTerm, IgdbApiProperties.GAME_QUERY_FIELDS_STRING);
|
||||
|
||||
assertThat(r1.getBody().readUtf8()).isEqualTo(r1_expectedQuery);
|
||||
|
||||
// Second query (should not contain brackets)
|
||||
RecordedRequest r2 = igdbApiMock.takeRequest();
|
||||
assertThat(r2.getRequestUrl()).isNotNull();
|
||||
assertThat(r2.getRequestUrl().encodedPath()).isEqualTo("/%s".formatted(IgdbApiProperties.ENDPOINT_GAMES_PROTOBUF));
|
||||
|
||||
String r2_expectedQuery = "search \"%s\";fields %s;where platforms = (6);".formatted(gameResult.getGames(0).getName(), IgdbApiProperties.GAME_QUERY_FIELDS_STRING);
|
||||
|
||||
assertThat(r2.getBody().readUtf8()).isEqualTo(r2_expectedQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findPlatforms() throws InterruptedException {
|
||||
Igdb.PlatformResult platformResult = Igdb.PlatformResult.newBuilder()
|
||||
.addAllPlatforms(List.of(
|
||||
Igdb.Platform.newBuilder().setSlug("platform_1").setName("Platform 1").build(),
|
||||
Igdb.Platform.newBuilder().setSlug("platform_2").setName("Platform 2").build(),
|
||||
Igdb.Platform.newBuilder().setSlug("platform_3").setName("Platform 3").build()))
|
||||
.build();
|
||||
|
||||
String searchTerm = platformResult.getPlatforms(0).getSlug();
|
||||
int limit = 10;
|
||||
|
||||
igdbApiMock.enqueue(new MockResponse()
|
||||
.setBody(toBuffer(platformResult))
|
||||
.setHeader("Content-Type", "application/protobuf")
|
||||
);
|
||||
|
||||
List<Platform> result = target.findPlatforms(searchTerm, limit);
|
||||
|
||||
assertThat(result.get(0).getSlug()).isEqualTo(searchTerm);
|
||||
|
||||
RecordedRequest r = igdbApiMock.takeRequest();
|
||||
assertThat(r.getRequestUrl()).isNotNull();
|
||||
assertThat(r.getRequestUrl().encodedPath()).isEqualTo("/%s".formatted(IgdbApiProperties.ENDPOINT_PLATFORMS_PROTOBUF));
|
||||
|
||||
String expectedQuery = "search \"%s\";fields slug,name;limit %s;".formatted(searchTerm, limit);
|
||||
|
||||
assertThat(r.getBody().readUtf8()).isEqualTo(expectedQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPlatformBySlug() throws InterruptedException {
|
||||
Igdb.PlatformResult platformResult = Igdb.PlatformResult.newBuilder()
|
||||
.addAllPlatforms(List.of(
|
||||
Igdb.Platform.newBuilder().setSlug("platform_1").setName("Platform 1").build()))
|
||||
.build();
|
||||
|
||||
String slug = platformResult.getPlatforms(0).getSlug();
|
||||
|
||||
igdbApiMock.enqueue(new MockResponse()
|
||||
.setBody(toBuffer(platformResult))
|
||||
.setHeader("Content-Type", "application/protobuf")
|
||||
);
|
||||
|
||||
Optional<Platform> result = target.getPlatformBySlug(slug);
|
||||
|
||||
assertThat(result).isPresent();
|
||||
|
||||
Platform platform = result.get();
|
||||
|
||||
assertThat(platform.getSlug()).isEqualTo(slug);
|
||||
|
||||
RecordedRequest r = igdbApiMock.takeRequest();
|
||||
assertThat(r.getRequestUrl()).isNotNull();
|
||||
assertThat(r.getRequestUrl().encodedPath()).isEqualTo("/%s".formatted(IgdbApiProperties.ENDPOINT_PLATFORMS_PROTOBUF));
|
||||
|
||||
String expectedQuery = "fields slug,name,platform_logo;where slug = \"%s\";".formatted(slug);
|
||||
|
||||
assertThat(r.getBody().readUtf8()).isEqualTo(expectedQuery);
|
||||
}
|
||||
|
||||
private static Buffer toBuffer(Message input) {
|
||||
Buffer b = new Buffer();
|
||||
b.write(input.toByteArray());
|
||||
return b;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.entities.Company;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CompanyMapperTest extends RandomMapperTest<Igdb.InvolvedCompany, Company> {
|
||||
|
||||
@Test
|
||||
void toCompany() {
|
||||
Igdb.InvolvedCompany input = generateRandomInput();
|
||||
|
||||
Company output = CompanyMapper.toCompany(input);
|
||||
|
||||
assertThat(output.getSlug()).isEqualTo(input.getCompany().getSlug());
|
||||
assertThat(output.getName()).isEqualTo(input.getCompany().getName());
|
||||
assertThat(output.getLogoId()).isEqualTo(input.getCompany().getLogo().getImageId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toCompanies() {
|
||||
List<Igdb.InvolvedCompany> input = List.of(generateRandomInput(), generateRandomInput(), generateRandomInput());
|
||||
|
||||
List<Company> output = CompanyMapper.toCompanies(input);
|
||||
|
||||
for (int i = 0; i < output.size(); i++) {
|
||||
assertThat(output.get(i).getSlug()).isEqualTo(input.get(i).getCompany().getSlug());
|
||||
assertThat(output.get(i).getName()).isEqualTo(input.get(i).getCompany().getName());
|
||||
assertThat(output.get(i).getLogoId()).isEqualTo(input.get(i).getCompany().getLogo().getImageId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.entities.Genre;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class GenreMapperTest extends RandomMapperTest<Igdb.Genre, Genre> {
|
||||
|
||||
@Test
|
||||
void toGenre() {
|
||||
Igdb.Genre input = generateRandomInput();
|
||||
|
||||
Genre output = GenreMapper.toGenre(input);
|
||||
|
||||
assertThat(output.getSlug()).isEqualTo(input.getSlug());
|
||||
assertThat(output.getName()).isEqualTo(input.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toGenres() {
|
||||
List<Igdb.Genre> input = List.of(generateRandomInput(), generateRandomInput(), generateRandomInput());
|
||||
|
||||
List<Genre> output = GenreMapper.toGenres(input);
|
||||
|
||||
for (int i = 0; i < output.size(); i++) {
|
||||
assertThat(output.get(i).getSlug()).isEqualTo(input.get(i).getSlug());
|
||||
assertThat(output.get(i).getName()).isEqualTo(input.get(i).getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package de.grimsi.gameyfin.mapper;
|
||||
|
||||
import com.google.protobuf.Message;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.jeasy.random.EasyRandomParameters;
|
||||
import org.springframework.core.GenericTypeResolver;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RandomMapperTest<Input extends Message, Output> {
|
||||
|
||||
private static final int DEFAULT_COUNT = 5;
|
||||
protected final EasyRandom easyRandom = new EasyRandom();
|
||||
|
||||
private final Class<Input> inputClass;
|
||||
private final Class<Output> outputClass;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public RandomMapperTest() {
|
||||
Class<?>[] typeArguments = GenericTypeResolver.resolveTypeArguments(getClass(), RandomMapperTest.class);
|
||||
assert typeArguments != null;
|
||||
inputClass = (Class<Input>) typeArguments[0];
|
||||
outputClass = (Class<Output>) typeArguments[1];
|
||||
}
|
||||
|
||||
protected Input generateRandomInput() {
|
||||
return easyRandom.nextObject(inputClass);
|
||||
}
|
||||
|
||||
protected List<Input> generateRandomInputs() {
|
||||
return easyRandom.objects(inputClass, DEFAULT_COUNT).toList();
|
||||
}
|
||||
|
||||
protected List<Input> generateRandomInputs(int count) {
|
||||
return easyRandom.objects(inputClass, count).toList();
|
||||
}
|
||||
|
||||
protected Output generateRandomOutput() {
|
||||
return easyRandom.nextObject(outputClass);
|
||||
}
|
||||
|
||||
protected List<Output> generateRandomOutputs() {
|
||||
return easyRandom.objects(outputClass, DEFAULT_COUNT).toList();
|
||||
}
|
||||
|
||||
protected List<Output> generateRandomOutputs(int count) {
|
||||
return easyRandom.objects(outputClass, count).toList();
|
||||
}
|
||||
}
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.service.FilesystemService;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DataJpaTest
|
||||
class DetectedGameRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private DetectedGameRepository target;
|
||||
|
||||
private final EasyRandom easyRandom = new EasyRandom();
|
||||
|
||||
@BeforeEach
|
||||
void dropTable() {
|
||||
target.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsByPath() {
|
||||
String path = "some/random/path";
|
||||
DetectedGame input = DetectedGame.builder()
|
||||
.slug("slug")
|
||||
.title("title")
|
||||
.coverId("coverId")
|
||||
.path(path)
|
||||
.build();
|
||||
|
||||
assertThat(target.existsByPath(path)).isFalse();
|
||||
|
||||
target.save(input);
|
||||
|
||||
assertThat(target.existsByPath(path)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsBySlug() {
|
||||
String slug = "some-random-slug";
|
||||
DetectedGame input = DetectedGame.builder()
|
||||
.slug(slug)
|
||||
.title("title")
|
||||
.coverId("coverId")
|
||||
.path("path")
|
||||
.build();
|
||||
|
||||
assertThat(target.existsBySlug(slug)).isFalse();
|
||||
|
||||
target.save(input);
|
||||
|
||||
assertThat(target.existsBySlug(slug)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPath() {
|
||||
String path = "some/random/path";
|
||||
DetectedGame input = DetectedGame.builder()
|
||||
.slug("slug")
|
||||
.title("title")
|
||||
.coverId("coverId")
|
||||
.path(path)
|
||||
.build();
|
||||
|
||||
assertThat(target.findByPath(path)).isEmpty();
|
||||
|
||||
target.save(input);
|
||||
|
||||
Optional<DetectedGame> optionalResult = target.findByPath(path);
|
||||
|
||||
assertThat(optionalResult).isPresent();
|
||||
|
||||
DetectedGame result = optionalResult.get();
|
||||
|
||||
assertThat(result).isEqualTo(input);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", "some/random/library/path/"})
|
||||
void getAllByPathNotInAndPathStartsWith(String library) {
|
||||
String libraryPath = Path.of(library).toString();
|
||||
String otherLibraryPath = Path.of("another/random/library/path/").toString();
|
||||
|
||||
List<DetectedGame> detectedGames = easyRandom.objects(DetectedGame.class, 2).peek(g -> g.setPath(Path.of(libraryPath, g.getPath()).toString())).toList();
|
||||
List<DetectedGame> detectedGamesDifferentLibrary = easyRandom.objects(DetectedGame.class, 2).peek(g -> g.setPath(Path.of(otherLibraryPath, g.getPath()).toString())).toList();
|
||||
List<DetectedGame> deletedGames = easyRandom.objects(DetectedGame.class, 2).peek(g -> g.setPath(Path.of(libraryPath, g.getPath()).toString())).toList();
|
||||
List<Path> gamePaths = detectedGames.stream().map(DetectedGame::getPath).map(Path::of).collect(Collectors.toList());
|
||||
gamePaths.addAll(detectedGamesDifferentLibrary.stream().map(DetectedGame::getPath).map(Path::of).toList());
|
||||
|
||||
target.saveAll(detectedGames);
|
||||
target.saveAll(detectedGamesDifferentLibrary);
|
||||
|
||||
assertThat(target.getAllByPathNotInAndPathStartsWith(gamePaths, libraryPath)).isEmpty();
|
||||
|
||||
target.saveAll(deletedGames);
|
||||
|
||||
List<DetectedGame> result = target.getAllByPathNotInAndPathStartsWith(gamePaths, libraryPath);
|
||||
assertThat(result)
|
||||
.hasSize(2)
|
||||
.containsOnlyOnceElementsOf(deletedGames);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.Library;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DataJpaTest
|
||||
class LibraryRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private LibraryRepository target;
|
||||
|
||||
@Test
|
||||
void existsByPathIgnoreCase() {
|
||||
String path = "Some/Random/Path";
|
||||
Library input = Library.builder().path(path).build();
|
||||
|
||||
assertThat(target.existsByPathIgnoreCase(path)).isFalse();
|
||||
|
||||
target.save(input);
|
||||
|
||||
assertThat(target.existsByPathIgnoreCase(path)).isTrue();
|
||||
assertThat(target.existsByPathIgnoreCase(path.toLowerCase(Locale.ENGLISH))).isTrue();
|
||||
assertThat(target.existsByPathIgnoreCase(path.toUpperCase(Locale.ENGLISH))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPath() {
|
||||
String path = "Some/Random/Path";
|
||||
Library input = Library.builder().path(path).build();
|
||||
|
||||
target.save(input);
|
||||
|
||||
Optional<Library> optionalResult = target.findByPath(path);
|
||||
|
||||
assertThat(optionalResult).isPresent();
|
||||
|
||||
Library result = optionalResult.get();
|
||||
|
||||
assertThat(result).isEqualTo(input);
|
||||
}
|
||||
}
|
||||
-86
@@ -1,86 +0,0 @@
|
||||
package de.grimsi.gameyfin.repositories;
|
||||
|
||||
import de.grimsi.gameyfin.entities.UnmappableFile;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DataJpaTest
|
||||
class UnmappableFileRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private UnmappableFileRepository target;
|
||||
|
||||
private final EasyRandom easyRandom = new EasyRandom();
|
||||
|
||||
@BeforeEach
|
||||
void dropTable() {
|
||||
target.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void existsByPath() {
|
||||
String path = "some/random/path";
|
||||
UnmappableFile input = new UnmappableFile(path);
|
||||
|
||||
assertThat(target.existsByPath(path)).isFalse();
|
||||
|
||||
target.save(input);
|
||||
|
||||
assertThat(target.existsByPath(path)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findByPath() {
|
||||
String path = "some/random/path";
|
||||
UnmappableFile input = new UnmappableFile(path);
|
||||
|
||||
assertThat(target.findByPath(path)).isEmpty();
|
||||
|
||||
target.save(input);
|
||||
|
||||
Optional<UnmappableFile> optionalResult = target.findByPath(path);
|
||||
|
||||
assertThat(optionalResult).isPresent();
|
||||
|
||||
UnmappableFile result = optionalResult.get();
|
||||
|
||||
assertThat(result).isEqualTo(input);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", "some/random/library/path/"})
|
||||
void getAllByPathNotInAndPathStartsWith(String library) {
|
||||
String libraryPath = Path.of(library).toString();
|
||||
String otherLibraryPath = Path.of("another/random/library/path/").toString();
|
||||
|
||||
List<UnmappableFile> UnmappableFiles = easyRandom.objects(UnmappableFile.class, 2).peek(g -> g.setPath(Path.of(libraryPath, g.getPath()).toString())).toList();
|
||||
List<UnmappableFile> UnmappableFilesDifferentLibrary = easyRandom.objects(UnmappableFile.class, 2).peek(g -> g.setPath(Path.of(otherLibraryPath, g.getPath()).toString())).toList();
|
||||
List<UnmappableFile> deletedGames = easyRandom.objects(UnmappableFile.class, 2).peek(g -> g.setPath(Path.of(libraryPath, g.getPath()).toString())).toList();
|
||||
List<Path> gamePaths = UnmappableFiles.stream().map(UnmappableFile::getPath).map(Path::of).collect(Collectors.toList());
|
||||
gamePaths.addAll(UnmappableFilesDifferentLibrary.stream().map(UnmappableFile::getPath).map(Path::of).toList());
|
||||
|
||||
target.saveAll(UnmappableFiles);
|
||||
target.saveAll(UnmappableFilesDifferentLibrary);
|
||||
|
||||
assertThat(target.getAllByPathNotInAndPathStartsWith(gamePaths, libraryPath)).isEmpty();
|
||||
|
||||
target.saveAll(deletedGames);
|
||||
|
||||
List<UnmappableFile> result = target.getAllByPathNotInAndPathStartsWith(gamePaths, libraryPath);
|
||||
assertThat(result)
|
||||
.hasSize(2)
|
||||
.containsOnlyOnceElementsOf(deletedGames);
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package de.grimsi.gameyfin.rest;
|
||||
|
||||
import de.grimsi.gameyfin.dto.GameOverviewDto;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.service.DownloadService;
|
||||
import de.grimsi.gameyfin.service.GameService;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.in;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GamesControllerTest {
|
||||
|
||||
@InjectMocks
|
||||
private GamesController target;
|
||||
|
||||
@Mock
|
||||
private GameService gameServiceMock;
|
||||
|
||||
@Mock
|
||||
private DownloadService downloadServiceMock;
|
||||
|
||||
private final EasyRandom easyRandom = new EasyRandom();
|
||||
|
||||
@Test
|
||||
void getAllGames() {
|
||||
List<DetectedGame> input = easyRandom.objects(DetectedGame.class, 5).toList();
|
||||
|
||||
when(gameServiceMock.getAllDetectedGames()).thenReturn(input);
|
||||
|
||||
List<DetectedGame> result = target.getAllGames();
|
||||
|
||||
verify(gameServiceMock, times(1)).getAllDetectedGames();
|
||||
assertThat(result).hasSameElementsAs(input);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGame() {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
String slug = input.getSlug();
|
||||
|
||||
when(gameServiceMock.getDetectedGame(slug)).thenReturn(input);
|
||||
|
||||
DetectedGame result = target.getGame(slug);
|
||||
|
||||
verify(gameServiceMock, times(1)).getDetectedGame(slug);
|
||||
assertThat(result).isEqualTo(input);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGameOverviews() {
|
||||
List<GameOverviewDto> input = easyRandom.objects(GameOverviewDto.class, 5).toList();
|
||||
|
||||
when(gameServiceMock.getGameOverviews()).thenReturn(input);
|
||||
|
||||
List<GameOverviewDto> result = target.getGameOverviews();
|
||||
|
||||
verify(gameServiceMock, times(1)).getGameOverviews();
|
||||
assertThat(result).hasSameElementsAs(input);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGameMappings() {
|
||||
Map<String, String> input = easyRandom.objects(String.class, 5).collect(Collectors.toMap(String::toLowerCase, String::toUpperCase));
|
||||
|
||||
when(gameServiceMock.getAllMappings()).thenReturn(input);
|
||||
|
||||
Map<String, String> result = target.getGameMappings();
|
||||
|
||||
verify(gameServiceMock, times(1)).getAllMappings();
|
||||
assertThat(result).isEqualTo(input);
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadGameFiles_File() {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
String slug = input.getSlug();
|
||||
String downloadFilename = input.getSlug();
|
||||
long downloadFileSize = 1337L;
|
||||
|
||||
when(gameServiceMock.getDetectedGame(slug)).thenReturn(input);
|
||||
when(downloadServiceMock.getDownloadFileName(input)).thenReturn(downloadFilename);
|
||||
when(downloadServiceMock.getDownloadFileSize(input)).thenReturn(downloadFileSize);
|
||||
|
||||
ResponseEntity<StreamingResponseBody> result = target.downloadGameFiles(slug);
|
||||
|
||||
HttpHeaders expectedHeaders = new HttpHeaders();
|
||||
expectedHeaders.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"%s\"".formatted(downloadFilename));
|
||||
expectedHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
|
||||
expectedHeaders.add(HttpHeaders.PRAGMA, "no-cache");
|
||||
expectedHeaders.add(HttpHeaders.EXPIRES, "0");
|
||||
expectedHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
||||
expectedHeaders.setContentLength(downloadFileSize);
|
||||
|
||||
verify(gameServiceMock, times(1)).getDetectedGame(slug);
|
||||
verify(downloadServiceMock, times(1)).getDownloadFileName(input);
|
||||
verify(downloadServiceMock, times(1)).getDownloadFileSize(input);
|
||||
assertThat(result.getHeaders()).isEqualTo(expectedHeaders);
|
||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadGameFiles_Zip() {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
String slug = input.getSlug();
|
||||
String downloadFilename = input.getSlug() + ".zip";
|
||||
long downloadFileSize = 0L;
|
||||
|
||||
when(gameServiceMock.getDetectedGame(slug)).thenReturn(input);
|
||||
when(downloadServiceMock.getDownloadFileName(input)).thenReturn(downloadFilename);
|
||||
when(downloadServiceMock.getDownloadFileSize(input)).thenReturn(downloadFileSize);
|
||||
|
||||
ResponseEntity<StreamingResponseBody> result = target.downloadGameFiles(slug);
|
||||
|
||||
HttpHeaders expectedHeaders = new HttpHeaders();
|
||||
expectedHeaders.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"%s\"".formatted(downloadFilename));
|
||||
expectedHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
|
||||
expectedHeaders.add(HttpHeaders.PRAGMA, "no-cache");
|
||||
expectedHeaders.add(HttpHeaders.EXPIRES, "0");
|
||||
expectedHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
||||
|
||||
verify(gameServiceMock, times(1)).getDetectedGame(slug);
|
||||
verify(downloadServiceMock, times(1)).getDownloadFileName(input);
|
||||
verify(downloadServiceMock, times(1)).getDownloadFileSize(input);
|
||||
assertThat(result.getHeaders()).isEqualTo(expectedHeaders);
|
||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshGame() {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
String slug = input.getSlug();
|
||||
|
||||
when(gameServiceMock.refreshGame(slug)).thenReturn(input);
|
||||
|
||||
DetectedGame result = target.refreshGame(slug);
|
||||
|
||||
verify(gameServiceMock, times(1)).refreshGame(slug);
|
||||
assertThat(result).isEqualTo(input);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package de.grimsi.gameyfin.rest;
|
||||
|
||||
import de.grimsi.gameyfin.service.DownloadService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.in;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static reactor.core.publisher.Mono.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ImageControllerTest {
|
||||
|
||||
@InjectMocks
|
||||
private ImageController target;
|
||||
|
||||
@Mock
|
||||
private DownloadService downloadServiceMock;
|
||||
|
||||
@Test
|
||||
void getImage() {
|
||||
byte[] content = "content".getBytes();
|
||||
Resource resource = new ByteArrayResource(content);
|
||||
String input = "imageId";
|
||||
|
||||
doReturn(resource).when(downloadServiceMock).sendImageToClient(input);
|
||||
|
||||
ResponseEntity<Resource> result = target.getImage(input);
|
||||
|
||||
verify(downloadServiceMock, times(1)).sendImageToClient(input);
|
||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(result.getBody()).isEqualTo(resource);
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
package de.grimsi.gameyfin.rest;
|
||||
|
||||
import de.grimsi.gameyfin.dto.ImageDownloadResultDto;
|
||||
import de.grimsi.gameyfin.dto.LibraryScanRequestDto;
|
||||
import de.grimsi.gameyfin.dto.LibraryScanResult;
|
||||
import de.grimsi.gameyfin.dto.LibraryScanResultDto;
|
||||
import de.grimsi.gameyfin.entities.Library;
|
||||
import de.grimsi.gameyfin.service.ImageService;
|
||||
import de.grimsi.gameyfin.service.LibraryService;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LibraryControllerTest {
|
||||
|
||||
@InjectMocks
|
||||
private LibraryController target;
|
||||
|
||||
@Mock
|
||||
private LibraryService libraryServiceMock;
|
||||
|
||||
@Mock
|
||||
private ImageService imageServiceMock;
|
||||
|
||||
private final EasyRandom easyRandom = new EasyRandom();
|
||||
|
||||
@Test
|
||||
void scanLibraries_All_NoImages() {
|
||||
int libraryCount = 5;
|
||||
LibraryScanRequestDto input = new LibraryScanRequestDto("", false);
|
||||
List<Library> libraries = easyRandom.objects(Library.class, libraryCount).toList();
|
||||
LibraryScanResult lsr = easyRandom.nextObject(LibraryScanResult.class);
|
||||
|
||||
when(libraryServiceMock.getLibraries()).thenReturn(libraries);
|
||||
when(libraryServiceMock.scanGameLibrary(any(Library.class))).thenReturn(lsr);
|
||||
|
||||
LibraryScanResultDto result = target.scanLibraries(input);
|
||||
|
||||
verify(libraryServiceMock, times(1)).getLibraries();
|
||||
verify(libraryServiceMock, never()).getLibrary(any(String.class));
|
||||
verify(libraryServiceMock, times(libraryCount)).scanGameLibrary(any(Library.class));
|
||||
verify(imageServiceMock, never()).downloadCompanyLogosFromIgdb();
|
||||
verify(imageServiceMock, never()).downloadGameCoversFromIgdb();
|
||||
verify(imageServiceMock, never()).downloadGameScreenshotsFromIgdb();
|
||||
assertThat(result.getNewGames()).isEqualTo(lsr.getNewGames() * libraries.size());
|
||||
assertThat(result.getDeletedGames()).isEqualTo(lsr.getDeletedGames() * libraries.size());
|
||||
assertThat(result.getNewUnmappableFiles()).isEqualTo(lsr.getNewUnmappableFiles() * libraries.size());
|
||||
assertThat(result.getTotalGames()).isEqualTo(lsr.getTotalGames() * libraries.size());
|
||||
assertThat(result.getCoverDownloads()).isZero();
|
||||
assertThat(result.getScreenshotDownloads()).isZero();
|
||||
assertThat(result.getCompanyLogoDownloads()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanLibraries_Single_NoImages() {
|
||||
String libraryPath = "some/random/path";
|
||||
LibraryScanRequestDto input = new LibraryScanRequestDto(libraryPath, false);
|
||||
Library library = easyRandom.nextObject(Library.class);
|
||||
LibraryScanResult lsr = easyRandom.nextObject(LibraryScanResult.class);
|
||||
|
||||
when(libraryServiceMock.getLibrary(libraryPath)).thenReturn(library);
|
||||
when(libraryServiceMock.scanGameLibrary(any(Library.class))).thenReturn(lsr);
|
||||
|
||||
LibraryScanResultDto result = target.scanLibraries(input);
|
||||
|
||||
verify(libraryServiceMock, never()).getLibraries();
|
||||
verify(libraryServiceMock, times(1)).getLibrary(libraryPath);
|
||||
verify(libraryServiceMock, times(1)).scanGameLibrary(any(Library.class));
|
||||
verify(imageServiceMock, never()).downloadCompanyLogosFromIgdb();
|
||||
verify(imageServiceMock, never()).downloadGameCoversFromIgdb();
|
||||
verify(imageServiceMock, never()).downloadGameScreenshotsFromIgdb();
|
||||
assertThat(result.getNewGames()).isEqualTo(lsr.getNewGames());
|
||||
assertThat(result.getDeletedGames()).isEqualTo(lsr.getDeletedGames());
|
||||
assertThat(result.getNewUnmappableFiles()).isEqualTo(lsr.getNewUnmappableFiles());
|
||||
assertThat(result.getTotalGames()).isEqualTo(lsr.getTotalGames());
|
||||
assertThat(result.getCoverDownloads()).isZero();
|
||||
assertThat(result.getScreenshotDownloads()).isZero();
|
||||
assertThat(result.getCompanyLogoDownloads()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanLibraries_All_DownloadImages() {
|
||||
int libraryCount = 5;
|
||||
LibraryScanRequestDto input = new LibraryScanRequestDto("", true);
|
||||
List<Library> libraries = easyRandom.objects(Library.class, libraryCount).toList();
|
||||
LibraryScanResult lsr = easyRandom.nextObject(LibraryScanResult.class);
|
||||
|
||||
when(libraryServiceMock.getLibraries()).thenReturn(libraries);
|
||||
when(libraryServiceMock.scanGameLibrary(any(Library.class))).thenReturn(lsr);
|
||||
when(imageServiceMock.downloadGameCoversFromIgdb()).thenReturn(1);
|
||||
when(imageServiceMock.downloadGameScreenshotsFromIgdb()).thenReturn(1);
|
||||
when(imageServiceMock.downloadCompanyLogosFromIgdb()).thenReturn(1);
|
||||
|
||||
LibraryScanResultDto result = target.scanLibraries(input);
|
||||
|
||||
verify(libraryServiceMock, times(1)).getLibraries();
|
||||
verify(libraryServiceMock, never()).getLibrary(any(String.class));
|
||||
verify(libraryServiceMock, times(libraryCount)).scanGameLibrary(any(Library.class));
|
||||
verify(imageServiceMock, times(1)).downloadCompanyLogosFromIgdb();
|
||||
verify(imageServiceMock, times(1)).downloadGameCoversFromIgdb();
|
||||
verify(imageServiceMock, times(1)).downloadGameScreenshotsFromIgdb();
|
||||
assertThat(result.getNewGames()).isEqualTo(lsr.getNewGames() * libraries.size());
|
||||
assertThat(result.getDeletedGames()).isEqualTo(lsr.getDeletedGames() * libraries.size());
|
||||
assertThat(result.getNewUnmappableFiles()).isEqualTo(lsr.getNewUnmappableFiles() * libraries.size());
|
||||
assertThat(result.getTotalGames()).isEqualTo(lsr.getTotalGames() * libraries.size());
|
||||
assertThat(result.getCoverDownloads()).isEqualTo(1);
|
||||
assertThat(result.getScreenshotDownloads()).isEqualTo(1);
|
||||
assertThat(result.getCompanyLogoDownloads()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanLibraries_Single_DownloadImages() {
|
||||
String libraryPath = "some/random/path";
|
||||
LibraryScanRequestDto input = new LibraryScanRequestDto(libraryPath, true);
|
||||
Library library = easyRandom.nextObject(Library.class);
|
||||
LibraryScanResult lsr = easyRandom.nextObject(LibraryScanResult.class);
|
||||
|
||||
when(libraryServiceMock.getLibrary(libraryPath)).thenReturn(library);
|
||||
when(libraryServiceMock.scanGameLibrary(any(Library.class))).thenReturn(lsr);
|
||||
when(imageServiceMock.downloadGameCoversFromIgdb()).thenReturn(1);
|
||||
when(imageServiceMock.downloadGameScreenshotsFromIgdb()).thenReturn(1);
|
||||
when(imageServiceMock.downloadCompanyLogosFromIgdb()).thenReturn(1);
|
||||
|
||||
LibraryScanResultDto result = target.scanLibraries(input);
|
||||
|
||||
verify(libraryServiceMock, never()).getLibraries();
|
||||
verify(libraryServiceMock, times(1)).getLibrary(libraryPath);
|
||||
verify(libraryServiceMock, times(1)).scanGameLibrary(any(Library.class));
|
||||
verify(imageServiceMock, times(1)).downloadCompanyLogosFromIgdb();
|
||||
verify(imageServiceMock, times(1)).downloadGameCoversFromIgdb();
|
||||
verify(imageServiceMock, times(1)).downloadGameScreenshotsFromIgdb();
|
||||
assertThat(result.getNewGames()).isEqualTo(lsr.getNewGames());
|
||||
assertThat(result.getDeletedGames()).isEqualTo(lsr.getDeletedGames());
|
||||
assertThat(result.getNewUnmappableFiles()).isEqualTo(lsr.getNewUnmappableFiles());
|
||||
assertThat(result.getTotalGames()).isEqualTo(lsr.getTotalGames());
|
||||
assertThat(result.getCoverDownloads()).isEqualTo(1);
|
||||
assertThat(result.getScreenshotDownloads()).isEqualTo(1);
|
||||
assertThat(result.getCompanyLogoDownloads()).isEqualTo(1);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void downloadImages() {
|
||||
when(imageServiceMock.downloadGameCoversFromIgdb()).thenReturn(1);
|
||||
when(imageServiceMock.downloadGameScreenshotsFromIgdb()).thenReturn(1);
|
||||
when(imageServiceMock.downloadCompanyLogosFromIgdb()).thenReturn(1);
|
||||
|
||||
ImageDownloadResultDto result = target.downloadImages();
|
||||
|
||||
verify(imageServiceMock, times(1)).downloadCompanyLogosFromIgdb();
|
||||
verify(imageServiceMock, times(1)).downloadGameCoversFromIgdb();
|
||||
verify(imageServiceMock, times(1)).downloadGameScreenshotsFromIgdb();
|
||||
assertThat(result.getScreenshotDownloads()).isOne();
|
||||
assertThat(result.getCoverDownloads()).isOne();
|
||||
assertThat(result.getCompanyLogoDownloads()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllFiles() {
|
||||
List<Path> gameFiles = easyRandom.objects(String.class, 5).map(Path::of).toList();
|
||||
|
||||
when(libraryServiceMock.getGameFiles()).thenReturn(gameFiles);
|
||||
|
||||
List<String> result = target.getAllFiles();
|
||||
|
||||
verify(libraryServiceMock, times(1)).getGameFiles();
|
||||
assertThat(result).hasSameElementsAs(gameFiles.stream().map(Path::toString).toList());
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package de.grimsi.gameyfin.rest;
|
||||
|
||||
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
|
||||
import de.grimsi.gameyfin.dto.PathToSlugDto;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.entities.Library;
|
||||
import de.grimsi.gameyfin.entities.Platform;
|
||||
import de.grimsi.gameyfin.service.GameService;
|
||||
import de.grimsi.gameyfin.service.ImageService;
|
||||
import de.grimsi.gameyfin.service.LibraryService;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LibraryManagementControllerTest {
|
||||
|
||||
@InjectMocks
|
||||
private LibraryManagementController target;
|
||||
|
||||
@Mock
|
||||
private GameService gameServiceMock;
|
||||
|
||||
@Mock
|
||||
private ImageService imageServiceMock;
|
||||
|
||||
@Mock
|
||||
private LibraryService libraryServiceMock;
|
||||
|
||||
private final EasyRandom easyRandom = new EasyRandom();
|
||||
|
||||
@Test
|
||||
void deleteGame() {
|
||||
String slug = easyRandom.nextObject(String.class);
|
||||
|
||||
target.deleteGame(slug);
|
||||
|
||||
verify(gameServiceMock, times(1)).deleteGame(slug);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteUnmappedFile() {
|
||||
Long id = easyRandom.nextLong();
|
||||
|
||||
target.deleteUnmappedFile(id);
|
||||
|
||||
verify(gameServiceMock, times(1)).deleteUnmappedFile(id);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void confirmMatch(boolean confirm) {
|
||||
String slug = easyRandom.nextObject(String.class);
|
||||
|
||||
target.confirmMatch(slug, confirm);
|
||||
|
||||
verify(gameServiceMock, times(1)).confirmGame(slug, confirm);
|
||||
}
|
||||
|
||||
@Test
|
||||
void manuallyMapPathToSlug() {
|
||||
PathToSlugDto input = easyRandom.nextObject(PathToSlugDto.class);
|
||||
DetectedGame game = easyRandom.nextObject(DetectedGame.class);
|
||||
|
||||
when(gameServiceMock.mapPathToGame(input.getPath(), input.getSlug())).thenReturn(game);
|
||||
|
||||
DetectedGame result = target.manuallyMapPathToSlug(input);
|
||||
|
||||
verify(gameServiceMock, times(1)).mapPathToGame(input.getPath(), input.getSlug());
|
||||
verify(imageServiceMock, times(1)).downloadGameScreenshotsFromIgdb();
|
||||
verify(imageServiceMock, times(1)).downloadGameCoversFromIgdb();
|
||||
verify(imageServiceMock, times(1)).downloadCompanyLogosFromIgdb();
|
||||
|
||||
assertThat(result).isEqualTo(game);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUnmappedFiles() {
|
||||
target.getUnmappedFiles();
|
||||
|
||||
verify(gameServiceMock, times(1)).getAllUnmappedFiles();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAutocompleteSuggestions() {
|
||||
String searchTerm = easyRandom.nextObject(String.class);
|
||||
int limit = 10;
|
||||
List<AutocompleteSuggestionDto> a = easyRandom.objects(AutocompleteSuggestionDto.class, limit).toList();
|
||||
|
||||
when(libraryServiceMock.getAutocompleteSuggestions(searchTerm, limit)).thenReturn(a);
|
||||
|
||||
List<AutocompleteSuggestionDto> result = target.getAutocompleteSuggestions(searchTerm, limit);
|
||||
|
||||
verify(libraryServiceMock, times(1)).getAutocompleteSuggestions(searchTerm, limit);
|
||||
assertThat(result).isEqualTo(a);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPlatforms() {
|
||||
String searchTerm = easyRandom.nextObject(String.class);
|
||||
int limit = 10;
|
||||
List<Platform> p = easyRandom.objects(Platform.class, limit).toList();
|
||||
|
||||
when(libraryServiceMock.getPlatforms(searchTerm, limit)).thenReturn(p);
|
||||
|
||||
List<Platform> result = target.getPlatforms(searchTerm, limit);
|
||||
|
||||
verify(libraryServiceMock, times(1)).getPlatforms(searchTerm, limit);
|
||||
assertThat(result).isEqualTo(p);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLibraries() {
|
||||
target.getLibraries();
|
||||
|
||||
verify(libraryServiceMock, times(1)).getOrCreateLibraries();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapPathToPlatform() {
|
||||
PathToSlugDto input = easyRandom.nextObject(PathToSlugDto.class);
|
||||
Library l = easyRandom.nextObject(Library.class);
|
||||
|
||||
when(libraryServiceMock.mapPlatformsToLibrary(input.getPath(), input.getSlug())).thenReturn(l);
|
||||
|
||||
Library result = target.mapPathToPlatform(input);
|
||||
|
||||
verify(libraryServiceMock, times(1)).mapPlatformsToLibrary(input.getPath(), input.getSlug());
|
||||
assertThat(result).isEqualTo(l);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package de.grimsi.gameyfin.service;
|
||||
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DownloadServiceTest {
|
||||
|
||||
@Mock
|
||||
private FilesystemService filesystemServiceMock;
|
||||
|
||||
@InjectMocks
|
||||
private DownloadService target;
|
||||
|
||||
private final EasyRandom easyRandom = new EasyRandom();
|
||||
|
||||
@Test
|
||||
void getDownloadFileName_File() {
|
||||
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
|
||||
filesMock.when(() -> Files.isDirectory(any())).thenReturn(false);
|
||||
when(filesystemServiceMock.getPath(any())).thenReturn(Path.of(input.getPath()));
|
||||
|
||||
String result = target.getDownloadFileName(input);
|
||||
|
||||
assertThat(result).isEqualTo(input.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDownloadFileName_Folder() {
|
||||
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
|
||||
filesMock.when(() -> Files.isDirectory(any())).thenReturn(true);
|
||||
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
when(filesystemServiceMock.getPath(any())).thenReturn(Path.of(input.getPath()));
|
||||
|
||||
String result = target.getDownloadFileName(input);
|
||||
|
||||
assertThat(result).isEqualTo("%s.zip".formatted(input.getPath()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDownloadFileSize_File() throws IOException {
|
||||
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
|
||||
filesMock.when(() -> Files.isDirectory(any())).thenReturn(false);
|
||||
when(filesystemServiceMock.getSizeOnDisk(any())).thenReturn(1337L);
|
||||
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
|
||||
Long result = target.getDownloadFileSize(input);
|
||||
|
||||
assertThat(result).isEqualTo(1337L);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDownloadFileSize_Folder() {
|
||||
try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {
|
||||
filesMock.when(() -> Files.isDirectory(any())).thenReturn(true);
|
||||
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
|
||||
Long result = target.getDownloadFileSize(input);
|
||||
|
||||
assertThat(result).isZero();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
package de.grimsi.gameyfin.service;
|
||||
|
||||
import com.google.common.jimfs.Configuration;
|
||||
import com.google.common.jimfs.Jimfs;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.buffer.*;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Named.named;
|
||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FilesystemServiceTest {
|
||||
|
||||
@InjectMocks
|
||||
private FilesystemService target;
|
||||
|
||||
private final EasyRandom easyRandom = new EasyRandom();
|
||||
|
||||
private static final FileSystem unixFS = Jimfs.newFileSystem(Configuration.unix());
|
||||
private static final FileSystem osxFS = Jimfs.newFileSystem(Configuration.osX());
|
||||
private static final FileSystem winFS = Jimfs.newFileSystem(Configuration.windows());
|
||||
|
||||
private static final String CACHE_PATH = "path/to/cache";
|
||||
|
||||
void setup(FileSystem fileSystem) {
|
||||
ReflectionTestUtils.setField(target, "fileSystem", fileSystem);
|
||||
ReflectionTestUtils.setField(target, "cacheFolderPath", CACHE_PATH);
|
||||
target.createCacheFolder();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void closeFileSystems() throws IOException {
|
||||
unixFS.close();
|
||||
osxFS.close();
|
||||
winFS.close();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void getPath(FileSystem fileSystem) throws IOException {
|
||||
setup(fileSystem);
|
||||
|
||||
String testPath = "some/random/path";
|
||||
Path input = fileSystem.getPath(testPath);
|
||||
Files.createDirectories(input);
|
||||
|
||||
Path result = target.getPath(testPath);
|
||||
|
||||
assertThat(result).isEqualTo(input);
|
||||
|
||||
Files.deleteIfExists(input);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void createCacheFolder(FileSystem fileSystem) throws IOException {
|
||||
setup(fileSystem);
|
||||
|
||||
Path cache = fileSystem.getPath(CACHE_PATH);
|
||||
Files.deleteIfExists(cache);
|
||||
|
||||
assertThat(Files.exists(cache)).isFalse();
|
||||
|
||||
target.createCacheFolder();
|
||||
|
||||
assertThat(Files.exists(cache)).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void saveFileToCache(FileSystem fileSystem) throws IOException {
|
||||
setup(fileSystem);
|
||||
|
||||
String fileName = easyRandom.nextObject(String.class);
|
||||
byte[] fileContent = new byte[1024];
|
||||
easyRandom.nextBytes(fileContent);
|
||||
Path savedFilePath = fileSystem.getPath(CACHE_PATH, fileName);
|
||||
|
||||
try(InputStream i = new ByteArrayInputStream(fileContent)) {
|
||||
DataBufferFactory dbFactory = new DefaultDataBufferFactory();
|
||||
Flux<DataBuffer> d = DataBufferUtils.readInputStream(() -> i, dbFactory, 1);
|
||||
|
||||
target.saveFileToCache(d, fileName);
|
||||
|
||||
assertThat(Files.readAllBytes(savedFilePath)).isEqualTo(fileContent);
|
||||
}
|
||||
|
||||
Files.deleteIfExists(savedFilePath);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void getFileFromCache(FileSystem fileSystem) throws IOException {
|
||||
setup(fileSystem);
|
||||
|
||||
String fileName = easyRandom.nextObject(String.class);
|
||||
byte[] fileContent = new byte[1024];
|
||||
easyRandom.nextBytes(fileContent);
|
||||
Path savedFilePath = fileSystem.getPath(CACHE_PATH, fileName);
|
||||
|
||||
Files.write(savedFilePath, fileContent);
|
||||
|
||||
ByteArrayResource expected = new ByteArrayResource(fileContent);
|
||||
|
||||
assertThat(target.getFileFromCache(fileName)).isEqualTo(expected);
|
||||
|
||||
Files.deleteIfExists(savedFilePath);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void deleteFileFromCache(FileSystem fileSystem) throws IOException {
|
||||
setup(fileSystem);
|
||||
|
||||
String fileName = easyRandom.nextObject(String.class);
|
||||
byte[] fileContent = new byte[1024];
|
||||
easyRandom.nextBytes(fileContent);
|
||||
Path savedFilePath = fileSystem.getPath(CACHE_PATH, fileName);
|
||||
|
||||
Files.write(savedFilePath, fileContent);
|
||||
assertThat(Files.exists(savedFilePath)).isTrue();
|
||||
|
||||
target.deleteFileFromCache(fileName);
|
||||
|
||||
assertThat(Files.exists(savedFilePath)).isFalse();
|
||||
|
||||
Files.deleteIfExists(savedFilePath);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void isCachedFileCorrupt_True(FileSystem fileSystem) throws IOException {
|
||||
setup(fileSystem);
|
||||
|
||||
String fileName = easyRandom.nextObject(String.class);
|
||||
Path savedFilePath = fileSystem.getPath(CACHE_PATH, fileName);
|
||||
|
||||
Files.write(savedFilePath, new byte[0]);
|
||||
assertThat(target.isCachedFileCorrupt(fileName)).isTrue();
|
||||
|
||||
Files.deleteIfExists(savedFilePath);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void isCachedFileCorrupt_False(FileSystem fileSystem) throws IOException {
|
||||
setup(fileSystem);
|
||||
|
||||
String fileName = easyRandom.nextObject(String.class);
|
||||
byte[] fileContent = new byte[1024];
|
||||
easyRandom.nextBytes(fileContent);
|
||||
Path savedFilePath = fileSystem.getPath(CACHE_PATH, fileName);
|
||||
|
||||
Files.write(savedFilePath, fileContent);
|
||||
assertThat(target.isCachedFileCorrupt(fileName)).isFalse();
|
||||
|
||||
Files.deleteIfExists(savedFilePath);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void doesCachedFileExist(FileSystem fileSystem) throws IOException {
|
||||
setup(fileSystem);
|
||||
|
||||
String fileName = easyRandom.nextObject(String.class);
|
||||
byte[] fileContent = new byte[1024];
|
||||
easyRandom.nextBytes(fileContent);
|
||||
Path savedFilePath = fileSystem.getPath(CACHE_PATH, fileName);
|
||||
|
||||
assertThat(target.doesCachedFileExist(fileName)).isFalse();
|
||||
|
||||
Files.write(savedFilePath, fileContent);
|
||||
|
||||
assertThat(target.doesCachedFileExist(fileName)).isTrue();
|
||||
|
||||
Files.deleteIfExists(savedFilePath);
|
||||
}
|
||||
|
||||
@Disabled("Due to JimFS not supporting the \"Path.toFile()\" call")
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void getSizeOnDisk_Directory(FileSystem fileSystem) throws IOException {
|
||||
setup(fileSystem);
|
||||
|
||||
String directoryName = easyRandom.nextObject(String.class);
|
||||
int fileSize = 1024;
|
||||
int fileCount = 5;
|
||||
|
||||
Files.createDirectories(fileSystem.getPath(directoryName));
|
||||
|
||||
for(int i = 0; i < fileCount; i++) {
|
||||
String fileName = easyRandom.nextObject(String.class);
|
||||
byte[] fileContent = new byte[fileSize];
|
||||
easyRandom.nextBytes(fileContent);
|
||||
Files.write(fileSystem.getPath(directoryName, fileName), fileContent);
|
||||
}
|
||||
|
||||
long directorySize = target.getSizeOnDisk(fileSystem.getPath(directoryName));
|
||||
|
||||
assertThat(directorySize).isEqualTo(fileSize * fileCount);
|
||||
}
|
||||
|
||||
@Disabled("Due to JimFS not supporting the \"Path.toFile()\" call")
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void getSizeOnDisk_File(FileSystem fileSystem) throws IOException {
|
||||
setup(fileSystem);
|
||||
|
||||
String directoryName = easyRandom.nextObject(String.class);
|
||||
int fileSize = 1024;
|
||||
String fileName = easyRandom.nextObject(String.class);
|
||||
byte[] fileContent = new byte[fileSize];
|
||||
easyRandom.nextBytes(fileContent);
|
||||
|
||||
Files.write(fileSystem.getPath(directoryName, fileName), fileContent);
|
||||
|
||||
long directorySize = target.getSizeOnDisk(fileSystem.getPath(directoryName));
|
||||
|
||||
assertThat(directorySize).isEqualTo(fileSize);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> fileSystems() {
|
||||
return Stream.of(
|
||||
arguments(named("Unix", unixFS)),
|
||||
arguments(named("OSX", osxFS)),
|
||||
arguments(named("Windows", winFS))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
package de.grimsi.gameyfin.service;
|
||||
|
||||
import com.igdb.proto.Igdb;
|
||||
import de.grimsi.gameyfin.dto.GameOverviewDto;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import de.grimsi.gameyfin.entities.Library;
|
||||
import de.grimsi.gameyfin.entities.Platform;
|
||||
import de.grimsi.gameyfin.entities.UnmappableFile;
|
||||
import de.grimsi.gameyfin.igdb.IgdbApiProperties;
|
||||
import de.grimsi.gameyfin.igdb.IgdbWrapper;
|
||||
import de.grimsi.gameyfin.mapper.GameMapper;
|
||||
import de.grimsi.gameyfin.repositories.DetectedGameRepository;
|
||||
import de.grimsi.gameyfin.repositories.LibraryRepository;
|
||||
import de.grimsi.gameyfin.repositories.UnmappableFileRepository;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.in;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GameServiceTest {
|
||||
|
||||
private final EasyRandom easyRandom = new EasyRandom();
|
||||
|
||||
@InjectMocks
|
||||
private GameService target;
|
||||
|
||||
@Mock
|
||||
private IgdbWrapper igdbWrapperMock;
|
||||
|
||||
@Mock
|
||||
private GameMapper gameMapperMock;
|
||||
|
||||
@Mock
|
||||
private DetectedGameRepository detectedGameRepositoryMock;
|
||||
|
||||
@Mock
|
||||
private UnmappableFileRepository unmappableFileRepositoryMock;
|
||||
|
||||
@Mock
|
||||
private LibraryRepository libraryRepositoryMock;
|
||||
|
||||
@Mock
|
||||
private FilesystemService filesystemServiceMock;
|
||||
|
||||
@Test
|
||||
void getAllDetectedGames() {
|
||||
List<DetectedGame> input = easyRandom.objects(DetectedGame.class, 5).toList();
|
||||
|
||||
when(detectedGameRepositoryMock.findAll()).thenReturn(input);
|
||||
|
||||
List<DetectedGame> result = target.getAllDetectedGames();
|
||||
|
||||
assertThat(result).hasSameElementsAs(input);
|
||||
verify(detectedGameRepositoryMock, times(1)).findAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDetectedGame() {
|
||||
String slug = easyRandom.nextObject(String.class);
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
|
||||
when(detectedGameRepositoryMock.findById(slug)).thenReturn(Optional.of(input));
|
||||
|
||||
DetectedGame result = target.getDetectedGame(slug);
|
||||
|
||||
assertThat(result).isEqualTo(input);
|
||||
verify(detectedGameRepositoryMock, times(1)).findById(slug);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDetectedGame_NotFound() {
|
||||
String slug = easyRandom.nextObject(String.class);
|
||||
|
||||
when(detectedGameRepositoryMock.findById(slug)).thenReturn(Optional.empty());
|
||||
|
||||
ResponseStatusException e = assertThrows(ResponseStatusException.class, () -> target.getDetectedGame(slug));
|
||||
|
||||
assertThat(e.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
verify(detectedGameRepositoryMock, times(1)).findById(slug);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllUnmappedFiles() {
|
||||
List<UnmappableFile> input = easyRandom.objects(UnmappableFile.class, 5).toList();
|
||||
|
||||
when(unmappableFileRepositoryMock.findAll()).thenReturn(input);
|
||||
|
||||
List<UnmappableFile> result = target.getAllUnmappedFiles();
|
||||
|
||||
assertThat(result).hasSameElementsAs(input);
|
||||
verify(unmappableFileRepositoryMock, times(1)).findAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllMappings() {
|
||||
Stream<DetectedGame> gameStream = easyRandom.objects(DetectedGame.class, 5);
|
||||
List<DetectedGame> games = gameStream.toList();
|
||||
Map<String, String> input = games.stream().collect(Collectors.toMap(DetectedGame::getPath, DetectedGame::getTitle));
|
||||
|
||||
when(detectedGameRepositoryMock.findAll()).thenReturn(games);
|
||||
|
||||
Map<String, String> result = target.getAllMappings();
|
||||
|
||||
assertThat(result).containsAllEntriesOf(input);
|
||||
verify(detectedGameRepositoryMock, times(1)).findAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGameOverviews() {
|
||||
Stream<DetectedGame> gameStream = easyRandom.objects(DetectedGame.class, 5);
|
||||
List<DetectedGame> games = gameStream.toList();
|
||||
List<GameOverviewDto> input = games.stream()
|
||||
.map(d -> GameOverviewDto.builder()
|
||||
.coverId(d.getCoverId())
|
||||
.slug(d.getSlug())
|
||||
.title(d.getTitle())
|
||||
.build())
|
||||
.toList();
|
||||
|
||||
when(detectedGameRepositoryMock.findAll()).thenReturn(games);
|
||||
when(gameMapperMock.toGameOverviewDto(any())).thenCallRealMethod();
|
||||
|
||||
List<GameOverviewDto> result = target.getGameOverviews();
|
||||
|
||||
assertThat(result).hasSameElementsAs(input);
|
||||
verify(detectedGameRepositoryMock, times(1)).findAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteGame() {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
|
||||
when(detectedGameRepositoryMock.findById(input.getSlug())).thenReturn(Optional.of(input));
|
||||
|
||||
target.deleteGame(input.getSlug());
|
||||
|
||||
verify(detectedGameRepositoryMock, times(1)).findById(input.getSlug());
|
||||
verify(unmappableFileRepositoryMock, times(1)).save(new UnmappableFile(input.getPath()));
|
||||
verify(detectedGameRepositoryMock, times(1)).delete(input);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteUnmappedFile() {
|
||||
Long input = easyRandom.nextLong();
|
||||
|
||||
target.deleteUnmappedFile(input);
|
||||
|
||||
verify(unmappableFileRepositoryMock, times(1)).deleteById(input);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void confirmGame(boolean confirmMatch) {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
input.setConfirmedMatch(!confirmMatch);
|
||||
|
||||
when(detectedGameRepositoryMock.findById(input.getSlug())).thenReturn(Optional.of(input));
|
||||
when(detectedGameRepositoryMock.save(any(DetectedGame.class))).thenAnswer(invocation -> invocation.getArgument(0, DetectedGame.class));
|
||||
|
||||
DetectedGame result = target.confirmGame(input.getSlug(), confirmMatch);
|
||||
|
||||
assertThat(result).usingRecursiveComparison()
|
||||
.ignoringFields("confirmedMatch")
|
||||
.isEqualTo(input);
|
||||
assertThat(result.isConfirmedMatch()).isEqualTo(confirmMatch);
|
||||
|
||||
verify(detectedGameRepositoryMock, times(1)).save(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapPathToGame_UnmappableFile() {
|
||||
DetectedGame mockedDetectedGame = easyRandom.nextObject(DetectedGame.class);
|
||||
mockedDetectedGame.setConfirmedMatch(false);
|
||||
UnmappableFile input = new UnmappableFile(mockedDetectedGame.getPath());
|
||||
String slug = easyRandom.nextObject(String.class);
|
||||
Library mockedLibrary = Library.builder()
|
||||
.path(input.getPath())
|
||||
.platforms(easyRandom.objects(Platform.class, 5).toList())
|
||||
.build();
|
||||
|
||||
when(detectedGameRepositoryMock.existsBySlug(slug)).thenReturn(false);
|
||||
when(detectedGameRepositoryMock.save(any(DetectedGame.class))).thenAnswer(invocation -> invocation.getArgument(0, DetectedGame.class));
|
||||
when(unmappableFileRepositoryMock.findByPath(input.getPath())).thenReturn(Optional.of(input));
|
||||
when(filesystemServiceMock.getPath(input.getPath())).thenReturn(Path.of("parent", input.getPath()));
|
||||
when(igdbWrapperMock.getGameBySlug(slug)).thenReturn(Optional.of(Igdb.Game.newBuilder().build()));
|
||||
when(libraryRepositoryMock.findByPath(any())).thenReturn(Optional.of(mockedLibrary));
|
||||
when(gameMapperMock.toDetectedGame(any(Igdb.Game.class), any(Path.class), any(Library.class))).thenReturn(mockedDetectedGame);
|
||||
|
||||
DetectedGame result = target.mapPathToGame(input.getPath(), slug);
|
||||
|
||||
verify(detectedGameRepositoryMock, times(1)).existsBySlug(slug);
|
||||
verify(detectedGameRepositoryMock, never()).findByPath(input.getPath());
|
||||
verify(unmappableFileRepositoryMock, times(1)).findByPath(input.getPath());
|
||||
|
||||
assertThat(result).usingRecursiveComparison()
|
||||
.ignoringFields("confirmedMatch")
|
||||
.isEqualTo(mockedDetectedGame);
|
||||
assertThat(result.isConfirmedMatch()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapPathToGame_DetectedGame() {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
input.setConfirmedMatch(false);
|
||||
String slug = easyRandom.nextObject(String.class);
|
||||
Library mockedLibrary = Library.builder()
|
||||
.path(input.getPath())
|
||||
.platforms(easyRandom.objects(Platform.class, 5).toList())
|
||||
.build();
|
||||
|
||||
when(detectedGameRepositoryMock.existsBySlug(slug)).thenReturn(false);
|
||||
when(detectedGameRepositoryMock.save(any(DetectedGame.class))).thenAnswer(invocation -> invocation.getArgument(0, DetectedGame.class));
|
||||
when(detectedGameRepositoryMock.findByPath(input.getPath())).thenReturn(Optional.of(input));
|
||||
when(unmappableFileRepositoryMock.findByPath(input.getPath())).thenReturn(Optional.empty());
|
||||
when(filesystemServiceMock.getPath(input.getPath())).thenReturn(Path.of("parent", input.getPath()));
|
||||
when(igdbWrapperMock.getGameBySlug(slug)).thenReturn(Optional.of(Igdb.Game.newBuilder().build()));
|
||||
when(libraryRepositoryMock.findByPath(any())).thenReturn(Optional.of(mockedLibrary));
|
||||
when(gameMapperMock.toDetectedGame(any(Igdb.Game.class), any(Path.class), any(Library.class))).thenReturn(input);
|
||||
|
||||
DetectedGame result = target.mapPathToGame(input.getPath(), slug);
|
||||
|
||||
verify(detectedGameRepositoryMock, times(1)).existsBySlug(slug);
|
||||
verify(detectedGameRepositoryMock, times(1)).findByPath(input.getPath());
|
||||
verify(unmappableFileRepositoryMock, times(1)).findByPath(input.getPath());
|
||||
|
||||
assertThat(result).usingRecursiveComparison()
|
||||
.ignoringFields("confirmedMatch")
|
||||
.isEqualTo(input);
|
||||
assertThat(result.isConfirmedMatch()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapPathToGame_SlugAlreadyInDatabase() {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
|
||||
when(detectedGameRepositoryMock.existsBySlug(input.getSlug())).thenReturn(true);
|
||||
|
||||
ResponseStatusException e = assertThrows(ResponseStatusException.class, () -> target.mapPathToGame(input.getPath(), input.getSlug()));
|
||||
|
||||
assertThat(e.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
|
||||
verify(detectedGameRepositoryMock, times(1)).existsBySlug(input.getSlug());
|
||||
verify(detectedGameRepositoryMock, never()).findByPath(input.getPath());
|
||||
verify(unmappableFileRepositoryMock, never()).findByPath(input.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapPathToGame_PathNotInDatabase() {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
|
||||
when(detectedGameRepositoryMock.existsBySlug(input.getSlug())).thenReturn(false);
|
||||
when(detectedGameRepositoryMock.findByPath(input.getPath())).thenReturn(Optional.empty());
|
||||
when(unmappableFileRepositoryMock.findByPath(input.getPath())).thenReturn(Optional.empty());
|
||||
|
||||
ResponseStatusException e = assertThrows(ResponseStatusException.class, () -> target.mapPathToGame(input.getPath(), input.getSlug()));
|
||||
|
||||
assertThat(e.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
verify(detectedGameRepositoryMock, times(1)).existsBySlug(input.getSlug());
|
||||
verify(detectedGameRepositoryMock, times(1)).findByPath(input.getPath());
|
||||
verify(unmappableFileRepositoryMock, times(1)).findByPath(input.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshGame() {
|
||||
DetectedGame input = easyRandom.nextObject(DetectedGame.class);
|
||||
input.setConfirmedMatch(true);
|
||||
Library mockedLibrary = Library.builder()
|
||||
.path(input.getPath())
|
||||
.platforms(easyRandom.objects(Platform.class, 5).toList())
|
||||
.build();
|
||||
|
||||
when(detectedGameRepositoryMock.findById(input.getSlug())).thenReturn(Optional.of(input));
|
||||
when(detectedGameRepositoryMock.save(any(DetectedGame.class))).thenAnswer(invocation -> invocation.getArgument(0, DetectedGame.class));
|
||||
when(filesystemServiceMock.getPath(input.getPath())).thenReturn(Path.of("parent", input.getPath()));
|
||||
when(igdbWrapperMock.getGameBySlug(input.getSlug())).thenReturn(Optional.of(Igdb.Game.newBuilder().build()));
|
||||
when(libraryRepositoryMock.findByPath(any())).thenReturn(Optional.of(mockedLibrary));
|
||||
when(gameMapperMock.toDetectedGame(any(Igdb.Game.class), any(Path.class), any(Library.class))).thenReturn(input);
|
||||
|
||||
DetectedGame result = target.refreshGame(input.getSlug());
|
||||
|
||||
assertThat(result).usingRecursiveComparison()
|
||||
.ignoringFields("confirmedMatch")
|
||||
.isEqualTo(input);
|
||||
assertThat(result.isConfirmedMatch()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshGame_NotFound() {
|
||||
String slug = easyRandom.nextObject(String.class);
|
||||
|
||||
when(detectedGameRepositoryMock.findById(slug)).thenReturn(Optional.empty());
|
||||
|
||||
ResponseStatusException e = assertThrows(ResponseStatusException.class, () -> target.refreshGame(slug));
|
||||
|
||||
assertThat(e.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
verify(detectedGameRepositoryMock, times(1)).findById(slug);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package de.grimsi.gameyfin.service;
|
||||
|
||||
import de.grimsi.gameyfin.entities.Company;
|
||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||
import org.jeasy.random.EasyRandom;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ImageServiceTest {
|
||||
|
||||
private final EasyRandom easyRandom = new EasyRandom();
|
||||
|
||||
private FilesystemService filesystemServiceMock;
|
||||
private GameService gameServiceMock;
|
||||
|
||||
private ImageService target;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
WebClient.Builder webClientBuilderMock = mock(WebClient.Builder.class);
|
||||
gameServiceMock = mock(GameService.class);
|
||||
filesystemServiceMock = mock(FilesystemService.class);
|
||||
|
||||
target = new ImageService(filesystemServiceMock, gameServiceMock, webClientBuilderMock);
|
||||
|
||||
ReflectionTestUtils.setField(target, "webclientBuilder", webClientBuilderMock);
|
||||
when(webClientBuilderMock.baseUrl(any(String.class))).thenReturn(WebClient.builder());
|
||||
|
||||
target.init();
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadGameCoversFromIgdb() {
|
||||
List<DetectedGame> detectedGames = easyRandom.objects(DetectedGame.class, 5).toList();
|
||||
|
||||
when(gameServiceMock.getAllDetectedGames()).thenReturn(detectedGames);
|
||||
|
||||
target.downloadGameCoversFromIgdb();
|
||||
|
||||
verify(gameServiceMock, times(1)).getAllDetectedGames();
|
||||
verify(filesystemServiceMock, times(detectedGames.size())).saveFileToCache(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadGameScreenshotsFromIgdb() {
|
||||
List<DetectedGame> detectedGames = easyRandom.objects(DetectedGame.class, 5).toList();
|
||||
List<String> screenshotIds = detectedGames.stream().flatMap(d -> d.getScreenshotIds().stream()).toList();
|
||||
|
||||
when(gameServiceMock.getAllDetectedGames()).thenReturn(detectedGames);
|
||||
|
||||
target.downloadGameScreenshotsFromIgdb();
|
||||
|
||||
verify(gameServiceMock, times(1)).getAllDetectedGames();
|
||||
verify(filesystemServiceMock, times(screenshotIds.size())).saveFileToCache(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadCompanyLogosFromIgdb() {
|
||||
List<DetectedGame> detectedGames = easyRandom.objects(DetectedGame.class, 5).toList();
|
||||
Set<String> companyLogoIds = detectedGames.stream().flatMap(d -> d.getCompanies().stream())
|
||||
.map(Company::getLogoId).collect(Collectors.toUnmodifiableSet());
|
||||
|
||||
when(gameServiceMock.getAllDetectedGames()).thenReturn(detectedGames);
|
||||
|
||||
target.downloadCompanyLogosFromIgdb();
|
||||
|
||||
verify(gameServiceMock, times(1)).getAllDetectedGames();
|
||||
verify(filesystemServiceMock, times(companyLogoIds.size())).saveFileToCache(any(), any());
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
package de.grimsi.gameyfin.util;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Named.named;
|
||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import de.grimsi.gameyfin.config.properties.GameyfinProperties;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import com.google.common.jimfs.Configuration;
|
||||
import com.google.common.jimfs.Jimfs;
|
||||
|
||||
class FilenameUtilTest {
|
||||
|
||||
private static final GameyfinProperties gameyfinPropertiesMock = mock(GameyfinProperties.class);
|
||||
private static final FileSystem unixFS = Jimfs.newFileSystem(Configuration.unix());
|
||||
private static final FileSystem osxFS = Jimfs.newFileSystem(Configuration.osX());
|
||||
private static final FileSystem winFS = Jimfs.newFileSystem(Configuration.windows());
|
||||
|
||||
private static final List<String> gameFileExtensions = List.of("extension_1", "extension_2", "extension_3");
|
||||
private static final List<String> possibleGameFileSuffixes = Arrays.asList("windows, win, english, win32, win64, opengl, stable".split(", "));
|
||||
|
||||
@BeforeAll
|
||||
static void init() {
|
||||
when(gameyfinPropertiesMock.fileExtensions()).thenReturn(gameFileExtensions);
|
||||
when(gameyfinPropertiesMock.fileSuffixes()).thenReturn(possibleGameFileSuffixes);
|
||||
|
||||
FilenameUtil filenameUtil = new FilenameUtil(gameyfinPropertiesMock);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void closeFileSystems() throws IOException {
|
||||
unixFS.close();
|
||||
osxFS.close();
|
||||
winFS.close();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void getFilenameWithoutExtension_File(FileSystem fileSystem) throws IOException {
|
||||
String filename = "example_file";
|
||||
|
||||
Path p = fileSystem.getPath("%s.%s".formatted(filename, gameFileExtensions.get(0)));
|
||||
Files.createFile(p);
|
||||
|
||||
String result = FilenameUtil.getFilenameWithoutExtension(p);
|
||||
|
||||
assertThat(result).isEqualTo(filename);
|
||||
|
||||
Files.deleteIfExists(p);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void getFilenameWithoutExtension_Folder(FileSystem fileSystem) throws IOException {
|
||||
String filename = "example_folder";
|
||||
|
||||
Path p = fileSystem.getPath("%s.%s".formatted(filename, gameFileExtensions.get(0)));
|
||||
Files.createDirectory(p);
|
||||
|
||||
String result = FilenameUtil.getFilenameWithoutExtension(p);
|
||||
|
||||
assertThat(result).isEqualTo("%s.%s".formatted(filename, gameFileExtensions.get(0)));
|
||||
|
||||
Files.deleteIfExists(p);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void getFilenameWithExtension(FileSystem fileSystem) throws IOException {
|
||||
String filename = "example_file";
|
||||
|
||||
Path p = fileSystem.getPath("%s.%s".formatted(filename, gameFileExtensions.get(0)));
|
||||
Files.createFile(p);
|
||||
|
||||
String result = FilenameUtil.getFilenameWithExtension(p);
|
||||
|
||||
assertThat(result).isEqualTo("%s.%s".formatted(filename, gameFileExtensions.get(0)));
|
||||
|
||||
Files.deleteIfExists(p);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void hasGameArchiveExtension_gameArchive(FileSystem fileSystem) throws IOException {
|
||||
String filename = "example_file";
|
||||
|
||||
Path p = fileSystem.getPath("%s.%s".formatted(filename, gameFileExtensions.get(0)));
|
||||
Files.createFile(p);
|
||||
|
||||
assertThat(FilenameUtil.hasGameArchiveExtension(p)).isTrue();
|
||||
|
||||
Files.deleteIfExists(p);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("fileSystems")
|
||||
void hasGameArchiveExtension_notGameArchive(FileSystem fileSystem) throws IOException {
|
||||
String filename = "example_file";
|
||||
|
||||
Path p = fileSystem.getPath("%s.%s".formatted(filename, "some_other_extension"));
|
||||
Files.createFile(p);
|
||||
|
||||
assertThat(FilenameUtil.hasGameArchiveExtension(p)).isFalse();
|
||||
|
||||
Files.deleteIfExists(p);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("exampleFilenames")
|
||||
void removeFileSuffixes(String filename) {
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s".formatted(filename, "-win"))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s".formatted(filename, "-v1.05.4"))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s".formatted(filename, "-win32"))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s".formatted(filename, "-win-opengl(windows)"))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s".formatted(filename, "-windows-stable"))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s".formatted(filename, "[windows]"))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s".formatted(filename, "[stable]"))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s".formatted(filename, "(opengl)"))));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("exampleFilenames")
|
||||
void removeFileSuffixesFileExtensions(String filename) {
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s.%s".formatted(filename, "-win", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s.%s".formatted(filename, "-v1.05.4", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s.%s".formatted(filename, "-win32", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s.%s".formatted(filename, "-win-opengl(windows)", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s.%s".formatted(filename, "-windows-stable", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s.%s".formatted(filename, "[windows]", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s.%s".formatted(filename, "[stable]", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath("%s.%s.%s".formatted(filename, "(opengl)", gameFileExtensions.get(0)))));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("exampleFilenames")
|
||||
void removeFileSuffixesWithAddedSpaces(String filename) {
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath(" %s.%s .%s".formatted(filename, "-win", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath(" %s.%s .%s".formatted(filename, "-v1.05.4", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath(" %s.%s .%s".formatted(filename, "-win32", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath(" %s.%s .%s".formatted(filename, "-win-opengl(windows)", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath(" %s.%s .%s".formatted(filename, "-windows-stable", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath(" %s.%s .%s".formatted(filename, "[windows]", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath(" %s.%s .%s".formatted(filename, "[stable]", gameFileExtensions.get(0)))));
|
||||
assertEquals(filename, FilenameUtil.getFilenameWithoutAdditions(unixFS.getPath(" %s.%s .%s".formatted(filename, "(opengl)", gameFileExtensions.get(0)))));
|
||||
}
|
||||
|
||||
|
||||
private static Stream<Arguments> fileSystems() {
|
||||
return Stream.of(
|
||||
arguments(named("Unix", unixFS)),
|
||||
arguments(named("OSX", osxFS)),
|
||||
arguments(named("Windows", winFS))
|
||||
);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> exampleFilenames() {
|
||||
return Stream.of(
|
||||
arguments(named("example_file", "example_file")),
|
||||
arguments(named("example-file", "example-file")),
|
||||
arguments(named("example file", "example file"))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package de.grimsi.gameyfin.util;
|
||||
|
||||
import com.google.protobuf.Timestamp;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ProtobufUtilTest {
|
||||
|
||||
@Test
|
||||
void toInstant() {
|
||||
Timestamp t = Timestamp.newBuilder().setSeconds(1).build();
|
||||
|
||||
Instant i = ProtobufUtil.toInstant(t);
|
||||
|
||||
assertThat(i.getEpochSecond()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
gameyfin:
|
||||
igdb:
|
||||
api:
|
||||
client-id: igdb_client_id
|
||||
client-secret: igdb_client_secret
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:${spring.datasource.db-name}
|
||||
@@ -1 +0,0 @@
|
||||
mock-maker-inline
|
||||
@@ -0,0 +1,63 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("org.springframework.boot") version "3.2.2"
|
||||
id("io.spring.dependency-management") version "1.1.4"
|
||||
id("com.vaadin") version "24.3.3"
|
||||
kotlin("jvm") version "1.9.22"
|
||||
kotlin("plugin.spring") version "1.9.22"
|
||||
kotlin("plugin.jpa") version "1.9.22"
|
||||
}
|
||||
|
||||
group = "de.grimsi"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom(configurations.annotationProcessor.get())
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
extra["vaadinVersion"] = "24.3.3"
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||
implementation("com.vaadin:vaadin-spring-boot-starter")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
runtimeOnly("com.h2database:h2")
|
||||
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
|
||||
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("org.springframework.security:spring-security-test")
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom("com.vaadin:vaadin-bom:${property("vaadinVersion")}")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += "-Xjsr305=strict"
|
||||
jvmTarget = "21"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
# Uncomment if you want to change the port from 8080
|
||||
# server.port=8081
|
||||
|
||||
# Gameyfin admin interface username and password
|
||||
gameyfin.user=<your username here>
|
||||
gameyfin.password=<your password here>
|
||||
|
||||
# Gameyfin source folders
|
||||
gameyfin.sources=<comma-seperated list of root folders of your game libraries>
|
||||
|
||||
# Uncomment if you want to specify the Gameyfin database path (default is <game library root>/.gameyfin/db)
|
||||
# gameyfin.db=<custom path to db file>
|
||||
|
||||
# Uncomment if you want to specify the Gameyfin cache path (default is <game library root>/.gameyfin/cache)
|
||||
# gameyfin.cache=<custom path to cache folder>
|
||||
|
||||
# Your twitch client-id and client-secret
|
||||
gameyfin.igdb.api.client-id=<your twitch client-id here>
|
||||
gameyfin.igdb.api.client-secret=<your twitch client-secret here>
|
||||
|
||||
# Preferred platforms
|
||||
# PC (Microsoft Windows) (id=6), Dreamcast (id=23), Game Boy (id=33), Game Boy Advance (id=24), Game Boy Color (id=22),
|
||||
# Linux (id=3), Mac (id=14), New Nintendo 3DS (id=137), Nintendo 3DS (id=37), Nintendo 64 (id=4), Nintendo DS (id=20),
|
||||
# Nintendo DSi (id=159), Nintendo Entertainment System (id=18), Nintendo GameCube (id=21), Nintendo PlayStation (id=131),
|
||||
# Nintendo Switch (id=130), PlayStation (id=7), PlayStation 2 (id=8), PlayStation 3 (id=9), PlayStation 4 (id=48),
|
||||
# PlayStation Portable (id=38), PlayStation Vita (id=46), PlayStation VR (id=165), Sega Game Gear (id=35),
|
||||
# Sega Master System/Mark III (id=64), Sega Mega Drive/Genesis (id=29), Sega Saturn (id=32), SteamVR (id=163),
|
||||
# Super Famicom (id=58), Super Nintendo Entertainment System (id=19), Virtual Console (Nintendo) (id=47), Wii (id=5),
|
||||
# Wii U (id=41), Xbox (id=11), Xbox 360 (id=12)
|
||||
gameyfin.igdb.config.preferred-platforms=6,23,33,24,22,3,14,137,37,4,20,159,18,21,130,7,8,9,48,38,46,165,35,64,29,32,58,19,47,5,41,11,12
|
||||
@@ -1,17 +0,0 @@
|
||||
version: "3"
|
||||
services:
|
||||
gameyfin:
|
||||
image: grimsi/gameyfin:latest
|
||||
container_name: gameyfin
|
||||
environment:
|
||||
- GAMEYFIN_USER=<your username here>
|
||||
- GAMEYFIN_PASSWORD=<your password here>
|
||||
- GAMEYFIN_IGDB_API_CLIENT_ID=<your twitch client-id here>
|
||||
- GAMEYFIN_IGDB_API_CLIENT_SECRET=<your twitch client-secret here>
|
||||
- GAMEYFIN_SOURCES=/opt/gameyfin-library/library-1,/opt/gameyfin-library/library-2,/opt/gameyfin-library/library-3
|
||||
volumes:
|
||||
- <Path on your host to the 1st source folder>:/opt/gameyfin-library/library-1
|
||||
- <Path on your host to the 2nd source folder>:/opt/gameyfin-library/library-2
|
||||
- <Path on your host to the 3rd source folder>:/opt/gameyfin-library/library-3
|
||||
ports:
|
||||
- "8080:8080"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user