Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.gameyfin.app.core.config

import org.gameyfin.app.core.interceptors.EntityUpdateInterceptor
import org.hibernate.cfg.AvailableSettings
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class JpaConfiguration {

@Bean
fun hibernatePropertiesCustomizer(entityUpdateInterceptor: EntityUpdateInterceptor): HibernatePropertiesCustomizer {
return HibernatePropertiesCustomizer { hibernateProperties ->
hibernateProperties[AvailableSettings.INTERCEPTOR] = entityUpdateInterceptor
}
}
}
10 changes: 7 additions & 3 deletions app/src/main/kotlin/org/gameyfin/app/core/events/Events.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ class RegistrationAttemptWithExistingEmailEvent(source: Any, val existingUser: U
class PasswordResetRequestEvent(source: Any, val token: Token<TokenType.PasswordReset>, val baseUrl: String) :
ApplicationEvent(source)

class AccountDeletedEvent(source: Any, val user: User, val baseUrl: String) : ApplicationEvent(source)

class LibraryScanScheduleUpdatedEvent(source: Any) : ApplicationEvent(source)

class GameCreatedEvent(source: Any, val game: Game) : ApplicationEvent(source)
class UserDeletedEvent(source: Any, val user: User, val baseUrl: String) : ApplicationEvent(source)
class UserUpdatedEvent(source: Any, val previousState: User, val currentState: User) : ApplicationEvent(source)

class GameCreatedEvent(source: Any, val game: Game) : ApplicationEvent(source)
class GameUpdatedEvent(source: Any, val previousState: Game, val currentState: Game) : ApplicationEvent(source)
class GameDeletedEvent(source: Any, val game: Game) : ApplicationEvent(source)

Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.gameyfin.app.core.interceptors

import org.gameyfin.app.core.events.GameUpdatedEvent
import org.gameyfin.app.core.events.UserUpdatedEvent
import org.gameyfin.app.games.entities.Game
import org.gameyfin.app.games.entities.Image
import org.gameyfin.app.users.entities.User
import org.gameyfin.app.util.EventPublisherHolder
import org.hibernate.Interceptor
import org.hibernate.type.Type
import org.springframework.stereotype.Component

@Component
class EntityUpdateInterceptor() : Interceptor {

override fun onFlushDirty(
entity: Any?,
id: Any?,
currentState: Array<out Any?>?,
previousState: Array<out Any?>?,
propertyNames: Array<out String>?,
types: Array<out Type>?
): Boolean {
if (entity == null || currentState == null || previousState == null || propertyNames == null) {
return false
}

when (entity) {
is Game -> {
val previousGame = reconstructGame(entity, previousState, propertyNames)
val currentGame = reconstructGame(entity, currentState, propertyNames)
EventPublisherHolder.publish(GameUpdatedEvent(this, previousGame, currentGame))
}

is User -> {
val previousUser = reconstructUser(entity, previousState, propertyNames)
val currentUser = reconstructUser(entity, currentState, propertyNames)
EventPublisherHolder.publish(UserUpdatedEvent(this, previousUser, currentUser))
}
}

return false
}

private fun reconstructGame(originalGame: Game, state: Array<out Any?>, propertyNames: Array<out String>): Game {
val reconstructed = Game(
library = originalGame.library,
metadata = originalGame.metadata
)

for (i in propertyNames.indices) {
when (propertyNames[i]) {
"id" -> reconstructed.id = state[i] as? Long
"createdAt" -> reconstructed.createdAt = state[i] as? java.time.Instant
"updatedAt" -> reconstructed.updatedAt = state[i] as? java.time.Instant
"title" -> reconstructed.title = state[i] as? String
"coverImage" -> reconstructed.coverImage = state[i] as? Image
"headerImage" -> reconstructed.headerImage = state[i] as? Image
"comment" -> reconstructed.comment = state[i] as? String
"summary" -> reconstructed.summary = state[i] as? String
"release" -> reconstructed.release = state[i] as? java.time.Instant
"userRating" -> reconstructed.userRating = state[i] as? Int
"criticRating" -> reconstructed.criticRating = state[i] as? Int
"images" -> {
@Suppress("UNCHECKED_CAST")
(state[i] as? MutableList<Image>)?.let { reconstructed.images = it }
}
}
}

return reconstructed
}

private fun reconstructUser(originalUser: User, state: Array<out Any?>, propertyNames: Array<out String>): User {
val reconstructed = User(
username = originalUser.username,
email = originalUser.email
)

for (i in propertyNames.indices) {
when (propertyNames[i]) {
"id" -> reconstructed.id = state[i] as? Long
"password" -> reconstructed.password = state[i] as? String
"oidcProviderId" -> reconstructed.oidcProviderId = state[i] as? String
"emailConfirmed" -> reconstructed.emailConfirmed = state[i] as? Boolean ?: false
"enabled" -> reconstructed.enabled = state[i] as? Boolean ?: false
"avatar" -> reconstructed.avatar = state[i] as? Image
"roles" -> {
@Suppress("UNCHECKED_CAST")
(state[i] as? List<org.gameyfin.app.core.Role>)?.let { reconstructed.roles = it }
}
}
}

return reconstructed
}
}
51 changes: 32 additions & 19 deletions app/src/main/kotlin/org/gameyfin/app/games/GameService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import reactor.core.publisher.Flux
import reactor.core.publisher.Sinks
import java.net.URI
import java.nio.file.Path
import java.time.ZoneId
import java.time.ZoneOffset
Expand Down Expand Up @@ -105,10 +104,9 @@ class GameService(
return entities.toDtos()
}

@Transactional
fun create(game: Game): Game? {
game.publishers = game.publishers.map { companyService.createOrGet(it) }
game.developers = game.developers.map { companyService.createOrGet(it) }
private fun create(game: Game): Game {
game.publishers = game.publishers.map { companyService.createOrGet(it) }.toMutableList()
game.developers = game.developers.map { companyService.createOrGet(it) }.toMutableList()

try {
game.coverImage?.let {
Expand All @@ -125,7 +123,6 @@ class GameService(
} catch (e: Exception) {
log.error { "Error downloading images for game '${game.title}' (${game.id}): ${e.message}" }
log.debug(e) {}
null
}

game.metadata.fileSize = filesystemService.calculateFileSize(game.metadata.path)
Expand All @@ -138,9 +135,11 @@ class GameService(
val gamesToBePersisted = games.filter { it.id == null }

gamesToBePersisted.forEach { game ->
game.publishers = game.publishers.map { companyService.createOrGet(it) }
game.developers = game.developers.map { companyService.createOrGet(it) }
game
game.publishers = game.publishers.map { companyService.createOrGet(it) }.toMutableList()
game.developers = game.developers.map { companyService.createOrGet(it) }.toMutableList()
game.coverImage?.let { game.coverImage = imageService.createOrGet(it) }
game.headerImage?.let { game.headerImage = imageService.createOrGet(it) }
game.images = game.images.map { imageService.createOrGet(it) }.toMutableList()
}

return gameRepository.saveAll(gamesToBePersisted)
Expand All @@ -166,14 +165,18 @@ class GameService(
existingGame.metadata.fields["release"]?.source = GameFieldUserSource(user = user)
}
gameUpdateDto.coverUrl?.let {
val newCoverImage = Image(originalUrl = URI.create(it).toURL(), type = ImageType.COVER)
val newCoverImage = imageService.createOrGet(
Image(originalUrl = it, type = ImageType.COVER)
)
imageService.downloadIfNew(newCoverImage)

existingGame.coverImage = newCoverImage
existingGame.metadata.fields["coverImage"]?.source = GameFieldUserSource(user = user)
}
gameUpdateDto.headerUrl?.let {
val newHeaderImage = Image(originalUrl = URI.create(it).toURL(), type = ImageType.HEADER)
val newHeaderImage = imageService.createOrGet(
Image(originalUrl = it, type = ImageType.HEADER)
)
imageService.downloadIfNew(newHeaderImage)

existingGame.headerImage = newHeaderImage
Expand All @@ -190,11 +193,13 @@ class GameService(
gameUpdateDto.developers?.let {
existingGame.developers =
it.map { name -> companyService.createOrGet(Company(name = name, type = CompanyType.DEVELOPER)) }
.toMutableList()
existingGame.metadata.fields["developers"]?.source = GameFieldUserSource(user = user)
}
gameUpdateDto.publishers?.let {
existingGame.publishers =
it.map { name -> companyService.createOrGet(Company(name = name, type = CompanyType.PUBLISHER)) }
.toMutableList()
existingGame.metadata.fields["publishers"]?.source = GameFieldUserSource(user = user)
}
gameUpdateDto.genres?.let {
Expand Down Expand Up @@ -378,7 +383,7 @@ class GameService(
"publishers",
game.publishers,
updatedGame.publishers,
{ game.publishers = it ?: emptyList() },
{ game.publishers = it ?: mutableListOf() },
updatedGame.metadata.fields["publishers"]
)

Expand All @@ -387,7 +392,7 @@ class GameService(
"developers",
game.developers,
updatedGame.developers,
{ game.developers = it ?: emptyList() },
{ game.developers = it ?: mutableListOf() },
updatedGame.metadata.fields["developers"]
)

Expand Down Expand Up @@ -441,7 +446,7 @@ class GameService(
"images",
game.images,
updatedGame.images,
{ game.images = it ?: emptyList() },
{ game.images = it ?: mutableListOf() },
updatedGame.metadata.fields["images"]
)

Expand Down Expand Up @@ -758,14 +763,18 @@ class GameService(
}
metadata.coverUrls?.firstOrNull()?.let { coverUrl ->
if (!metadataMap.containsKey("coverImage")) {
mergedGame.coverImage = Image(originalUrl = coverUrl.toURL(), type = ImageType.COVER)
mergedGame.coverImage = imageService.createOrGet(
Image(originalUrl = coverUrl.toString(), type = ImageType.COVER)
)
metadataMap["coverImage"] =
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
}
}
metadata.headerUrls?.firstOrNull()?.let { headerUrl ->
if (!metadataMap.containsKey("headerImage")) {
mergedGame.headerImage = Image(originalUrl = headerUrl.toURL(), type = ImageType.HEADER)
mergedGame.headerImage = imageService.createOrGet(
Image(originalUrl = headerUrl.toString(), type = ImageType.HEADER)
)
metadataMap["headerImage"] =
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
}
Expand Down Expand Up @@ -794,15 +803,15 @@ class GameService(
metadata.publishedBy?.takeIf { it.isNotEmpty() }?.let { publishedBy ->
if (!metadataMap.containsKey("publishers")) {
mergedGame.publishers =
publishedBy.map { Company(name = it, type = CompanyType.PUBLISHER) }
publishedBy.map { Company(name = it, type = CompanyType.PUBLISHER) }.toMutableList()
metadataMap["publishers"] =
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
}
}
metadata.developedBy?.takeIf { it.isNotEmpty() }?.let { developedBy ->
if (!metadataMap.containsKey("developers")) {
mergedGame.developers =
developedBy.map { Company(name = it, type = CompanyType.DEVELOPER) }
developedBy.map { Company(name = it, type = CompanyType.DEVELOPER) }.toMutableList()
metadataMap["developers"] =
GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
}
Expand Down Expand Up @@ -843,7 +852,11 @@ class GameService(
metadata.screenshotUrls?.takeIf { it.isNotEmpty() }?.let { screenshotUrls ->
if (!metadataMap.containsKey("images")) {
mergedGame.images = runBlocking {
screenshotUrls.map { Image(originalUrl = it.toURL(), type = ImageType.SCREENSHOT) }
screenshotUrls.map {
imageService.createOrGet(
Image(originalUrl = it.toString(), type = ImageType.SCREENSHOT)
)
}.toMutableList()
}
metadataMap["images"] = GameFieldMetadata(source = GameFieldPluginSource(plugin = sourcePlugin))
}
Expand Down
20 changes: 9 additions & 11 deletions app/src/main/kotlin/org/gameyfin/app/games/entities/Game.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.gameyfin.app.games.entities

import jakarta.persistence.*
import jakarta.persistence.CascadeType.*
import org.gameyfin.app.libraries.entities.Library
import org.gameyfin.pluginapi.gamemetadata.GameFeature
import org.gameyfin.pluginapi.gamemetadata.Genre
Expand Down Expand Up @@ -28,15 +29,14 @@ class Game(
var updatedAt: Instant? = null,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "library_id")
val library: Library,

var title: String? = null,

@OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true)
@ManyToOne(cascade = [PERSIST, MERGE, REFRESH], fetch = FetchType.EAGER)
var coverImage: Image? = null,

@OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true)
@ManyToOne(cascade = [PERSIST, MERGE, REFRESH], fetch = FetchType.EAGER)
var headerImage: Image? = null,

@Lob
Expand All @@ -53,11 +53,11 @@ class Game(

var criticRating: Int? = null,

@ManyToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH])
var publishers: List<Company> = emptyList(),
@ManyToMany(cascade = [PERSIST, MERGE, REFRESH], fetch = FetchType.EAGER)
var publishers: MutableList<Company> = mutableListOf(),

@ManyToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH])
var developers: List<Company> = emptyList(),
@ManyToMany(cascade = [PERSIST, MERGE, REFRESH], fetch = FetchType.EAGER)
var developers: MutableList<Company> = mutableListOf(),

@ElementCollection(targetClass = Genre::class)
var genres: List<Genre> = emptyList(),
Expand All @@ -74,16 +74,14 @@ class Game(
@ElementCollection(targetClass = PlayerPerspective::class)
var perspectives: List<PlayerPerspective> = emptyList(),

@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
var images: List<Image> = emptyList(),
@ManyToMany(cascade = [PERSIST, MERGE, REFRESH], fetch = FetchType.EAGER)
var images: MutableList<Image> = mutableListOf(),

@ElementCollection
var videoUrls: List<URI> = emptyList(),

@Embedded
var metadata: GameMetadata


) {
constructor(path: Path, library: Library) : this(library = library, metadata = GameMetadata(path = path.toString()))
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import jakarta.persistence.PostPersist
import jakarta.persistence.PostRemove
import jakarta.persistence.PostUpdate
import org.gameyfin.app.core.events.GameCreatedEvent
import org.gameyfin.app.core.events.GameDeletedEvent
import org.gameyfin.app.games.GameService
import org.gameyfin.app.games.dto.GameAdminEvent
import org.gameyfin.app.games.dto.GameUserEvent
Expand All @@ -24,11 +25,13 @@ class GameEntityListener {
fun updated(game: Game) {
GameService.emitUser(GameUserEvent.Updated(game.toUserDto()))
GameService.emitAdmin(GameAdminEvent.Updated(game.toAdminDto()))
// GameUpdateEvent triggered via {@link org.gameyfin.app.core.interceptors.EntityUpdateInterceptor#onFlushDirty}
}

@PostRemove
fun deleted(game: Game) {
GameService.emitUser(GameUserEvent.Deleted(game.id!!))
GameService.emitAdmin(GameAdminEvent.Deleted(game.id!!))
EventPublisherHolder.publish(GameDeletedEvent(this, game))
}
}
Loading
Loading