diff --git a/app/package.json b/app/package.json index 4d7345d..59d3828 100644 --- a/app/package.json +++ b/app/package.json @@ -265,4 +265,4 @@ "disableUsageStatistics": true, "hash": "962eccc3fa0735d5234901be4f9e384096113c45bec22564a53688096d62aef4" } -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/games/entities/GameEntityListener.kt b/app/src/main/kotlin/org/gameyfin/app/games/entities/GameEntityListener.kt index efe500b..9a04b3e 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/entities/GameEntityListener.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/entities/GameEntityListener.kt @@ -12,19 +12,19 @@ import org.gameyfin.app.games.extensions.toUserDto class GameEntityListener { @PostPersist fun created(game: Game) { - GameService.Companion.emitUser(GameUserEvent.Created(game.toUserDto())) - GameService.Companion.emitAdmin(GameAdminEvent.Created(game.toAdminDto())) + GameService.emitUser(GameUserEvent.Created(game.toUserDto())) + GameService.emitAdmin(GameAdminEvent.Created(game.toAdminDto())) } @PostUpdate fun updated(game: Game) { - GameService.Companion.emitUser(GameUserEvent.Updated(game.toUserDto())) - GameService.Companion.emitAdmin(GameAdminEvent.Updated(game.toAdminDto())) + GameService.emitUser(GameUserEvent.Updated(game.toUserDto())) + GameService.emitAdmin(GameAdminEvent.Updated(game.toAdminDto())) } @PostRemove fun deleted(game: Game) { - GameService.Companion.emitUser(GameUserEvent.Deleted(game.id!!)) - GameService.Companion.emitAdmin(GameAdminEvent.Deleted(game.id!!)) + GameService.emitUser(GameUserEvent.Deleted(game.id!!)) + GameService.emitAdmin(GameAdminEvent.Deleted(game.id!!)) } } \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/games/entities/Image.kt b/app/src/main/kotlin/org/gameyfin/app/games/entities/Image.kt index 4a1dc05..3bc44cf 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/entities/Image.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/entities/Image.kt @@ -1,15 +1,13 @@ package org.gameyfin.app.games.entities -import jakarta.persistence.Entity -import jakarta.persistence.GeneratedValue -import jakarta.persistence.GenerationType -import jakarta.persistence.Id +import jakarta.persistence.* import org.springframework.content.commons.annotations.ContentId import org.springframework.content.commons.annotations.ContentLength import org.springframework.content.commons.annotations.MimeType import java.net.URL @Entity +@EntityListeners(ImageEntityListener::class) class Image( @Id @GeneratedValue(strategy = GenerationType.AUTO) diff --git a/app/src/main/kotlin/org/gameyfin/app/games/entities/ImageEntityListener.kt b/app/src/main/kotlin/org/gameyfin/app/games/entities/ImageEntityListener.kt new file mode 100644 index 0000000..0486bfc --- /dev/null +++ b/app/src/main/kotlin/org/gameyfin/app/games/entities/ImageEntityListener.kt @@ -0,0 +1,27 @@ +package org.gameyfin.app.games.entities + +import jakarta.persistence.PostRemove +import org.gameyfin.app.media.ImageService +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware +import org.springframework.stereotype.Component + +@Component +class ImageEntityListener : ApplicationContextAware { + companion object { + private lateinit var applicationContext: ApplicationContext + } + + override fun setApplicationContext(context: ApplicationContext) { + applicationContext = context + } + + private fun getImageService(): ImageService { + return applicationContext.getBean(ImageService::class.java) + } + + @PostRemove + fun deleted(image: Image) { + getImageService().deleteFile(image) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryScanService.kt b/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryScanService.kt index e66d23e..bbc28e9 100644 --- a/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryScanService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryScanService.kt @@ -4,6 +4,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging import org.gameyfin.app.core.filesystem.FilesystemService import org.gameyfin.app.games.GameService import org.gameyfin.app.games.entities.Game +import org.gameyfin.app.games.entities.Image import org.gameyfin.app.libraries.dto.LibraryScanProgress import org.gameyfin.app.libraries.dto.LibraryScanStatus import org.gameyfin.app.libraries.dto.LibraryScanStep @@ -13,6 +14,7 @@ import org.gameyfin.app.media.ImageService import org.springframework.stereotype.Service import reactor.core.publisher.Flux import reactor.core.publisher.Sinks +import java.net.URL import java.nio.file.Path import java.time.Instant import java.util.concurrent.Callable @@ -331,39 +333,56 @@ class LibraryScanService( private fun downloadImages(games: List, progress: LibraryScanProgress): DownloadImagesResult { val completedImageDownload = AtomicInteger(0) - val imageDownloadTasks = games.map { game -> - Callable { + // Collect all images from all games in the batch + val allImages = games.flatMap { game -> + val images = mutableListOf() + game.coverImage?.let { images.add(it) } + game.headerImage?.let { images.add(it) } + images.addAll(game.images) + images + } + + // Deduplicate by originalUrl + val uniqueImages = allImages + .filter { it.originalUrl != null } + .distinctBy { it.originalUrl } + + // Map to track which Image entity was used for download per originalUrl + val downloadedImageMap = ConcurrentHashMap() + + // Download each unique image in parallel + val imageDownloadTasks = uniqueImages.map { image -> + Callable { try { - game.coverImage?.let { - imageService.downloadIfNew(it) - completedImageDownload.andIncrement + imageService.downloadIfNew(image) + image.originalUrl?.let { url -> + downloadedImageMap[url] = image } - - game.headerImage?.let { - imageService.downloadIfNew(it) - completedImageDownload.andIncrement - } - - game.images.map { - imageService.downloadIfNew(it) - completedImageDownload.andIncrement - } - - game } catch (e: Exception) { - log.error { "Error downloading images for game '${game.title}' (${game.id}): ${e.message}" } + log.error { "Error downloading image '${image.originalUrl}': ${e.message}" } log.debug(e) {} - null } finally { - progress.currentStep.current = completedImageDownload.get() + progress.currentStep.current = completedImageDownload.incrementAndGet() emit(progress) } } } + executor.invokeAll(imageDownloadTasks) - val gamesWithImages = executor.invokeAll(imageDownloadTasks).mapNotNull { it.get() } + // After downloads, associate the contentId with all other Image entities in the batch with the same originalUrl + for ((url, downloadedImage) in downloadedImageMap) { + val contentId = downloadedImage.contentId + if (contentId != null) { + allImages.filter { it.originalUrl.toString() == url.toString() && it !== downloadedImage } + .forEach { image -> + imageService.downloadIfNew(image) + progress.currentStep.current = completedImageDownload.incrementAndGet() + emit(progress) + } + } + } - return DownloadImagesResult(gamesWithImages = gamesWithImages) + return DownloadImagesResult(gamesWithImages = games) } private fun calculateFileSizes(games: List, progress: LibraryScanProgress): CalculateFilesizesResult {