From 5a3077d219a4bc3bb3c3a512e3f2833e58288124 Mon Sep 17 00:00:00 2001 From: Simon <9295182+grimsi@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:59:31 +0100 Subject: [PATCH] Release 2.2.1 (#799) * chore: bump version to v2.2.1-preview * Fix Platform filter being ignored in matchManually (#798) * Fix platforms being ignored in manual match * Update tests --- .../org/gameyfin/app/games/GameService.kt | 7 +- .../org/gameyfin/app/games/entities/Game.kt | 2 +- .../org/gameyfin/app/games/GameServiceTest.kt | 181 +++++++++++++++++- .../games/extensions/GameExtensionsTest.kt | 12 +- .../app/requests/GameRequestServiceTest.kt | 10 +- build.gradle.kts | 2 +- 6 files changed, 196 insertions(+), 18 deletions(-) diff --git a/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt b/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt index b9b7ef8..d3f402a 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt @@ -614,7 +614,10 @@ class GameService( // Step 3: Merge results into a single Game entity val mergedGame = mergeResults(validResults, path, library) - // Step 4: If a replaceGameId is provided, set it (overwriting the existing entity) + // Step 4: Filter platforms to only those supported by the library + mergedGame.platforms = library.platforms.intersect(mergedGame.platforms.toSet()).toMutableList() + + // Step 5: If a replaceGameId is provided, set it (overwriting the existing entity) if (replaceGameId != null) { val existingGame = getById(replaceGameId) @@ -802,7 +805,7 @@ class GameService( metadata.platforms?.takeIf { it.isNotEmpty() }?.let { platforms -> if (!metadataMap.containsKey("platforms")) { - mergedGame.platforms = platforms.toList() + mergedGame.platforms = platforms.toMutableList() metadataMap["platforms"] = GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin)) } diff --git a/app/src/main/kotlin/org/gameyfin/app/games/entities/Game.kt b/app/src/main/kotlin/org/gameyfin/app/games/entities/Game.kt index 9fe9681..63d4cfc 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/entities/Game.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/entities/Game.kt @@ -30,7 +30,7 @@ class Game( @ElementCollection(targetClass = Platform::class, fetch = FetchType.EAGER) @Enumerated(EnumType.STRING) - var platforms: List = emptyList(), + var platforms: MutableList = mutableListOf(), var title: String? = null, diff --git a/app/src/test/kotlin/org/gameyfin/app/games/GameServiceTest.kt b/app/src/test/kotlin/org/gameyfin/app/games/GameServiceTest.kt index 0d7606f..fddab06 100644 --- a/app/src/test/kotlin/org/gameyfin/app/games/GameServiceTest.kt +++ b/app/src/test/kotlin/org/gameyfin/app/games/GameServiceTest.kt @@ -639,7 +639,7 @@ class GameServiceTest { val game = createTestGame(1L) game.metadata.originalIds = mapOf(pluginEntry to "123") game.title = "Test Game" - game.platforms = listOf(Platform.PC_MICROSOFT_WINDOWS) + game.platforms = mutableListOf(Platform.PC_MICROSOFT_WINDOWS) // Add existing field metadata to simulate a previously matched game game.metadata.fields["title"] = GameFieldMetadata(source = GameFieldPluginSource(plugin = pluginEntry)) game.metadata.fields["platforms"] = GameFieldMetadata(source = GameFieldPluginSource(plugin = pluginEntry)) @@ -658,6 +658,7 @@ class GameServiceTest { every { pluginService.getPluginManagementEntry(provider.javaClass) } returns pluginEntry every { imageService.createOrGet(any()) } returns mockk(relaxed = true) every { filesystemService.calculateFileSize(any()) } returns 1000L + every { library.platforms } returns mutableListOf(Platform.PC_MICROSOFT_WINDOWS) val result = gameService.update(game) @@ -704,7 +705,7 @@ class GameServiceTest { val game = createTestGame(1L) game.metadata.originalIds = mapOf(pluginEntry to "123") game.title = "User Modified Title" - game.platforms = listOf(Platform.PC_MICROSOFT_WINDOWS) + game.platforms = mutableListOf(Platform.PC_MICROSOFT_WINDOWS) game.metadata.fields["title"] = GameFieldMetadata(source = GameFieldUserSource(user = mockUser)) game.metadata.fields["platforms"] = GameFieldMetadata(source = GameFieldPluginSource(plugin = pluginEntry)) @@ -722,6 +723,7 @@ class GameServiceTest { every { pluginService.getPluginManagementEntry(provider.javaClass) } returns pluginEntry every { imageService.createOrGet(any()) } returns mockk(relaxed = true) every { filesystemService.calculateFileSize(any()) } returns 1000L + every { library.platforms } returns mutableListOf(Platform.PC_MICROSOFT_WINDOWS) val result = gameService.update(game) @@ -1195,6 +1197,179 @@ class GameServiceTest { assertEquals(listOf(Genre.ACTION), result.genres) } + @Test + fun `matchManually should filter platforms to only those supported by the library`() { + val metadata = org.gameyfin.pluginapi.gamemetadata.GameMetadata( + originalId = "123", + title = "Multi-Platform Game", + platforms = setOf( + Platform.PC_MICROSOFT_WINDOWS, + Platform.PLAYSTATION_5, + Platform.XBOX_SERIES_X_S, + Platform.NINTENDO_SWITCH + ) + ) + + val provider = spyk(TestProvider(metadata)) + val pluginEntry = mockk(relaxed = true) { + every { pluginId } returns "test-plugin" + every { priority } returns 1 + } + + val originalIds = mapOf( + provider.javaClass.name to ExternalProviderIdDto("test-plugin", "123") + ) + val path = Path.of("/test/game.exe") + + // Library only supports PC and PlayStation 5 + val restrictedLibrary = mockk(relaxed = true) { + every { id } returns 1L + every { platforms } returns mutableListOf(Platform.PC_MICROSOFT_WINDOWS, Platform.PLAYSTATION_5) + } + + every { pluginManager.getExtensions(GameMetadataProvider::class.java) } returns listOf(provider) + every { pluginService.getPluginManagementEntry(provider.javaClass) } returns pluginEntry + every { imageService.createOrGet(any()) } returns mockk(relaxed = true) + every { filesystemService.calculateFileSize(any()) } returns 1000L + + val result = gameService.matchManually(originalIds, path, restrictedLibrary, null, persist = false) + + assertNotNull(result) + assertEquals("Multi-Platform Game", result.title) + // Platforms should be filtered to only PC and PlayStation 5 + assertEquals(2, result.platforms.size) + assertEquals( + setOf(Platform.PC_MICROSOFT_WINDOWS, Platform.PLAYSTATION_5), + result.platforms.toSet() + ) + } + + @Test + fun `matchManually should return empty platforms when library platforms don't match metadata platforms`() { + val metadata = org.gameyfin.pluginapi.gamemetadata.GameMetadata( + originalId = "123", + title = "Console Exclusive Game", + platforms = setOf(Platform.PLAYSTATION_5, Platform.XBOX_SERIES_X_S) + ) + + val provider = spyk(TestProvider(metadata)) + val pluginEntry = mockk(relaxed = true) { + every { pluginId } returns "test-plugin" + every { priority } returns 1 + } + + val originalIds = mapOf( + provider.javaClass.name to ExternalProviderIdDto("test-plugin", "123") + ) + val path = Path.of("/test/game.exe") + + // Library only supports PC + val pcOnlyLibrary = mockk(relaxed = true) { + every { id } returns 1L + every { platforms } returns mutableListOf(Platform.PC_MICROSOFT_WINDOWS) + } + + every { pluginManager.getExtensions(GameMetadataProvider::class.java) } returns listOf(provider) + every { pluginService.getPluginManagementEntry(provider.javaClass) } returns pluginEntry + every { imageService.createOrGet(any()) } returns mockk(relaxed = true) + every { filesystemService.calculateFileSize(any()) } returns 1000L + + val result = gameService.matchManually(originalIds, path, pcOnlyLibrary, null, persist = false) + + assertNotNull(result) + assertEquals("Console Exclusive Game", result.title) + // Platforms should be empty since there's no intersection + assertEquals(0, result.platforms.size) + } + + @Test + fun `matchManually should preserve all platforms when library platforms list is empty`() { + val metadata = org.gameyfin.pluginapi.gamemetadata.GameMetadata( + originalId = "123", + title = "Multi-Platform Game", + platforms = setOf(Platform.PC_MICROSOFT_WINDOWS, Platform.PLAYSTATION_5) + ) + + val provider = spyk(TestProvider(metadata)) + val pluginEntry = mockk(relaxed = true) { + every { pluginId } returns "test-plugin" + every { priority } returns 1 + } + + val originalIds = mapOf( + provider.javaClass.name to ExternalProviderIdDto("test-plugin", "123") + ) + val path = Path.of("/test/game.exe") + + // Library with no platform restrictions + val unrestrictedLibrary = mockk(relaxed = true) { + every { id } returns 1L + every { platforms } returns mutableListOf() + } + + every { pluginManager.getExtensions(GameMetadataProvider::class.java) } returns listOf(provider) + every { pluginService.getPluginManagementEntry(provider.javaClass) } returns pluginEntry + every { imageService.createOrGet(any()) } returns mockk(relaxed = true) + every { filesystemService.calculateFileSize(any()) } returns 1000L + + val result = gameService.matchManually(originalIds, path, unrestrictedLibrary, null, persist = false) + + assertNotNull(result) + assertEquals("Multi-Platform Game", result.title) + // When library has no platform restrictions, result should have no platforms (empty intersect empty) + assertEquals(0, result.platforms.size) + } + + @Test + fun `matchManually should filter platforms when replacing an existing game`() { + val metadata = org.gameyfin.pluginapi.gamemetadata.GameMetadata( + originalId = "123", + title = "Updated Game", + platforms = setOf( + Platform.PC_MICROSOFT_WINDOWS, + Platform.PLAYSTATION_5, + Platform.XBOX_SERIES_X_S + ) + ) + + val provider = spyk(TestProvider(metadata)) + val pluginEntry = mockk(relaxed = true) { + every { pluginId } returns "test-plugin" + every { priority } returns 1 + } + + val originalIds = mapOf( + provider.javaClass.name to ExternalProviderIdDto("test-plugin", "123") + ) + val path = Path.of("/test/game.exe") + val replaceGameId = 5L + val existingGame = createTestGame(replaceGameId) + + // Library only supports PC and PlayStation 5 + val restrictedLibrary = mockk(relaxed = true) { + every { id } returns 1L + every { platforms } returns mutableListOf(Platform.PC_MICROSOFT_WINDOWS, Platform.PLAYSTATION_5) + } + + every { pluginManager.getExtensions(GameMetadataProvider::class.java) } returns listOf(provider) + every { pluginService.getPluginManagementEntry(provider.javaClass) } returns pluginEntry + every { gameRepository.findByIdOrNull(replaceGameId) } returns existingGame + every { imageService.createOrGet(any()) } returns mockk(relaxed = true) + every { filesystemService.calculateFileSize(any()) } returns 1000L + + val result = gameService.matchManually(originalIds, path, restrictedLibrary, replaceGameId, persist = false) + + assertNotNull(result) + assertEquals(replaceGameId, result.id) + assertEquals("Updated Game", result.title) + // Platforms should be filtered even when replacing + assertEquals(2, result.platforms.size) + assertEquals( + setOf(Platform.PC_MICROSOFT_WINDOWS, Platform.PLAYSTATION_5), + result.platforms.toSet() + ) + } + private fun createTestGame(id: Long?, title: String = "Test Game"): Game { return Game( id = id, @@ -1202,7 +1377,7 @@ class GameServiceTest { updatedAt = Instant.now(), library = library, title = title, - platforms = listOf(Platform.PC_MICROSOFT_WINDOWS), + platforms = mutableListOf(Platform.PC_MICROSOFT_WINDOWS), coverImage = mockk(), headerImage = mockk(), comment = "Comment", diff --git a/app/src/test/kotlin/org/gameyfin/app/games/extensions/GameExtensionsTest.kt b/app/src/test/kotlin/org/gameyfin/app/games/extensions/GameExtensionsTest.kt index 927e660..9206283 100644 --- a/app/src/test/kotlin/org/gameyfin/app/games/extensions/GameExtensionsTest.kt +++ b/app/src/test/kotlin/org/gameyfin/app/games/extensions/GameExtensionsTest.kt @@ -159,7 +159,7 @@ class GameExtensionsTest { updatedAt = Instant.now(), library = library, title = "Test Game", - platforms = emptyList(), + platforms = mutableListOf(), metadata = org.gameyfin.app.games.entities.GameMetadata(path = "/test/path") ) @@ -183,7 +183,7 @@ class GameExtensionsTest { updatedAt = Instant.now(), library = library, title = "Test Game", - platforms = emptyList(), + platforms = mutableListOf(), metadata = org.gameyfin.app.games.entities.GameMetadata(path = "/test/path") ) @@ -208,7 +208,7 @@ class GameExtensionsTest { updatedAt = Instant.now(), library = library, title = "Test Game", - platforms = emptyList(), + platforms = mutableListOf(), release = releaseInstant, metadata = org.gameyfin.app.games.entities.GameMetadata(path = "/test/path") ) @@ -227,7 +227,7 @@ class GameExtensionsTest { updatedAt = Instant.now(), library = library, title = "Test Game", - platforms = emptyList(), + platforms = mutableListOf(), videoUrls = listOf(URI("https://example.com/video1"), URI("https://example.com/video2")), metadata = org.gameyfin.app.games.entities.GameMetadata(path = "/test/path") ) @@ -255,7 +255,7 @@ class GameExtensionsTest { updatedAt = Instant.now(), library = library, title = "Test Game", - platforms = emptyList(), + platforms = mutableListOf(), images = mutableListOf(image1, image2, image3), metadata = org.gameyfin.app.games.entities.GameMetadata(path = "/test/path") ) @@ -332,7 +332,7 @@ class GameExtensionsTest { updatedAt = Instant.now(), library = library, title = "Test Game", - platforms = listOf(Platform.PC_MICROSOFT_WINDOWS), + platforms = mutableListOf(Platform.PC_MICROSOFT_WINDOWS), coverImage = coverImage, headerImage = headerImage, comment = "Test comment", diff --git a/app/src/test/kotlin/org/gameyfin/app/requests/GameRequestServiceTest.kt b/app/src/test/kotlin/org/gameyfin/app/requests/GameRequestServiceTest.kt index 32e17bb..6d3ea47 100644 --- a/app/src/test/kotlin/org/gameyfin/app/requests/GameRequestServiceTest.kt +++ b/app/src/test/kotlin/org/gameyfin/app/requests/GameRequestServiceTest.kt @@ -639,7 +639,7 @@ class GameRequestServiceTest { @Test fun `onGameCreated should complete matching requests`() { val game = createTestGame(1L, "Test Game") - game.platforms = listOf(Platform.PC_MICROSOFT_WINDOWS, Platform.PLAYSTATION_5) + game.platforms = mutableListOf(Platform.PC_MICROSOFT_WINDOWS, Platform.PLAYSTATION_5) val request1 = createTestGameRequest(1L, "Test Game") request1.status = GameRequestStatus.PENDING val request2 = createTestGameRequest(2L, "Test Game") @@ -691,7 +691,7 @@ class GameRequestServiceTest { @Test fun `onGameCreated should handle multiple platforms`() { val game = createTestGame(1L, "Multi Platform Game") - game.platforms = listOf(Platform.PC_MICROSOFT_WINDOWS, Platform.PLAYSTATION_5, Platform.XBOX_SERIES_X_S) + game.platforms = mutableListOf(Platform.PC_MICROSOFT_WINDOWS, Platform.PLAYSTATION_5, Platform.XBOX_SERIES_X_S) val request1 = createTestGameRequest(1L, "Multi Platform Game") val request2 = createTestGameRequest(2L, "Multi Platform Game") val request3 = createTestGameRequest(3L, "Multi Platform Game") @@ -727,7 +727,7 @@ class GameRequestServiceTest { @Test fun `onGameUpdated should complete matching requests`() { val game = createTestGame(1L, "Updated Game") - game.platforms = listOf(Platform.NINTENDO_SWITCH) + game.platforms = mutableListOf(Platform.NINTENDO_SWITCH) val request = createTestGameRequest(1L, "Updated Game") request.status = GameRequestStatus.PENDING @@ -751,7 +751,7 @@ class GameRequestServiceTest { @Test fun `onGameCreated should not update already fulfilled requests`() { val game = createTestGame(1L, "Test Game") - game.platforms = listOf(Platform.PC_MICROSOFT_WINDOWS) + game.platforms = mutableListOf(Platform.PC_MICROSOFT_WINDOWS) val request = createTestGameRequest(1L, "Test Game") request.status = GameRequestStatus.FULFILLED @@ -810,7 +810,7 @@ class GameRequestServiceTest { updatedAt = Instant.now(), library = mockk(relaxed = true), title = title, - platforms = listOf(Platform.PC_MICROSOFT_WINDOWS), + platforms = mutableListOf(Platform.PC_MICROSOFT_WINDOWS), coverImage = null, headerImage = null, comment = null, diff --git a/build.gradle.kts b/build.gradle.kts index 69741e0..6887a99 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile import java.nio.file.Files group = "org.gameyfin" -version = "2.2.0" +version = "2.2.1-preview" allprojects { repositories {