diff --git a/pom.xml b/pom.xml index 249fa9a..8930416 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,11 @@ resilience4j-ratelimiter 1.7.1 + + io.github.resilience4j + resilience4j-bulkhead + 1.7.1 + diff --git a/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java b/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java index 41a8429..cdc1b09 100644 --- a/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java +++ b/src/main/java/de/grimsi/gameyfin/config/WebClientConfig.java @@ -1,5 +1,7 @@ package de.grimsi.gameyfin.config; +import io.github.resilience4j.bulkhead.Bulkhead; +import io.github.resilience4j.bulkhead.BulkheadConfig; import io.github.resilience4j.ratelimiter.RateLimiter; import io.github.resilience4j.ratelimiter.RateLimiterConfig; import io.netty.handler.logging.LogLevel; @@ -21,12 +23,21 @@ import java.time.Duration; @Configuration public class WebClientConfig implements WebClientCustomizer { + // The IGDB API has a rate limit of 4 req/s public static final RateLimiter IGDB_RATE_LIMITER = RateLimiter.of("igdb-rate-limiter", RateLimiterConfig.custom() - .limitRefreshPeriod(Duration.ofSeconds(1)) .limitForPeriod(4) - .timeoutDuration(Duration.ofMinutes(1)) // max wait time for a request, if reached then error - .build()); + .limitRefreshPeriod(Duration.ofSeconds(1)) + .timeoutDuration(Duration.ofMinutes(1)) + .build()); + + // According to the docs, there is a maximum of 8 concurrent requests, but in my tests the actual limit was 4 + // and even then it sometimes failed, so I set it to 3 to be sure + public static final Bulkhead IGDB_CONCURRENCY_LIMITER = Bulkhead.of("igdb-concurrency-limiter", + BulkheadConfig.custom() + .maxConcurrentCalls(2) + .maxWaitDuration(Duration.ofMinutes(1)) + .build()); @Override public void customize(WebClient.Builder webClientBuilder) { diff --git a/src/main/java/de/grimsi/gameyfin/entities/Category.java b/src/main/java/de/grimsi/gameyfin/entities/Category.java deleted file mode 100644 index d292123..0000000 --- a/src/main/java/de/grimsi/gameyfin/entities/Category.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.grimsi.gameyfin.entities; - -import lombok.*; -import org.hibernate.Hibernate; - -import javax.persistence.Entity; -import javax.persistence.Id; -import java.util.Objects; - -@Entity -@Getter -@Setter -@ToString -@RequiredArgsConstructor -public class Category { - @Id - private String slug; - - private String name; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; - Category category = (Category) o; - return slug != null && Objects.equals(slug, category.slug); - } - - @Override - public int hashCode() { - return getClass().hashCode(); - } -} diff --git a/src/main/java/de/grimsi/gameyfin/entities/Company.java b/src/main/java/de/grimsi/gameyfin/entities/Company.java index ca8a541..a0d88f0 100644 --- a/src/main/java/de/grimsi/gameyfin/entities/Company.java +++ b/src/main/java/de/grimsi/gameyfin/entities/Company.java @@ -1,20 +1,21 @@ package de.grimsi.gameyfin.entities; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.*; import org.hibernate.Hibernate; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.Version; import java.util.Objects; @Entity +@Builder @Getter @Setter @ToString +@AllArgsConstructor @RequiredArgsConstructor public class Company { @Id diff --git a/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java b/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java index 9ddb7db..ea4e853 100644 --- a/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java +++ b/src/main/java/de/grimsi/gameyfin/entities/DetectedGame.java @@ -1,6 +1,7 @@ package de.grimsi.gameyfin.entities; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.*; import org.hibernate.Hibernate; @@ -37,8 +38,7 @@ public class DetectedGame { private Integer totalRating; - @ManyToOne - private Category category; + private String category; private boolean offlineCoop; @@ -57,23 +57,23 @@ public class DetectedGame { @ElementCollection private List videoIds; - @ManyToMany + @ManyToMany(cascade = CascadeType.MERGE) @ToString.Exclude private List companies; - @ManyToMany + @ManyToMany(cascade = CascadeType.MERGE) @ToString.Exclude private List genres; - @ManyToMany + @ManyToMany(cascade = CascadeType.MERGE) @ToString.Exclude private List keywords; - @ManyToMany + @ManyToMany(cascade = CascadeType.MERGE) @ToString.Exclude private List themes; - @ManyToMany + @ManyToMany(cascade = CascadeType.MERGE) @ToString.Exclude private List playerPerspectives; diff --git a/src/main/java/de/grimsi/gameyfin/entities/Genre.java b/src/main/java/de/grimsi/gameyfin/entities/Genre.java index 68e6987..0a7cb32 100644 --- a/src/main/java/de/grimsi/gameyfin/entities/Genre.java +++ b/src/main/java/de/grimsi/gameyfin/entities/Genre.java @@ -1,19 +1,20 @@ package de.grimsi.gameyfin.entities; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.ToString; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.*; import org.hibernate.Hibernate; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.Version; import java.util.Objects; @Entity +@Builder @Getter @Setter @ToString +@AllArgsConstructor @RequiredArgsConstructor public class Genre { @Id diff --git a/src/main/java/de/grimsi/gameyfin/entities/Keyword.java b/src/main/java/de/grimsi/gameyfin/entities/Keyword.java index 1586862..2483adf 100644 --- a/src/main/java/de/grimsi/gameyfin/entities/Keyword.java +++ b/src/main/java/de/grimsi/gameyfin/entities/Keyword.java @@ -1,16 +1,20 @@ package de.grimsi.gameyfin.entities; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.*; import org.hibernate.Hibernate; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.Version; import java.util.Objects; @Entity +@Builder @Getter @Setter @ToString +@AllArgsConstructor @RequiredArgsConstructor public class Keyword { @Id diff --git a/src/main/java/de/grimsi/gameyfin/entities/PlayerPerspective.java b/src/main/java/de/grimsi/gameyfin/entities/PlayerPerspective.java index 876109b..51c49a1 100644 --- a/src/main/java/de/grimsi/gameyfin/entities/PlayerPerspective.java +++ b/src/main/java/de/grimsi/gameyfin/entities/PlayerPerspective.java @@ -1,16 +1,20 @@ package de.grimsi.gameyfin.entities; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.*; import org.hibernate.Hibernate; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.Version; import java.util.Objects; @Entity +@Builder @Getter @Setter @ToString +@AllArgsConstructor @RequiredArgsConstructor public class PlayerPerspective { @Id diff --git a/src/main/java/de/grimsi/gameyfin/entities/Theme.java b/src/main/java/de/grimsi/gameyfin/entities/Theme.java index aadb122..d1d5e80 100644 --- a/src/main/java/de/grimsi/gameyfin/entities/Theme.java +++ b/src/main/java/de/grimsi/gameyfin/entities/Theme.java @@ -1,16 +1,20 @@ package de.grimsi.gameyfin.entities; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.*; import org.hibernate.Hibernate; import javax.persistence.Entity; import javax.persistence.Id; +import javax.persistence.Version; import java.util.Objects; @Entity +@Builder @Getter @Setter @ToString +@AllArgsConstructor @RequiredArgsConstructor public class Theme { @Id diff --git a/src/main/java/de/grimsi/gameyfin/entities/UnmappableFile.java b/src/main/java/de/grimsi/gameyfin/entities/UnmappableFile.java index 13330f2..a75a985 100644 --- a/src/main/java/de/grimsi/gameyfin/entities/UnmappableFile.java +++ b/src/main/java/de/grimsi/gameyfin/entities/UnmappableFile.java @@ -1,5 +1,6 @@ package de.grimsi.gameyfin.entities; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.*; import org.hibernate.Hibernate; @@ -7,7 +8,6 @@ import javax.persistence.*; import java.util.Objects; @Entity -@Table(name = "blacklist") @Getter @Setter @ToString diff --git a/src/main/java/de/grimsi/gameyfin/igdb/IgdbApiProperties.java b/src/main/java/de/grimsi/gameyfin/igdb/IgdbApiProperties.java new file mode 100644 index 0000000..4dcec99 --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/igdb/IgdbApiProperties.java @@ -0,0 +1,17 @@ +package de.grimsi.gameyfin.igdb; + +import java.util.List; + +public class IgdbApiProperties { + public static final String IGDB_ENPOINT_GAMES_PROTOBUF = "games.pb"; + + public static final List GAME_QUERY_FIELDS = List.of( + "slug", "name", "summary", "first_release_date", "rating", "aggregated_rating", "total_rating", "category", "multiplayer_modes", "cover", "screenshots", "videos", // All top-level fields + "involved_companies.company.slug", "involved_companies.company.name", "involved_companies.company.logo.id", + "genres.slug", "genres.name", + "keywords.slug", "keywords.name", + "themes.slug", "themes.name", + "player_perspectives.slug", "player_perspectives.name" + ); + +} diff --git a/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java b/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java index 3d62ded..1961172 100644 --- a/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java +++ b/src/main/java/de/grimsi/gameyfin/igdb/IgdbWrapper.java @@ -3,6 +3,7 @@ package de.grimsi.gameyfin.igdb; import com.igdb.proto.Igdb; import de.grimsi.gameyfin.config.WebClientConfig; import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto; +import io.github.resilience4j.reactor.bulkhead.operator.BulkheadOperator; import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -20,7 +21,6 @@ import java.util.Optional; @Slf4j @Service public class IgdbWrapper { - @Value("${gameyfin.igdb.api.client-id}") private String clientId; @@ -66,13 +66,11 @@ public class IgdbWrapper { } public Optional getGameById(Long id) { - Igdb.GameResult gameResult = igdbApiClient.post() - .uri("games.pb") - .bodyValue("fields *; where id = %d; limit 1;".formatted(id)) - .retrieve() - .bodyToMono(Igdb.GameResult.class) - .transformDeferred(RateLimiterOperator.of(WebClientConfig.IGDB_RATE_LIMITER)) - .block(); + Igdb.GameResult gameResult = queryIgdbApi( + IgdbApiProperties.IGDB_ENPOINT_GAMES_PROTOBUF, + "fields *; where id = %d; limit 1;".formatted(id), + Igdb.GameResult.class + ); if (gameResult == null) return Optional.empty(); @@ -80,13 +78,11 @@ public class IgdbWrapper { } public Optional getGameBySlug(String slug) { - Igdb.GameResult gameResult = igdbApiClient.post() - .uri("games.pb") - .bodyValue("fields *; where slug = \"%s\"; limit 1;".formatted(slug)) - .retrieve() - .bodyToMono(Igdb.GameResult.class) - .transformDeferred(RateLimiterOperator.of(WebClientConfig.IGDB_RATE_LIMITER)) - .block(); + Igdb.GameResult gameResult = queryIgdbApi( + IgdbApiProperties.IGDB_ENPOINT_GAMES_PROTOBUF, + "fields *; where slug = \"%s\"; limit 1;".formatted(slug), + Igdb.GameResult.class + ); if (gameResult == null) return Optional.empty(); @@ -94,13 +90,12 @@ public class IgdbWrapper { } public Optional searchForGameByTitle(String searchTerm) { - Igdb.GameResult gameResult = igdbApiClient.post() - .uri("games.pb") - .bodyValue("fields *; search \"%s\"; where platforms = (%s);".formatted(searchTerm, preferredPlatforms)) - .retrieve() - .bodyToMono(Igdb.GameResult.class) - .transformDeferred(RateLimiterOperator.of(WebClientConfig.IGDB_RATE_LIMITER)) - .block(); + Igdb.GameResult gameResult = queryIgdbApi( + IgdbApiProperties.IGDB_ENPOINT_GAMES_PROTOBUF, + "search \"%s\"; fields %s; where platforms = (%s);" + .formatted(searchTerm, String.join(",", IgdbApiProperties.GAME_QUERY_FIELDS), preferredPlatforms), + Igdb.GameResult.class + ); if (gameResult == null) { log.warn("Could not find game for title '{}'", searchTerm); @@ -110,7 +105,7 @@ public class IgdbWrapper { List games = gameResult.getGamesList(); // If we only get one game, we don't have to check for exact matches, so return it directly - if(games.size() == 1) return Optional.ofNullable(games.get(0)); + if (games.size() == 1) return Optional.ofNullable(games.get(0)); // First check if there are any matches with the exact search term // If no exact match has been found, check if there are matches where the name ends with the search term @@ -141,4 +136,15 @@ public class IgdbWrapper { .filter(WebClientConfig.fixProtobufContentTypeInterceptor()) .build(); } + + private T queryIgdbApi(String endpoint, String query, Class responseClass) { + return igdbApiClient.post() + .uri(endpoint) + .bodyValue(query) + .retrieve() + .bodyToMono(responseClass) + .transformDeferred(BulkheadOperator.of(WebClientConfig.IGDB_CONCURRENCY_LIMITER)) + .transformDeferred(RateLimiterOperator.of(WebClientConfig.IGDB_RATE_LIMITER)) + .block(); + } } diff --git a/src/main/java/de/grimsi/gameyfin/mapper/CompanyMapper.java b/src/main/java/de/grimsi/gameyfin/mapper/CompanyMapper.java new file mode 100644 index 0000000..29a1ce4 --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/mapper/CompanyMapper.java @@ -0,0 +1,21 @@ +package de.grimsi.gameyfin.mapper; + +import com.igdb.proto.Igdb; +import de.grimsi.gameyfin.entities.Company; + +import java.util.List; + +public class CompanyMapper { + + public static Company toCompany(Igdb.InvolvedCompany c) { + return Company.builder() + .slug(c.getCompany().getSlug()) + .name(c.getCompany().getName()) + .logoId(c.getCompany().getLogo().getId()) + .build(); + } + + public static List toCompanies(List c) { + return c.stream().map(CompanyMapper::toCompany).toList(); + } +} diff --git a/src/main/java/de/grimsi/gameyfin/mapper/GameMapper.java b/src/main/java/de/grimsi/gameyfin/mapper/GameMapper.java index 23ce997..0bb7bee 100644 --- a/src/main/java/de/grimsi/gameyfin/mapper/GameMapper.java +++ b/src/main/java/de/grimsi/gameyfin/mapper/GameMapper.java @@ -1,12 +1,11 @@ package de.grimsi.gameyfin.mapper; import com.igdb.proto.Igdb; -import de.grimsi.gameyfin.entities.Category; import de.grimsi.gameyfin.entities.DetectedGame; +import de.grimsi.gameyfin.entities.PlayerPerspective; import de.grimsi.gameyfin.util.ProtobufUtils; import java.nio.file.Path; -import java.util.Comparator; import java.util.List; public class GameMapper { @@ -24,7 +23,7 @@ public class GameMapper { .userRating((int) g.getRating()) .criticsRating((int) g.getAggregatedRating()) .totalRating((int) g.getTotalRating()) - //.category() + .category(g.getCategory().name()) .offlineCoop(hasOfflineCoop(multiplayerModes)) .onlineCoop(hasOnlineCoop(multiplayerModes)) .lanSupport(hasLanSupport(multiplayerModes)) @@ -32,11 +31,11 @@ public class GameMapper { .coverId(g.getCover().getId()) .screenshotIds(screenshotIds) .videoIds(videoIds) - //.companies() - //.genres() - //.keywords() - //.themes() - //.playerPerspectives() + .companies(CompanyMapper.toCompanies(g.getInvolvedCompaniesList())) + .genres(GenreMapper.toGenres(g.getGenresList())) + .keywords(KeywordMapper.toKeywords(g.getKeywordsList())) + .themes(ThemeMapper.toThemes(g.getThemesList())) + .playerPerspectives(PlayerPerspectiveMapper.toPlayerPerspectives(g.getPlayerPerspectivesList())) .path(path.toString()) .isFolder(path.toFile().isDirectory()) .build(); diff --git a/src/main/java/de/grimsi/gameyfin/mapper/GenreMapper.java b/src/main/java/de/grimsi/gameyfin/mapper/GenreMapper.java new file mode 100644 index 0000000..eb9208f --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/mapper/GenreMapper.java @@ -0,0 +1,20 @@ +package de.grimsi.gameyfin.mapper; + +import com.igdb.proto.Igdb; +import de.grimsi.gameyfin.entities.Genre; + +import java.util.List; + +public class GenreMapper { + + public static Genre toGenre(Igdb.Genre g) { + return Genre.builder() + .slug(g.getSlug()) + .name(g.getName()) + .build(); + } + + public static List toGenres(List g) { + return g.stream().map(GenreMapper::toGenre).toList(); + } +} diff --git a/src/main/java/de/grimsi/gameyfin/mapper/KeywordMapper.java b/src/main/java/de/grimsi/gameyfin/mapper/KeywordMapper.java new file mode 100644 index 0000000..fbaca16 --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/mapper/KeywordMapper.java @@ -0,0 +1,19 @@ +package de.grimsi.gameyfin.mapper; + +import com.igdb.proto.Igdb; +import de.grimsi.gameyfin.entities.Keyword; + +import java.util.List; + +public class KeywordMapper { + public static Keyword toKeyword(Igdb.Keyword g) { + return Keyword.builder() + .slug(g.getSlug()) + .name(g.getName()) + .build(); + } + + public static List toKeywords(List g) { + return g.stream().map(KeywordMapper::toKeyword).toList(); + } +} diff --git a/src/main/java/de/grimsi/gameyfin/mapper/PlayerPerspectiveMapper.java b/src/main/java/de/grimsi/gameyfin/mapper/PlayerPerspectiveMapper.java new file mode 100644 index 0000000..67a5529 --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/mapper/PlayerPerspectiveMapper.java @@ -0,0 +1,20 @@ +package de.grimsi.gameyfin.mapper; + +import com.igdb.proto.Igdb; +import de.grimsi.gameyfin.entities.PlayerPerspective; + +import java.util.List; + +public class PlayerPerspectiveMapper { + + public static PlayerPerspective toPlayerPerspective(Igdb.PlayerPerspective g) { + return PlayerPerspective.builder() + .slug(g.getSlug()) + .name(g.getName()) + .build(); +} + + public static List toPlayerPerspectives(List g) { + return g.stream().map(PlayerPerspectiveMapper::toPlayerPerspective).toList(); + } +} diff --git a/src/main/java/de/grimsi/gameyfin/mapper/ThemeMapper.java b/src/main/java/de/grimsi/gameyfin/mapper/ThemeMapper.java new file mode 100644 index 0000000..5102b5c --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/mapper/ThemeMapper.java @@ -0,0 +1,19 @@ +package de.grimsi.gameyfin.mapper; + +import com.igdb.proto.Igdb; +import de.grimsi.gameyfin.entities.Theme; + +import java.util.List; + +public class ThemeMapper { + public static Theme toTheme(Igdb.Theme g) { + return Theme.builder() + .slug(g.getSlug()) + .name(g.getName()) + .build(); + } + + public static List toThemes(List g) { + return g.stream().map(ThemeMapper::toTheme).toList(); + } +} diff --git a/src/main/java/de/grimsi/gameyfin/repositories/CompanyRepository.java b/src/main/java/de/grimsi/gameyfin/repositories/CompanyRepository.java new file mode 100644 index 0000000..9e2f7cc --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/repositories/CompanyRepository.java @@ -0,0 +1,7 @@ +package de.grimsi.gameyfin.repositories; + +import de.grimsi.gameyfin.entities.Company; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CompanyRepository extends JpaRepository { +} diff --git a/src/main/java/de/grimsi/gameyfin/repositories/DetectedGameRepository.java b/src/main/java/de/grimsi/gameyfin/repositories/DetectedGameRepository.java index 78ac2d0..ea2eb02 100644 --- a/src/main/java/de/grimsi/gameyfin/repositories/DetectedGameRepository.java +++ b/src/main/java/de/grimsi/gameyfin/repositories/DetectedGameRepository.java @@ -3,8 +3,21 @@ package de.grimsi.gameyfin.repositories; import de.grimsi.gameyfin.entities.DetectedGame; import org.springframework.data.jpa.repository.JpaRepository; +import javax.transaction.Transactional; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + public interface DetectedGameRepository extends JpaRepository { boolean existsByPath(String path); + boolean existsBySlug(String slug); + + List getAllByPathNotIn(Collection paths); + + default List getAllByPathNotIn(List paths) { + List pathStrings = paths.stream().map(Path::toString).toList(); + return getAllByPathNotIn(pathStrings); + } } diff --git a/src/main/java/de/grimsi/gameyfin/repositories/GenreRepository.java b/src/main/java/de/grimsi/gameyfin/repositories/GenreRepository.java new file mode 100644 index 0000000..08b49aa --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/repositories/GenreRepository.java @@ -0,0 +1,7 @@ +package de.grimsi.gameyfin.repositories; + +import de.grimsi.gameyfin.entities.Genre; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface GenreRepository extends JpaRepository { +} diff --git a/src/main/java/de/grimsi/gameyfin/repositories/KeywordRepository.java b/src/main/java/de/grimsi/gameyfin/repositories/KeywordRepository.java new file mode 100644 index 0000000..7d8ecaf --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/repositories/KeywordRepository.java @@ -0,0 +1,7 @@ +package de.grimsi.gameyfin.repositories; + +import de.grimsi.gameyfin.entities.Keyword; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface KeywordRepository extends JpaRepository { +} diff --git a/src/main/java/de/grimsi/gameyfin/repositories/PlayerPerspectiveRepository.java b/src/main/java/de/grimsi/gameyfin/repositories/PlayerPerspectiveRepository.java new file mode 100644 index 0000000..8762c53 --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/repositories/PlayerPerspectiveRepository.java @@ -0,0 +1,7 @@ +package de.grimsi.gameyfin.repositories; + +import de.grimsi.gameyfin.entities.PlayerPerspective; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PlayerPerspectiveRepository extends JpaRepository { +} diff --git a/src/main/java/de/grimsi/gameyfin/repositories/ThemeRepository.java b/src/main/java/de/grimsi/gameyfin/repositories/ThemeRepository.java new file mode 100644 index 0000000..d29c7ff --- /dev/null +++ b/src/main/java/de/grimsi/gameyfin/repositories/ThemeRepository.java @@ -0,0 +1,7 @@ +package de.grimsi.gameyfin.repositories; + +import de.grimsi.gameyfin.entities.Keyword; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ThemeRepository extends JpaRepository { +} diff --git a/src/main/java/de/grimsi/gameyfin/service/FilesystemService.java b/src/main/java/de/grimsi/gameyfin/service/FilesystemService.java index 70527e7..a5ecb59 100644 --- a/src/main/java/de/grimsi/gameyfin/service/FilesystemService.java +++ b/src/main/java/de/grimsi/gameyfin/service/FilesystemService.java @@ -12,6 +12,7 @@ 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; +import org.springframework.util.StopWatch; import java.io.IOException; import java.nio.file.Files; @@ -19,6 +20,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; @@ -59,12 +61,20 @@ public class FilesystemService { } public void scanGameLibrary() { - log.info("Starting scan..."); + StopWatch stopWatch = new StopWatch(); - AtomicInteger newBlacklistCounter = new AtomicInteger(); + log.info("Starting scan..."); + stopWatch.start(); + + AtomicInteger newUnmappedFilesCounter = new AtomicInteger(); List gameFiles = getGameFiles(); + // Check if any games that are in the library have been removed from the file system + // This would include renamed files, but they will be re-detected by the next step + List deletedGames = detectedGameRepository.getAllByPathNotIn(gameFiles); + detectedGameRepository.deleteAll(deletedGames); + // 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())) @@ -74,12 +84,13 @@ public class FilesystemService { // For each new game, load the info from IGDB // If a game is not found on IGDB, add it to the list of unmapped files so we won't query the API later on for the same path + // If a game is not found on IGDB, blacklist the path, so we won't query the API later for the same path List newDetectedGames = gameFiles.parallelStream() .map(p -> { Optional optionalGame = igdbWrapper.searchForGameByTitle(getFilename(p)); return optionalGame.map(game -> Map.entry(p, game)).or(() -> { unmappableFileRepository.save(new UnmappableFile(p.toString())); - newBlacklistCounter.getAndIncrement(); + newUnmappedFilesCounter.getAndIncrement(); log.info("Added path '{}' to blacklist", p); return Optional.empty(); }); @@ -91,7 +102,15 @@ public class FilesystemService { newDetectedGames = detectedGameRepository.saveAll(newDetectedGames); - log.info("Scan finished: Found {} new games, deleted {} games, could not map {} files/folders, {} games total.", newDetectedGames.size(), "NOT_IMPLEMENTED_YET", newBlacklistCounter.get(), detectedGameRepository.count()); + stopWatch.stop(); + + String scanDuration = "%dmin : %ds".formatted( + TimeUnit.MILLISECONDS.toMinutes(stopWatch.getLastTaskTimeMillis()), + TimeUnit.MILLISECONDS.toSeconds(stopWatch.getLastTaskTimeMillis()) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(stopWatch.getLastTaskTimeMillis())) + ); + + log.info("Scan finished in {}: Found {} new games, deleted {} games, could not map {} files/folders, {} games total.", + scanDuration, newDetectedGames.size(), deletedGames.size(), newUnmappedFilesCounter.get(), detectedGameRepository.count()); } private String getFilename(Path p) { diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5d47901..2572ec2 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,5 +1,5 @@ gameyfin: - root: \\NAS-Simon\Öffentlich\Spiele + root: D:\Games cache: C:\Projects\privat\gameyfin-library\.gameyfin db: C:\Projects\privat\gameyfin-library\.gameyfin igdb: @@ -9,4 +9,7 @@ gameyfin: logging: level: - de.grimsi: debug \ No newline at end of file + de.grimsi: debug + org.springframework.web.reactive.function.client.ExchangeFunctions: debug + +spring.mvc.log-request-details: true \ No newline at end of file diff --git a/src/test/java/de/grimsi/gameyfin/GameyfinApplicationTests.java b/src/test/java/de/grimsi/gameyfin/GameyfinApplicationTests.java deleted file mode 100644 index 26b2b88..0000000 --- a/src/test/java/de/grimsi/gameyfin/GameyfinApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.grimsi.gameyfin; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class GameyfinApplicationTests { - - @Test - void contextLoads() { - } - -}