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)
+ }
}
}