Implement clean-up of image files
This commit is contained in:
Simon
2025-08-29 19:20:43 +02:00
committed by GitHub
parent 20c9bf47da
commit 021d165fb1
5 changed files with 77 additions and 33 deletions
+1 -1
View File
@@ -265,4 +265,4 @@
"disableUsageStatistics": true,
"hash": "962eccc3fa0735d5234901be4f9e384096113c45bec22564a53688096d62aef4"
}
}
}
@@ -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!!))
}
}
@@ -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)
@@ -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)
}
}
@@ -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<Game>, progress: LibraryScanProgress): DownloadImagesResult {
val completedImageDownload = AtomicInteger(0)
val imageDownloadTasks = games.map { game ->
Callable<Game?> {
// Collect all images from all games in the batch
val allImages = games.flatMap { game ->
val images = mutableListOf<Image>()
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<URL, Image>()
// 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<Game>, progress: LibraryScanProgress): CalculateFilesizesResult {