Implement entity listeners for Game and Library

This commit is contained in:
grimsi
2025-05-26 10:03:44 +02:00
parent fe561c42b4
commit daa8b7ee6c
8 changed files with 116 additions and 79 deletions
@@ -15,7 +15,7 @@ class GameEndpoint(
private val gameService: GameService private val gameService: GameService
) { ) {
fun subscribe(): Flux<GameEvent> { fun subscribe(): Flux<GameEvent> {
return gameService.subscribe() return GameService.subscribe()
} }
fun getAll(): List<GameDto> = gameService.getAll() fun getAll(): List<GameDto> = gameService.getAll()
@@ -25,5 +25,4 @@ class GameEndpoint(
@RolesAllowed(Role.Names.ADMIN) @RolesAllowed(Role.Names.ADMIN)
fun deleteGame(gameId: Long) = gameService.delete(gameId) fun deleteGame(gameId: Long) = gameService.delete(gameId)
} }
@@ -17,7 +17,6 @@ import de.grimsi.gameyfin.libraries.Library
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.persistence.EntityManager
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -37,26 +36,31 @@ class GameService(
private val pluginService: PluginService, private val pluginService: PluginService,
private val config: ConfigService, private val config: ConfigService,
private val companyService: CompanyService, private val companyService: CompanyService,
private val gameRepository: GameRepository, private val gameRepository: GameRepository
private val entityManager: EntityManager
) { ) {
companion object { companion object {
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
/* Websockets */
private val gameEvents = Sinks.many().multicast().onBackpressureBuffer<GameEvent>(1024, false)
fun subscribe(): Flux<GameEvent> {
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<GameMetadataProvider> private val metadataPlugins: List<GameMetadataProvider>
get() = pluginManager.getExtensions(GameMetadataProvider::class.java) get() = pluginManager.getExtensions(GameMetadataProvider::class.java)
private val gameEvents = Sinks.many().multicast().onBackpressureBuffer<GameEvent>(1024, false)
fun subscribe(): Flux<GameEvent> {
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<GameDto> { fun getAll(): List<GameDto> {
val entities = gameRepository.findAll() val entities = gameRepository.findAll()
@@ -73,16 +77,7 @@ class GameService(
game game
} }
val games = gameRepository.saveAll(gamesToBePersisted) return 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
} }
fun update(gameUpdateDto: GameUpdateDto) { fun update(gameUpdateDto: GameUpdateDto) {
@@ -94,18 +89,11 @@ class GameService(
gameUpdateDto.comment?.let { existingGame.comment = it } gameUpdateDto.comment?.let { existingGame.comment = it }
gameUpdateDto.summary?.let { existingGame.summary = it } gameUpdateDto.summary?.let { existingGame.summary = it }
val updatedGame = gameRepository.save(existingGame) gameRepository.save(existingGame)
val updatedGameDto = updatedGame.toDto()
gameEvents.tryEmitNext(GameEvent.Updated(updatedGameDto))
} }
fun delete(gameId: Long) { fun delete(gameId: Long) {
gameRepository.deleteById(gameId) gameRepository.deleteById(gameId)
gameEvents.tryEmitNext(GameEvent.Deleted(gameId))
}
fun emitDeletionEvent(gameId: Long) {
gameEvents.tryEmitNext(GameEvent.Deleted(gameId))
} }
fun matchFromFile(path: Path, library: Library): Game? { fun matchFromFile(path: Path, library: Library): Game? {
@@ -13,6 +13,7 @@ import java.net.URI
import java.time.Instant import java.time.Instant
@Entity @Entity
@EntityListeners(GameEntityListener::class)
class Game( class Game(
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@@ -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)
}
}
@@ -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)
}
}
@@ -1,9 +1,11 @@
package de.grimsi.gameyfin.libraries package de.grimsi.gameyfin.libraries
import de.grimsi.gameyfin.games.entities.Game import de.grimsi.gameyfin.games.entities.Game
import de.grimsi.gameyfin.games.entities.LibraryEntityListener
import jakarta.persistence.* import jakarta.persistence.*
@Entity @Entity
@EntityListeners(LibraryEntityListener::class)
class Library( class Library(
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@@ -16,7 +16,7 @@ class LibraryEndpoint(
private val libraryService: LibraryService private val libraryService: LibraryService
) { ) {
fun subscribe(): Flux<LibraryEvent> { fun subscribe(): Flux<LibraryEvent> {
return libraryService.subscribe() return LibraryService.subscribe()
} }
fun getAll() = libraryService.getAll() fun getAll() = libraryService.getAll()
@@ -28,25 +28,31 @@ class LibraryService(
companion object { companion object {
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
private val executor = Executors.newVirtualThreadPerTaskExecutor() private val executor = Executors.newVirtualThreadPerTaskExecutor()
/* Websockets */
private val libraryEvents = Sinks.many().multicast().onBackpressureBuffer<LibraryEvent>(1024, false)
fun subscribe(): Flux<LibraryEvent> {
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<LibraryEvent>(1024, false)
fun subscribe(): Flux<LibraryEvent> {
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. * Retrieves all libraries from the repository.
*/ */
fun getAll(): List<LibraryDto> { fun getAll(): List<LibraryDto> {
val entities = libraryRepository.findAll() val entities = libraryRepository.findAll()
return entities.map { toDto(it) } return entities.map { it.toDto() }
} }
/** /**
@@ -57,8 +63,6 @@ class LibraryService(
*/ */
fun create(library: LibraryDto) { fun create(library: LibraryDto) {
val entity = libraryRepository.save(toEntity(library)) 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) libraryRepository.save(existingLibrary)
val updatedLibraryDto = toDto(updatedLibrary)
libraryEvents.tryEmitNext(LibraryEvent.Updated(updatedLibraryDto))
} }
/** /**
@@ -92,13 +94,7 @@ class LibraryService(
* @param libraryId: ID of the library to delete. * @param libraryId: ID of the library to delete.
*/ */
fun delete(libraryId: Long) { fun delete(libraryId: Long) {
val gameIds = libraryRepository.findByIdOrNull(libraryId)?.games?.mapNotNull { it.id }
?: throw IllegalArgumentException("Library with ID $libraryId not found")
libraryRepository.deleteById(libraryId) 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. * Adds a collection of games to the library.
* *
@@ -307,3 +280,20 @@ class LibraryService(
) )
} }
} }
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
)
}