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!

-
+
- +

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 @@ -
+
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 cover
-
+

{{game.title}}

-

Rating: {{game.totalRating}}/100

Release: {{game.releaseDate | date: 'longDate'}}

-

{{game.summary}}

+

{{game.summary}}

+ +
+

Critics Rating ({{game.criticsRating}}/100)

+ +
+ +
+

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}}

-
-

Critics Rating ({{game.criticsRating}}/100)

- -
- -
-

User Rating ({{game.userRating}}/100)

- +
+

Developed by

+
+
+ {{company.name}} +
+
@@ -37,17 +40,34 @@

Genres

- {{genre.name}} + {{genre.name}}

Themes

- {{theme.name}} + {{theme.name}}
+ +
+
+

Critics Rating ({{game.criticsRating}}/100) +

+ +
+ +
+

User Rating ({{game.userRating}}/100)

+ +
+
@@ -56,14 +76,16 @@

Screenshots

- +

Videos

- +
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 cover -
+ +
+ Game cover +
-
-

{{game.title}}

-

Release: {{game.releaseDate | date: 'longDate'}}

+
+
+

{{game.title}}

+

{{game.releaseDate | date: 'yyyy'}}

-

Description

-

{{game.summary}}

+

Description

+

{{game.summary}}

+
-
-

Developed by

-
-
- {{company.name}} +
+

Developed by

+
+
+ {{company.name}} +
-
+
@@ -71,23 +75,26 @@
-
+

Screenshots

-
- -
+ + + + +
-
+

Videos

-
- -
+ + + + +
+
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 @@
{{company.name}} + {{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 @@
-
+

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}}

-
+

Developed by

-
- {{company.name}} - {{company.name}} +
+ {{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