Implemented persistence layer for games

Implemented blacklist for paths
This commit is contained in:
grimsi
2022-07-15 23:00:04 +02:00
parent 4991bfb461
commit 183f58c64c
11 changed files with 166 additions and 17 deletions
@@ -20,7 +20,7 @@ public class WebClientConfig implements WebClientCustomizer {
@Override
public void customize(WebClient.Builder webClientBuilder) {
HttpClient httpClient = HttpClient.create()
.wiretap(this.getClass().getCanonicalName(), LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL) // Enable full request / response logging (active only in DEV profile)
.wiretap(this.getClass().getCanonicalName(), LogLevel.TRACE, AdvancedByteBufFormat.TEXTUAL) // Enable full request / response logging in TRACE
.proxyWithSystemProperties(); // Enable use of system proxy
webClientBuilder.clientConnector(new ReactorClientHttpConnector(httpClient));
@@ -16,7 +16,7 @@ import java.util.List;
public class GameDto {
private String name;
private String publisher;
private Long igdbGameId;
private String slug;
private Instant releaseDate;
private List<File> files;
@@ -0,0 +1,34 @@
package de.grimsi.gameyfin.entities;
import lombok.*;
import org.hibernate.Hibernate;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Objects;
@Entity
@Table(name = "blacklist")
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class BlacklistEntry {
@Id
private String path;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
BlacklistEntry that = (BlacklistEntry) o;
return path != null && Objects.equals(path, that.path);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
@@ -14,6 +14,7 @@ import java.util.Objects;
@Getter
@Setter
@ToString
@AllArgsConstructor
@RequiredArgsConstructor
public class DetectedGame {
@@ -24,6 +25,8 @@ public class DetectedGame {
@Column(nullable = false)
private String title;
@Lob
@Column(columnDefinition="CLOB")
private String summary;
private Instant releaseDate;
@@ -87,7 +87,10 @@ public class IgdbWrapper {
.bodyToMono(Igdb.GameResult.class)
.block();
if (gameResult == null) return Optional.empty();
if (gameResult == null) {
log.warn("Could not find game for title '{}'", searchTerm);
return Optional.empty();
}
List<Igdb.Game> games = gameResult.getGamesList();
@@ -0,0 +1,9 @@
package de.grimsi.gameyfin.repositories;
import de.grimsi.gameyfin.entities.BlacklistEntry;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BlacklistRepository extends JpaRepository<BlacklistEntry, String> {
boolean existsByPath(String path);
}
@@ -0,0 +1,9 @@
package de.grimsi.gameyfin.repositories;
import de.grimsi.gameyfin.entities.DetectedGame;
import org.springframework.data.jpa.repository.JpaRepository;
public interface DetectedGameRepository extends JpaRepository<DetectedGame, String> {
boolean existsByPath(String path);
}
@@ -2,8 +2,10 @@ package de.grimsi.gameyfin.rest;
import com.igdb.proto.Igdb;
import de.grimsi.gameyfin.dto.GameDto;
import de.grimsi.gameyfin.entities.DetectedGame;
import de.grimsi.gameyfin.igdb.IgdbWrapper;
import de.grimsi.gameyfin.service.FilesystemService;
import de.grimsi.gameyfin.service.GameService;
import de.grimsi.gameyfin.util.ProtobufUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@@ -26,6 +28,9 @@ public class GameyfinDevController {
@Autowired
private FilesystemService filesystemService;
@Autowired
private GameService gameService;
@GetMapping(value = "/dev/findGameByTitle/{title}", produces = MediaType.APPLICATION_JSON_VALUE)
public GameDto findGameByTitle(@PathVariable("title") String title) {
Igdb.Game game = igdbWrapper.searchForGameByTitle(title)
@@ -54,14 +59,13 @@ public class GameyfinDevController {
}
@GetMapping(value = "/dev/games", produces = MediaType.APPLICATION_JSON_VALUE)
public List<GameDto> getAllGames() {
return filesystemService.getGameFileNames().parallelStream()
.map(t -> igdbWrapper.searchForGameByTitle(t).orElse(null))
.filter(Objects::nonNull)
.map(g -> GameDto.builder()
.name(g.getName())
.releaseDate(ProtobufUtils.toInstant(g.getFirstReleaseDate()))
.build())
.toList();
public List<DetectedGame> getAllGames() {
return gameService.getAllDetectedGames();
}
@GetMapping(value = "/dev/startScan", produces = MediaType.APPLICATION_JSON_VALUE)
public void scanLibrary() {
filesystemService.scanGameLibrary();
}
}
@@ -1,6 +1,15 @@
package de.grimsi.gameyfin.service;
import com.igdb.proto.Igdb;
import de.grimsi.gameyfin.entities.BlacklistEntry;
import de.grimsi.gameyfin.entities.DetectedGame;
import de.grimsi.gameyfin.igdb.IgdbWrapper;
import de.grimsi.gameyfin.mapper.GameMapper;
import de.grimsi.gameyfin.repositories.BlacklistRepository;
import de.grimsi.gameyfin.repositories.DetectedGameRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@@ -8,9 +17,12 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
@Slf4j
@Service
public class FilesystemService {
@Value("${gameyfin.root}")
@@ -19,18 +31,70 @@ public class FilesystemService {
@Value("${gameyfin.file-extensions}")
private List<String> possibleGameFileExtensions;
@Autowired
private IgdbWrapper igdbWrapper;
@Autowired
private DetectedGameRepository detectedGameRepository;
@Autowired
private BlacklistRepository blacklistRepository;
public List<Path> getGameFiles() {
Path rootFolder = Path.of(rootFolderPath);
try(Stream<Path> stream = Files.list(rootFolder)) {
try (Stream<Path> stream = Files.list(rootFolder)) {
// return all sub-folders (non-recursive) and files that have an extension that indicates that they are a downloadable file
return stream.filter(p -> Files.isDirectory(p) || possibleGameFileExtensions.contains(FilenameUtils.getExtension(p.getFileName().toString()))).toList();
return stream
.filter(p -> Files.isDirectory(p) || possibleGameFileExtensions.contains(FilenameUtils.getExtension(p.getFileName().toString())))
.toList();
} catch (IOException e) {
throw new RuntimeException("Error while opening root folder", e);
}
}
public List<String> getGameFileNames() {
return this.getGameFiles().stream().map(p -> FilenameUtils.getBaseName(p.toString())).toList();
return this.getGameFiles().stream().map(this::getFilename).toList();
}
public void scanGameLibrary() {
log.info("Starting scan...");
AtomicInteger newBlacklistCounter = new AtomicInteger();
List<Path> gameFiles = getGameFiles();
// Filter out the games we already know and the ones we already tried to map to a game without success
gameFiles = gameFiles.stream()
.filter(g -> !detectedGameRepository.existsByPath(g.toString()))
.filter(g -> !blacklistRepository.existsByPath(g.toString()))
.peek(p -> log.info("Found new potential game: {}", p))
.toList();
// For each new game, load the info from IGDB
// If a game is not found on IGDB, blacklist the path so we won't query the API later on for the same path
List<DetectedGame> newDetectedGames = gameFiles.parallelStream()
.map(p -> {
Optional<Igdb.Game> optionalGame = igdbWrapper.searchForGameByTitle(getFilename(p));
return optionalGame.map(game -> Map.entry(p, game)).or(() -> {
blacklistRepository.save(new BlacklistEntry(p.toString()));
newBlacklistCounter.getAndIncrement();
log.info("Added path '{}' to blacklist", p);
return Optional.empty();
});
})
.filter(Optional::isPresent)
.map(Optional::get)
.map(e -> GameMapper.toDetectedGame(e.getValue(), e.getKey()))
.toList();
newDetectedGames = detectedGameRepository.saveAll(newDetectedGames);
log.info("Scan finished: Found {} new games, deleted {} games, backlisted {} files/folders, {} games total.", newDetectedGames.size(), "NOT_IMPLEMENTED_YET", newBlacklistCounter.get(), detectedGameRepository.count());
}
private String getFilename(Path p) {
return FilenameUtils.getBaseName(p.toString());
}
}
@@ -0,0 +1,23 @@
package de.grimsi.gameyfin.service;
import de.grimsi.gameyfin.entities.DetectedGame;
import de.grimsi.gameyfin.repositories.BlacklistRepository;
import de.grimsi.gameyfin.repositories.DetectedGameRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class GameService {
@Autowired
private DetectedGameRepository detectedGameRepository;
@Autowired
private BlacklistRepository blacklistRepository;
public List<DetectedGame> getAllDetectedGames() {
return detectedGameRepository.findAll();
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
gameyfin:
root: C:\Projects\privat\gameyfin-library
root: D:\Games
cache: C:\Projects\privat\gameyfin-library\.gameyfin
db: C:\Projects\privat\gameyfin-library\.gameyfin
igdb: