Refactor structure of Game metadata

This commit is contained in:
grimsi
2025-05-29 11:36:12 +02:00
parent 489a6c7aed
commit 7bfa173c07
15 changed files with 107 additions and 93 deletions
@@ -77,7 +77,7 @@ function LibraryManagementLayout({getConfig, formik}: any) {
removeLibrary={removeLibrary} key={library.name}/> removeLibrary={removeLibrary} key={library.name}/>
)} )}
</div> : </div> :
"No libraries configured. Add your first library!" <p className="mt-4 text-center text-default-500">No libraries found</p>
} }
<LibraryCreationModal <LibraryCreationModal
@@ -32,7 +32,7 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
{new Date(item.createdAt).toLocaleString()} {new Date(item.createdAt).toLocaleString()}
</TableCell> </TableCell>
<TableCell> <TableCell>
{item.path} {item.metadata.path}
</TableCell> </TableCell>
<TableCell className="flex flex-row gap-2"> <TableCell className="flex flex-row gap-2">
<Button isIconOnly size="sm" isDisabled={true}><CheckCircle/></Button> <Button isIconOnly size="sm" isDisabled={true}><CheckCircle/></Button>
@@ -66,7 +66,7 @@ export default function GameView() {
<p className="text-foreground/60">{game.release !== undefined ? new Date(game.release).getFullYear() : "unknown"}</p> <p className="text-foreground/60">{game.release !== undefined ? new Date(game.release).getFullYear() : "unknown"}</p>
</div> </div>
</div> </div>
{downloadOptions && <ComboButton description={humanFileSize(game.fileSize)} {downloadOptions && <ComboButton description={humanFileSize(game.metadata.fileSize)}
options={downloadOptions} options={downloadOptions}
preferredOptionKey="preferred-download-method" preferredOptionKey="preferred-download-method"
/>} />}
@@ -21,7 +21,7 @@ class DownloadEndpoint(
@RequestParam provider: String @RequestParam provider: String
): ResponseEntity<StreamingResponseBody> { ): ResponseEntity<StreamingResponseBody> {
val game = gameService.getById(gameId) val game = gameService.getById(gameId)
val download = downloadService.getDownload(game.path, provider) val download = downloadService.getDownload(game.metadata.path, provider)
return when (download) { return when (download) {
is FileDownload -> { is FileDownload -> {
@@ -110,7 +110,7 @@ class FilesystemService(
} }
// Get all paths already in the library as game files or as unmatched paths // Get all paths already in the library as game files or as unmatched paths
val currentLibraryGamePaths = library.games.map { Path(it.path) } val currentLibraryGamePaths = library.games.map { Path(it.metadata.path) }
val currentLibraryUnmatchedPaths = library.unmatchedPaths.map { Path(it) } val currentLibraryUnmatchedPaths = library.unmatchedPaths.map { Path(it) }
val allCurrentLibraryPaths = currentLibraryGamePaths + currentLibraryUnmatchedPaths val allCurrentLibraryPaths = currentLibraryGamePaths + currentLibraryUnmatchedPaths
@@ -7,14 +7,10 @@ import de.grimsi.gameyfin.core.filterValuesNotNull
import de.grimsi.gameyfin.core.plugins.PluginService import de.grimsi.gameyfin.core.plugins.PluginService
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
import de.grimsi.gameyfin.core.replaceRomanNumerals import de.grimsi.gameyfin.core.replaceRomanNumerals
import de.grimsi.gameyfin.games.dto.GameDto import de.grimsi.gameyfin.games.dto.*
import de.grimsi.gameyfin.games.dto.GameEvent
import de.grimsi.gameyfin.games.dto.GameMetadataDto
import de.grimsi.gameyfin.games.dto.GameUpdateDto
import de.grimsi.gameyfin.games.entities.* import de.grimsi.gameyfin.games.entities.*
import de.grimsi.gameyfin.games.repositories.GameRepository import de.grimsi.gameyfin.games.repositories.GameRepository
import de.grimsi.gameyfin.libraries.Library import de.grimsi.gameyfin.libraries.Library
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 kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -31,6 +27,7 @@ import reactor.core.publisher.Sinks
import java.nio.file.Path import java.nio.file.Path
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.toJavaDuration import kotlin.time.toJavaDuration
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata as PluginApiMetadata
@Service @Service
class GameService( class GameService(
@@ -125,7 +122,7 @@ class GameService(
} }
fun getAllByPaths(paths: List<String>): List<Game> { fun getAllByPaths(paths: List<String>): List<Game> {
return gameRepository.findAllByPathIn(paths) return gameRepository.findAllByMetadata_PathIn(paths)
} }
fun getById(id: Long): Game { fun getById(id: Long): Game {
@@ -137,7 +134,7 @@ class GameService(
* Runs the queries concurrently and asynchronously * Runs the queries concurrently and asynchronously
* @return A map of metadata plugins and their respective results * @return A map of metadata plugins and their respective results
*/ */
private fun queryPlugins(gameTitle: String): Map<GameMetadataProvider, GameMetadata?> { private fun queryPlugins(gameTitle: String): Map<GameMetadataProvider, PluginApiMetadata?> {
return runBlocking { return runBlocking {
coroutineScope { coroutineScope {
metadataPlugins.associateWith { metadataPlugins.associateWith {
@@ -160,8 +157,8 @@ class GameService(
*/ */
private fun filterResults( private fun filterResults(
originalQuery: String, originalQuery: String,
results: Map<GameMetadataProvider, GameMetadata> results: Map<GameMetadataProvider, PluginApiMetadata>
): Map<GameMetadataProvider, GameMetadata> { ): Map<GameMetadataProvider, PluginApiMetadata> {
val providerToTitle = results.entries.associate { val providerToTitle = results.entries.associate {
pluginManager.whichPlugin(it.key.javaClass).pluginId to it.value.title pluginManager.whichPlugin(it.key.javaClass).pluginId to it.value.title
} }
@@ -183,12 +180,12 @@ class GameService(
* The plugin with the highest possible priority is used as the source for each field * The plugin with the highest possible priority is used as the source for each field
*/ */
private fun mergeResults( private fun mergeResults(
results: Map<GameMetadataProvider, GameMetadata?>, results: Map<GameMetadataProvider, PluginApiMetadata?>,
path: Path, path: Path,
library: Library library: Library
): Game { ): Game {
val mergedGame = Game(path = path.toString(), library = library) val mergedGame = Game(path = path, library = library)
val metadataMap = mutableMapOf<String, FieldMetadata>() val metadataMap = mutableMapOf<String, GameFieldMetadata>()
val originalIdsMap = mutableMapOf<PluginManagementEntry, String>() val originalIdsMap = mutableMapOf<PluginManagementEntry, String>()
// Cache the plugin management entries for each provider // Cache the plugin management entries for each provider
@@ -209,81 +206,81 @@ class GameService(
metadata.title.takeIf { it.isNotBlank() }?.let { title -> metadata.title.takeIf { it.isNotBlank() }?.let { title ->
if (!metadataMap.containsKey("title")) { if (!metadataMap.containsKey("title")) {
mergedGame.title = title mergedGame.title = title
metadataMap["title"] = FieldMetadata(sourcePlugin) metadataMap["title"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.description?.takeIf { it.isNotBlank() }?.let { description -> metadata.description?.takeIf { it.isNotBlank() }?.let { description ->
if (!metadataMap.containsKey("summary")) { if (!metadataMap.containsKey("summary")) {
mergedGame.summary = description mergedGame.summary = description
metadataMap["summary"] = FieldMetadata(sourcePlugin) metadataMap["summary"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.coverUrl?.let { coverUrl -> metadata.coverUrl?.let { coverUrl ->
if (!metadataMap.containsKey("coverImage")) { if (!metadataMap.containsKey("coverImage")) {
mergedGame.coverImage = Image(originalUrl = coverUrl.toURL(), type = ImageType.COVER) mergedGame.coverImage = Image(originalUrl = coverUrl.toURL(), type = ImageType.COVER)
metadataMap["coverImage"] = FieldMetadata(sourcePlugin) metadataMap["coverImage"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.release?.let { release -> metadata.release?.let { release ->
if (!metadataMap.containsKey("release")) { if (!metadataMap.containsKey("release")) {
mergedGame.release = release mergedGame.release = release
metadataMap["release"] = FieldMetadata(sourcePlugin) metadataMap["release"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.userRating?.let { userRating -> metadata.userRating?.let { userRating ->
if (!metadataMap.containsKey("userRating")) { if (!metadataMap.containsKey("userRating")) {
mergedGame.userRating = userRating mergedGame.userRating = userRating
metadataMap["userRating"] = FieldMetadata(sourcePlugin) metadataMap["userRating"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.criticRating?.let { criticRating -> metadata.criticRating?.let { criticRating ->
if (!metadataMap.containsKey("criticRating")) { if (!metadataMap.containsKey("criticRating")) {
mergedGame.criticRating = criticRating mergedGame.criticRating = criticRating
metadataMap["criticRating"] = FieldMetadata(sourcePlugin) metadataMap["criticRating"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.publishedBy?.takeIf { it.isNotEmpty() }?.let { publishedBy -> metadata.publishedBy?.takeIf { it.isNotEmpty() }?.let { publishedBy ->
if (!metadataMap.containsKey("publishers")) { if (!metadataMap.containsKey("publishers")) {
mergedGame.publishers = mergedGame.publishers =
publishedBy.map { Company(name = it, type = CompanyType.PUBLISHER) } publishedBy.map { Company(name = it, type = CompanyType.PUBLISHER) }
metadataMap["publishers"] = FieldMetadata(sourcePlugin) metadataMap["publishers"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.developedBy?.takeIf { it.isNotEmpty() }?.let { developedBy -> metadata.developedBy?.takeIf { it.isNotEmpty() }?.let { developedBy ->
if (!metadataMap.containsKey("developers")) { if (!metadataMap.containsKey("developers")) {
mergedGame.developers = mergedGame.developers =
developedBy.map { Company(name = it, type = CompanyType.DEVELOPER) } developedBy.map { Company(name = it, type = CompanyType.DEVELOPER) }
metadataMap["developers"] = FieldMetadata(sourcePlugin) metadataMap["developers"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.genres?.takeIf { it.isNotEmpty() }?.let { genres -> metadata.genres?.takeIf { it.isNotEmpty() }?.let { genres ->
if (!metadataMap.containsKey("genres")) { if (!metadataMap.containsKey("genres")) {
mergedGame.genres = genres.toList() mergedGame.genres = genres.toList()
metadataMap["genres"] = FieldMetadata(sourcePlugin) metadataMap["genres"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.themes?.takeIf { it.isNotEmpty() }?.let { themes -> metadata.themes?.takeIf { it.isNotEmpty() }?.let { themes ->
if (!metadataMap.containsKey("themes")) { if (!metadataMap.containsKey("themes")) {
mergedGame.themes = themes.toList() mergedGame.themes = themes.toList()
metadataMap["themes"] = FieldMetadata(sourcePlugin) metadataMap["themes"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.keywords?.takeIf { it.isNotEmpty() }?.let { keywords -> metadata.keywords?.takeIf { it.isNotEmpty() }?.let { keywords ->
if (!metadataMap.containsKey("keywords")) { if (!metadataMap.containsKey("keywords")) {
mergedGame.keywords = keywords.toList() mergedGame.keywords = keywords.toList()
metadataMap["keywords"] = FieldMetadata(sourcePlugin) metadataMap["keywords"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.features?.takeIf { it.isNotEmpty() }?.let { features -> metadata.features?.takeIf { it.isNotEmpty() }?.let { features ->
if (!metadataMap.containsKey("features")) { if (!metadataMap.containsKey("features")) {
mergedGame.features = features.toList() mergedGame.features = features.toList()
metadataMap["features"] = FieldMetadata(sourcePlugin) metadataMap["features"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.perspectives?.takeIf { it.isNotEmpty() }?.let { perspectives -> metadata.perspectives?.takeIf { it.isNotEmpty() }?.let { perspectives ->
if (!metadataMap.containsKey("perspectives")) { if (!metadataMap.containsKey("perspectives")) {
mergedGame.perspectives = perspectives.toList() mergedGame.perspectives = perspectives.toList()
metadataMap["perspectives"] = FieldMetadata(sourcePlugin) metadataMap["perspectives"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.screenshotUrls?.takeIf { it.isNotEmpty() }?.let { screenshotUrls -> metadata.screenshotUrls?.takeIf { it.isNotEmpty() }?.let { screenshotUrls ->
@@ -291,20 +288,20 @@ class GameService(
mergedGame.images = runBlocking { mergedGame.images = runBlocking {
screenshotUrls.map { Image(originalUrl = it.toURL(), type = ImageType.SCREENSHOT) } screenshotUrls.map { Image(originalUrl = it.toURL(), type = ImageType.SCREENSHOT) }
} }
metadataMap["images"] = FieldMetadata(sourcePlugin) metadataMap["images"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
metadata.videoUrls?.takeIf { it.isNotEmpty() }?.let { videoUrls -> metadata.videoUrls?.takeIf { it.isNotEmpty() }?.let { videoUrls ->
if (!metadataMap.containsKey("videoUrls")) { if (!metadataMap.containsKey("videoUrls")) {
mergedGame.videoUrls = videoUrls.toList() mergedGame.videoUrls = videoUrls.toList()
metadataMap["videoUrls"] = FieldMetadata(sourcePlugin) metadataMap["videoUrls"] = GameFieldMetadata(source = sourcePlugin)
} }
} }
} }
} }
mergedGame.metadata = metadataMap mergedGame.metadata.fields = metadataMap
mergedGame.originalIds = originalIdsMap mergedGame.metadata.originalIds = originalIdsMap
return mergedGame return mergedGame
} }
@@ -320,30 +317,29 @@ class GameService(
fun Game.toDto(): GameDto { fun Game.toDto(): GameDto {
// Helper functions // Helper functions
fun toDto(metadata: FieldMetadata): GameMetadataDto { fun toDto(fieldMetadata: GameFieldMetadata): GameFieldMetadataDto {
return GameMetadataDto( return GameFieldMetadataDto(
source = metadata.source.pluginId, source = fieldMetadata.source.pluginId,
lastUpdated = metadata.lastUpdated updatedAt = fieldMetadata.updatedAt!!
) )
} }
fun toDto(metadata: Map<String, FieldMetadata>): Map<String, GameMetadataDto> { fun toDto(metadata: GameMetadata): GameMetadataDto {
return metadata.mapValues { toDto(it.value) } return GameMetadataDto(
fileSize = metadata.fileSize ?: 0L,
downloadCount = metadata.downloadCount,
path = metadata.path,
fields = metadata.fields.mapValues { toDto(it.value) },
originalIds = metadata.originalIds.mapKeys { it.key.pluginId }
)
} }
val id = this.id ?: throw IllegalArgumentException("ID is null")
val createdAt = this.createdAt ?: throw IllegalArgumentException("creation timestamp is null")
val updatedAt = this.updatedAt ?: throw IllegalArgumentException("update timestamp is null")
val libraryId = this.library.id ?: throw IllegalArgumentException("library ID is null")
val title = this.title ?: throw IllegalArgumentException("title is null")
return GameDto( return GameDto(
id = id, id = id!!,
createdAt = createdAt, createdAt = createdAt!!,
updatedAt = updatedAt, updatedAt = updatedAt!!,
libraryId = libraryId, libraryId = this.library.id!!,
title = title, title = title!!,
coverId = this.coverImage?.id, coverId = this.coverImage?.id,
comment = this.comment, comment = this.comment,
summary = this.summary, summary = this.summary,
@@ -359,9 +355,6 @@ fun Game.toDto(): GameDto {
perspectives = this.perspectives?.map { it.name }, perspectives = this.perspectives?.map { it.name },
imageIds = this.images.mapNotNull { it.id }, imageIds = this.images.mapNotNull { it.id },
videoUrls = this.videoUrls.map { it.toString() }, videoUrls = this.videoUrls.map { it.toString() },
path = this.path, metadata = toDto(this.metadata)
fileSize = this.fileSize ?: 0L,
metadata = toDto(this.metadata),
originalIds = this.originalIds.mapKeys { it.key.pluginId }
) )
} }
@@ -1,7 +1,9 @@
package de.grimsi.gameyfin.games.dto package de.grimsi.gameyfin.games.dto
import com.fasterxml.jackson.annotation.JsonInclude
import java.time.Instant import java.time.Instant
@JsonInclude(JsonInclude.Include.NON_NULL)
class GameDto( class GameDto(
val id: Long, val id: Long,
val createdAt: Instant, val createdAt: Instant,
@@ -23,8 +25,5 @@ class GameDto(
val perspectives: List<String>?, val perspectives: List<String>?,
val imageIds: List<Long>?, val imageIds: List<Long>?,
val videoUrls: List<String>?, val videoUrls: List<String>?,
val path: String, val metadata: GameMetadataDto
val fileSize: Long,
val metadata: Map<String, GameMetadataDto>,
val originalIds: Map<String, String>
) )
@@ -0,0 +1,8 @@
package de.grimsi.gameyfin.games.dto
import java.time.Instant
class GameFieldMetadataDto(
val source: String,
val updatedAt: Instant
)
@@ -1,8 +1,12 @@
package de.grimsi.gameyfin.games.dto package de.grimsi.gameyfin.games.dto
import java.time.Instant import com.fasterxml.jackson.annotation.JsonInclude
@JsonInclude(JsonInclude.Include.NON_NULL)
class GameMetadataDto( class GameMetadataDto(
val source: String, val path: String?,
val lastUpdated: Instant val fileSize: Long,
val fields: Map<String, GameFieldMetadataDto>?,
val originalIds: Map<String, String>?,
val downloadCount: Int
) )
@@ -1,6 +1,5 @@
package de.grimsi.gameyfin.games.entities package de.grimsi.gameyfin.games.entities
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
import de.grimsi.gameyfin.libraries.Library import de.grimsi.gameyfin.libraries.Library
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameFeature import de.grimsi.gameyfin.pluginapi.gamemetadata.GameFeature
import de.grimsi.gameyfin.pluginapi.gamemetadata.Genre import de.grimsi.gameyfin.pluginapi.gamemetadata.Genre
@@ -10,6 +9,7 @@ import jakarta.persistence.*
import org.hibernate.annotations.CreationTimestamp import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.UpdateTimestamp import org.hibernate.annotations.UpdateTimestamp
import java.net.URI import java.net.URI
import java.nio.file.Path
import java.time.Instant import java.time.Instant
@Entity @Entity
@@ -76,16 +76,8 @@ class Game(
@ElementCollection @ElementCollection
var videoUrls: List<URI> = emptyList(), var videoUrls: List<URI> = emptyList(),
@Column(unique = true) @Embedded
val path: String, var metadata: GameMetadata
) {
var fileSize: Long? = null, constructor(path: Path, library: Library) : this(library = library, metadata = GameMetadata(path = path.toString()))
}
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
var metadata: Map<String, FieldMetadata> = emptyMap(),
@ElementCollection
var originalIds: Map<PluginManagementEntry, String> = emptyMap(),
var downloadCount: Int = 0
)
@@ -2,10 +2,11 @@ package de.grimsi.gameyfin.games.entities
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
import jakarta.persistence.* import jakarta.persistence.*
import org.hibernate.annotations.UpdateTimestamp
import java.time.Instant import java.time.Instant
@Entity @Entity
class FieldMetadata( class GameFieldMetadata(
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
var id: Long? = null, var id: Long? = null,
@@ -13,7 +14,6 @@ class FieldMetadata(
@ManyToOne @ManyToOne
val source: PluginManagementEntry, val source: PluginManagementEntry,
val lastUpdated: Instant @UpdateTimestamp
) { var updatedAt: Instant? = Instant.now()
constructor(source: PluginManagementEntry) : this(null, source, Instant.now()) )
}
@@ -0,0 +1,20 @@
package de.grimsi.gameyfin.games.entities
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
import jakarta.persistence.*
@Embeddable
class GameMetadata(
@Column(unique = true)
val path: String,
var fileSize: Long? = null,
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true, fetch = FetchType.EAGER)
var fields: Map<String, GameFieldMetadata> = emptyMap(),
@ElementCollection
var originalIds: Map<PluginManagementEntry, String> = emptyMap(),
var downloadCount: Int = 0
)
@@ -1,6 +1,6 @@
package de.grimsi.gameyfin.games.repositories package de.grimsi.gameyfin.games.repositories
import de.grimsi.gameyfin.games.entities.FieldMetadata import de.grimsi.gameyfin.games.entities.GameFieldMetadata
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
interface FieldMetadataRepository : JpaRepository<FieldMetadata, Long> interface FieldMetadataRepository : JpaRepository<GameFieldMetadata, Long>
@@ -5,8 +5,8 @@ import org.springframework.data.domain.Limit
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
interface GameRepository : JpaRepository<Game, Long> { interface GameRepository : JpaRepository<Game, Long> {
fun findByPath(path: String): Game? fun findByMetadata_Path(path: String): Game?
fun findAllByPathIn(paths: List<String>): List<Game> fun findAllByMetadata_PathIn(paths: List<String>): List<Game>
fun findByOrderByCreatedAtDesc(limit: Limit): List<Game> fun findByOrderByCreatedAtDesc(limit: Limit): List<Game>
fun findByOrderByUpdatedAtDesc(limit: Limit): List<Game> fun findByOrderByUpdatedAtDesc(limit: Limit): List<Game>
} }
@@ -256,9 +256,9 @@ class LibraryService(
val calculateFileSizeTask = gamesWithImages.map { game -> val calculateFileSizeTask = gamesWithImages.map { game ->
Callable { Callable {
game.path.let { path -> game.metadata.path.let { path ->
val fileSize = filesystemService.calculateFileSize(path) val fileSize = filesystemService.calculateFileSize(path)
game.fileSize = fileSize game.metadata.fileSize = fileSize
progress.currentStep.current = calculatedFileSize.incrementAndGet() progress.currentStep.current = calculatedFileSize.incrementAndGet()
emit(progress) emit(progress)
@@ -324,21 +324,19 @@ class LibraryService(
name = library.name, name = library.name,
directories = library.directories.map { directories = library.directories.map {
DirectoryMapping(internalPath = it.internalPath, externalPath = it.externalPath) DirectoryMapping(internalPath = it.internalPath, externalPath = it.externalPath)
}.toMutableList() }.toMutableList(),
) )
} }
} }
fun Library.toDto(): LibraryDto { fun Library.toDto(): LibraryDto {
val libraryId = this.id ?: throw IllegalArgumentException("Library ID is null")
val statsDto = LibraryStatsDto( val statsDto = LibraryStatsDto(
gamesCount = this.games.size, gamesCount = this.games.size,
downloadedGamesCount = this.games.sumOf { it.downloadCount } downloadedGamesCount = this.games.sumOf { it.metadata.downloadCount }
) )
return LibraryDto( return LibraryDto(
id = libraryId, id = this.id!!,
name = this.name, name = this.name,
directories = this.directories.map { DirectoryMappingDto(it.internalPath, it.externalPath) }, directories = this.directories.map { DirectoryMappingDto(it.internalPath, it.externalPath) },
games = this.games.mapNotNull { it.id }, games = this.games.mapNotNull { it.id },