mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Release 2.0.1
This commit is contained in:
+1
-1
@@ -265,4 +265,4 @@
|
||||
"disableUsageStatistics": true,
|
||||
"hash": "962eccc3fa0735d5234901be4f9e384096113c45bec22564a53688096d62aef4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.enabled")}/>
|
||||
|
||||
<Section title="SSO user handling"/>
|
||||
<div className="flex flex-row items-baseline">
|
||||
<div className="flex flex-row items-baseline mb-4">
|
||||
<CheckboxGroup className="flex flex-col flex-1 items-baseline gap-2"
|
||||
value={["auto-register-new-users"]}>
|
||||
<div className="flex flex-row gap-2">
|
||||
@@ -70,6 +70,13 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
|
||||
!formik.values.sso.oidc["auto-register-new-users"]}/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.roles-claim")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.oauth-scopes")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
</div>
|
||||
|
||||
<Section title="SSO provider configuration"/>
|
||||
<ConfigFormField configElement={getConfig("sso.oidc.client-id")}
|
||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function GameView() {
|
||||
}, {} as Record<string, ComboButtonOption>);
|
||||
setDownloadOptions(options);
|
||||
});
|
||||
}, []);
|
||||
}, [gameId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.isLoaded && (!gameId || !state.state[parseInt(gameId)])) {
|
||||
|
||||
@@ -147,6 +147,20 @@ sealed class ConfigProperties<T : Serializable>(
|
||||
true
|
||||
)
|
||||
|
||||
data object RolesClaim : ConfigProperties<String>(
|
||||
String::class,
|
||||
"sso.oidc.roles-claim",
|
||||
"JWT claim to extract roles from",
|
||||
"roles"
|
||||
)
|
||||
|
||||
data object OAuthScopes : ConfigProperties<Array<String>>(
|
||||
Array<String>::class,
|
||||
"sso.oidc.oauth-scopes",
|
||||
"OAuth2 scopes to request",
|
||||
arrayOf("openid", "profile", "email", "roles")
|
||||
)
|
||||
|
||||
data object ClientId : ConfigProperties<String>(
|
||||
String::class,
|
||||
"sso.oidc.client-id",
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<OidcUserAuthority>()
|
||||
.flatMap { oidcUserAuthority ->
|
||||
val userInfo = oidcUserAuthority.userInfo
|
||||
val roles = userInfo.getClaim<List<String>>("roles") ?: return@flatMap emptySequence()
|
||||
val rolesClaim = configService.get(ConfigProperties.SSO.OIDC.RolesClaim)
|
||||
val roles = userInfo.getClaim<List<String>>(rolesClaim) ?: return@flatMap emptySequence()
|
||||
roles.asSequence().mapNotNull {
|
||||
if (it.startsWith(SSO_ROLE_PREFIX)) SimpleGrantedAuthority(
|
||||
it.replace(SSO_ROLE_PREFIX, INTERNAL_ROLE_PREFIX)
|
||||
|
||||
+1
-1
@@ -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<Int>("trackerPort")
|
||||
val path = "announce"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user