mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +00:00
Start development of v2
This commit is contained in:
+16
-9
@@ -1,8 +1,10 @@
|
|||||||
|
node_modules
|
||||||
HELP.md
|
HELP.md
|
||||||
target/
|
.gradle
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
build/
|
||||||
!**/src/main/**/target/
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
!**/src/test/**/target/
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
### STS ###
|
### STS ###
|
||||||
.apt_generated
|
.apt_generated
|
||||||
@@ -12,12 +14,18 @@ target/
|
|||||||
.settings
|
.settings
|
||||||
.springBeans
|
.springBeans
|
||||||
.sts4-cache
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
### IntelliJ IDEA ###
|
### IntelliJ IDEA ###
|
||||||
.idea
|
.idea
|
||||||
*.iws
|
*.iws
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
### NetBeans ###
|
### NetBeans ###
|
||||||
/nbproject/private/
|
/nbproject/private/
|
||||||
@@ -25,16 +33,15 @@ target/
|
|||||||
/dist/
|
/dist/
|
||||||
/nbdist/
|
/nbdist/
|
||||||
/.nb-gradle/
|
/.nb-gradle/
|
||||||
build/
|
|
||||||
!**/src/main/**/build/
|
|
||||||
!**/src/test/**/build/
|
|
||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
/.mvn/
|
|
||||||
|
### Kotlin ###
|
||||||
|
.kotlin
|
||||||
|
|
||||||
### Custom ###
|
### Custom ###
|
||||||
/data/
|
/data/
|
||||||
/backend/src/main/resources/static/
|
/backend/src/main/resources/static/
|
||||||
/docker/docker-compose.yml
|
/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">
|
<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">
|
<img src="assets/Gameyfin_Logo_White_Border.svg" height="128px" width="auto" alt="Gameyfin Logo">
|
||||||
<h1>Gameyfin</h1>
|
<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>
|
</div>
|
||||||
|
|
||||||
# Overview
|
# Overview
|
||||||
|
|
||||||
Name and functionality inspired by [Jellyfin](https://jellyfin.org/).
|
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
|
## Features
|
||||||
|
|
||||||
* Automatically scans your game library folder and downloads additional metadata from IGDB
|
✨ Automatically scans and indexes your game libraries
|
||||||
* Access your library via your Web-Browser
|
⬇️ Access your library via your web browser & download games directly from there
|
||||||
* Download games directly from your browser
|
👥 Share your library with friends & family
|
||||||
* LAN-friendly (everything is cached locally)
|
⚛️ LAN-friendly (everything is cached locally)
|
||||||
* Native Docker support (alternatively it's only one .jar file to run on bare metal)
|
🐋 Runs in a container or as single <binary file / JAR file> on bare metal
|
||||||
* Light and dark theme
|
🌈 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
|
## 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`
|
Gameyfin v2 is written in Kotlin and uses the following libraries/frameworks:
|
||||||
2. Edit the configuration values to your liking
|
* Spring Boot 3 for the backend
|
||||||
3. Run `docker-compose up -d`
|
* Vaadin for the frontend
|
||||||
|
* PF4J for the plugin system
|
||||||
### Bare metal
|
* H2 database for persistence
|
||||||
|
* Micrometer and Prometheus for monitoring
|
||||||
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
|
|
||||||
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