diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b24b904..ba7c5da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - + - name: Set up JDK uses: actions/setup-java@v3 with: @@ -47,7 +47,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar + run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=grimsi_gameyfin - name: Upload build artifact uses: actions/upload-artifact@v3 diff --git a/backend/pom.xml b/backend/pom.xml index ee8d887..ce82109 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -254,6 +254,21 @@ + + + + org.jacoco + jacoco-maven-plugin + + + report + + report-aggregate + + verify + + + 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 156988a..a8358f2 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/rest/GamesController.java +++ b/backend/src/main/java/de/grimsi/gameyfin/rest/GamesController.java @@ -5,11 +5,16 @@ import de.grimsi.gameyfin.entities.DetectedGame; import de.grimsi.gameyfin.service.DownloadService; import de.grimsi.gameyfin.service.GameService; import lombok.RequiredArgsConstructor; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.Map; @@ -44,17 +49,33 @@ public class GamesController { return gameService.getAllMappings(); } - @GetMapping(value="/game/{slug}/download") + @GetMapping(value = "/game/{slug}/download") public ResponseEntity downloadGameFiles(@PathVariable String slug) { DetectedGame game = gameService.getDetectedGame(slug); String downloadFileName = downloadService.getDownloadFileName(game); + long downloadFileSize = downloadService.getDownloadFileSize(game); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"%s\"".formatted(downloadFileName)); + headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + headers.add(HttpHeaders.PRAGMA, "no-cache"); + headers.add(HttpHeaders.EXPIRES, "0"); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + if (downloadFileSize > 0) { + headers.setContentLength(downloadFileSize); + } return ResponseEntity .ok() - .header("Content-Disposition", "attachment; filename=\"%s\"".formatted(downloadFileName)) + .headers(headers) .body(out -> downloadService.sendGamefilesToClient(game, out)); } + @GetMapping(value = "/game/{slug}/refresh", produces = MediaType.APPLICATION_JSON_VALUE) + public DetectedGame refreshGame(@PathVariable String slug) { + return gameService.refreshGame(slug); + } + } 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 563e522..c29fe20 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/service/DownloadService.java +++ b/backend/src/main/java/de/grimsi/gameyfin/service/DownloadService.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.ClientAbortException; +import org.apache.commons.io.FileUtils; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import org.springframework.util.StopWatch; @@ -34,6 +35,23 @@ public class DownloadService { return getFilenameWithExtension(path) + ".zip"; } + public long getDownloadFileSize(DetectedGame game) { + Path path = Path.of(game.getPath()); + + try { + if (!path.toFile().isDirectory()) { + long fileSize = filesystemService.getSizeOnDisk(path); + log.info("Calculated file size for {} ({} MB).", path, Math.divideExact(fileSize, 1000000L)); + return fileSize; + } else { + // return zero since we cannot set content length for ZipOutputStreams that are used to archive directories + return 0; + } + } catch (IOException e) { + throw new DownloadAbortedException(); + } + } + public Resource sendImageToClient(String imageId) { String filename = "%s.png".formatted(imageId); return filesystemService.getFileFromCache(filename); @@ -78,6 +96,7 @@ public class DownloadService { } private void sendGamefilesAsZipToClient(Path path, OutputStream outputStream) { + log.info("Archiving game path {} for download...", path); ZipOutputStream zos = new ZipOutputStream(outputStream) {{ def.setLevel(Deflater.NO_COMPRESSION); }}; @@ -87,6 +106,7 @@ public class DownloadService { @SneakyThrows public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { zos.putNextEntry(new ZipEntry(path.relativize(file).toString())); + log.debug("Adding file {} to archive...", file); Files.copy(file, zos); zos.closeEntry(); 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 3fd2a82..0c36729 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/service/GameService.java +++ b/backend/src/main/java/de/grimsi/gameyfin/service/GameService.java @@ -9,6 +9,7 @@ import de.grimsi.gameyfin.mapper.GameMapper; import de.grimsi.gameyfin.repositories.DetectedGameRepository; import de.grimsi.gameyfin.repositories.UnmappableFileRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; @@ -20,6 +21,7 @@ import java.util.Optional; import java.util.stream.Collectors; @RequiredArgsConstructor +@Slf4j @Service public class GameService { @@ -87,6 +89,17 @@ public class GameService { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Path '%s' not in database".formatted(path)); } + public DetectedGame refreshGame(String slug) { + Optional optionalDetectedGame = detectedGameRepository.findById(slug); + + if (optionalDetectedGame.isPresent()) { + log.info("Refreshing game with slug '{}'", slug); + return mapDetectedGame(optionalDetectedGame.get(), slug); + } + + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with slug '%s' not found in database".formatted(slug)); + } + private DetectedGame mapUnmappableFile(UnmappableFile unmappableFile, String slug) { Igdb.Game igdbGame = igdbWrapper.getGameBySlug(slug) .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Game with slug '%s' does not exist on IGDB.".formatted(slug))); @@ -106,8 +119,6 @@ public class GameService { DetectedGame game = gameMapper.toDetectedGame(igdbGame, Path.of(existingGame.getPath())); game = detectedGameRepository.save(game); - detectedGameRepository.deleteById(existingGame.getSlug()); - return game; } } diff --git a/frontend/src/app/api/GamesApi.ts b/frontend/src/app/api/GamesApi.ts index b198553..aa24c14 100644 --- a/frontend/src/app/api/GamesApi.ts +++ b/frontend/src/app/api/GamesApi.ts @@ -7,4 +7,5 @@ export interface GamesApi { getGame(slug: String): Observable; getGameOverviews(): Observable; getAllGameMappings(): Observable>; + refreshGame(slug: String): 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 8d2d39b..f5b284b 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 @@ -3,7 +3,7 @@
- +
Game cover
@@ -20,8 +20,8 @@

Developed by

-
- {{company.name}} +
+ {{company.name}}
@@ -33,6 +33,9 @@
+ diff --git a/frontend/src/app/components/game-detail-view/game-detail-view.component.scss b/frontend/src/app/components/game-detail-view/game-detail-view.component.scss index e69de29..fda9bd6 100644 --- a/frontend/src/app/components/game-detail-view/game-detail-view.component.scss +++ b/frontend/src/app/components/game-detail-view/game-detail-view.component.scss @@ -0,0 +1,8 @@ +.mat-card { +// min-height: max-content; + + .company-logos img { + max-height: 52px; + max-width: 260px; + } +} 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 444f1b0..88f6db7 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 @@ -48,6 +48,24 @@ export class GameDetailViewComponent { this.gamesService.downloadGame(this.game.slug); } + public refreshGame(): void { + this.gamesService.refreshGame(this.game.slug).subscribe({ + 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']); + } else { + console.error(error); + } + } + }); + } + public bytesAsHumanReadableString(bytes: number): string { const thresh = 1024; diff --git a/frontend/src/app/services/games.service.ts b/frontend/src/app/services/games.service.ts index 87bd6f4..ea7a5b8 100644 --- a/frontend/src/app/services/games.service.ts +++ b/frontend/src/app/services/games.service.ts @@ -92,6 +92,10 @@ export class GamesService implements GamesApi { window.open(`v1${this.apiPath}/game/${slug}/download`, '_top'); } + refreshGame(slug: String): Observable { + return this.http.get(`${this.apiPath}/game/${slug}/refresh`); + } + getGameOverviews(): Observable { return this.http.get(`${this.apiPath}/game-overviews`); } diff --git a/pom.xml b/pom.xml index c2c5cf9..cef79b4 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 de.grimsi @@ -19,7 +20,7 @@ org.springframework.boot spring-boot-starter-parent 2.7.4 - + @@ -27,7 +28,7 @@ scm:git:https://github.com/grimsi/gameyfin.git scm:git:https://github.com/grimsi/gameyfin.git - + grimsi-github https://sonarcloud.io diff --git a/sonar-project.properties b/sonar-project.properties index 3593a3c..efe7579 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,4 +1,2 @@ -sonar.projectKey=grimsi_gameyfin - # Point SONAR to the compiled Java classes sonar.java.binaries=./backend/target