mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +00:00
Implemented persistence layer for games
Implemented blacklist for paths
This commit is contained in:
@@ -20,7 +20,7 @@ public class WebClientConfig implements WebClientCustomizer {
|
|||||||
@Override
|
@Override
|
||||||
public void customize(WebClient.Builder webClientBuilder) {
|
public void customize(WebClient.Builder webClientBuilder) {
|
||||||
HttpClient httpClient = HttpClient.create()
|
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
|
.proxyWithSystemProperties(); // Enable use of system proxy
|
||||||
|
|
||||||
webClientBuilder.clientConnector(new ReactorClientHttpConnector(httpClient));
|
webClientBuilder.clientConnector(new ReactorClientHttpConnector(httpClient));
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import java.util.List;
|
|||||||
public class GameDto {
|
public class GameDto {
|
||||||
private String name;
|
private String name;
|
||||||
private String publisher;
|
private String publisher;
|
||||||
private Long igdbGameId;
|
private String slug;
|
||||||
private Instant releaseDate;
|
private Instant releaseDate;
|
||||||
|
|
||||||
private List<File> files;
|
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
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@ToString
|
@ToString
|
||||||
|
@AllArgsConstructor
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DetectedGame {
|
public class DetectedGame {
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ public class DetectedGame {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
|
@Lob
|
||||||
|
@Column(columnDefinition="CLOB")
|
||||||
private String summary;
|
private String summary;
|
||||||
|
|
||||||
private Instant releaseDate;
|
private Instant releaseDate;
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ public class IgdbWrapper {
|
|||||||
.bodyToMono(Igdb.GameResult.class)
|
.bodyToMono(Igdb.GameResult.class)
|
||||||
.block();
|
.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();
|
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 com.igdb.proto.Igdb;
|
||||||
import de.grimsi.gameyfin.dto.GameDto;
|
import de.grimsi.gameyfin.dto.GameDto;
|
||||||
|
import de.grimsi.gameyfin.entities.DetectedGame;
|
||||||
import de.grimsi.gameyfin.igdb.IgdbWrapper;
|
import de.grimsi.gameyfin.igdb.IgdbWrapper;
|
||||||
import de.grimsi.gameyfin.service.FilesystemService;
|
import de.grimsi.gameyfin.service.FilesystemService;
|
||||||
|
import de.grimsi.gameyfin.service.GameService;
|
||||||
import de.grimsi.gameyfin.util.ProtobufUtils;
|
import de.grimsi.gameyfin.util.ProtobufUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -26,6 +28,9 @@ public class GameyfinDevController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FilesystemService filesystemService;
|
private FilesystemService filesystemService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private GameService gameService;
|
||||||
|
|
||||||
@GetMapping(value = "/dev/findGameByTitle/{title}", produces = MediaType.APPLICATION_JSON_VALUE)
|
@GetMapping(value = "/dev/findGameByTitle/{title}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public GameDto findGameByTitle(@PathVariable("title") String title) {
|
public GameDto findGameByTitle(@PathVariable("title") String title) {
|
||||||
Igdb.Game game = igdbWrapper.searchForGameByTitle(title)
|
Igdb.Game game = igdbWrapper.searchForGameByTitle(title)
|
||||||
@@ -54,14 +59,13 @@ public class GameyfinDevController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/dev/games", produces = MediaType.APPLICATION_JSON_VALUE)
|
@GetMapping(value = "/dev/games", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public List<GameDto> getAllGames() {
|
public List<DetectedGame> getAllGames() {
|
||||||
return filesystemService.getGameFileNames().parallelStream()
|
return gameService.getAllDetectedGames();
|
||||||
.map(t -> igdbWrapper.searchForGameByTitle(t).orElse(null))
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.map(g -> GameDto.builder()
|
|
||||||
.name(g.getName())
|
|
||||||
.releaseDate(ProtobufUtils.toInstant(g.getFirstReleaseDate()))
|
|
||||||
.build())
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/dev/startScan", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public void scanLibrary() {
|
||||||
|
filesystemService.scanGameLibrary();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
package de.grimsi.gameyfin.service;
|
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.apache.commons.io.FilenameUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -8,9 +17,12 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class FilesystemService {
|
public class FilesystemService {
|
||||||
@Value("${gameyfin.root}")
|
@Value("${gameyfin.root}")
|
||||||
@@ -19,18 +31,70 @@ public class FilesystemService {
|
|||||||
@Value("${gameyfin.file-extensions}")
|
@Value("${gameyfin.file-extensions}")
|
||||||
private List<String> possibleGameFileExtensions;
|
private List<String> possibleGameFileExtensions;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IgdbWrapper igdbWrapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DetectedGameRepository detectedGameRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BlacklistRepository blacklistRepository;
|
||||||
|
|
||||||
public List<Path> getGameFiles() {
|
public List<Path> getGameFiles() {
|
||||||
|
|
||||||
Path rootFolder = Path.of(rootFolderPath);
|
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 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) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("Error while opening root folder", e);
|
throw new RuntimeException("Error while opening root folder", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getGameFileNames() {
|
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,5 +1,5 @@
|
|||||||
gameyfin:
|
gameyfin:
|
||||||
root: C:\Projects\privat\gameyfin-library
|
root: D:\Games
|
||||||
cache: C:\Projects\privat\gameyfin-library\.gameyfin
|
cache: C:\Projects\privat\gameyfin-library\.gameyfin
|
||||||
db: C:\Projects\privat\gameyfin-library\.gameyfin
|
db: C:\Projects\privat\gameyfin-library\.gameyfin
|
||||||
igdb:
|
igdb:
|
||||||
|
|||||||
Reference in New Issue
Block a user