From 53e9ac2b62258dc3236d1a9c4e48cbd73f9ff290 Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Wed, 17 Aug 2022 15:00:14 +0200
Subject: [PATCH 01/11] Release 1.1.4
---
backend/pom.xml | 2 +-
frontend/pom.xml | 2 +-
pom.xml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/backend/pom.xml b/backend/pom.xml
index b4e55e4..0bfaaf9 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -7,7 +7,7 @@
gameyfin
de.grimsi
- 1.1.3
+ 1.1.4
gameyfin-backend
diff --git a/frontend/pom.xml b/frontend/pom.xml
index b3961bd..6203568 100644
--- a/frontend/pom.xml
+++ b/frontend/pom.xml
@@ -5,7 +5,7 @@
gameyfin
de.grimsi
- 1.1.3
+ 1.1.4
4.0.0
diff --git a/pom.xml b/pom.xml
index 5170e7e..5d265dc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
de.grimsi
gameyfin
- 1.1.3
+ 1.1.4
gameyfin
gameyfin
From 881b4a3d1d2785f6e1a3c7ad38ee0f7975487f43 Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Wed, 17 Aug 2022 15:03:23 +0200
Subject: [PATCH 02/11] Fix "gameyfin.cache" and "gameyfin.db" properties are
ignored
---
.../gameyfin/config/FilesystemConfig.java | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java b/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java
index 5b9cf98..41078ba 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java
@@ -9,6 +9,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.*;
import org.springframework.util.PropertyPlaceholderHelper;
+import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.util.Arrays;
@@ -21,14 +22,27 @@ public class FilesystemConfig {
@Value("#{'${gameyfin.sources}'.split(',')[0]}")
private String firstLibraryPath;
+ @Value("${gameyfin.db}")
+ private String dbPath;
+
+ @Value("${gameyfin.cache}")
+ private String cachePath;
+
@Autowired
Environment env;
@Autowired
public void setConfigurableEnvironment(ConfigurableEnvironment env) {
Properties props = new Properties();
- props.setProperty("gameyfin.db", "%s/.gameyfin/db".formatted(firstLibraryPath));
- props.setProperty("gameyfin.cache", "%s/.gameyfin/cache".formatted(firstLibraryPath));
+
+ if(!StringUtils.hasText(dbPath)) {
+ props.setProperty("gameyfin.db", "%s/.gameyfin/db".formatted(firstLibraryPath));
+ }
+
+ if(StringUtils.hasText(cachePath)) {
+ props.setProperty("gameyfin.cache", "%s/.gameyfin/cache".formatted(firstLibraryPath));
+ }
+
env.getPropertySources().addFirst(new PropertiesPropertySource("gameyfinFilesystemProperties", props));
}
From c097c6fdc02fe02e51686bc6c8c6318be288f494 Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Wed, 17 Aug 2022 15:13:14 +0200
Subject: [PATCH 03/11] Fix library overview layout gaps when screen is not
filled with covers
---
.../library-overview.component.html | 22 +++++++++++++------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/frontend/src/app/components/library-overview/library-overview.component.html b/frontend/src/app/components/library-overview/library-overview.component.html
index 7501b07..e4bbf04 100644
--- a/frontend/src/app/components/library-overview/library-overview.component.html
+++ b/frontend/src/app/components/library-overview/library-overview.component.html
@@ -11,14 +11,16 @@
-
+
videogame_asset_off
Your game library is empty!
-
+
-
0" [expanded]="activePlayerPerspectiveFilters.length > 0">
+ 0"
+ [expanded]="activePlayerPerspectiveFilters.length > 0">
Player Perspectives
- {{playerPerspective.name}}
@@ -114,12 +118,16 @@
-
From 1fbfeb1c7e59e5b9e9ce1d5cfeca479578186d58 Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Thu, 18 Aug 2022 20:26:09 +0200
Subject: [PATCH 04/11] Refactored file-system code Fixed logging when aborting
download of single files
---
backend/pom.xml | 2 +-
.../gameyfin/config/FilesystemConfig.java | 2 +-
.../de/grimsi/gameyfin/igdb/IgdbWrapper.java | 12 +-
.../de/grimsi/gameyfin/mapper/GameMapper.java | 38 ++--
.../grimsi/gameyfin/rest/GamesController.java | 2 +-
.../grimsi/gameyfin/rest/ImageController.java | 2 +-
.../gameyfin/rest/LibraryController.java | 9 +-
.../rest/LibraryManagementController.java | 12 +-
.../gameyfin/service/DownloadService.java | 190 ++----------------
.../gameyfin/service/FilesystemService.java | 84 ++++++++
.../grimsi/gameyfin/service/GameService.java | 23 +--
.../grimsi/gameyfin/service/ImageService.java | 142 +++++++++++++
.../gameyfin/service/LibraryService.java | 4 +-
.../de/grimsi/gameyfin/util/FilenameUtil.java | 3 +-
backend/src/main/resources/application.yml | 4 +-
.../src/main/resources/config/gameyfin.yml | 4 +-
frontend/pom.xml | 2 +-
pom.xml | 2 +-
18 files changed, 307 insertions(+), 230 deletions(-)
create mode 100644 backend/src/main/java/de/grimsi/gameyfin/service/FilesystemService.java
create mode 100644 backend/src/main/java/de/grimsi/gameyfin/service/ImageService.java
diff --git a/backend/pom.xml b/backend/pom.xml
index 0bfaaf9..66bbe98 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -7,7 +7,7 @@
gameyfin
de.grimsi
- 1.1.4
+ 1.1.4-RC1
gameyfin-backend
diff --git a/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java b/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java
index 41078ba..10df1fc 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/config/FilesystemConfig.java
@@ -39,7 +39,7 @@ public class FilesystemConfig {
props.setProperty("gameyfin.db", "%s/.gameyfin/db".formatted(firstLibraryPath));
}
- if(StringUtils.hasText(cachePath)) {
+ if(!StringUtils.hasText(cachePath)) {
props.setProperty("gameyfin.cache", "%s/.gameyfin/cache".formatted(firstLibraryPath));
}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java b/backend/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java
index 26fe5ad..c4bc180 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java
@@ -7,6 +7,7 @@ import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto;
import de.grimsi.gameyfin.mapper.GameMapper;
import io.github.resilience4j.reactor.bulkhead.operator.BulkheadOperator;
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
+import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -24,6 +25,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
+@RequiredArgsConstructor
@Service
public class IgdbWrapper {
@Value("${gameyfin.igdb.api.client-id}")
@@ -35,11 +37,9 @@ public class IgdbWrapper {
@Value("${gameyfin.igdb.config.preferred-platforms:6}")
private String preferredPlatforms;
- @Autowired
- private WebClient.Builder webclientBuilder;
-
- @Autowired
- private WebClientConfig webClientConfig;
+ private final WebClient.Builder webclientBuilder;
+ private final WebClientConfig webClientConfig;
+ private final GameMapper gameMapper;
private WebClient twitchApiClient;
@@ -106,7 +106,7 @@ public class IgdbWrapper {
if(gameResult == null) return Collections.emptyList();
- return gameResult.getGamesList().stream().map(GameMapper::toAutocompleteSuggestionDto).toList();
+ return gameResult.getGamesList().stream().map(gameMapper::toAutocompleteSuggestionDto).toList();
}
public Optional searchForGameByTitle(String searchTerm) {
diff --git a/backend/src/main/java/de/grimsi/gameyfin/mapper/GameMapper.java b/backend/src/main/java/de/grimsi/gameyfin/mapper/GameMapper.java
index fd63e13..bc4ddb3 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/mapper/GameMapper.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/mapper/GameMapper.java
@@ -4,6 +4,7 @@ import com.igdb.proto.Igdb;
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
import de.grimsi.gameyfin.dto.GameOverviewDto;
import de.grimsi.gameyfin.entities.DetectedGame;
+import de.grimsi.gameyfin.service.FilesystemService;
import de.grimsi.gameyfin.service.LibraryService;
import de.grimsi.gameyfin.util.ProtobufUtil;
import lombok.RequiredArgsConstructor;
@@ -24,9 +25,13 @@ import java.util.List;
import java.util.stream.Stream;
@Slf4j
+@RequiredArgsConstructor
+@Component
public class GameMapper {
- public static DetectedGame toDetectedGame(Igdb.Game g, Path path) {
+ private final FilesystemService filesystemService;
+
+ public DetectedGame toDetectedGame(Igdb.Game g, Path path) {
List multiplayerModes = g.getMultiplayerModesList();
List screenshotIds = g.getScreenshotsList().stream().map(Igdb.Screenshot::getImageId).toList();
List videoIds = g.getVideosList().stream().map(Igdb.GameVideo::getVideoId).toList();
@@ -58,7 +63,7 @@ public class GameMapper {
.build();
}
- public static GameOverviewDto toGameOverviewDto(DetectedGame game) {
+ public GameOverviewDto toGameOverviewDto(DetectedGame game) {
return GameOverviewDto.builder()
.slug(game.getSlug())
.title(game.getTitle())
@@ -66,7 +71,7 @@ public class GameMapper {
.build();
}
- public static AutocompleteSuggestionDto toAutocompleteSuggestionDto(Igdb.Game game) {
+ public AutocompleteSuggestionDto toAutocompleteSuggestionDto(Igdb.Game game) {
return AutocompleteSuggestionDto.builder()
.slug(game.getSlug())
.title(game.getName())
@@ -74,7 +79,7 @@ public class GameMapper {
.build();
}
- private static String getCoverId(Igdb.Game g) {
+ private String getCoverId(Igdb.Game g) {
String coverId = g.getCover().getImageId();
if(StringUtils.hasText(coverId)) return coverId;
@@ -82,23 +87,23 @@ public class GameMapper {
return "nocover";
}
- private static boolean hasOfflineCoop(List modes) {
+ private boolean hasOfflineCoop(List modes) {
return modes.stream().anyMatch(Igdb.MultiplayerMode::getOfflinecoop);
}
- private static boolean hasLanSupport(List modes) {
+ private boolean hasLanSupport(List modes) {
return modes.stream().anyMatch(Igdb.MultiplayerMode::getLancoop);
}
- private static boolean hasOnlineCoop(List modes) {
+ private boolean hasOnlineCoop(List modes) {
return modes.stream().anyMatch(Igdb.MultiplayerMode::getOnlinecoop);
}
- private static int getMaxPlayers(List modes) {
+ private int getMaxPlayers(List modes) {
return modes.stream().mapToInt(Igdb.MultiplayerMode::getOnlinecoopmax).max().orElse(0);
}
- private static long calculateDiskSize(Igdb.Game g, Path path) {
+ private long calculateDiskSize(Igdb.Game g, Path path) {
StopWatch stopWatch = new StopWatch();
log.info("Calculating disk size for game '{}'...", g.getName());
@@ -106,16 +111,11 @@ public class GameMapper {
long fileSize;
- if(Files.isDirectory(path)) {
- // Some benchmarks I did have shown that trying to parallelize this process makes it slower instead of faster
- fileSize = FileUtils.sizeOfDirectory(path.toFile());
- } else {
- try{
- fileSize = Files.size(path);
- } catch (IOException e) {
- log.error("Error while calculating size of file '{}'.", path);
- fileSize = -1L;
- }
+ try {
+ fileSize = filesystemService.getSizeOnDisk(path);
+ } catch(IOException e) {
+ log.error("Error while calculating disk size for game '{}'", g.getName());
+ fileSize = -1L;
}
stopWatch.stop();
diff --git a/backend/src/main/java/de/grimsi/gameyfin/rest/GamesController.java b/backend/src/main/java/de/grimsi/gameyfin/rest/GamesController.java
index fdcc7ba..156988a 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/rest/GamesController.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/rest/GamesController.java
@@ -54,7 +54,7 @@ public class GamesController {
return ResponseEntity
.ok()
.header("Content-Disposition", "attachment; filename=\"%s\"".formatted(downloadFileName))
- .body(out -> downloadService.downloadGameFiles(game, out));
+ .body(out -> downloadService.sendGamefilesToClient(game, out));
}
}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/rest/ImageController.java b/backend/src/main/java/de/grimsi/gameyfin/rest/ImageController.java
index 9d2bd79..d4f535a 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/rest/ImageController.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/rest/ImageController.java
@@ -27,6 +27,6 @@ public class ImageController {
public ResponseEntity getCoverImageForGame(@PathVariable String imageId) {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic())
- .body(downloadService.downloadImage(imageId));
+ .body(downloadService.sendImageToClient(imageId));
}
}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java b/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java
index 8e3090b..a0a14b0 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java
@@ -1,6 +1,7 @@
package de.grimsi.gameyfin.rest;
import de.grimsi.gameyfin.service.DownloadService;
+import de.grimsi.gameyfin.service.ImageService;
import de.grimsi.gameyfin.service.LibraryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -25,7 +26,7 @@ import java.util.List;
public class LibraryController {
private final LibraryService libraryService;
- private final DownloadService downloadService;
+ private final ImageService imageService;
@GetMapping(value = "/scan", produces = MediaType.APPLICATION_JSON_VALUE)
public void scanLibrary(@RequestParam(value = "download_images", defaultValue = "true") boolean downloadImages) {
@@ -36,9 +37,9 @@ public class LibraryController {
@GetMapping(value = "/download-images")
public void downloadImages() {
- downloadService.downloadGameCoversFromIgdb();
- downloadService.downloadGameScreenshotsFromIgdb();
- downloadService.downloadCompanyLogosFromIgdb();
+ imageService.downloadGameCoversFromIgdb();
+ imageService.downloadGameScreenshotsFromIgdb();
+ imageService.downloadCompanyLogosFromIgdb();
log.info("Downloading images completed.");
}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryManagementController.java b/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryManagementController.java
index d5d4ce6..05d9da0 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryManagementController.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryManagementController.java
@@ -8,6 +8,7 @@ import de.grimsi.gameyfin.entities.UnmappableFile;
import de.grimsi.gameyfin.repositories.DetectedGameRepository;
import de.grimsi.gameyfin.service.DownloadService;
import de.grimsi.gameyfin.service.GameService;
+import de.grimsi.gameyfin.service.ImageService;
import de.grimsi.gameyfin.service.LibraryService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
@@ -23,8 +24,7 @@ import java.util.List;
public class LibraryManagementController {
private final GameService gameService;
- private final DownloadService downloadService;
-
+ private final ImageService imageService;
private final LibraryService libraryService;
@DeleteMapping(value = "/delete-game/{slug}", produces = MediaType.APPLICATION_JSON_VALUE)
@@ -45,10 +45,10 @@ public class LibraryManagementController {
@PostMapping(value = "/map-path", produces = MediaType.APPLICATION_JSON_VALUE)
public DetectedGame manuallyMapPathToSlug(@RequestBody PathToSlugDto pathToSlugDto) {
DetectedGame game = gameService.mapPathToGame(pathToSlugDto.getPath(), pathToSlugDto.getSlug());
-
- downloadService.downloadGameCoversFromIgdb();
- downloadService.downloadGameScreenshotsFromIgdb();
- downloadService.downloadCompanyLogosFromIgdb();
+
+ imageService.downloadGameCoversFromIgdb();
+ imageService.downloadGameScreenshotsFromIgdb();
+ imageService.downloadCompanyLogosFromIgdb();
return game;
}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/DownloadService.java b/backend/src/main/java/de/grimsi/gameyfin/service/DownloadService.java
index ecf6dea..563e522 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/service/DownloadService.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/service/DownloadService.java
@@ -1,41 +1,19 @@
package de.grimsi.gameyfin.service;
-import de.grimsi.gameyfin.entities.Company;
import de.grimsi.gameyfin.entities.DetectedGame;
import de.grimsi.gameyfin.exceptions.DownloadAbortedException;
-import de.grimsi.gameyfin.igdb.IgdbApiProperties;
-import de.grimsi.gameyfin.repositories.DetectedGameRepository;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
-import org.springframework.core.io.buffer.DataBuffer;
-import org.springframework.core.io.buffer.DataBufferUtils;
-import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.util.MultiValueMap;
import org.springframework.util.StopWatch;
-import org.springframework.util.StringUtils;
-import org.springframework.web.reactive.function.client.WebClient;
-import org.springframework.web.reactive.function.client.WebClientResponseException;
-import org.springframework.web.server.ResponseStatusException;
-import reactor.core.publisher.Flux;
-import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -47,19 +25,7 @@ import static de.grimsi.gameyfin.util.FilenameUtil.getFilenameWithExtension;
@RequiredArgsConstructor
public class DownloadService {
- @Value("${gameyfin.cache}")
- private String cacheFolderPath;
-
- private final DetectedGameRepository detectedGameRepository;
-
- @Autowired
- private WebClient.Builder webclientBuilder;
- private WebClient igdbImageClient;
-
- @PostConstruct
- public void init() {
- igdbImageClient = webclientBuilder.baseUrl(IgdbApiProperties.IMAGES_BASE_URL).build();
- }
+ private final FilesystemService filesystemService;
public String getDownloadFileName(DetectedGame g) {
Path path = Path.of(g.getPath());
@@ -68,17 +34,12 @@ public class DownloadService {
return getFilenameWithExtension(path) + ".zip";
}
- public Resource downloadImage(String imageId) {
+ public Resource sendImageToClient(String imageId) {
String filename = "%s.png".formatted(imageId);
-
- try {
- return new ByteArrayResource(Files.readAllBytes(Paths.get("%s/%s".formatted(cacheFolderPath, filename))));
- } catch (IOException e) {
- throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find image file %s".formatted(filename));
- }
+ return filesystemService.getFileFromCache(filename);
}
- public void downloadGameFiles(DetectedGame game, OutputStream outputStream) {
+ public void sendGamefilesToClient(DetectedGame game, OutputStream outputStream) {
StopWatch stopWatch = new StopWatch();
@@ -88,18 +49,16 @@ public class DownloadService {
Path path = Path.of(game.getPath());
- if (path.toFile().isDirectory()) {
-
- try {
- downloadFilesAsZip(path, outputStream);
- } catch(DownloadAbortedException e) {
- stopWatch.stop();
- log.info("Download of game {} was aborted by client after {} seconds", game.getTitle(), (int) stopWatch.getTotalTimeSeconds());
- return;
+ try {
+ if (path.toFile().isDirectory()) {
+ sendGamefilesAsZipToClient(path, outputStream);
+ } else {
+ sendGamefileToClient(path, outputStream);
}
-
- } else {
- downloadFile(path, outputStream);
+ } catch (DownloadAbortedException e) {
+ stopWatch.stop();
+ log.info("Download of game {} was aborted by client after {} seconds", game.getTitle(), (int) stopWatch.getTotalTimeSeconds());
+ return;
}
stopWatch.stop();
@@ -107,70 +66,18 @@ public class DownloadService {
log.info("Downloaded game files of {} in {} seconds.", game.getTitle(), (int) stopWatch.getTotalTimeSeconds());
}
-
- public void downloadGameCoversFromIgdb() {
- StopWatch stopWatch = new StopWatch();
-
- log.info("Starting game cover download...");
- stopWatch.start();
-
- MultiValueMap gameToImageIds = new LinkedMultiValueMap<>(
- detectedGameRepository.findAll().stream()
- .collect(Collectors.toMap(DetectedGame::getSlug, g -> Collections.singletonList(g.getCoverId()))));
-
- int downloadCount = downloadImagesIntoCache(gameToImageIds, IgdbApiProperties.COVER_IMAGE_SIZE, "cover", "game");
-
- stopWatch.stop();
-
- log.info("Downloaded {} covers in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
- }
-
- public void downloadGameScreenshotsFromIgdb() {
- StopWatch stopWatch = new StopWatch();
-
- log.info("Starting game screenshot download...");
- stopWatch.start();
-
- MultiValueMap gamesToImageIds = new LinkedMultiValueMap<>(
- detectedGameRepository.findAll().stream()
- .collect(Collectors.toMap(DetectedGame::getSlug, DetectedGame::getScreenshotIds)));
-
- int downloadCount = downloadImagesIntoCache(gamesToImageIds, IgdbApiProperties.SCREENSHOT_IMAGE_SIZE, "screenshot", "game");
-
- stopWatch.stop();
-
- log.info("Downloaded {} screenshots in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
- }
-
- public void downloadCompanyLogosFromIgdb() {
- StopWatch stopWatch = new StopWatch();
-
- log.info("Starting company logo download...");
- stopWatch.start();
-
- Map> companyToLogoIdMap = detectedGameRepository.findAll().stream()
- .flatMap(g -> g.getCompanies().stream())
- .collect(Collectors.toMap(Company::getSlug, c -> Collections.singletonList(c.getLogoId()), (c1, c2) -> c1));
-
- MultiValueMap companiesToLogoIds = new LinkedMultiValueMap<>(companyToLogoIdMap);
-
- int downloadCount = downloadImagesIntoCache(companiesToLogoIds, IgdbApiProperties.LOGO_IMAGE_SIZE, "logo", "company");
-
- stopWatch.stop();
-
- log.info("Downloaded {} company logos in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
- }
-
- private void downloadFile(Path path, OutputStream outputStream) {
+ private void sendGamefileToClient(Path path, OutputStream outputStream) {
try {
Files.copy(path, outputStream);
+ } catch (ClientAbortException e) {
+ // Aborted downloads will be handled gracefully
+ throw new DownloadAbortedException();
} catch (IOException e) {
log.error("Error while downloading file:", e);
- throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Could not load file '%s'.".formatted(path));
}
}
- private void downloadFilesAsZip(Path path, OutputStream outputStream) {
+ private void sendGamefilesAsZipToClient(Path path, OutputStream outputStream) {
ZipOutputStream zos = new ZipOutputStream(outputStream) {{
def.setLevel(Deflater.NO_COMPRESSION);
}};
@@ -195,65 +102,4 @@ public class DownloadService {
log.error("Error while zipping files:", e);
}
}
-
- private int downloadImagesIntoCache(MultiValueMap entityToImageIds, String imageSize, String imageType, String entityType) {
- AtomicInteger downloadCounter = new AtomicInteger();
- Path cacheFolder = Path.of(cacheFolderPath);
-
- try {
- Files.createDirectories(cacheFolder);
- } catch (IOException e) {
- throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Could not create cache folder.");
- }
-
- entityToImageIds.entrySet().parallelStream().forEach(entry ->
- entry.getValue().forEach(imageId -> {
-
- if (!StringUtils.hasText(imageId)) return;
-
- String imgFileName = "%s.png".formatted(imageId);
- String imgUrl = "t_%s/%s".formatted(imageSize, imgFileName);
-
- if (Files.exists(Path.of(cacheFolderPath, imgFileName))) {
-
- Path existingImageFile = Path.of(cacheFolderPath, imgFileName);
-
- try {
- if(Files.size(existingImageFile) == 0L) {
- log.info("File '{}' is corrupt, retrying download...", imgFileName);
- Files.delete(existingImageFile);
- } else {
- log.debug("{} for {} '{}' already downloaded ({}), skipping.",
- imageType.substring(0, 1).toUpperCase() + imageType.substring(1).toLowerCase(),
- entityType,
- entry.getKey(),
- imgFileName);
- return;
- }
- } catch (IOException e) {
- log.error("Error while checking file '{}'.", existingImageFile);
- }
- }
-
- Flux dataBuffer = igdbImageClient.get()
- .uri(imgUrl)
- .retrieve()
- .bodyToFlux(DataBuffer.class);
-
- try {
- DataBufferUtils.write(dataBuffer, cacheFolder.resolve(imgFileName), StandardOpenOption.CREATE)
- .share().block();
- } catch (WebClientResponseException e) {
- if (e.getStatusCode().is4xxClientError()) {
- log.error("Could not download {} for {} '{}' from {}: {}", imageType, entityType, entry.getKey(), IgdbApiProperties.IMAGES_BASE_URL + imgUrl, e.getStatusCode());
- }
- }
-
- downloadCounter.getAndIncrement();
- log.info("Downloaded {} for {} '{}' from {}", imageType, entityType, entry.getKey(), IgdbApiProperties.IMAGES_BASE_URL + imgUrl);
- }));
-
- return downloadCounter.get();
- }
-
}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/FilesystemService.java b/backend/src/main/java/de/grimsi/gameyfin/service/FilesystemService.java
new file mode 100644
index 0000000..25e551a
--- /dev/null
+++ b/backend/src/main/java/de/grimsi/gameyfin/service/FilesystemService.java
@@ -0,0 +1,84 @@
+package de.grimsi.gameyfin.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FileUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.server.ResponseStatusException;
+import reactor.core.publisher.Flux;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+
+@Slf4j
+@Service
+public class FilesystemService {
+
+ @Value("${gameyfin.cache}")
+ private String cacheFolderPath;
+
+ @PostConstruct
+ public void createCacheFolder() throws IOException {
+ Files.createDirectories(Path.of(cacheFolderPath));
+ }
+
+ public void saveFileToCache(Flux dataBuffer, String filename) {
+ DataBufferUtils.write(dataBuffer, Path.of(cacheFolderPath).resolve(filename), StandardOpenOption.CREATE)
+ .share().block();
+ }
+
+ public ByteArrayResource getFileFromCache(String filename) {
+ try {
+ return new ByteArrayResource(Files.readAllBytes(Paths.get("%s/%s".formatted(cacheFolderPath, filename))));
+ } catch (IOException e) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Could not find image file %s".formatted(filename));
+ }
+ }
+
+ public void deleteFileFromCache(String filename) {
+ try {
+ Files.delete(getPathFromFilename(filename));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean isCachedFileCorrupt(String filename) {
+ try {
+ return Files.size(getPathFromFilename(filename)) == 0L;
+ } catch (IOException e) {
+ log.error("Could not determine file size of '{}'", filename);
+ return true;
+ }
+ }
+
+ public boolean doesCachedFileExist(String filename) {
+ return Files.exists(getPathFromFilename(filename));
+ }
+
+ public long getSizeOnDisk(Path path) throws IOException {
+ if (Files.isDirectory(path)) {
+ // Some benchmarks I did have shown that trying to parallelize this process makes it slower instead of faster
+ return FileUtils.sizeOfDirectory(path.toFile());
+ } else {
+ try {
+ return Files.size(path);
+ } catch (IOException e) {
+ log.error("Error while calculating size of file '{}'.", path);
+ throw e;
+ }
+ }
+ }
+
+ private Path getPathFromFilename(String filename) {
+ return Path.of(cacheFolderPath, filename);
+ }
+}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/GameService.java b/backend/src/main/java/de/grimsi/gameyfin/service/GameService.java
index 2e2f338..3fd2a82 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/service/GameService.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/service/GameService.java
@@ -8,7 +8,7 @@ import de.grimsi.gameyfin.igdb.IgdbWrapper;
import de.grimsi.gameyfin.mapper.GameMapper;
import de.grimsi.gameyfin.repositories.DetectedGameRepository;
import de.grimsi.gameyfin.repositories.UnmappableFileRepository;
-import org.springframework.beans.factory.annotation.Autowired;
+import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
@@ -19,17 +19,14 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
+@RequiredArgsConstructor
@Service
public class GameService {
- @Autowired
- private IgdbWrapper igdbWrapper;
-
- @Autowired
- private DetectedGameRepository detectedGameRepository;
-
- @Autowired
- private UnmappableFileRepository unmappableFileRepository;
+ private final IgdbWrapper igdbWrapper;
+ private final GameMapper gameMapper;
+ private final DetectedGameRepository detectedGameRepository;
+ private final UnmappableFileRepository unmappableFileRepository;
public List getAllDetectedGames() {
return detectedGameRepository.findAll();
@@ -48,13 +45,13 @@ public class GameService {
}
public List getGameOverviews() {
- return detectedGameRepository.findAll().stream().map(GameMapper::toGameOverviewDto).toList();
+ return detectedGameRepository.findAll().stream().map(gameMapper::toGameOverviewDto).toList();
}
public void deleteGame(String slug) {
DetectedGame gameToBeDeleted = getDetectedGame(slug);
- // Add the path of the game to be deleted to the unmappable files
+ // Add the path of the game to be deleted to the unmappable files,
// so it doesn't get re-indexed on the next library scan
unmappableFileRepository.save(new UnmappableFile(gameToBeDeleted.getPath()));
@@ -94,7 +91,7 @@ public class GameService {
Igdb.Game igdbGame = igdbWrapper.getGameBySlug(slug)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with slug '%s' does not exist on IGDB.".formatted(slug)));
- DetectedGame game = GameMapper.toDetectedGame(igdbGame, Path.of(unmappableFile.getPath()));
+ DetectedGame game = gameMapper.toDetectedGame(igdbGame, Path.of(unmappableFile.getPath()));
game = detectedGameRepository.save(game);
unmappableFileRepository.delete(unmappableFile);
@@ -106,7 +103,7 @@ public class GameService {
Igdb.Game igdbGame = igdbWrapper.getGameBySlug(slug)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with slug '%s' does not exist on IGDB.".formatted(slug)));
- DetectedGame game = GameMapper.toDetectedGame(igdbGame, Path.of(existingGame.getPath()));
+ DetectedGame game = gameMapper.toDetectedGame(igdbGame, Path.of(existingGame.getPath()));
game = detectedGameRepository.save(game);
detectedGameRepository.deleteById(existingGame.getSlug());
diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/ImageService.java b/backend/src/main/java/de/grimsi/gameyfin/service/ImageService.java
new file mode 100644
index 0000000..292f764
--- /dev/null
+++ b/backend/src/main/java/de/grimsi/gameyfin/service/ImageService.java
@@ -0,0 +1,142 @@
+package de.grimsi.gameyfin.service;
+
+import de.grimsi.gameyfin.entities.Company;
+import de.grimsi.gameyfin.entities.DetectedGame;
+import de.grimsi.gameyfin.igdb.IgdbApiProperties;
+import de.grimsi.gameyfin.repositories.DetectedGameRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.stereotype.Service;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StopWatch;
+import org.springframework.util.StringUtils;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+import reactor.core.publisher.Flux;
+
+import javax.annotation.PostConstruct;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class ImageService {
+
+ @Value("${gameyfin.cache}")
+ private String cacheFolderPath;
+
+ private final FilesystemService filesystemService;
+ private final DetectedGameRepository detectedGameRepository;
+ private final WebClient.Builder webclientBuilder;
+ private WebClient igdbImageClient;
+
+ @PostConstruct
+ public void init() {
+ igdbImageClient = webclientBuilder.baseUrl(IgdbApiProperties.IMAGES_BASE_URL).build();
+ }
+
+ public void downloadGameCoversFromIgdb() {
+ StopWatch stopWatch = new StopWatch();
+
+ log.info("Starting game cover download...");
+ stopWatch.start();
+
+ MultiValueMap gameToImageIds = new LinkedMultiValueMap<>(
+ detectedGameRepository.findAll().stream()
+ .collect(Collectors.toMap(DetectedGame::getSlug, g -> Collections.singletonList(g.getCoverId()))));
+
+ int downloadCount = saveImagesIntoCache(gameToImageIds, IgdbApiProperties.COVER_IMAGE_SIZE, "cover", "game");
+
+ stopWatch.stop();
+
+ log.info("Downloaded {} covers in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
+ }
+
+ public void downloadGameScreenshotsFromIgdb() {
+ StopWatch stopWatch = new StopWatch();
+
+ log.info("Starting game screenshot download...");
+ stopWatch.start();
+
+ MultiValueMap gamesToImageIds = new LinkedMultiValueMap<>(
+ detectedGameRepository.findAll().stream()
+ .collect(Collectors.toMap(DetectedGame::getSlug, DetectedGame::getScreenshotIds)));
+
+ int downloadCount = saveImagesIntoCache(gamesToImageIds, IgdbApiProperties.SCREENSHOT_IMAGE_SIZE, "screenshot", "game");
+
+ stopWatch.stop();
+
+ log.info("Downloaded {} screenshots in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
+ }
+
+ public void downloadCompanyLogosFromIgdb() {
+ StopWatch stopWatch = new StopWatch();
+
+ log.info("Starting company logo download...");
+ stopWatch.start();
+
+ Map> companyToLogoIdMap = detectedGameRepository.findAll().stream()
+ .flatMap(g -> g.getCompanies().stream())
+ .collect(Collectors.toMap(Company::getSlug, c -> Collections.singletonList(c.getLogoId()), (c1, c2) -> c1));
+
+ MultiValueMap companiesToLogoIds = new LinkedMultiValueMap<>(companyToLogoIdMap);
+
+ int downloadCount = saveImagesIntoCache(companiesToLogoIds, IgdbApiProperties.LOGO_IMAGE_SIZE, "logo", "company");
+
+ stopWatch.stop();
+
+ log.info("Downloaded {} company logos in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
+ }
+
+ private int saveImagesIntoCache(MultiValueMap entityToImageIds, String imageSize, String imageType, String entityType) {
+ AtomicInteger downloadCounter = new AtomicInteger();
+
+ entityToImageIds.entrySet().parallelStream().forEach(entry ->
+ entry.getValue().forEach(imageId -> {
+
+ if (!StringUtils.hasText(imageId)) return;
+
+ String imgFileName = "%s.png".formatted(imageId);
+ String imgUrl = "t_%s/%s".formatted(imageSize, imgFileName);
+
+ if (filesystemService.doesCachedFileExist(imgFileName)) {
+ if (filesystemService.isCachedFileCorrupt(imgFileName)) {
+ log.info("File '{}' is corrupt, retrying download...", imgFileName);
+ filesystemService.deleteFileFromCache(imgFileName);
+ } else {
+ log.debug("{} for {} '{}' already downloaded ({}), skipping.",
+ imageType.substring(0, 1).toUpperCase() + imageType.substring(1).toLowerCase(),
+ entityType,
+ entry.getKey(),
+ imgFileName);
+ return;
+ }
+ }
+
+ Flux dataBuffer = igdbImageClient.get()
+ .uri(imgUrl)
+ .retrieve()
+ .bodyToFlux(DataBuffer.class);
+
+ try {
+ filesystemService.saveFileToCache(dataBuffer, imgFileName);
+ } catch (WebClientResponseException e) {
+ if (e.getStatusCode().is4xxClientError()) {
+ log.error("Could not download {} for {} '{}' from {}: {}", imageType, entityType, entry.getKey(), IgdbApiProperties.IMAGES_BASE_URL + imgUrl, e.getStatusCode());
+ }
+ }
+
+ downloadCounter.getAndIncrement();
+ log.info("Downloaded {} for {} '{}' from {}", imageType, entityType, entry.getKey(), IgdbApiProperties.IMAGES_BASE_URL + imgUrl);
+ }));
+
+ return downloadCounter.get();
+ }
+}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java
index 7e5dd67..9930bc3 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java
@@ -32,7 +32,9 @@ public class LibraryService {
@Value("${gameyfin.sources}")
private List libraryFolders;
+
private final IgdbWrapper igdbWrapper;
+ private final GameMapper gameMapper;
private final DetectedGameRepository detectedGameRepository;
private final UnmappableFileRepository unmappableFileRepository;
@@ -105,7 +107,7 @@ public class LibraryService {
.filter(Optional::isPresent)
.map(Optional::get)
.peek(e -> log.info("Mapped file '{}' to game '{}' (slug: {})", e.getKey(), e.getValue().getName(), e.getValue().getSlug()))
- .map(e -> GameMapper.toDetectedGame(e.getValue(), e.getKey()))
+ .map(e -> gameMapper.toDetectedGame(e.getValue(), e.getKey()))
.collect(Collectors.toList());
List duplicateGames = getDuplicates(newDetectedGames);
diff --git a/backend/src/main/java/de/grimsi/gameyfin/util/FilenameUtil.java b/backend/src/main/java/de/grimsi/gameyfin/util/FilenameUtil.java
index 2c0b8b9..e8ec6a1 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/util/FilenameUtil.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/util/FilenameUtil.java
@@ -4,6 +4,7 @@ import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@@ -21,7 +22,7 @@ public class FilenameUtil {
// If the path points to a folder, return the folder name
// Folders like "Counter Strike 1.6" would otherwise be returned as "Counter Strike 1"
- if(p.toFile().isDirectory()) return FilenameUtils.getName(p.toString());
+ if(Files.isDirectory(p)) return FilenameUtils.getName(p.toString());
return FilenameUtils.getBaseName(p.toString());
}
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index bf05140..708facf 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -1,3 +1,5 @@
# General
logging.level:
- root: info
\ No newline at end of file
+ root: info
+ # Hides an error log on the first aborted download
+ org.apache.catalina.core.ContainerBase: off
diff --git a/backend/src/main/resources/config/gameyfin.yml b/backend/src/main/resources/config/gameyfin.yml
index f70079e..1d7eef8 100644
--- a/backend/src/main/resources/config/gameyfin.yml
+++ b/backend/src/main/resources/config/gameyfin.yml
@@ -1,4 +1,6 @@
gameyfin:
+ db: ""
+ cache: ""
file-extensions: iso, zip, rar, 7z, exe
igdb:
api:
@@ -7,4 +9,4 @@ gameyfin:
max-concurrent-requests: 2
max-requests-per-second: 4
config:
- preferred-platforms: 6
\ No newline at end of file
+ preferred-platforms: 6
diff --git a/frontend/pom.xml b/frontend/pom.xml
index 6203568..01da9ac 100644
--- a/frontend/pom.xml
+++ b/frontend/pom.xml
@@ -5,7 +5,7 @@
gameyfin
de.grimsi
- 1.1.4
+ 1.1.4-RC1
4.0.0
diff --git a/pom.xml b/pom.xml
index 5d265dc..1959753 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
de.grimsi
gameyfin
- 1.1.4
+ 1.1.4-RC1
gameyfin
gameyfin
From 1a7016d2abbfc9945420d86236b16b9af175ecc3 Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Thu, 18 Aug 2022 22:17:13 +0200
Subject: [PATCH 05/11] Changed hover animation for game-covers
---
.../components/game-cover/game-cover.component.html | 2 +-
.../components/game-cover/game-cover.component.scss | 11 +++++++----
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/frontend/src/app/components/game-cover/game-cover.component.html b/frontend/src/app/components/game-cover/game-cover.component.html
index 6031834..37cf2f8 100644
--- a/frontend/src/app/components/game-cover/game-cover.component.html
+++ b/frontend/src/app/components/game-cover/game-cover.component.html
@@ -1,5 +1,5 @@
-
+
{{game.title}}
diff --git a/frontend/src/app/components/game-cover/game-cover.component.scss b/frontend/src/app/components/game-cover/game-cover.component.scss
index c120af0..f7831d0 100644
--- a/frontend/src/app/components/game-cover/game-cover.component.scss
+++ b/frontend/src/app/components/game-cover/game-cover.component.scss
@@ -6,12 +6,15 @@
width: 264px;
background-repeat: no-repeat;
background-position: center bottom;
-
- @include mat.elevation-transition();
@include mat.elevation(4);
+}
- &:hover {
- @include mat.elevation(24);
+.enlarge {
+ transition: transform 280ms ease-out;
+
+ &:hover,
+ &:focus {
+ transform: scale(1.05);
}
}
From f6523972574b1ab362f4b77343caf5812f904b2c Mon Sep 17 00:00:00 2001
From: Simon Grimme <9295182+grimsi@users.noreply.github.com>
Date: Fri, 19 Aug 2022 01:21:49 +0200
Subject: [PATCH 06/11] Added nicer display for ratings in the detail view
---
frontend/src/app/app.module.ts | 6 +++-
.../game-detail-view.component.html | 16 ++++++---
.../game-detail-view.component.ts | 36 ++++++++++---------
.../progress-bar-color.directive.spec.ts | 8 +++++
.../progress-bar-color.directive.ts | 34 ++++++++++++++++++
5 files changed, 78 insertions(+), 22 deletions(-)
create mode 100644 frontend/src/app/directives/progress-bar-color.directive.spec.ts
create mode 100644 frontend/src/app/directives/progress-bar-color.directive.ts
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index bc2d9d4..266abab 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -51,6 +51,8 @@ import { NgModelChangeDebouncedDirective } from './directives/ng-model-change-de
import { FooterComponent } from './components/footer/footer.component';
import {MatExpansionModule} from "@angular/material/expansion";
import {MatSelectModule} from "@angular/material/select";
+import {MatProgressBarModule} from "@angular/material/progress-bar";
+import { ProgressBarColorDirective } from './directives/progress-bar-color.directive';
@NgModule({
declarations: [
@@ -69,6 +71,7 @@ import {MatSelectModule} from "@angular/material/select";
MappedGamesTableComponent,
UnmappedFilesTableComponent,
NgModelChangeDebouncedDirective,
+ ProgressBarColorDirective,
FooterComponent
],
imports: [
@@ -108,7 +111,8 @@ import {MatSelectModule} from "@angular/material/select";
MatListModule,
MatAutocompleteModule,
MatExpansionModule,
- MatSelectModule
+ MatSelectModule,
+ MatProgressBarModule,
],
providers: [
{
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.html b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
index b35200e..3222aa0 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.html
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
@@ -6,11 +6,20 @@
-
+
{{game.title}}
-
Rating: {{game.totalRating}}/100
Release: {{game.releaseDate | date: 'longDate'}}
-
{{game.summary}}
+
{{game.summary}}
+
+
0">
+
Critics Rating ({{game.criticsRating}}/100)
+
+
+
+
0">
+
User Rating ({{game.userRating}}/100)
+
+
@@ -57,7 +66,6 @@
-
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.ts b/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
index c7a92c8..b499f52 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
@@ -1,4 +1,4 @@
-import {Component, OnInit} from '@angular/core';
+import {Component} from '@angular/core';
import {ActivatedRoute, Params, Router} from "@angular/router";
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {GamesService} from "../../services/games.service";
@@ -8,30 +8,25 @@ import {GamesService} from "../../services/games.service";
templateUrl: './game-detail-view.component.html',
styleUrls: ['./game-detail-view.component.scss']
})
-export class GameDetailViewComponent implements OnInit {
+export class GameDetailViewComponent {
game!: DetectedGameDto;
constructor(private route: ActivatedRoute,
private router: Router,
private gamesService: GamesService) {
- this.route.params.subscribe( params => {
- this.gamesService.getGame(params['slug']).subscribe({
- next: game => this.game = game,
- error: error => {
- if(error.status === 404) {
- this.router.navigate(['/library']);
- } else {
- console.error(error);
- }
- }
- });
+ this.gamesService.getGame(this.route.snapshot.params['slug']).subscribe({
+ next: game => this.game = game,
+ error: error => {
+ if (error.status === 404) {
+ this.router.navigate(['/library']);
+ } else {
+ console.error(error);
+ }
+ }
});
}
- ngOnInit(): void {
- }
-
public downloadGame(): void {
this.gamesService.downloadGame(this.game.slug);
}
@@ -46,7 +41,7 @@ export class GameDetailViewComponent implements OnInit {
const units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const dp = 1;
let u = -1;
- const r = 10**dp;
+ const r = 10 ** dp;
do {
bytes /= thresh;
@@ -62,4 +57,11 @@ export class GameDetailViewComponent implements OnInit {
this.router.navigate(['/library'], {queryParams: params});
}
+ mapRatingToColor(rating: number): string {
+ if(rating >= 75) return '#388e3c';
+ if(rating >= 50) return '#fbc02d';
+ if(rating >= 25) return '#f57c00';
+ return '#d32f2f';
+ }
+
}
diff --git a/frontend/src/app/directives/progress-bar-color.directive.spec.ts b/frontend/src/app/directives/progress-bar-color.directive.spec.ts
new file mode 100644
index 0000000..8410bb0
--- /dev/null
+++ b/frontend/src/app/directives/progress-bar-color.directive.spec.ts
@@ -0,0 +1,8 @@
+import { ProgressBarColorDirective } from './progress-bar-color.directive';
+
+describe('ProgressBarColorDirective', () => {
+ it('should create an instance', () => {
+ const directive = new ProgressBarColorDirective();
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/directives/progress-bar-color.directive.ts b/frontend/src/app/directives/progress-bar-color.directive.ts
new file mode 100644
index 0000000..474082c
--- /dev/null
+++ b/frontend/src/app/directives/progress-bar-color.directive.ts
@@ -0,0 +1,34 @@
+import { Directive, Input, OnChanges, SimpleChanges, ElementRef } from '@angular/core';
+
+@Directive({
+ selector: '[progressBarColor]'
+})
+export class ProgressBarColorDirective implements OnChanges{
+ static counter = 0;
+
+ @Input() progressBarColor!: string;
+ styleEl:HTMLStyleElement = document.createElement('style');
+
+ //generate unique attribule which we will use to minimise the scope of our dynamic style
+ uniqueAttr = `app-progress-bar-color-${ProgressBarColorDirective.counter++}`;
+
+ constructor(private el: ElementRef) {
+ const nativeEl: HTMLElement = this.el.nativeElement;
+ nativeEl.setAttribute(this.uniqueAttr,'');
+ nativeEl.appendChild(this.styleEl);
+ }
+
+ ngOnChanges(changes: SimpleChanges): void{
+ this.updateColor();
+ }
+
+ updateColor(): void{
+ // update dynamic style with the uniqueAttr
+ this.styleEl.innerText = `
+ [${this.uniqueAttr}] .mat-progress-bar-fill::after {
+ background-color: ${this.progressBarColor};
+ }
+ `;
+ }
+
+}
From 5de215a11a22434d0a6b6d60d9ad63e8353ee073 Mon Sep 17 00:00:00 2001
From: Simon Grimme <9295182+grimsi@users.noreply.github.com>
Date: Fri, 19 Aug 2022 01:47:15 +0200
Subject: [PATCH 07/11] Added company logos to detail view
---
.../game-detail-view.component.html | 48 ++++++++++++++-----
1 file changed, 35 insertions(+), 13 deletions(-)
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.html b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
index 3222aa0..61f71ce 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.html
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
@@ -1,4 +1,5 @@
-
+
@@ -9,16 +10,18 @@
{{game.title}}
Release: {{game.releaseDate | date: 'longDate'}}
+
+
Description
{{game.summary}}
-
0">
-
Critics Rating ({{game.criticsRating}}/100)
-
-
-
-
0">
-
User Rating ({{game.userRating}}/100)
-
+
0">
+
Developed by
+
+
+
![]()
0" style="height: 52px;"
+ src="v1/images/{{company.logoId}}" alt="{{company.name}}">
+
+
@@ -37,17 +40,34 @@
0">
Genres
- {{genre.name}}
+ {{genre.name}}
0">
Themes
- {{theme.name}}
+ {{theme.name}}
+
+
+
0">
+
Critics Rating ({{game.criticsRating}}/100)
+
+
+
+
+
0">
+
User Rating ({{game.userRating}}/100)
+
+
+
@@ -56,14 +76,16 @@
From a37a05551c5e531b9916d796422c56f970251ef0 Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Fri, 19 Aug 2022 12:35:32 +0200
Subject: [PATCH 08/11] Made Screenshot and Video grid responsive in game
detail view
---
.../game-detail-view.component.html | 59 +++++++++++--------
.../game-detail-view.component.ts | 32 ++++++++--
2 files changed, 60 insertions(+), 31 deletions(-)
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.html b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
index 61f71ce..27f5437 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.html
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
@@ -1,29 +1,33 @@
-
+
-
-

-
+
+
+

+
-
-
{{game.title}}
-
Release: {{game.releaseDate | date: 'longDate'}}
+
+
+
{{game.title}}
+
{{game.releaseDate | date: 'yyyy'}}
-
Description
-
{{game.summary}}
+
Description
+
{{game.summary}}
+
-
0">
-
Developed by
-
-
-
![]()
0" style="height: 52px;"
- src="v1/images/{{company.logoId}}" alt="{{company.name}}">
+
0">
+
Developed by
+
+
+
![]()
0" style="height: 52px;"
+ src="v1/images/{{company.logoId}}" alt="{{company.name}}">
+
-
+
@@ -71,23 +75,26 @@
-
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.ts b/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
index b499f52..b08c91b 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
@@ -1,7 +1,8 @@
-import {Component} from '@angular/core';
+import {Component, HostListener} from '@angular/core';
import {ActivatedRoute, Params, Router} from "@angular/router";
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {GamesService} from "../../services/games.service";
+import {MediaObserver} from "@angular/flex-layout";
@Component({
selector: 'app-game-detail-view',
@@ -12,9 +13,12 @@ export class GameDetailViewComponent {
game!: DetectedGameDto;
+ gridColumnCount: number;
+
constructor(private route: ActivatedRoute,
private router: Router,
- private gamesService: GamesService) {
+ private gamesService: GamesService,
+ private mediaObserver: MediaObserver) {
this.gamesService.getGame(this.route.snapshot.params['slug']).subscribe({
next: game => this.game = game,
error: error => {
@@ -25,6 +29,13 @@ export class GameDetailViewComponent {
}
}
});
+
+ this.gridColumnCount = this.calculateColumnCount();
+ }
+
+ @HostListener('window:resize', ['$event'])
+ onResize() {
+ this.gridColumnCount = this.calculateColumnCount();
}
public downloadGame(): void {
@@ -58,10 +69,21 @@ export class GameDetailViewComponent {
}
mapRatingToColor(rating: number): string {
- if(rating >= 75) return '#388e3c';
- if(rating >= 50) return '#fbc02d';
- if(rating >= 25) return '#f57c00';
+ if (rating >= 75) return '#388e3c';
+ if (rating >= 50) return '#fbc02d';
+ if (rating >= 25) return '#f57c00';
return '#d32f2f';
}
+ private calculateColumnCount(): number {
+ const elementWidth: number = 555;
+ const containerWidth: number | undefined = document.getElementById('game-media')?.offsetWidth;
+ const defaultColumnCount = 3;
+
+ if (containerWidth === undefined) return defaultColumnCount
+ if (containerWidth < elementWidth) return 1;
+
+ return Math.floor(containerWidth / elementWidth);
+ }
+
}
From 963b667a4a9923998319d7a213a46c7170be7ff5 Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Fri, 19 Aug 2022 13:01:11 +0200
Subject: [PATCH 09/11] Small fixes
---
frontend/angular.json | 4 ++--
.../game-detail-view/game-detail-view.component.html | 1 +
.../components/game-detail-view/game-detail-view.component.ts | 4 +---
3 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/frontend/angular.json b/frontend/angular.json
index ab8b6da..a0a58b4 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -37,8 +37,8 @@
"budgets": [
{
"type": "initial",
- "maximumWarning": "500kb",
- "maximumError": "1mb"
+ "maximumWarning": "1mb",
+ "maximumError": "2mb"
},
{
"type": "anyComponentStyle",
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.html b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
index 27f5437..d2f6ae7 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.html
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
@@ -23,6 +23,7 @@
![]()
0" style="height: 52px;"
src="v1/images/{{company.logoId}}" alt="{{company.name}}">
+
0">{{company.name}}
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.ts b/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
index b08c91b..8366c89 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
@@ -2,7 +2,6 @@ import {Component, HostListener} from '@angular/core';
import {ActivatedRoute, Params, Router} from "@angular/router";
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {GamesService} from "../../services/games.service";
-import {MediaObserver} from "@angular/flex-layout";
@Component({
selector: 'app-game-detail-view',
@@ -17,8 +16,7 @@ export class GameDetailViewComponent {
constructor(private route: ActivatedRoute,
private router: Router,
- private gamesService: GamesService,
- private mediaObserver: MediaObserver) {
+ private gamesService: GamesService) {
this.gamesService.getGame(this.route.snapshot.params['slug']).subscribe({
next: game => this.game = game,
error: error => {
From 3325f066bdbc4d60e3d97b918916538945dda45b Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Fri, 19 Aug 2022 13:02:04 +0200
Subject: [PATCH 10/11] Small fix
---
.../components/game-detail-view/game-detail-view.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.html b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
index d2f6ae7..e564261 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.html
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
@@ -87,7 +87,7 @@
-
0">
+
0">
Videos
From 3877b5defd63087f30ebcffe8290748969e0931b Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Tue, 23 Aug 2022 13:17:56 +0200
Subject: [PATCH 11/11] Added detailed library scan result Small layout fixes
in game detail view
---
backend/pom.xml | 2 +-
.../gameyfin/dto/ImageDownloadResultDto.java | 10 +++++
.../gameyfin/dto/LibraryScanResult.java | 13 ++++++
.../gameyfin/dto/LibraryScanResultDto.java | 21 +++++++++
.../gameyfin/rest/LibraryController.java | 44 ++++++++++++++++---
.../grimsi/gameyfin/service/ImageService.java | 9 ++--
.../gameyfin/service/LibraryService.java | 10 ++++-
frontend/pom.xml | 2 +-
frontend/src/app/api/LibraryApi.ts | 9 ++--
.../game-detail-view.component.html | 8 ++--
.../game-detail-view.component.ts | 10 ++++-
.../app/components/header/header.component.ts | 25 ++++++++---
.../app/models/dtos/ImageDownloadResultDto.ts | 5 +++
.../app/models/dtos/LibraryScanResultDto.ts | 10 +++++
frontend/src/app/services/library.service.ts | 12 ++---
pom.xml | 2 +-
16 files changed, 159 insertions(+), 33 deletions(-)
create mode 100644 backend/src/main/java/de/grimsi/gameyfin/dto/ImageDownloadResultDto.java
create mode 100644 backend/src/main/java/de/grimsi/gameyfin/dto/LibraryScanResult.java
create mode 100644 backend/src/main/java/de/grimsi/gameyfin/dto/LibraryScanResultDto.java
create mode 100644 frontend/src/app/models/dtos/ImageDownloadResultDto.ts
create mode 100644 frontend/src/app/models/dtos/LibraryScanResultDto.ts
diff --git a/backend/pom.xml b/backend/pom.xml
index 66bbe98..1fc2007 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -7,7 +7,7 @@
gameyfin
de.grimsi
- 1.1.4-RC1
+ 1.2.0
gameyfin-backend
diff --git a/backend/src/main/java/de/grimsi/gameyfin/dto/ImageDownloadResultDto.java b/backend/src/main/java/de/grimsi/gameyfin/dto/ImageDownloadResultDto.java
new file mode 100644
index 0000000..7e0d103
--- /dev/null
+++ b/backend/src/main/java/de/grimsi/gameyfin/dto/ImageDownloadResultDto.java
@@ -0,0 +1,10 @@
+package de.grimsi.gameyfin.dto;
+
+import lombok.Data;
+
+@Data
+public class ImageDownloadResultDto {
+ private int coverDownloads;
+ private int screenshotDownloads;
+ private int companyLogoDownloads;
+}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/dto/LibraryScanResult.java b/backend/src/main/java/de/grimsi/gameyfin/dto/LibraryScanResult.java
new file mode 100644
index 0000000..dd0fbe8
--- /dev/null
+++ b/backend/src/main/java/de/grimsi/gameyfin/dto/LibraryScanResult.java
@@ -0,0 +1,13 @@
+package de.grimsi.gameyfin.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class LibraryScanResult {
+ private int newGames;
+ private int deletedGames;
+ private int newUnmappableFiles;
+ private int totalGames;
+}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/dto/LibraryScanResultDto.java b/backend/src/main/java/de/grimsi/gameyfin/dto/LibraryScanResultDto.java
new file mode 100644
index 0000000..48d7307
--- /dev/null
+++ b/backend/src/main/java/de/grimsi/gameyfin/dto/LibraryScanResultDto.java
@@ -0,0 +1,21 @@
+package de.grimsi.gameyfin.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class LibraryScanResultDto {
+ private int newGames;
+ private int deletedGames;
+ private int newUnmappableFiles;
+ private int totalGames;
+ private int coverDownloads;
+ private int screenshotDownloads;
+ private int companyLogoDownloads;
+ private int scanDuration;
+}
diff --git a/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java b/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java
index a0a14b0..0643817 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/rest/LibraryController.java
@@ -1,5 +1,8 @@
package de.grimsi.gameyfin.rest;
+import de.grimsi.gameyfin.dto.ImageDownloadResultDto;
+import de.grimsi.gameyfin.dto.LibraryScanResult;
+import de.grimsi.gameyfin.dto.LibraryScanResultDto;
import de.grimsi.gameyfin.service.DownloadService;
import de.grimsi.gameyfin.service.ImageService;
import de.grimsi.gameyfin.service.LibraryService;
@@ -7,6 +10,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -29,19 +33,45 @@ public class LibraryController {
private final ImageService imageService;
@GetMapping(value = "/scan", produces = MediaType.APPLICATION_JSON_VALUE)
- public void scanLibrary(@RequestParam(value = "download_images", defaultValue = "true") boolean downloadImages) {
- libraryService.scanGameLibrary();
+ public LibraryScanResultDto scanLibrary(@RequestParam(value = "download_images", defaultValue = "true") boolean downloadImages) {
+ StopWatch stopWatch = new StopWatch();
+ stopWatch.start();
- if(downloadImages) downloadImages();
+ LibraryScanResultDto lscDto = new LibraryScanResultDto();
+
+ LibraryScanResult lsc = libraryService.scanGameLibrary();
+ lscDto.setNewGames(lsc.getNewGames());
+ lscDto.setDeletedGames(lsc.getDeletedGames());
+ lscDto.setNewUnmappableFiles(lsc.getNewUnmappableFiles());
+ lscDto.setTotalGames(lsc.getTotalGames());
+
+ if(downloadImages) {
+ ImageDownloadResultDto idrDto = downloadImages();
+
+ lscDto.setCoverDownloads(idrDto.getCoverDownloads());
+ lscDto.setScreenshotDownloads(idrDto.getScreenshotDownloads());
+ lscDto.setCompanyLogoDownloads(idrDto.getCompanyLogoDownloads());
+ }
+
+ stopWatch.stop();
+ lscDto.setScanDuration((int) stopWatch.getTotalTimeSeconds());
+
+ log.info("Library scan completed in {} seconds.", (int) stopWatch.getTotalTimeSeconds());
+
+ return lscDto;
}
@GetMapping(value = "/download-images")
- public void downloadImages() {
- imageService.downloadGameCoversFromIgdb();
- imageService.downloadGameScreenshotsFromIgdb();
- imageService.downloadCompanyLogosFromIgdb();
+ public ImageDownloadResultDto downloadImages() {
+ ImageDownloadResultDto idrDto = new ImageDownloadResultDto();
+
+ idrDto.setCoverDownloads(imageService.downloadGameCoversFromIgdb());
+ idrDto.setScreenshotDownloads(imageService.downloadGameScreenshotsFromIgdb());
+ idrDto.setCompanyLogoDownloads(imageService.downloadCompanyLogosFromIgdb());
log.info("Downloading images completed.");
+
+ return idrDto;
}
@GetMapping(value = "/files", produces = MediaType.APPLICATION_JSON_VALUE)
diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/ImageService.java b/backend/src/main/java/de/grimsi/gameyfin/service/ImageService.java
index 292f764..b492125 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/service/ImageService.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/service/ImageService.java
@@ -42,7 +42,7 @@ public class ImageService {
igdbImageClient = webclientBuilder.baseUrl(IgdbApiProperties.IMAGES_BASE_URL).build();
}
- public void downloadGameCoversFromIgdb() {
+ public int downloadGameCoversFromIgdb() {
StopWatch stopWatch = new StopWatch();
log.info("Starting game cover download...");
@@ -57,9 +57,10 @@ public class ImageService {
stopWatch.stop();
log.info("Downloaded {} covers in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
+ return downloadCount;
}
- public void downloadGameScreenshotsFromIgdb() {
+ public int downloadGameScreenshotsFromIgdb() {
StopWatch stopWatch = new StopWatch();
log.info("Starting game screenshot download...");
@@ -74,9 +75,10 @@ public class ImageService {
stopWatch.stop();
log.info("Downloaded {} screenshots in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
+ return downloadCount;
}
- public void downloadCompanyLogosFromIgdb() {
+ public int downloadCompanyLogosFromIgdb() {
StopWatch stopWatch = new StopWatch();
log.info("Starting company logo download...");
@@ -93,6 +95,7 @@ public class ImageService {
stopWatch.stop();
log.info("Downloaded {} company logos in {} seconds.", downloadCount, (int) stopWatch.getTotalTimeSeconds());
+ return downloadCount;
}
private int saveImagesIntoCache(MultiValueMap entityToImageIds, String imageSize, String imageType, String entityType) {
diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java
index 9930bc3..a171e0e 100644
--- a/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java
+++ b/backend/src/main/java/de/grimsi/gameyfin/service/LibraryService.java
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin.service;
import com.igdb.proto.Igdb;
import de.grimsi.gameyfin.dto.AutocompleteSuggestionDto;
+import de.grimsi.gameyfin.dto.LibraryScanResult;
import de.grimsi.gameyfin.entities.DetectedGame;
import de.grimsi.gameyfin.entities.UnmappableFile;
import de.grimsi.gameyfin.igdb.IgdbWrapper;
@@ -57,7 +58,7 @@ public class LibraryService {
return gamefiles;
}
- public void scanGameLibrary() {
+ public LibraryScanResult scanGameLibrary() {
StopWatch stopWatch = new StopWatch();
log.info("Starting scan...");
@@ -120,6 +121,13 @@ public class LibraryService {
log.info("Scan finished in {} seconds: Found {} new games, deleted {} games, could not map {} files/folders, {} games total.",
(int) stopWatch.getTotalTimeSeconds(), newDetectedGames.size(), deletedGames.size() + deletedUnmappableFiles.size(), newUnmappedFilesCounter.get(), detectedGameRepository.count());
+
+ return LibraryScanResult.builder()
+ .newGames(newDetectedGames.size())
+ .deletedGames(deletedGames.size() + deletedUnmappableFiles.size())
+ .newUnmappableFiles(newUnmappedFilesCounter.get())
+ .totalGames((int) detectedGameRepository.count())
+ .build();
}
public List getAutocompleteSuggestions(String searchTerm, int limit) {
diff --git a/frontend/pom.xml b/frontend/pom.xml
index 01da9ac..b7da3fa 100644
--- a/frontend/pom.xml
+++ b/frontend/pom.xml
@@ -5,7 +5,7 @@
gameyfin
de.grimsi
- 1.1.4-RC1
+ 1.2.0
4.0.0
diff --git a/frontend/src/app/api/LibraryApi.ts b/frontend/src/app/api/LibraryApi.ts
index fb8df25..9c3eda4 100644
--- a/frontend/src/app/api/LibraryApi.ts
+++ b/frontend/src/app/api/LibraryApi.ts
@@ -1,8 +1,11 @@
import {Observable} from "rxjs";
-import {HttpResponse} from "@angular/common/http";
+import {LibraryScanResultDto} from "../models/dtos/LibraryScanResultDto";
+import {ImageDownloadResultDto} from "../models/dtos/ImageDownloadResultDto";
export interface LibraryApi {
- scanLibrary(): Observable>;
- downloadImages(): Observable>;
+ scanLibrary(): Observable;
+
+ downloadImages(): Observable;
+
getFiles(): Observable;
}
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.html b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
index e564261..8d2d39b 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.html
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.html
@@ -17,13 +17,11 @@
{{game.summary}}
-
0">
+
0">
Developed by
-
-
![]()
0" style="height: 52px;"
- src="v1/images/{{company.logoId}}" alt="{{company.name}}">
-
0">{{company.name}}
+
+
diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.ts b/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
index 8366c89..444f1b0 100644
--- a/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
+++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.ts
@@ -2,6 +2,7 @@ import {Component, HostListener} from '@angular/core';
import {ActivatedRoute, Params, Router} from "@angular/router";
import {DetectedGameDto} from "../../models/dtos/DetectedGameDto";
import {GamesService} from "../../services/games.service";
+import {CompanyDto} from "../../models/dtos/CompanyDto";
@Component({
selector: 'app-game-detail-view',
@@ -12,13 +13,20 @@ export class GameDetailViewComponent {
game!: DetectedGameDto;
+ companiesWithLogo: CompanyDto[]= [];
+
gridColumnCount: number;
constructor(private route: ActivatedRoute,
private router: Router,
private gamesService: GamesService) {
this.gamesService.getGame(this.route.snapshot.params['slug']).subscribe({
- next: game => this.game = game,
+ next: game => {
+ this.game = game;
+ if(game.companies !== undefined) {
+ this.companiesWithLogo = game.companies.filter(c => c.logoId !== undefined && c.logoId.length > 0);
+ }
+ },
error: error => {
if (error.status === 404) {
this.router.navigate(['/library']);
diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts
index 71b017a..eb3e802 100644
--- a/frontend/src/app/components/header/header.component.ts
+++ b/frontend/src/app/components/header/header.component.ts
@@ -1,7 +1,6 @@
import {Component} from '@angular/core';
import {LibraryService} from "../../services/library.service";
import {MatSnackBar} from '@angular/material/snack-bar';
-import {timeInterval} from "rxjs";
import {Router} from "@angular/router";
import {GamesService} from "../../services/games.service";
import {ThemingService} from "../../services/theming.service";
@@ -26,11 +25,27 @@ export class HeaderComponent {
}
scanLibrary(): void {
- this.libraryService.scanLibrary().pipe(timeInterval()).subscribe({
- next: value => {
+ this.libraryService.scanLibrary().subscribe({
+ next: result => {
// Refresh the current page "angular style"
- this.router.navigate([this.router.url]).then(() =>
- this.snackBar.open(`Library scan completed in ${Math.trunc(value.interval / 1000)} seconds.`, undefined, {duration: 5000})
+ this.router.navigate([this.router.url]).then(() => {
+ const snackBarDuration: number = 10000;
+
+ let snackbarContent: string = 'Library scan completed in ' + result.scanDuration + ' seconds:\n' +
+ '- ' + result.newGames + ' new games\n' +
+ '- ' + result.deletedGames + ' games removed\n' +
+ '- ' + result.newUnmappableFiles + ' files/folders could not be mapped\n' +
+ '- ' + result.totalGames + ' games currently in your library';
+
+ if (result.companyLogoDownloads !== undefined && result.coverDownloads !== undefined && result.screenshotDownloads !== undefined) {
+ snackbarContent = snackbarContent.concat('\n' +
+ '- ' + result.coverDownloads + ' covers downloaded\n' +
+ '- ' + result.screenshotDownloads + ' screenshots downloaded\n' +
+ '- ' + result.companyLogoDownloads + ' company logos downloaded');
+ }
+
+ this.snackBar.open(snackbarContent, undefined, {duration: snackBarDuration});
+ }
)
},
error: error => this.snackBar.open(`Error while scanning library: ${error.error.message}`, undefined, {duration: 5000})
diff --git a/frontend/src/app/models/dtos/ImageDownloadResultDto.ts b/frontend/src/app/models/dtos/ImageDownloadResultDto.ts
new file mode 100644
index 0000000..a7048c0
--- /dev/null
+++ b/frontend/src/app/models/dtos/ImageDownloadResultDto.ts
@@ -0,0 +1,5 @@
+export class ImageDownloadResultDto {
+ coverDownloads!: number;
+ screenshotDownloads!: number;
+ companyLogoDownloads!: number;
+}
diff --git a/frontend/src/app/models/dtos/LibraryScanResultDto.ts b/frontend/src/app/models/dtos/LibraryScanResultDto.ts
new file mode 100644
index 0000000..f34977a
--- /dev/null
+++ b/frontend/src/app/models/dtos/LibraryScanResultDto.ts
@@ -0,0 +1,10 @@
+export class LibraryScanResultDto {
+ newGames!: number;
+ deletedGames!: number;
+ newUnmappableFiles!: number;
+ totalGames!: number;
+ coverDownloads!: number;
+ screenshotDownloads!: number;
+ companyLogoDownloads!: number;
+ scanDuration!: number;
+}
diff --git a/frontend/src/app/services/library.service.ts b/frontend/src/app/services/library.service.ts
index e0cd2b5..3fd7d82 100644
--- a/frontend/src/app/services/library.service.ts
+++ b/frontend/src/app/services/library.service.ts
@@ -1,7 +1,9 @@
import {Injectable} from '@angular/core';
-import {HttpClient, HttpResponse} from "@angular/common/http";
+import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {LibraryApi} from "../api/LibraryApi";
+import {LibraryScanResultDto} from "../models/dtos/LibraryScanResultDto";
+import {ImageDownloadResultDto} from "../models/dtos/ImageDownloadResultDto";
@Injectable({
providedIn: 'root'
@@ -13,12 +15,12 @@ export class LibraryService implements LibraryApi {
constructor(private http: HttpClient) {
}
- scanLibrary(): Observable
> {
- return this.http.get>(`${this.apiPath}/scan`);
+ scanLibrary(): Observable {
+ return this.http.get(`${this.apiPath}/scan`);
}
- downloadImages(): Observable> {
- return this.http.get>(`${this.apiPath}/download-images`);
+ downloadImages(): Observable {
+ return this.http.get(`${this.apiPath}/download-images`);
}
getFiles(): Observable {
diff --git a/pom.xml b/pom.xml
index 1959753..e34baa5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
de.grimsi
gameyfin
- 1.1.4-RC1
+ 1.2.0
gameyfin
gameyfin