mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-17 00:30:04 +00:00
Release 1.3.2 (#74)
* Fixes #71 * [GH-61] Fix manual mapping leading to duplicates in DB * [GH-73] Fix Gameyfin only detecting PC games * Improve game title matching (#77) * Implement some filename suffix logic Removes some common file suffixes from files downloaded from for example itch.io. Also removes trailing/leading whitespace/-/_/./() and version numbers starting with a "v" like "v1.2.3". * Add edge cases for game titles (#76) * Fix SONAR code smells Co-authored-by: tr7zw <tr7zw@live.de> Co-authored-by: Pfuenzle <dark.leon64@gmail.com> * Validate some combinations of filename with added suffixes (#79) Also fixes a bug of not removing trailing empty []. * Improve test coverage (#70) * Implemented missing testcases for IgdbWrapper Refactored getPlatformBySlug to return Optional<> * Fixed SONAR findings * Implemented integration tests for the DB * Started implementing tests for controller * Finished GamesControllerTest * Added ImageControllerTest * Implemented LibraryControllerTest * Add LibraryManagementControllerTest * Updated some dependencies * Add DownloadServiceTest * Introduced "gameyfin.data" property to specify a folder for both cache and DB. De-facto removed "gameyfin.db" and "gameyfin.cache" properties Refactored file-system code to be cleaner and easier to test * Refactored filesystem code Implemented FilesystemServiceTest * Fix SONAR code smells * Implemented GameServiceTest * Implemented ImageServiceTest * Fix website scroll position when clicking on game covers in the library view (#94) Fixes #81 * Expansion panels are now not collapsing when last active filter is de-selected (#95) Fixes #86 --------- Co-authored-by: tr7zw <tr7zw@live.de> Co-authored-by: Pfuenzle <dark.leon64@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Angular Application" type="JavascriptDebugType" uri="http://localhost:4200" useFirstLineBreakpoints="true">
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<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>
|
||||||
+3
-2
@@ -15,7 +15,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<java.version>18</java.version>
|
<java.version>18</java.version>
|
||||||
|
|
||||||
<springdoc-openapi-ui.version>1.6.11</springdoc-openapi-ui.version>
|
<springdoc-openapi-ui.version>1.6.12</springdoc-openapi-ui.version>
|
||||||
<resilience4j.version>1.7.1</resilience4j.version>
|
<resilience4j.version>1.7.1</resilience4j.version>
|
||||||
<commons-io.version>2.11.0</commons-io.version>
|
<commons-io.version>2.11.0</commons-io.version>
|
||||||
<commons-compress.version>1.21</commons-compress.version>
|
<commons-compress.version>1.21</commons-compress.version>
|
||||||
@@ -126,12 +126,13 @@
|
|||||||
<version>${easy-random.version}</version>
|
<version>${easy-random.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Temporarily disabled due to a StackOverflow bug: https://github.com/murdos/easy-random-protobuf/issues/136
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.murdos</groupId>
|
<groupId>io.github.murdos</groupId>
|
||||||
<artifactId>easy-random-protobuf</artifactId>
|
<artifactId>easy-random-protobuf</artifactId>
|
||||||
<version>${easy-random-protobuf.version}</version>
|
<version>${easy-random-protobuf.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.squareup.okhttp3</groupId>
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
<artifactId>okhttp</artifactId>
|
<artifactId>okhttp</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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,109 +1,64 @@
|
|||||||
package de.grimsi.gameyfin.config;
|
package de.grimsi.gameyfin.config;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import de.grimsi.gameyfin.service.FilesystemService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
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.Configuration;
|
||||||
import org.springframework.context.annotation.Primary;
|
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.core.env.*;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.LinkOption;
|
import java.nio.file.LinkOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.stream.StreamSupport;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class FilesystemConfig {
|
public class FilesystemConfig {
|
||||||
|
|
||||||
private static final String INTERNAL_FOLDER_NAME = ".gameyfin";
|
@Value("${gameyfin.folders.data}")
|
||||||
|
private String dataFolderPath;
|
||||||
|
|
||||||
@Value("#{'${gameyfin.sources}'.split(',')[0]}")
|
private final FilesystemService filesystemService;
|
||||||
private String firstLibraryPath;
|
|
||||||
|
|
||||||
@Value("${gameyfin.db}")
|
/**
|
||||||
private String dbPath;
|
* This will create the cache folder for Gameyfin.
|
||||||
|
* The path of this folder is specified in the "gameyfin.cache" parameter which is either:
|
||||||
@Value("${gameyfin.cache}")
|
* 1. Derived from the first configured library folder path or
|
||||||
private String cachePath;
|
* 2. Explicitly set by the user
|
||||||
|
* <p>
|
||||||
@Autowired
|
* For more details see {@link GameyfinFolderConfig#setConfigurableEnvironment(ConfigurableEnvironment)}
|
||||||
Environment env;
|
*/
|
||||||
|
@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.
|
* 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
|
* On UNIX-based systems files and folders starting with a dot are hidden
|
||||||
*/
|
*/
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
@Order(2)
|
||||||
public void hideInternalFolderOnDOS() throws IOException {
|
public void hideInternalFolderOnDOS() throws IOException {
|
||||||
if (!isRunningOnWindows()) return;
|
if (!isRunningOnWindows()) return;
|
||||||
|
|
||||||
Path internalFolder = Paths.get("%s/%s".formatted(firstLibraryPath, INTERNAL_FOLDER_NAME));
|
Path internalFolder = filesystemService.getPath(dataFolderPath);
|
||||||
|
|
||||||
if (!Files.exists(internalFolder) || !Files.isDirectory(internalFolder)) return;
|
if (!Files.exists(internalFolder) || !Files.isDirectory(internalFolder)) return;
|
||||||
|
|
||||||
Files.setAttribute(internalFolder, "dos:hidden", Boolean.TRUE, LinkOption.NOFOLLOW_LINKS);
|
Files.setAttribute(internalFolder, "dos:hidden", Boolean.TRUE, LinkOption.NOFOLLOW_LINKS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public void setConfigurableEnvironment(ConfigurableEnvironment env) {
|
|
||||||
Properties props = new Properties();
|
|
||||||
|
|
||||||
if (!StringUtils.hasText(dbPath)) {
|
|
||||||
props.setProperty("gameyfin.db", "%s/%s/db".formatted(firstLibraryPath, INTERNAL_FOLDER_NAME));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringUtils.hasText(cachePath)) {
|
|
||||||
props.setProperty("gameyfin.cache", "%s/%s/cache".formatted(firstLibraryPath, 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(ps -> ps instanceof EnumerablePropertySource)
|
|
||||||
.map(ps -> ((EnumerablePropertySource<?>) ps).getPropertyNames())
|
|
||||||
.flatMap(Arrays::stream)
|
|
||||||
.forEach(propName -> props.setProperty(propName, env.getProperty(propName)));
|
|
||||||
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isRunningOnWindows() {
|
private boolean isRunningOnWindows() {
|
||||||
return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows");
|
return System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
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 {
|
||||||
|
|
||||||
|
private static final String INTERNAL_FOLDER_NAME = ".gameyfin";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,8 +1,12 @@
|
|||||||
package de.grimsi.gameyfin.dto;
|
package de.grimsi.gameyfin.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class LibraryScanRequestDto {
|
public class LibraryScanRequestDto {
|
||||||
private String path;
|
private String path;
|
||||||
private boolean downloadImages;
|
private boolean downloadImages;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class DetectedGame {
|
|||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@Lob
|
@Lob
|
||||||
@Column(columnDefinition="CLOB")
|
@Column(columnDefinition = "CLOB")
|
||||||
private String summary;
|
private String summary;
|
||||||
|
|
||||||
private Instant releaseDate;
|
private Instant releaseDate;
|
||||||
@@ -81,8 +81,9 @@ public class DetectedGame {
|
|||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
private List<Platform> platforms;
|
private List<Platform> platforms;
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne(cascade = CascadeType.MERGE)
|
||||||
@JoinColumn(name = "library")
|
@JoinColumn(name = "library")
|
||||||
|
@ToString.Exclude
|
||||||
private Library library;
|
private Library library;
|
||||||
|
|
||||||
// Technical properties
|
// Technical properties
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import com.igdb.proto.Igdb;
|
|||||||
import de.grimsi.gameyfin.config.WebClientConfig;
|
import de.grimsi.gameyfin.config.WebClientConfig;
|
||||||
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
|
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
|
||||||
import de.grimsi.gameyfin.entities.Platform;
|
import de.grimsi.gameyfin.entities.Platform;
|
||||||
import de.grimsi.gameyfin.igdb.IgdbApiQueryBuilder.Condition;
|
import de.grimsi.gameyfin.igdb.IgdbApiQueryBuilder.*;
|
||||||
import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto;
|
import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto;
|
||||||
import de.grimsi.gameyfin.mapper.GameMapper;
|
import de.grimsi.gameyfin.mapper.GameMapper;
|
||||||
import de.grimsi.gameyfin.mapper.PlatformMapper;
|
import de.grimsi.gameyfin.mapper.PlatformMapper;
|
||||||
@@ -159,11 +159,23 @@ public class IgdbWrapper {
|
|||||||
|
|
||||||
if (hasBrackets.find()) {
|
if (hasBrackets.find()) {
|
||||||
String searchTermWithoutBrackets = searchTerm.split(brackets.pattern())[0].trim();
|
String searchTermWithoutBrackets = searchTerm.split(brackets.pattern())[0].trim();
|
||||||
log.warn("Trying again with search term '{}'", searchTermWithoutBrackets);
|
log.warn("Removed brackets, trying again with search term '{}'", searchTermWithoutBrackets);
|
||||||
return searchForGameByTitle(searchTermWithoutBrackets, platformSlugs);
|
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 Optional.empty();
|
return getGameBySlug(searchTerm.replace(" ", "-").toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Igdb.Game> games = gameResult.getGamesList();
|
List<Igdb.Game> games = gameResult.getGamesList();
|
||||||
@@ -189,6 +201,37 @@ public class IgdbWrapper {
|
|||||||
return Optional.of(games.get(0));
|
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() {
|
private void initIgdbClient() {
|
||||||
if (accessToken == null) {
|
if (accessToken == null) {
|
||||||
authenticate();
|
authenticate();
|
||||||
@@ -212,35 +255,4 @@ public class IgdbWrapper {
|
|||||||
.transformDeferred(RateLimiterOperator.of(webClientConfig.getIgdbRateLimiter()))
|
.transformDeferred(RateLimiterOperator.of(webClientConfig.getIgdbRateLimiter()))
|
||||||
.block();
|
.block();
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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 null;
|
|
||||||
|
|
||||||
return platformResult.getPlatformsList().stream().map(PlatformMapper::toPlatform).findFirst().orElse(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
package de.grimsi.gameyfin.repositories;
|
package de.grimsi.gameyfin.repositories;
|
||||||
|
|
||||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||||
import de.grimsi.gameyfin.entities.UnmappableFile;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||||
|
|
||||||
@@ -24,6 +21,7 @@ public interface DetectedGameRepository extends JpaRepository<DetectedGame, Stri
|
|||||||
List<DetectedGame> findByPathStartsWithAndLibraryIsNull(String path);
|
List<DetectedGame> findByPathStartsWithAndLibraryIsNull(String path);
|
||||||
|
|
||||||
List<DetectedGame> getAllByPathNotIn(Collection<String> paths);
|
List<DetectedGame> getAllByPathNotIn(Collection<String> paths);
|
||||||
|
|
||||||
List<DetectedGame> getAllByPathNotInAndPathStartsWith(Collection<String> paths, String libraryPath);
|
List<DetectedGame> getAllByPathNotInAndPathStartsWith(Collection<String> paths, String libraryPath);
|
||||||
|
|
||||||
default List<DetectedGame> getAllByPathNotInAndPathStartsWith(List<Path> paths, String libraryPath) {
|
default List<DetectedGame> getAllByPathNotInAndPathStartsWith(List<Path> paths, String libraryPath) {
|
||||||
@@ -31,7 +29,4 @@ public interface DetectedGameRepository extends JpaRepository<DetectedGame, Stri
|
|||||||
// get games that are not in the paths list but are starting with libraryPath if libraryPath is not empty
|
// 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);
|
return isBlank(libraryPath) ? getAllByPathNotIn(pathStrings) : getAllByPathNotInAndPathStartsWith(pathStrings, libraryPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,15 @@ import de.grimsi.gameyfin.entities.DetectedGame;
|
|||||||
import de.grimsi.gameyfin.service.DownloadService;
|
import de.grimsi.gameyfin.service.DownloadService;
|
||||||
import de.grimsi.gameyfin.service.GameService;
|
import de.grimsi.gameyfin.service.GameService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
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 org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class ImageController {
|
|||||||
private final DownloadService downloadService;
|
private final DownloadService downloadService;
|
||||||
|
|
||||||
@GetMapping(value = "/{imageId}", produces = MediaType.IMAGE_PNG_VALUE)
|
@GetMapping(value = "/{imageId}", produces = MediaType.IMAGE_PNG_VALUE)
|
||||||
public ResponseEntity<Resource> getCoverImageForGame(@PathVariable String imageId) {
|
public ResponseEntity<Resource> getImage(@PathVariable String imageId) {
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.cacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic())
|
.cacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic())
|
||||||
.body(downloadService.sendImageToClient(imageId));
|
.body(downloadService.sendImageToClient(imageId));
|
||||||
|
|||||||
@@ -29,17 +29,17 @@ public class DownloadService {
|
|||||||
private final FilesystemService filesystemService;
|
private final FilesystemService filesystemService;
|
||||||
|
|
||||||
public String getDownloadFileName(DetectedGame g) {
|
public String getDownloadFileName(DetectedGame g) {
|
||||||
Path path = Path.of(g.getPath());
|
Path path = filesystemService.getPath(g.getPath());
|
||||||
|
|
||||||
if (!path.toFile().isDirectory()) return getFilenameWithExtension(path);
|
if (!Files.isDirectory(path)) return getFilenameWithExtension(path);
|
||||||
return getFilenameWithExtension(path) + ".zip";
|
return getFilenameWithExtension(path) + ".zip";
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getDownloadFileSize(DetectedGame game) {
|
public long getDownloadFileSize(DetectedGame game) {
|
||||||
Path path = Path.of(game.getPath());
|
Path path = filesystemService.getPath(game.getPath());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!path.toFile().isDirectory()) {
|
if (!Files.isDirectory(path)) {
|
||||||
long fileSize = filesystemService.getSizeOnDisk(path);
|
long fileSize = filesystemService.getSizeOnDisk(path);
|
||||||
log.info("Calculated file size for {} ({} MB).", path, Math.divideExact(fileSize, 1000000L));
|
log.info("Calculated file size for {} ({} MB).", path, Math.divideExact(fileSize, 1000000L));
|
||||||
return fileSize;
|
return fileSize;
|
||||||
@@ -65,7 +65,7 @@ public class DownloadService {
|
|||||||
|
|
||||||
stopWatch.start();
|
stopWatch.start();
|
||||||
|
|
||||||
Path path = Path.of(game.getPath());
|
Path path = filesystemService.getPath(game.getPath());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (path.toFile().isDirectory()) {
|
if (path.toFile().isDirectory()) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.grimsi.gameyfin.service;
|
package de.grimsi.gameyfin.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -11,33 +12,55 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.net.URI;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.*;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardOpenOption;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class handles all filesystem operations for Gameyfin.
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class FilesystemService {
|
public class FilesystemService {
|
||||||
|
|
||||||
@Value("${gameyfin.cache}")
|
@Value("${gameyfin.cache}")
|
||||||
private String cacheFolderPath;
|
private String cacheFolderPath;
|
||||||
|
|
||||||
@PostConstruct
|
private final FileSystem fileSystem;
|
||||||
public void createCacheFolder() throws IOException {
|
|
||||||
Files.createDirectories(Path.of(cacheFolderPath));
|
/**
|
||||||
|
* 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) {
|
public void saveFileToCache(Flux<DataBuffer> dataBuffer, String filename) {
|
||||||
DataBufferUtils.write(dataBuffer, Path.of(cacheFolderPath).resolve(filename), StandardOpenOption.CREATE)
|
DataBufferUtils.write(dataBuffer, getPath(cacheFolderPath).resolve(filename), StandardOpenOption.CREATE)
|
||||||
.share().block();
|
.share().block();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ByteArrayResource getFileFromCache(String filename) {
|
public ByteArrayResource getFileFromCache(String filename) {
|
||||||
try {
|
try {
|
||||||
return new ByteArrayResource(Files.readAllBytes(Paths.get("%s/%s".formatted(cacheFolderPath, filename))));
|
return new ByteArrayResource(Files.readAllBytes(getPath(cacheFolderPath, filename)));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find image file %s".formatted(filename));
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find image file %s".formatted(filename));
|
||||||
}
|
}
|
||||||
@@ -79,6 +102,6 @@ public class FilesystemService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Path getPathFromFilename(String filename) {
|
private Path getPathFromFilename(String filename) {
|
||||||
return Path.of(cacheFolderPath, filename);
|
return getPath(cacheFolderPath, filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,15 +32,16 @@ public class GameService {
|
|||||||
private final GameMapper gameMapper;
|
private final GameMapper gameMapper;
|
||||||
private final DetectedGameRepository detectedGameRepository;
|
private final DetectedGameRepository detectedGameRepository;
|
||||||
private final UnmappableFileRepository unmappableFileRepository;
|
private final UnmappableFileRepository unmappableFileRepository;
|
||||||
|
|
||||||
private final LibraryRepository libraryRepository;
|
private final LibraryRepository libraryRepository;
|
||||||
|
private final FilesystemService filesystemService;
|
||||||
|
|
||||||
public List<DetectedGame> getAllDetectedGames() {
|
public List<DetectedGame> getAllDetectedGames() {
|
||||||
return detectedGameRepository.findAll();
|
return detectedGameRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DetectedGame getDetectedGame(String slug) {
|
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)));
|
return detectedGameRepository.findById(slug)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with slug '%s' not found in library.".formatted(slug)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<UnmappableFile> getAllUnmappedFiles() {
|
public List<UnmappableFile> getAllUnmappedFiles() {
|
||||||
@@ -62,7 +63,7 @@ public class GameService {
|
|||||||
// so it doesn't get re-indexed on the next library scan
|
// so it doesn't get re-indexed on the next library scan
|
||||||
unmappableFileRepository.save(new UnmappableFile(gameToBeDeleted.getPath()));
|
unmappableFileRepository.save(new UnmappableFile(gameToBeDeleted.getPath()));
|
||||||
|
|
||||||
detectedGameRepository.deleteById(slug);
|
detectedGameRepository.delete(gameToBeDeleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUnmappedFile(Long id) {
|
public void deleteUnmappedFile(Long id) {
|
||||||
@@ -81,12 +82,13 @@ public class GameService {
|
|||||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Game with slug '%s' already exists in database.".formatted(slug));
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "Game with slug '%s' already exists in database.".formatted(slug));
|
||||||
|
|
||||||
Optional<UnmappableFile> optionalUnmappableFile = unmappableFileRepository.findByPath(path);
|
Optional<UnmappableFile> optionalUnmappableFile = unmappableFileRepository.findByPath(path);
|
||||||
Optional<DetectedGame> optionalDetectedGame = detectedGameRepository.findByPath(path);
|
|
||||||
|
|
||||||
if (optionalUnmappableFile.isPresent()) {
|
if (optionalUnmappableFile.isPresent()) {
|
||||||
return mapUnmappableFile(optionalUnmappableFile.get(), slug);
|
return mapUnmappableFile(optionalUnmappableFile.get(), slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Optional<DetectedGame> optionalDetectedGame = detectedGameRepository.findByPath(path);
|
||||||
|
|
||||||
if (optionalDetectedGame.isPresent()) {
|
if (optionalDetectedGame.isPresent()) {
|
||||||
return mapDetectedGame(optionalDetectedGame.get(), slug);
|
return mapDetectedGame(optionalDetectedGame.get(), slug);
|
||||||
}
|
}
|
||||||
@@ -106,30 +108,23 @@ public class GameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private DetectedGame mapUnmappableFile(UnmappableFile unmappableFile, String slug) {
|
private DetectedGame mapUnmappableFile(UnmappableFile unmappableFile, String slug) {
|
||||||
Igdb.Game igdbGame = igdbWrapper.getGameBySlug(slug)
|
DetectedGame game = mapPathToGame(filesystemService.getPath(unmappableFile.getPath()), slug);
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with slug '%s' does not exist on IGDB.".formatted(slug)));
|
|
||||||
|
|
||||||
Path path = Path.of(unmappableFile.getPath());
|
|
||||||
// parent file should equal to the library
|
|
||||||
File libraryPath = path.toFile().getParentFile();
|
|
||||||
Library library = libraryRepository.findByPath(libraryPath.toString()).orElse(null);
|
|
||||||
DetectedGame game = gameMapper.toDetectedGame(igdbGame, path, library);
|
|
||||||
game.setConfirmedMatch(true);
|
|
||||||
|
|
||||||
game = detectedGameRepository.save(game);
|
|
||||||
|
|
||||||
unmappableFileRepository.delete(unmappableFile);
|
unmappableFileRepository.delete(unmappableFile);
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DetectedGame mapDetectedGame(DetectedGame existingGame, String slug) {
|
private DetectedGame mapDetectedGame(DetectedGame existingGame, String slug) {
|
||||||
Igdb.Game igdbGame = igdbWrapper.getGameBySlug(slug)
|
DetectedGame game = mapPathToGame(filesystemService.getPath(existingGame.getPath()), slug);
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with slug '%s' does not exist on IGDB.".formatted(slug)));
|
detectedGameRepository.delete(existingGame);
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
Path path = Path.of(existingGame.getPath());
|
private DetectedGame mapPathToGame(Path path, String slug) {
|
||||||
// parent file should equal to the library
|
Igdb.Game igdbGame = igdbWrapper.getGameBySlug(slug)
|
||||||
File libraryPath = path.toFile().getParentFile();
|
.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);
|
Library library = libraryRepository.findByPath(libraryPath.toString()).orElse(null);
|
||||||
DetectedGame game = gameMapper.toDetectedGame(igdbGame, path, library);
|
DetectedGame game = gameMapper.toDetectedGame(igdbGame, path, library);
|
||||||
game.setConfirmedMatch(true);
|
game.setConfirmedMatch(true);
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ package de.grimsi.gameyfin.service;
|
|||||||
import de.grimsi.gameyfin.entities.Company;
|
import de.grimsi.gameyfin.entities.Company;
|
||||||
import de.grimsi.gameyfin.entities.DetectedGame;
|
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||||
import de.grimsi.gameyfin.igdb.IgdbApiProperties;
|
import de.grimsi.gameyfin.igdb.IgdbApiProperties;
|
||||||
import de.grimsi.gameyfin.repositories.DetectedGameRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.core.io.buffer.DataBuffer;
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
@@ -29,11 +27,8 @@ import java.util.stream.Collectors;
|
|||||||
@Service
|
@Service
|
||||||
public class ImageService {
|
public class ImageService {
|
||||||
|
|
||||||
@Value("${gameyfin.cache}")
|
|
||||||
private String cacheFolderPath;
|
|
||||||
|
|
||||||
private final FilesystemService filesystemService;
|
private final FilesystemService filesystemService;
|
||||||
private final DetectedGameRepository detectedGameRepository;
|
private final GameService gameService;
|
||||||
private final WebClient.Builder webclientBuilder;
|
private final WebClient.Builder webclientBuilder;
|
||||||
private WebClient igdbImageClient;
|
private WebClient igdbImageClient;
|
||||||
|
|
||||||
@@ -49,7 +44,7 @@ public class ImageService {
|
|||||||
stopWatch.start();
|
stopWatch.start();
|
||||||
|
|
||||||
MultiValueMap<String, String> gameToImageIds = new LinkedMultiValueMap<>(
|
MultiValueMap<String, String> gameToImageIds = new LinkedMultiValueMap<>(
|
||||||
detectedGameRepository.findAll().stream()
|
gameService.getAllDetectedGames().stream()
|
||||||
.collect(Collectors.toMap(DetectedGame::getSlug, g -> Collections.singletonList(g.getCoverId()))));
|
.collect(Collectors.toMap(DetectedGame::getSlug, g -> Collections.singletonList(g.getCoverId()))));
|
||||||
|
|
||||||
int downloadCount = saveImagesIntoCache(gameToImageIds, IgdbApiProperties.COVER_IMAGE_SIZE, "cover", "game");
|
int downloadCount = saveImagesIntoCache(gameToImageIds, IgdbApiProperties.COVER_IMAGE_SIZE, "cover", "game");
|
||||||
@@ -67,7 +62,7 @@ public class ImageService {
|
|||||||
stopWatch.start();
|
stopWatch.start();
|
||||||
|
|
||||||
MultiValueMap<String, String> gamesToImageIds = new LinkedMultiValueMap<>(
|
MultiValueMap<String, String> gamesToImageIds = new LinkedMultiValueMap<>(
|
||||||
detectedGameRepository.findAll().stream()
|
gameService.getAllDetectedGames().stream()
|
||||||
.collect(Collectors.toMap(DetectedGame::getSlug, DetectedGame::getScreenshotIds)));
|
.collect(Collectors.toMap(DetectedGame::getSlug, DetectedGame::getScreenshotIds)));
|
||||||
|
|
||||||
int downloadCount = saveImagesIntoCache(gamesToImageIds, IgdbApiProperties.SCREENSHOT_IMAGE_SIZE, "screenshot", "game");
|
int downloadCount = saveImagesIntoCache(gamesToImageIds, IgdbApiProperties.SCREENSHOT_IMAGE_SIZE, "screenshot", "game");
|
||||||
@@ -84,7 +79,7 @@ public class ImageService {
|
|||||||
log.info("Starting company logo download...");
|
log.info("Starting company logo download...");
|
||||||
stopWatch.start();
|
stopWatch.start();
|
||||||
|
|
||||||
Map<String, List<String>> companyToLogoIdMap = detectedGameRepository.findAll().stream()
|
Map<String, List<String>> companyToLogoIdMap = gameService.getAllDetectedGames().stream()
|
||||||
.flatMap(g -> g.getCompanies().stream())
|
.flatMap(g -> g.getCompanies().stream())
|
||||||
.collect(Collectors.toMap(Company::getSlug, c -> Collections.singletonList(c.getLogoId()), (c1, c2) -> c1));
|
.collect(Collectors.toMap(Company::getSlug, c -> Collections.singletonList(c.getLogoId()), (c1, c2) -> c1));
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import java.util.function.Predicate;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static de.grimsi.gameyfin.util.FilenameUtil.getFilenameWithoutExtension;
|
import static de.grimsi.gameyfin.util.FilenameUtil.getFilenameWithoutAdditions;
|
||||||
import static de.grimsi.gameyfin.util.FilenameUtil.hasGameArchiveExtension;
|
import static de.grimsi.gameyfin.util.FilenameUtil.hasGameArchiveExtension;
|
||||||
import static java.util.stream.Collectors.toList;
|
import static java.util.stream.Collectors.toList;
|
||||||
import static java.util.stream.Collectors.toSet;
|
import static java.util.stream.Collectors.toSet;
|
||||||
@@ -51,6 +51,7 @@ public class LibraryService {
|
|||||||
private final UnmappableFileRepository unmappableFileRepository;
|
private final UnmappableFileRepository unmappableFileRepository;
|
||||||
private final LibraryRepository libraryRepository;
|
private final LibraryRepository libraryRepository;
|
||||||
private final PlatformRepository platformRepository;
|
private final PlatformRepository platformRepository;
|
||||||
|
private final FilesystemService filesystemService;
|
||||||
|
|
||||||
public List<Path> getGameFiles() {
|
public List<Path> getGameFiles() {
|
||||||
return getGameFiles(null);
|
return getGameFiles(null);
|
||||||
@@ -96,10 +97,6 @@ public class LibraryService {
|
|||||||
return gamefiles;
|
return gamefiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Predicate<Path> allPathsOrSpecific(String path) {
|
|
||||||
return p -> isBlank(path) || p.equals(Path.of(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
public LibraryScanResult scanGameLibrary(Library library) {
|
public LibraryScanResult scanGameLibrary(Library library) {
|
||||||
StopWatch stopWatch = new StopWatch();
|
StopWatch stopWatch = new StopWatch();
|
||||||
|
|
||||||
@@ -141,7 +138,7 @@ public class LibraryService {
|
|||||||
// If a game is not found on IGDB, blacklist the path, so we won't query the API later 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.parallelStream()
|
List<DetectedGame> newDetectedGames = gameFiles.parallelStream()
|
||||||
.map(p -> {
|
.map(p -> {
|
||||||
Optional<Igdb.Game> optionalGame = igdbWrapper.searchForGameByTitle(getFilenameWithoutExtension(p), platformsFilter);
|
Optional<Igdb.Game> optionalGame = igdbWrapper.searchForGameByTitle(getFilenameWithoutAdditions(p), platformsFilter);
|
||||||
|
|
||||||
if (optionalGame.isPresent() && detectedGameRepository.existsBySlug(optionalGame.get().getSlug())) {
|
if (optionalGame.isPresent() && detectedGameRepository.existsBySlug(optionalGame.get().getSlug())) {
|
||||||
log.warn("Game with slug '{}' already exists in database", optionalGame.get().getSlug());
|
log.warn("Game with slug '{}' already exists in database", optionalGame.get().getSlug());
|
||||||
@@ -238,10 +235,14 @@ public class LibraryService {
|
|||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find library for path %s".formatted(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());
|
Set<String> platformSlugs = Arrays.stream(slugs.split(",")).collect(toSet());
|
||||||
|
|
||||||
List<Platform> platforms = platformSlugs.stream()
|
List<Platform> platforms = platformSlugs.stream()
|
||||||
.map(slug -> platformRepository.findBySlug(slug).
|
.map(slug -> {
|
||||||
orElseGet(() -> igdbWrapper.getPlatformBySlug(slug)))
|
Optional<Platform> p = platformRepository.findBySlug(slug);
|
||||||
.filter(Objects::nonNull)
|
return p.isPresent() ? p : igdbWrapper.getPlatformBySlug(slug);
|
||||||
|
})
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get)
|
||||||
.collect(toList());
|
.collect(toList());
|
||||||
|
|
||||||
library.setPlatforms(platforms);
|
library.setPlatforms(platforms);
|
||||||
@@ -249,4 +250,8 @@ public class LibraryService {
|
|||||||
|
|
||||||
return library;
|
return library;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Predicate<Path> allPathsOrSpecific(String path) {
|
||||||
|
return p -> isBlank(path) || p.equals(filesystemService.getPath(path));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,30 @@ import org.springframework.stereotype.Service;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class FilenameUtil {
|
public class FilenameUtil {
|
||||||
|
|
||||||
private static List<String> possibleGameFileExtensions;
|
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+)");
|
||||||
|
private static final Pattern trailingNoisePattern = Pattern.compile("( |\\(\\)|\\[\\]|[-_.])+$");
|
||||||
|
private static final Pattern headingNoisePattern = Pattern.compile("^( |\\(\\)|\\[\\]|[-_.])+");
|
||||||
|
|
||||||
@Value("${gameyfin.file-extensions}")
|
@Value("${gameyfin.file-extensions}")
|
||||||
public void setPossibleGameFileExtensions(List<String> possibleGameFileExtensions) {
|
public void setPossibleGameFileExtensions(List<String> possibleGameFileExtensions) {
|
||||||
FilenameUtil.possibleGameFileExtensions = possibleGameFileExtensions;
|
FilenameUtil.possibleGameFileExtensions = possibleGameFileExtensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Value("${gameyfin.file-suffixes}")
|
||||||
|
public void setPossibleGameFileSuffixes(List<String> possibleGameFileSuffixes) {
|
||||||
|
// Sort in descending length, so for example "windows" gets checked before "win"
|
||||||
|
possibleGameFileSuffixes.sort((s1,s2) -> Integer.compare(s2.length(), s1.length()));
|
||||||
|
FilenameUtil.possibleGameFileSuffixes = possibleGameFileSuffixes;
|
||||||
|
}
|
||||||
|
|
||||||
public static String getFilenameWithoutExtension(Path p) {
|
public static String getFilenameWithoutExtension(Path p) {
|
||||||
|
|
||||||
@@ -34,5 +48,26 @@ public class FilenameUtil {
|
|||||||
public static boolean hasGameArchiveExtension(Path p) {
|
public static boolean hasGameArchiveExtension(Path p) {
|
||||||
return possibleGameFileExtensions.contains(FilenameUtils.getExtension(p.getFileName().toString()));
|
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,7 +1,8 @@
|
|||||||
gameyfin:
|
gameyfin:
|
||||||
db: ""
|
folders:
|
||||||
cache: ""
|
data: ""
|
||||||
file-extensions: iso, zip, rar, 7z, exe
|
file-extensions: iso, zip, rar, 7z, exe
|
||||||
|
file-suffixes: windows, win, english, win32, win64, opengl, stable
|
||||||
igdb:
|
igdb:
|
||||||
api:
|
api:
|
||||||
endpoints:
|
endpoints:
|
||||||
@@ -12,4 +13,4 @@ gameyfin:
|
|||||||
max-concurrent-requests: 2
|
max-concurrent-requests: 2
|
||||||
max-requests-per-second: 4
|
max-requests-per-second: 4
|
||||||
config:
|
config:
|
||||||
preferred-platforms: 6
|
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
|
||||||
|
|||||||
@@ -24,4 +24,7 @@ management:
|
|||||||
health:
|
health:
|
||||||
enabled: true
|
enabled: true
|
||||||
endpoints:
|
endpoints:
|
||||||
enabled-by-default: false
|
enabled-by-default: false
|
||||||
|
|
||||||
|
gameyfin:
|
||||||
|
internal-folder: .gameyfin
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.google.protobuf.Timestamp;
|
|||||||
import com.igdb.proto.Igdb;
|
import com.igdb.proto.Igdb;
|
||||||
import de.grimsi.gameyfin.config.WebClientConfig;
|
import de.grimsi.gameyfin.config.WebClientConfig;
|
||||||
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
|
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
|
||||||
|
import de.grimsi.gameyfin.entities.Platform;
|
||||||
import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto;
|
import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto;
|
||||||
import de.grimsi.gameyfin.mapper.GameMapper;
|
import de.grimsi.gameyfin.mapper.GameMapper;
|
||||||
import io.github.resilience4j.bulkhead.Bulkhead;
|
import io.github.resilience4j.bulkhead.Bulkhead;
|
||||||
@@ -32,6 +33,7 @@ import java.util.Optional;
|
|||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import static de.grimsi.gameyfin.igdb.IgdbApiQueryBuilder.equal;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -298,6 +300,67 @@ class IgdbWrapperTest {
|
|||||||
assertThat(r2.getBody().readUtf8()).isEqualTo(r2_expectedQuery);
|
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) {
|
private static Buffer toBuffer(Message input) {
|
||||||
Buffer b = new Buffer();
|
Buffer b = new Buffer();
|
||||||
b.write(input.toByteArray());
|
b.write(input.toByteArray());
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
class CompanyMapperTest extends RandomMapperTest<Igdb.InvolvedCompany, Company> {
|
class CompanyMapperTest extends RandomMapperTest<Igdb.InvolvedCompany, Company> {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
|
||||||
void toCompany() {
|
void toCompany() {
|
||||||
Igdb.InvolvedCompany input = generateRandomInput();
|
Igdb.InvolvedCompany input = generateRandomInput();
|
||||||
|
|
||||||
@@ -25,7 +24,6 @@ class CompanyMapperTest extends RandomMapperTest<Igdb.InvolvedCompany, Company>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
|
||||||
void toCompanies() {
|
void toCompanies() {
|
||||||
List<Igdb.InvolvedCompany> input = List.of(generateRandomInput(), generateRandomInput(), generateRandomInput());
|
List<Igdb.InvolvedCompany> input = List.of(generateRandomInput(), generateRandomInput(), generateRandomInput());
|
||||||
|
|
||||||
@@ -37,18 +35,4 @@ class CompanyMapperTest extends RandomMapperTest<Igdb.InvolvedCompany, Company>
|
|||||||
assertThat(output.get(i).getLogoId()).isEqualTo(input.get(i).getCompany().getLogo().getImageId());
|
assertThat(output.get(i).getLogoId()).isEqualTo(input.get(i).getCompany().getLogo().getImageId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Igdb.InvolvedCompany generateRandomInvolvedCompany() {
|
|
||||||
Igdb.Company c = Igdb.Company.newBuilder()
|
|
||||||
.setSlug(UUID.randomUUID().toString())
|
|
||||||
.setName(UUID.randomUUID().toString())
|
|
||||||
.setLogo(Igdb.CompanyLogo.newBuilder()
|
|
||||||
.setImageId(UUID.randomUUID().toString())
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return Igdb.InvolvedCompany.newBuilder()
|
|
||||||
.setCompany(c)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
class GenreMapperTest extends RandomMapperTest<Igdb.Genre, Genre> {
|
class GenreMapperTest extends RandomMapperTest<Igdb.Genre, Genre> {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
|
||||||
void toGenre() {
|
void toGenre() {
|
||||||
Igdb.Genre input = generateRandomInput();
|
Igdb.Genre input = generateRandomInput();
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ class GenreMapperTest extends RandomMapperTest<Igdb.Genre, Genre> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
|
||||||
void toGenres() {
|
void toGenres() {
|
||||||
List<Igdb.Genre> input = List.of(generateRandomInput(), generateRandomInput(), generateRandomInput());
|
List<Igdb.Genre> input = List.of(generateRandomInput(), generateRandomInput(), generateRandomInput());
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import java.util.List;
|
|||||||
public class RandomMapperTest<Input extends Message, Output> {
|
public class RandomMapperTest<Input extends Message, Output> {
|
||||||
|
|
||||||
private static final int DEFAULT_COUNT = 5;
|
private static final int DEFAULT_COUNT = 5;
|
||||||
private final EasyRandom easyRandom = new EasyRandom();
|
protected final EasyRandom easyRandom = new EasyRandom();
|
||||||
|
|
||||||
private final Class<Input> inputClass;
|
private final Class<Input> inputClass;
|
||||||
private final Class<Output> outputClass;
|
private final Class<Output> outputClass;
|
||||||
|
|||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package de.grimsi.gameyfin.repositories;
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.entities.UnmappableFile;
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
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))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
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.getStatus()).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.getStatus()).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.getStatus()).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.getStatus()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||||
|
verify(detectedGameRepositoryMock, times(1)).findById(slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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,27 +1,26 @@
|
|||||||
package de.grimsi.gameyfin.util;
|
package de.grimsi.gameyfin.util;
|
||||||
|
|
||||||
import com.google.common.jimfs.Configuration;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import com.google.common.jimfs.Jimfs;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import org.apache.commons.io.FileUtils;
|
import static org.junit.jupiter.api.Named.named;
|
||||||
|
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||||
|
|
||||||
|
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 org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
import java.io.IOException;
|
import com.google.common.jimfs.Configuration;
|
||||||
import java.nio.file.FileSystem;
|
import com.google.common.jimfs.Jimfs;
|
||||||
import java.nio.file.FileSystems;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.junit.jupiter.api.Named.named;
|
|
||||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
|
||||||
|
|
||||||
class FilenameUtilTest {
|
class FilenameUtilTest {
|
||||||
|
|
||||||
@@ -30,10 +29,13 @@ class FilenameUtilTest {
|
|||||||
private static final FileSystem winFS = Jimfs.newFileSystem(Configuration.windows());
|
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> 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
|
@BeforeAll
|
||||||
static void init() {
|
static void init() {
|
||||||
new FilenameUtil().setPossibleGameFileExtensions(gameFileExtensions);
|
FilenameUtil filenameUtil = new FilenameUtil();
|
||||||
|
filenameUtil.setPossibleGameFileExtensions(gameFileExtensions);
|
||||||
|
filenameUtil.setPossibleGameFileSuffixes(possibleGameFileSuffixes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
@@ -113,6 +115,46 @@ class FilenameUtilTest {
|
|||||||
|
|
||||||
Files.deleteIfExists(p);
|
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() {
|
private static Stream<Arguments> fileSystems() {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
@@ -121,4 +163,12 @@ class FilenameUtilTest {
|
|||||||
arguments(named("Windows", winFS))
|
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"))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,4 +2,8 @@ gameyfin:
|
|||||||
igdb:
|
igdb:
|
||||||
api:
|
api:
|
||||||
client-id: igdb_client_id
|
client-id: igdb_client_id
|
||||||
client-secret: igdb_client_secret
|
client-secret: igdb_client_secret
|
||||||
|
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:h2:mem:${spring.datasource.db-name}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
mock-maker-inline
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "1.3.0-SNAPSHOT",
|
"version": "1.3.2-SNAPSHOT",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "1.3.0-SNAPSHOT",
|
"version": "1.3.2-SNAPSHOT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^14.0.0",
|
"@angular/animations": "^14.0.0",
|
||||||
"@angular/cdk": "^14.1.0",
|
"@angular/cdk": "^14.1.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "1.3.0-SNAPSHOT",
|
"version": "1.3.2-SNAPSHOT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const appRoutes: Routes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(appRoutes)],
|
imports: [RouterModule.forRoot(appRoutes, { scrollPositionRestoration: 'enabled' })],
|
||||||
exports: [RouterModule]
|
exports: [RouterModule]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -56,11 +56,10 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<mat-expansion-panel [expanded]="offlineCoopFilterEnabled || onlineCoopFilterEnabled || lanSupportFilterEnabled">
|
<mat-expansion-panel [expanded]="filterExpansionState.gamemodes">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="6px">
|
<mat-panel-title fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="6px">
|
||||||
<h3 class="filter-category-title">Gamemodes</h3>
|
<h3 class="filter-category-title">Gamemodes</h3>
|
||||||
<mat-icon matTooltip="Filter may not work correctly, working on a fix" color="warn">error</mat-icon>
|
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
<mat-expansion-panel *ngIf="availableGenres.length > 0" [expanded]="activeGenreFilters.length > 0">
|
<mat-expansion-panel *ngIf="availableGenres.length > 0" [expanded]="filterExpansionState.genres">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<h3 class="filter-category-title">Genres</h3>
|
<h3 class="filter-category-title">Genres</h3>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
@@ -89,7 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
<mat-expansion-panel *ngIf="availableThemes.length > 0" [expanded]="activeThemeFilters.length > 0">
|
<mat-expansion-panel *ngIf="availableThemes.length > 0" [expanded]="filterExpansionState.themes">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<h3 class="filter-category-title">Themes</h3>
|
<h3 class="filter-category-title">Themes</h3>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
@@ -102,7 +101,7 @@
|
|||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
<mat-expansion-panel *ngIf="availablePlayerPerspectives.length > 0"
|
<mat-expansion-panel *ngIf="availablePlayerPerspectives.length > 0"
|
||||||
[expanded]="activePlayerPerspectiveFilters.length > 0">
|
[expanded]="filterExpansionState.playerPerspectives">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<h3 class="filter-category-title">Player Perspectives</h3>
|
<h3 class="filter-category-title">Player Perspectives</h3>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
@@ -115,7 +114,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
|
||||||
<mat-expansion-panel *ngIf="availablePlatforms.length > 0" [expanded]="activePlatformFilters.length > 0">
|
<mat-expansion-panel *ngIf="availablePlatforms.length > 0" [expanded]="filterExpansionState.platforms">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<h3 class="filter-category-title">Platforms</h3>
|
<h3 class="filter-category-title">Platforms</h3>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {firstValueFrom, forkJoin, Observable} from "rxjs";
|
|||||||
import {SortDirection} from "@angular/material/sort";
|
import {SortDirection} from "@angular/material/sort";
|
||||||
import {PlayerPerspectiveDto} from "../../models/dtos/PlayerPerspectiveDto";
|
import {PlayerPerspectiveDto} from "../../models/dtos/PlayerPerspectiveDto";
|
||||||
import {PlatformDto} from "../../models/dtos/PlatformDto";
|
import {PlatformDto} from "../../models/dtos/PlatformDto";
|
||||||
import {ActivatedRoute, ActivatedRouteSnapshot, Params, Router} from "@angular/router";
|
import {ActivatedRoute, Params, Router} from "@angular/router";
|
||||||
import {Location} from "@angular/common";
|
import {Location} from "@angular/common";
|
||||||
|
|
||||||
class SortOption {
|
class SortOption {
|
||||||
@@ -54,6 +54,7 @@ export class LibraryOverviewComponent implements AfterContentInit {
|
|||||||
activeGenreFilters: string[] = [];
|
activeGenreFilters: string[] = [];
|
||||||
activePlayerPerspectiveFilters: string[] = [];
|
activePlayerPerspectiveFilters: string[] = [];
|
||||||
activePlatformFilters: string[] = [];
|
activePlatformFilters: string[] = [];
|
||||||
|
filterExpansionState: FilterExpansionState = {};
|
||||||
|
|
||||||
games: DetectedGameDto[] = [];
|
games: DetectedGameDto[] = [];
|
||||||
availableGenres: GenreDto[] = [];
|
availableGenres: GenreDto[] = [];
|
||||||
@@ -102,6 +103,14 @@ export class LibraryOverviewComponent implements AfterContentInit {
|
|||||||
if (this.previousStateParams['playerPerspectives'] !== undefined) this.activePlayerPerspectiveFilters = this.matchSelectedFilters(this.availablePlayerPerspectives, this.previousStateParams['playerPerspectives']);
|
if (this.previousStateParams['playerPerspectives'] !== undefined) this.activePlayerPerspectiveFilters = this.matchSelectedFilters(this.availablePlayerPerspectives, this.previousStateParams['playerPerspectives']);
|
||||||
if (this.previousStateParams['platforms'] !== undefined) this.activePlatformFilters = this.matchSelectedFilters(this.availablePlatforms, this.previousStateParams['platforms']);
|
if (this.previousStateParams['platforms'] !== undefined) this.activePlatformFilters = this.matchSelectedFilters(this.availablePlatforms, this.previousStateParams['platforms']);
|
||||||
|
|
||||||
|
this.filterExpansionState = {
|
||||||
|
gamemodes: this.getActiveGameModesFilters().length > 0,
|
||||||
|
genres: this.activeGenreFilters.length > 0,
|
||||||
|
themes: this.activeThemeFilters.length > 0,
|
||||||
|
playerPerspectives: this.activePlayerPerspectiveFilters.length > 0,
|
||||||
|
platforms: this.activePlatformFilters.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
this.refreshLibraryView().then(() => this.loading = false);
|
this.refreshLibraryView().then(() => this.loading = false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -274,3 +283,11 @@ export class LibraryOverviewComponent implements AfterContentInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FilterExpansionState {
|
||||||
|
gamemodes?: boolean;
|
||||||
|
genres?: boolean;
|
||||||
|
themes?: boolean;
|
||||||
|
playerPerspectives?: boolean;
|
||||||
|
platforms?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>2.7.4</version>
|
<version>2.7.5</version>
|
||||||
<relativePath /> <!-- lookup parent from repository -->
|
<relativePath /> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user