From daa8b7ee6c82547c7b5fd60eb9e76ef3686f268a Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Mon, 26 May 2025 10:03:44 +0200 Subject: [PATCH] Implement entity listeners for Game and Library --- .../de/grimsi/gameyfin/games/GameEndpoint.kt | 3 +- .../de/grimsi/gameyfin/games/GameService.kt | 50 +++++------- .../de/grimsi/gameyfin/games/entities/Game.kt | 1 + .../games/entities/GameEntityListener.kt | 28 +++++++ .../games/entities/LibraryEntityListener.kt | 29 +++++++ .../de/grimsi/gameyfin/libraries/Library.kt | 2 + .../gameyfin/libraries/LibraryEndpoint.kt | 2 +- .../gameyfin/libraries/LibraryService.kt | 80 ++++++++----------- 8 files changed, 116 insertions(+), 79 deletions(-) create mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/GameEntityListener.kt create mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/LibraryEntityListener.kt diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt index 890fcec..943311d 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameEndpoint.kt @@ -15,7 +15,7 @@ class GameEndpoint( private val gameService: GameService ) { fun subscribe(): Flux { - return gameService.subscribe() + return GameService.subscribe() } fun getAll(): List = gameService.getAll() @@ -25,5 +25,4 @@ class GameEndpoint( @RolesAllowed(Role.Names.ADMIN) fun deleteGame(gameId: Long) = gameService.delete(gameId) - } \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt index cf5d184..2a303cf 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt @@ -17,7 +17,6 @@ import de.grimsi.gameyfin.libraries.Library import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider import io.github.oshai.kotlinlogging.KotlinLogging -import jakarta.persistence.EntityManager import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking @@ -37,26 +36,31 @@ class GameService( private val pluginService: PluginService, private val config: ConfigService, private val companyService: CompanyService, - private val gameRepository: GameRepository, - private val entityManager: EntityManager + private val gameRepository: GameRepository ) { companion object { private val log = KotlinLogging.logger {} + + /* Websockets */ + private val gameEvents = Sinks.many().multicast().onBackpressureBuffer(1024, false) + + fun subscribe(): Flux { + log.debug { "New subscription for gameUpdates" } + return gameEvents.asFlux() + .doOnSubscribe { log.debug { "Subscriber added to gameEvents [${gameEvents.currentSubscriberCount()}]" } } + .doFinally { + log.debug { "Subscriber removed from gameEvents with signal type $it [${gameEvents.currentSubscriberCount()}]" } + } + } + + fun emit(event: GameEvent) { + gameEvents.tryEmitNext(event) + } } private val metadataPlugins: List get() = pluginManager.getExtensions(GameMetadataProvider::class.java) - private val gameEvents = Sinks.many().multicast().onBackpressureBuffer(1024, false) - - fun subscribe(): Flux { - log.debug { "New subscription for gameUpdates" } - return gameEvents.asFlux() - .doOnSubscribe { log.debug { "Subscriber added to gameEvents [${gameEvents.currentSubscriberCount()}]" } } - .doFinally { - log.debug { "Subscriber removed from gameEvents with signal type $it [${gameEvents.currentSubscriberCount()}]" } - } - } fun getAll(): List { val entities = gameRepository.findAll() @@ -73,16 +77,7 @@ class GameService( game } - val games = gameRepository.saveAll(gamesToBePersisted) - - // force flush to populate creation and update timestamp - entityManager.flush() - - games.forEach { game -> - val gameDto = game.toDto() - gameEvents.tryEmitNext(GameEvent.Created(gameDto)) - } - return games + return gameRepository.saveAll(gamesToBePersisted) } fun update(gameUpdateDto: GameUpdateDto) { @@ -94,18 +89,11 @@ class GameService( gameUpdateDto.comment?.let { existingGame.comment = it } gameUpdateDto.summary?.let { existingGame.summary = it } - val updatedGame = gameRepository.save(existingGame) - val updatedGameDto = updatedGame.toDto() - gameEvents.tryEmitNext(GameEvent.Updated(updatedGameDto)) + gameRepository.save(existingGame) } fun delete(gameId: Long) { gameRepository.deleteById(gameId) - gameEvents.tryEmitNext(GameEvent.Deleted(gameId)) - } - - fun emitDeletionEvent(gameId: Long) { - gameEvents.tryEmitNext(GameEvent.Deleted(gameId)) } fun matchFromFile(path: Path, library: Library): Game? { diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt index 5a254e0..eb1e070 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt @@ -13,6 +13,7 @@ import java.net.URI import java.time.Instant @Entity +@EntityListeners(GameEntityListener::class) class Game( @Id @GeneratedValue(strategy = GenerationType.AUTO) diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/GameEntityListener.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/GameEntityListener.kt new file mode 100644 index 0000000..f056c81 --- /dev/null +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/GameEntityListener.kt @@ -0,0 +1,28 @@ +package de.grimsi.gameyfin.games.entities + +import de.grimsi.gameyfin.games.GameService +import de.grimsi.gameyfin.games.dto.GameEvent +import de.grimsi.gameyfin.games.toDto +import jakarta.persistence.PostPersist +import jakarta.persistence.PostRemove +import jakarta.persistence.PostUpdate + +class GameEntityListener { + @PostPersist + fun created(game: Game) { + val event = GameEvent.Created(game.toDto()) + GameService.emit(event) + } + + @PostUpdate + fun updated(game: Game) { + val event = GameEvent.Updated(game.toDto()) + GameService.emit(event) + } + + @PostRemove + fun deleted(game: Game) { + val event = GameEvent.Deleted(game.id!!) + GameService.emit(event) + } +} \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/LibraryEntityListener.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/LibraryEntityListener.kt new file mode 100644 index 0000000..85ac207 --- /dev/null +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/LibraryEntityListener.kt @@ -0,0 +1,29 @@ +package de.grimsi.gameyfin.games.entities + +import de.grimsi.gameyfin.libraries.Library +import de.grimsi.gameyfin.libraries.LibraryService +import de.grimsi.gameyfin.libraries.dto.LibraryEvent +import de.grimsi.gameyfin.libraries.toDto +import jakarta.persistence.PostPersist +import jakarta.persistence.PostRemove +import jakarta.persistence.PostUpdate + +class LibraryEntityListener { + @PostPersist + fun created(library: Library) { + val event = LibraryEvent.Created(library.toDto()) + LibraryService.emit(event) + } + + @PostUpdate + fun updated(library: Library) { + val event = LibraryEvent.Updated(library.toDto()) + LibraryService.emit(event) + } + + @PostRemove + fun deleted(library: Library) { + val event = LibraryEvent.Deleted(library.id!!) + LibraryService.emit(event) + } +} \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt index e726054..48411d6 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt @@ -1,9 +1,11 @@ package de.grimsi.gameyfin.libraries import de.grimsi.gameyfin.games.entities.Game +import de.grimsi.gameyfin.games.entities.LibraryEntityListener import jakarta.persistence.* @Entity +@EntityListeners(LibraryEntityListener::class) class Library( @Id @GeneratedValue(strategy = GenerationType.AUTO) diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt index e237f90..8c174fb 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt @@ -16,7 +16,7 @@ class LibraryEndpoint( private val libraryService: LibraryService ) { fun subscribe(): Flux { - return libraryService.subscribe() + return LibraryService.subscribe() } fun getAll() = libraryService.getAll() diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt index 6da7bae..690d724 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt @@ -28,25 +28,31 @@ class LibraryService( companion object { private val log = KotlinLogging.logger {} private val executor = Executors.newVirtualThreadPerTaskExecutor() + + /* Websockets */ + private val libraryEvents = Sinks.many().multicast().onBackpressureBuffer(1024, false) + + fun subscribe(): Flux { + log.debug { "New subscription for libraryEvents" } + return libraryEvents.asFlux() + .doOnSubscribe { log.debug { "Subscriber added to libraryEvents [${libraryEvents.currentSubscriberCount()}]" } } + .doFinally { + log.debug { "Subscriber removed from libraryEvents with signal type $it [${libraryEvents.currentSubscriberCount()}]" } + } + } + + fun emit(event: LibraryEvent) { + libraryEvents.tryEmitNext(event) + } } - private val libraryEvents = Sinks.many().multicast().onBackpressureBuffer(1024, false) - - fun subscribe(): Flux { - log.debug { "New subscription for libraryEvents" } - return libraryEvents.asFlux() - .doOnSubscribe { log.debug { "Subscriber added to libraryEvents [${libraryEvents.currentSubscriberCount()}]" } } - .doFinally { - log.debug { "Subscriber removed from libraryEvents with signal type $it [${libraryEvents.currentSubscriberCount()}]" } - } - } /** * Retrieves all libraries from the repository. */ fun getAll(): List { val entities = libraryRepository.findAll() - return entities.map { toDto(it) } + return entities.map { it.toDto() } } /** @@ -57,8 +63,6 @@ class LibraryService( */ fun create(library: LibraryDto) { val entity = libraryRepository.save(toEntity(library)) - val libraryDto = toDto(entity) - libraryEvents.tryEmitNext(LibraryEvent.Created(libraryDto)) } /** @@ -81,9 +85,7 @@ class LibraryService( ) } - val updatedLibrary = libraryRepository.save(existingLibrary) - val updatedLibraryDto = toDto(updatedLibrary) - libraryEvents.tryEmitNext(LibraryEvent.Updated(updatedLibraryDto)) + libraryRepository.save(existingLibrary) } /** @@ -92,13 +94,7 @@ class LibraryService( * @param libraryId: ID of the library to delete. */ fun delete(libraryId: Long) { - val gameIds = libraryRepository.findByIdOrNull(libraryId)?.games?.mapNotNull { it.id } - ?: throw IllegalArgumentException("Library with ID $libraryId not found") - libraryRepository.deleteById(libraryId) - - libraryEvents.tryEmitNext(LibraryEvent.Deleted(libraryId)) - gameIds.forEach { gameService.emitDeletionEvent(it) } } /** @@ -256,29 +252,6 @@ class LibraryService( } } - /** - * Converts a Library entity to a LibraryDto. - * - * @param library: The Library entity to convert. - * @return The converted LibraryDto. - */ - private fun toDto(library: Library): LibraryDto { - val libraryId = library.id ?: throw IllegalArgumentException("Library ID is null") - - val statsDto = LibraryStatsDto( - gamesCount = library.games.size, - downloadedGamesCount = library.games.sumOf { it.downloadCount } - ) - - return LibraryDto( - id = libraryId, - name = library.name, - directories = library.directories.map { DirectoryMappingDto(it.internalPath, it.externalPath) }, - games = library.games.mapNotNull { it.id }, - stats = statsDto - ) - } - /** * Adds a collection of games to the library. * @@ -306,4 +279,21 @@ class LibraryService( }.toMutableList() ) } +} + +fun Library.toDto(): LibraryDto { + val libraryId = this.id ?: throw IllegalArgumentException("Library ID is null") + + val statsDto = LibraryStatsDto( + gamesCount = this.games.size, + downloadedGamesCount = this.games.sumOf { it.downloadCount } + ) + + return LibraryDto( + id = libraryId, + name = this.name, + directories = this.directories.map { DirectoryMappingDto(it.internalPath, it.externalPath) }, + games = this.games.mapNotNull { it.id }, + stats = statsDto + ) } \ No newline at end of file