Implement field metadata

Implement result merging for GameMetadata
This commit is contained in:
grimsi
2025-03-28 21:09:27 +01:00
parent 2981e87e76
commit 80bb4cefef
4 changed files with 137 additions and 54 deletions
@@ -10,5 +10,5 @@ data class PluginManagementEntry(
var enabled: Boolean = true,
var priority: Int = Int.MAX_VALUE
var priority: Int = 0
)
@@ -44,18 +44,19 @@ class GameService(
fun createFromFile(path: Path): GameDto {
val metadataResults = queryPlugins(path.fileName.toString())
val (plugin, metadata) = metadataResults.entries.firstOrNull { it.value != null }
?: throw NoMatchException("Could not match game at $path")
if (metadata == null) {
throw NoMatchException("Plugin ${plugin.javaClass} returned invalid metadata for game at $path")
val validResults = metadataResults.filterValues { it != null }
if (validResults.isEmpty()) {
throw NoMatchException("Could not match game at $path")
}
var game = toEntity(metadata, path, plugin)
game = createOrUpdate(game)
val sortedResults = validResults.entries.sortedByDescending {
pluginManagementService.getPluginManagementEntry(it.key.javaClass).priority
}
return toDto(game)
val mergedGame = mergeResults(sortedResults, path)
val savedGame = createOrUpdate(mergedGame)
return toDto(savedGame)
}
fun getAllGames(): Collection<GameDto> {
@@ -93,12 +94,112 @@ class GameService(
}
}
private fun mergeResults(results: List<Map.Entry<GameMetadataProvider, GameMetadata?>>, path: Path): Game {
val mergedGame = Game(path = path.toString())
val metadataMap = mutableMapOf<String, FieldMetadata>()
// Sort results by plugin priority
val sortedResults = results.sortedByDescending {
pluginManagementService.getPluginManagementEntry(it.key.javaClass).priority
}
sortedResults.forEach { (provider, metadata) ->
val sourcePlugin = pluginManagementService.getPluginManagementEntry(provider.javaClass)
metadata?.let {
it.title.takeIf { it.isNotBlank() }?.let { title ->
if (!metadataMap.containsKey("title")) {
mergedGame.title = title
metadataMap["title"] = FieldMetadata(sourcePlugin)
}
}
it.description?.takeIf { it.isNotBlank() }?.let { description ->
if (!metadataMap.containsKey("summary")) {
mergedGame.summary = description
metadataMap["summary"] = FieldMetadata(sourcePlugin)
}
}
it.coverUrl?.let { coverUrl ->
if (!metadataMap.containsKey("coverImage")) {
mergedGame.coverImage = downloadAndPersist(coverUrl, ImageType.COVER)
metadataMap["coverImage"] = FieldMetadata(sourcePlugin)
}
}
it.release?.let { release ->
if (!metadataMap.containsKey("release")) {
mergedGame.release = release
metadataMap["release"] = FieldMetadata(sourcePlugin)
}
}
it.publishedBy?.takeIf { it.isNotEmpty() }?.let { publishedBy ->
if (!metadataMap.containsKey("publishers")) {
mergedGame.publishers =
publishedBy.map { name -> toEntity(name, CompanyType.PUBLISHER) }.toSet()
metadataMap["publishers"] = FieldMetadata(sourcePlugin)
}
}
it.developedBy?.takeIf { it.isNotEmpty() }?.let { developedBy ->
if (!metadataMap.containsKey("developers")) {
mergedGame.developers =
developedBy.map { name -> toEntity(name, CompanyType.DEVELOPER) }.toSet()
metadataMap["developers"] = FieldMetadata(sourcePlugin)
}
}
it.genres?.takeIf { it.isNotEmpty() }?.let { genres ->
if (!metadataMap.containsKey("genres")) {
mergedGame.genres = genres
metadataMap["genres"] = FieldMetadata(sourcePlugin)
}
}
it.themes?.takeIf { it.isNotEmpty() }?.let { themes ->
if (!metadataMap.containsKey("themes")) {
mergedGame.themes = themes
metadataMap["themes"] = FieldMetadata(sourcePlugin)
}
}
it.keywords?.takeIf { it.isNotEmpty() }?.let { keywords ->
if (!metadataMap.containsKey("keywords")) {
mergedGame.keywords = keywords
metadataMap["keywords"] = FieldMetadata(sourcePlugin)
}
}
it.features?.takeIf { it.isNotEmpty() }?.let { features ->
if (!metadataMap.containsKey("features")) {
mergedGame.features = features
metadataMap["features"] = FieldMetadata(sourcePlugin)
}
}
it.perspectives?.takeIf { it.isNotEmpty() }?.let { perspectives ->
if (!metadataMap.containsKey("perspectives")) {
mergedGame.perspectives = perspectives
metadataMap["perspectives"] = FieldMetadata(sourcePlugin)
}
}
it.screenshotUrls?.takeIf { it.isNotEmpty() }?.let { screenshotUrls ->
if (!metadataMap.containsKey("images")) {
mergedGame.images =
screenshotUrls.map { url -> downloadAndPersist(url, ImageType.SCREENSHOT) }.toSet()
metadataMap["images"] = FieldMetadata(sourcePlugin)
}
}
it.videoUrls?.takeIf { it.isNotEmpty() }?.let { videoUrls ->
if (!metadataMap.containsKey("videoUrls")) {
mergedGame.videoUrls = videoUrls
metadataMap["videoUrls"] = FieldMetadata(sourcePlugin)
}
}
}
}
mergedGame.metadata = metadataMap
return mergedGame
}
private fun toDto(game: Game): GameDto {
val gameId = game.id ?: throw IllegalArgumentException("Game ID is null")
return GameDto(
id = gameId,
title = game.title,
title = game.title!!,
coverId = game.coverImage?.id,
comment = game.comment,
summary = game.summary,
@@ -128,28 +229,6 @@ class GameService(
)
}
private fun toEntity(metadata: GameMetadata, path: Path, source: GameMetadataProvider): Game {
val sourcePlugin = pluginManagementService.getPluginManagementEntry(source.javaClass)
return Game(
title = metadata.title,
summary = metadata.description,
coverImage = metadata.coverUrl?.let { downloadAndPersist(it, ImageType.COVER) },
release = metadata.release,
publishers = metadata.publishedBy?.map { toEntity(it, CompanyType.PUBLISHER) }?.toSet(),
developers = metadata.developedBy?.map { toEntity(it, CompanyType.DEVELOPER) }?.toSet(),
genres = metadata.genres,
themes = metadata.themes,
keywords = metadata.keywords,
features = metadata.features,
perspectives = metadata.perspectives,
images = metadata.screenshotUrls?.map { downloadAndPersist(it, ImageType.SCREENSHOT) }?.toSet(),
videoUrls = metadata.videoUrls,
path = path.toString(),
metadata = mapOf("title" to FieldMetadata(sourcePlugin))
)
}
private fun toEntity(companyName: String, companyType: CompanyType): Company {
companyRepository.findByNameAndType(companyName, companyType)?.let { return it }
val company = Company(name = companyName, type = companyType)
@@ -14,10 +14,10 @@ class Game(
@GeneratedValue(strategy = GenerationType.AUTO)
var id: Long? = null,
val title: String,
var title: String? = null,
@OneToOne(cascade = [CascadeType.MERGE])
val coverImage: Image? = null,
var coverImage: Image? = null,
@Lob
@Column(columnDefinition = "CLOB")
@@ -25,40 +25,40 @@ class Game(
@Lob
@Column(columnDefinition = "CLOB")
val summary: String? = null,
var summary: String? = null,
val release: Instant? = null,
var release: Instant? = null,
@ManyToMany(cascade = [CascadeType.MERGE])
val publishers: Set<Company>? = null,
var publishers: Set<Company>? = null,
@ManyToMany(cascade = [CascadeType.MERGE])
val developers: Set<Company>? = null,
var developers: Set<Company>? = null,
@ElementCollection(targetClass = Genre::class)
var genres: Set<Genre>? = null,
@ElementCollection(targetClass = Theme::class)
var themes: Set<Theme>? = null,
@ElementCollection
val genres: Set<Genre>? = null,
var keywords: Set<String>? = null,
@ElementCollection
val themes: Set<Theme>? = null,
@ElementCollection(targetClass = GameFeature::class)
var features: Set<GameFeature>? = null,
@ElementCollection
val keywords: Set<String>? = null,
@ElementCollection
val features: Set<GameFeature>? = null,
@ElementCollection
val perspectives: Set<PlayerPerspective>? = null,
@ElementCollection(targetClass = PlayerPerspective::class)
var perspectives: Set<PlayerPerspective>? = null,
@OneToMany(cascade = [CascadeType.MERGE])
val images: Set<Image>? = null,
var images: Set<Image>? = null,
@ElementCollection
val videoUrls: Set<URI>? = null,
var videoUrls: Set<URI>? = null,
@Column(unique = true)
val path: String,
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
val metadata: Map<String, FieldMetadata> = emptyMap()
var metadata: Map<String, FieldMetadata> = emptyMap()
)
@@ -93,7 +93,11 @@ class UserPreferencesService(
userPreference.value = castedValue.toString()
}
userPreferenceRepository.save(userPreference)
try {
userPreferenceRepository.save(userPreference)
} catch (e: Exception) {
log.warn { "Error saving user preference '$key': ${e.message}" }
}
}
/**