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/frontend/components/administration/SsoManagement.tsx b/app/src/main/frontend/components/administration/SsoManagement.tsx index 7db75e9..f873d8b 100644 --- a/app/src/main/frontend/components/administration/SsoManagement.tsx +++ b/app/src/main/frontend/components/administration/SsoManagement.tsx @@ -49,7 +49,7 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
-
+
@@ -70,6 +70,13 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) { !formik.values.sso.oidc["auto-register-new-users"]}/>
+
+ + +
+
diff --git a/app/src/main/frontend/views/GameView.tsx b/app/src/main/frontend/views/GameView.tsx index 4ef6d24..44ac19e 100644 --- a/app/src/main/frontend/views/GameView.tsx +++ b/app/src/main/frontend/views/GameView.tsx @@ -46,7 +46,7 @@ export default function GameView() { }, {} as Record); setDownloadOptions(options); }); - }, []); + }, [gameId]); useEffect(() => { if (state.isLoaded && (!gameId || !state.state[parseInt(gameId)])) { diff --git a/app/src/main/kotlin/org/gameyfin/app/config/ConfigProperties.kt b/app/src/main/kotlin/org/gameyfin/app/config/ConfigProperties.kt index 23e928e..a65cf0a 100644 --- a/app/src/main/kotlin/org/gameyfin/app/config/ConfigProperties.kt +++ b/app/src/main/kotlin/org/gameyfin/app/config/ConfigProperties.kt @@ -147,6 +147,20 @@ sealed class ConfigProperties( true ) + data object RolesClaim : ConfigProperties( + String::class, + "sso.oidc.roles-claim", + "JWT claim to extract roles from", + "roles" + ) + + data object OAuthScopes : ConfigProperties>( + Array::class, + "sso.oidc.oauth-scopes", + "OAuth2 scopes to request", + arrayOf("openid", "profile", "email", "roles") + ) + data object ClientId : ConfigProperties( String::class, "sso.oidc.client-id", diff --git a/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityConfig.kt b/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityConfig.kt index 08df09c..3caaa62 100644 --- a/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityConfig.kt +++ b/app/src/main/kotlin/org/gameyfin/app/core/security/SecurityConfig.kt @@ -98,7 +98,7 @@ class SecurityConfig( val clientRegistration = ClientRegistration.withRegistrationId(SSO_PROVIDER_KEY) .clientId(config.get(ConfigProperties.SSO.OIDC.ClientId)) .clientSecret(config.get(ConfigProperties.SSO.OIDC.ClientSecret)) - .scope("openid", "profile", "email") + .scope(config.get(ConfigProperties.SSO.OIDC.OAuthScopes)?.toList()) .userNameAttributeName("preferred_username") .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .issuerUri(config.get(ConfigProperties.SSO.OIDC.IssuerUrl)) 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 { diff --git a/app/src/main/kotlin/org/gameyfin/app/users/RoleService.kt b/app/src/main/kotlin/org/gameyfin/app/users/RoleService.kt index d5feeef..c729574 100644 --- a/app/src/main/kotlin/org/gameyfin/app/users/RoleService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/users/RoleService.kt @@ -1,5 +1,7 @@ package org.gameyfin.app.users +import org.gameyfin.app.config.ConfigProperties +import org.gameyfin.app.config.ConfigService import org.gameyfin.app.core.Role import org.gameyfin.app.users.entities.User import org.gameyfin.app.users.persistence.UserRepository @@ -11,7 +13,8 @@ import org.springframework.stereotype.Service @Service class RoleService( - private val userRepository: UserRepository + private val userRepository: UserRepository, + private val configService: ConfigService ) { companion object { @@ -66,7 +69,8 @@ class RoleService( .filterIsInstance() .flatMap { oidcUserAuthority -> val userInfo = oidcUserAuthority.userInfo - val roles = userInfo.getClaim>("roles") ?: return@flatMap emptySequence() + val rolesClaim = configService.get(ConfigProperties.SSO.OIDC.RolesClaim) + val roles = userInfo.getClaim>(rolesClaim) ?: return@flatMap emptySequence() roles.asSequence().mapNotNull { if (it.startsWith(SSO_ROLE_PREFIX)) SimpleGrantedAuthority( it.replace(SSO_ROLE_PREFIX, INTERNAL_ROLE_PREFIX) diff --git a/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPlugin.kt b/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPlugin.kt index d3cc38d..d65eb46 100644 --- a/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPlugin.kt +++ b/plugins/torrentdownload/src/main/kotlin/org/gameyfin/plugins/download/torrent/TorrentDownloadPlugin.kt @@ -150,7 +150,7 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin private fun getTrackerUri(): URI { val protocol = "http" // No SSL support in ttorrent: https://github.com/mpetazzoni/ttorrent/issues/4 - val host = getHostname().getCanonicalHostName() + val host = getHostname().getHostName() val port = config("trackerPort") val path = "announce" diff --git a/plugins/torrentdownload/src/main/resources/MANIFEST.MF b/plugins/torrentdownload/src/main/resources/MANIFEST.MF index ee430df..c947776 100644 --- a/plugins/torrentdownload/src/main/resources/MANIFEST.MF +++ b/plugins/torrentdownload/src/main/resources/MANIFEST.MF @@ -1,4 +1,4 @@ -Plugin-Version: 1.0.0 +Plugin-Version: 1.0.1 Plugin-Class: org.gameyfin.plugins.download.torrent.TorrentDownloadPlugin Plugin-Id: org.gameyfin.plugins.download.torrent Plugin-Name: Torrent Download