diff --git a/app/src/main/frontend/components/administration/MessageManagement.tsx b/app/src/main/frontend/components/administration/MessageManagement.tsx index 8ad552b..1844e43 100644 --- a/app/src/main/frontend/components/administration/MessageManagement.tsx +++ b/app/src/main/frontend/components/administration/MessageManagement.tsx @@ -8,6 +8,7 @@ import {PaperPlaneRight, Pencil} from "@phosphor-icons/react"; import MessageTemplateDto from "Frontend/generated/org/gameyfin/app/messages/templates/MessageTemplateDto"; import SendTestNotificationModal from "Frontend/components/administration/messages/SendTestNotificationModal"; import EditTemplateModal from "Frontend/components/administration/messages/EditTemplateModel"; +import * as Yup from "yup"; function MessageManagementLayout({getConfig, formik}: any) { @@ -126,4 +127,21 @@ function MessageManagementLayout({getConfig, formik}: any) { ); } -export const MessageManagement = withConfigPage(MessageManagementLayout, "Messages", "messages"); \ No newline at end of file +const validationSchema = Yup.object({ + messages: Yup.object({ + providers: Yup.object({ + email: Yup.object({ + enabled: Yup.boolean().required("Required"), + host: Yup.string().required("Host is required"), + port: Yup.number().required("Port is required") + .min(0, "Port must be between 0 and 65535") + .max(65535, "Port must be between 0 and 65535"), + username: Yup.string() + .email("Invalid email address") + .required("Username is required"), + }) + }) + }) +}); + +export const MessageManagement = withConfigPage(MessageManagementLayout, "Messages", validationSchema); \ No newline at end of file diff --git a/app/src/main/frontend/routes.tsx b/app/src/main/frontend/routes.tsx index a57c23c..33ede5c 100644 --- a/app/src/main/frontend/routes.tsx +++ b/app/src/main/frontend/routes.tsx @@ -23,20 +23,18 @@ import SearchView from "Frontend/views/SearchView"; import RecentlyAddedView from "Frontend/views/RecentlyAddedView"; import LibraryView from "Frontend/views/LibraryView"; import {RouterConfigurationBuilder} from "@vaadin/hilla-file-router/runtime.js"; -import {ConfigEndpoint} from "Frontend/generated/endpoints"; export const {router, routes} = new RouterConfigurationBuilder() .withReactRoutes([ { element: , - handle: {requiresLogin: false}, children: [ { element: , - handle: {requiresLogin: !ConfigEndpoint.isPublicAccessEnabled()}, children: [ { - index: true, element: + index: true, + element: }, { path: 'search', @@ -65,7 +63,6 @@ export const {router, routes} = new RouterConfigurationBuilder() { path: 'administration', element: , - handle: {requiresLogin: true}, children: [ { path: 'libraries', @@ -86,19 +83,19 @@ export const {router, routes} = new RouterConfigurationBuilder() ] }, { - path: 'login', element: , handle: {requiresLogin: false} + path: 'login', element: }, { - path: 'setup', element: , handle: {requiresLogin: false} + path: 'setup', element: }, { - path: 'accept-invitation', element: , handle: {requiresLogin: false} + path: 'accept-invitation', element: }, { - path: 'reset-password', element: , handle: {requiresLogin: false} + path: 'reset-password', element: }, { - path: 'confirm-email', element: , handle: {requiresLogin: true} + path: 'confirm-email', element: }, ] } 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 1319366..3cd9ebf 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 @@ -44,6 +44,7 @@ class SecurityConfig( .requestMatchers("/reset-password").permitAll() .requestMatchers("/accept-invitation").permitAll() .requestMatchers("/public/**").permitAll() + .requestMatchers("/images/**").permitAll() // Dynamic public access for certain endpoints auth.requestMatchers("/").access(DynamicPublicAccessAuthorizationManager(config)) @@ -51,8 +52,6 @@ class SecurityConfig( .requestMatchers("/library/**").access(DynamicPublicAccessAuthorizationManager(config)) .requestMatchers("/search/**").access(DynamicPublicAccessAuthorizationManager(config)) .requestMatchers("/download/**").access(DynamicPublicAccessAuthorizationManager(config)) - .requestMatchers("/images/**").access(DynamicPublicAccessAuthorizationManager(config)) - .requestMatchers("/images/**").access(DynamicPublicAccessAuthorizationManager(config)) } http.sessionManagement { sessionManagement -> diff --git a/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt b/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt index e496747..d84df0d 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt @@ -35,6 +35,8 @@ import java.net.URI import java.nio.file.Path import java.time.ZoneId import java.time.ZoneOffset +import java.util.concurrent.Executors +import java.util.concurrent.Future import kotlin.time.Duration.Companion.milliseconds import kotlin.time.toJavaDuration import org.gameyfin.pluginapi.gamemetadata.GameMetadata as PluginApiMetadata @@ -71,6 +73,8 @@ class GameService( fun emit(event: GameEvent) { gameEvents.tryEmitNext(event) } + + private val executor = Executors.newVirtualThreadPerTaskExecutor() } private val metadataPlugins: List @@ -237,15 +241,18 @@ class GameService( fun getPotentialMatches(searchTerm: String): List { // 1. Query all plugins for up to 10 results each - val results = metadataPlugins.flatMap { plugin -> - try { - plugin.fetchByTitle(searchTerm, 10) - .map { plugin to it } - } catch (e: Exception) { - log.error(e) { "Error fetching metadata for game with plugin ${plugin.javaClass.name}" } - emptyList() + val futures: List>>> = metadataPlugins.map { plugin -> + executor.submit>> { + try { + plugin.fetchByTitle(searchTerm, 10).map { plugin to it } + } catch (e: Exception) { + log.error(e) { "Error fetching metadata for searchterm '$searchTerm' with plugin ${plugin.javaClass.name}" } + emptyList() + } } } + val results = futures.flatMap { it.get() } + val providerToManagementEntry = results.toMap().entries.associate { it.key to pluginService.getPluginManagementEntry(it.key.javaClass) } @@ -423,20 +430,17 @@ class GameService( * @return A map of metadata plugins and their respective results */ private fun queryPlugins(gameTitle: String): Map { - return runBlocking { - coroutineScope { - metadataPlugins.associateWith { - async { - try { - it.fetchByTitle(gameTitle).firstOrNull() - } catch (e: Exception) { - log.error(e) { "Error fetching metadata for game with plugin ${it.javaClass.name}" } - null - } - }.await() + val futures = metadataPlugins.associateWith { plugin -> + executor.submit { + try { + plugin.fetchByTitle(gameTitle).firstOrNull() + } catch (_: Exception) { + log.error { "Error fetching metadata for game title '$gameTitle' with plugin ${plugin.javaClass.name}" } + null } } } + return futures.mapValues { it.value.get() } } /** diff --git a/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt b/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt index ab67587..7a84315 100644 --- a/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/users/UserService.kt @@ -299,7 +299,7 @@ class UserService( username = user.username, email = user.email, emailConfirmed = user.emailConfirmed, - isEnabled = user.enabled, + enabled = user.enabled, hasAvatar = user.avatar != null, avatarId = user.avatar?.id, managedBySso = user.oidcProviderId != null, diff --git a/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoDto.kt b/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoDto.kt index 33881d1..26d3d96 100644 --- a/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoDto.kt +++ b/app/src/main/kotlin/org/gameyfin/app/users/dto/UserInfoDto.kt @@ -7,7 +7,7 @@ data class UserInfoDto( val managedBySso: Boolean, val email: String, val emailConfirmed: Boolean, - val isEnabled: Boolean, + val enabled: Boolean, val hasAvatar: Boolean, val avatarId: Long? = null, var roles: List diff --git a/plugins/steamgriddb/src/main/kotlin/org/gameyfin/plugins/metadata/steamgriddb/SteamGridDbPlugin.kt b/plugins/steamgriddb/src/main/kotlin/org/gameyfin/plugins/metadata/steamgriddb/SteamGridDbPlugin.kt index 8c25d0c..aae93b3 100644 --- a/plugins/steamgriddb/src/main/kotlin/org/gameyfin/plugins/metadata/steamgriddb/SteamGridDbPlugin.kt +++ b/plugins/steamgriddb/src/main/kotlin/org/gameyfin/plugins/metadata/steamgriddb/SteamGridDbPlugin.kt @@ -1,5 +1,8 @@ package org.gameyfin.plugins.metadata.steamgriddb +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking import org.gameyfin.pluginapi.core.config.* import org.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin @@ -81,18 +84,21 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra override fun fetchByTitle(gameTitle: String, maxResults: Int): List { return runBlocking { val results = searchSteamGridDb(gameTitle) - - results.map { game -> - val grids = getGridsForGame(game.id) - val heroes = getHeroesForGame(game.id) - GameMetadata( - originalId = game.id.toString(), - title = game.name, - release = game.releaseDate, - coverUrls = grids?.map { URI(it.url) }, - headerUrls = heroes?.map { URI(it.url) } - ) - }.take(maxResults) + coroutineScope { + results.map { game -> + async { + val grids = getGridsForGame(game.id) + val heroes = getHeroesForGame(game.id) + GameMetadata( + originalId = game.id.toString(), + title = game.name, + release = game.releaseDate, + coverUrls = grids?.map { URI(it.url) }, + headerUrls = heroes?.map { URI(it.url) } + ) + } + }.awaitAll().take(maxResults) + } } }