2.0.0.beta5 (#629)

* Remove unnecessary "requiresLogin" handles

* Rename property in UserInfoDto

* Fix #628

* Fix ant matchers (again)

* Major performance improvements for game matching

* Minor logging improvements
This commit is contained in:
Simon
2025-07-17 17:53:40 +02:00
committed by GitHub
parent e506ad1bc2
commit 13d5fcc80a
7 changed files with 69 additions and 45 deletions
@@ -8,6 +8,7 @@ import {PaperPlaneRight, Pencil} from "@phosphor-icons/react";
import MessageTemplateDto from "Frontend/generated/org/gameyfin/app/messages/templates/MessageTemplateDto"; import MessageTemplateDto from "Frontend/generated/org/gameyfin/app/messages/templates/MessageTemplateDto";
import SendTestNotificationModal from "Frontend/components/administration/messages/SendTestNotificationModal"; import SendTestNotificationModal from "Frontend/components/administration/messages/SendTestNotificationModal";
import EditTemplateModal from "Frontend/components/administration/messages/EditTemplateModel"; import EditTemplateModal from "Frontend/components/administration/messages/EditTemplateModel";
import * as Yup from "yup";
function MessageManagementLayout({getConfig, formik}: any) { function MessageManagementLayout({getConfig, formik}: any) {
@@ -126,4 +127,21 @@ function MessageManagementLayout({getConfig, formik}: any) {
); );
} }
export const MessageManagement = withConfigPage(MessageManagementLayout, "Messages", "messages"); 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);
+7 -10
View File
@@ -23,20 +23,18 @@ import SearchView from "Frontend/views/SearchView";
import RecentlyAddedView from "Frontend/views/RecentlyAddedView"; import RecentlyAddedView from "Frontend/views/RecentlyAddedView";
import LibraryView from "Frontend/views/LibraryView"; import LibraryView from "Frontend/views/LibraryView";
import {RouterConfigurationBuilder} from "@vaadin/hilla-file-router/runtime.js"; import {RouterConfigurationBuilder} from "@vaadin/hilla-file-router/runtime.js";
import {ConfigEndpoint} from "Frontend/generated/endpoints";
export const {router, routes} = new RouterConfigurationBuilder() export const {router, routes} = new RouterConfigurationBuilder()
.withReactRoutes([ .withReactRoutes([
{ {
element: <App/>, element: <App/>,
handle: {requiresLogin: false},
children: [ children: [
{ {
element: <MainLayout/>, element: <MainLayout/>,
handle: {requiresLogin: !ConfigEndpoint.isPublicAccessEnabled()},
children: [ children: [
{ {
index: true, element: <HomeView/> index: true,
element: <HomeView/>
}, },
{ {
path: 'search', path: 'search',
@@ -65,7 +63,6 @@ export const {router, routes} = new RouterConfigurationBuilder()
{ {
path: 'administration', path: 'administration',
element: <AdministrationView/>, element: <AdministrationView/>,
handle: {requiresLogin: true},
children: [ children: [
{ {
path: 'libraries', path: 'libraries',
@@ -86,19 +83,19 @@ export const {router, routes} = new RouterConfigurationBuilder()
] ]
}, },
{ {
path: 'login', element: <LoginView/>, handle: {requiresLogin: false} path: 'login', element: <LoginView/>
}, },
{ {
path: 'setup', element: <SetupView/>, handle: {requiresLogin: false} path: 'setup', element: <SetupView/>
}, },
{ {
path: 'accept-invitation', element: <InvitationRegistrationView/>, handle: {requiresLogin: false} path: 'accept-invitation', element: <InvitationRegistrationView/>
}, },
{ {
path: 'reset-password', element: <PasswordResetView/>, handle: {requiresLogin: false} path: 'reset-password', element: <PasswordResetView/>
}, },
{ {
path: 'confirm-email', element: <EmailConfirmationView/>, handle: {requiresLogin: true} path: 'confirm-email', element: <EmailConfirmationView/>
}, },
] ]
} }
@@ -44,6 +44,7 @@ class SecurityConfig(
.requestMatchers("/reset-password").permitAll() .requestMatchers("/reset-password").permitAll()
.requestMatchers("/accept-invitation").permitAll() .requestMatchers("/accept-invitation").permitAll()
.requestMatchers("/public/**").permitAll() .requestMatchers("/public/**").permitAll()
.requestMatchers("/images/**").permitAll()
// Dynamic public access for certain endpoints // Dynamic public access for certain endpoints
auth.requestMatchers("/").access(DynamicPublicAccessAuthorizationManager(config)) auth.requestMatchers("/").access(DynamicPublicAccessAuthorizationManager(config))
@@ -51,8 +52,6 @@ class SecurityConfig(
.requestMatchers("/library/**").access(DynamicPublicAccessAuthorizationManager(config)) .requestMatchers("/library/**").access(DynamicPublicAccessAuthorizationManager(config))
.requestMatchers("/search/**").access(DynamicPublicAccessAuthorizationManager(config)) .requestMatchers("/search/**").access(DynamicPublicAccessAuthorizationManager(config))
.requestMatchers("/download/**").access(DynamicPublicAccessAuthorizationManager(config)) .requestMatchers("/download/**").access(DynamicPublicAccessAuthorizationManager(config))
.requestMatchers("/images/**").access(DynamicPublicAccessAuthorizationManager(config))
.requestMatchers("/images/**").access(DynamicPublicAccessAuthorizationManager(config))
} }
http.sessionManagement { sessionManagement -> http.sessionManagement { sessionManagement ->
@@ -35,6 +35,8 @@ import java.net.URI
import java.nio.file.Path import java.nio.file.Path
import java.time.ZoneId import java.time.ZoneId
import java.time.ZoneOffset import java.time.ZoneOffset
import java.util.concurrent.Executors
import java.util.concurrent.Future
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.toJavaDuration import kotlin.time.toJavaDuration
import org.gameyfin.pluginapi.gamemetadata.GameMetadata as PluginApiMetadata import org.gameyfin.pluginapi.gamemetadata.GameMetadata as PluginApiMetadata
@@ -71,6 +73,8 @@ class GameService(
fun emit(event: GameEvent) { fun emit(event: GameEvent) {
gameEvents.tryEmitNext(event) gameEvents.tryEmitNext(event)
} }
private val executor = Executors.newVirtualThreadPerTaskExecutor()
} }
private val metadataPlugins: List<GameMetadataProvider> private val metadataPlugins: List<GameMetadataProvider>
@@ -237,15 +241,18 @@ class GameService(
fun getPotentialMatches(searchTerm: String): List<GameSearchResultDto> { fun getPotentialMatches(searchTerm: String): List<GameSearchResultDto> {
// 1. Query all plugins for up to 10 results each // 1. Query all plugins for up to 10 results each
val results = metadataPlugins.flatMap { plugin -> val futures: List<Future<List<Pair<GameMetadataProvider, PluginApiMetadata>>>> = metadataPlugins.map { plugin ->
try { executor.submit<List<Pair<GameMetadataProvider, PluginApiMetadata>>> {
plugin.fetchByTitle(searchTerm, 10) try {
.map { plugin to it } plugin.fetchByTitle(searchTerm, 10).map { plugin to it }
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Error fetching metadata for game with plugin ${plugin.javaClass.name}" } log.error(e) { "Error fetching metadata for searchterm '$searchTerm' with plugin ${plugin.javaClass.name}" }
emptyList() emptyList()
}
} }
} }
val results = futures.flatMap { it.get() }
val providerToManagementEntry = val providerToManagementEntry =
results.toMap().entries.associate { it.key to pluginService.getPluginManagementEntry(it.key.javaClass) } 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 * @return A map of metadata plugins and their respective results
*/ */
private fun queryPlugins(gameTitle: String): Map<GameMetadataProvider, PluginApiMetadata?> { private fun queryPlugins(gameTitle: String): Map<GameMetadataProvider, PluginApiMetadata?> {
return runBlocking { val futures = metadataPlugins.associateWith { plugin ->
coroutineScope { executor.submit<PluginApiMetadata?> {
metadataPlugins.associateWith { try {
async { plugin.fetchByTitle(gameTitle).firstOrNull()
try { } catch (_: Exception) {
it.fetchByTitle(gameTitle).firstOrNull() log.error { "Error fetching metadata for game title '$gameTitle' with plugin ${plugin.javaClass.name}" }
} catch (e: Exception) { null
log.error(e) { "Error fetching metadata for game with plugin ${it.javaClass.name}" }
null
}
}.await()
} }
} }
} }
return futures.mapValues { it.value.get() }
} }
/** /**
@@ -299,7 +299,7 @@ class UserService(
username = user.username, username = user.username,
email = user.email, email = user.email,
emailConfirmed = user.emailConfirmed, emailConfirmed = user.emailConfirmed,
isEnabled = user.enabled, enabled = user.enabled,
hasAvatar = user.avatar != null, hasAvatar = user.avatar != null,
avatarId = user.avatar?.id, avatarId = user.avatar?.id,
managedBySso = user.oidcProviderId != null, managedBySso = user.oidcProviderId != null,
@@ -7,7 +7,7 @@ data class UserInfoDto(
val managedBySso: Boolean, val managedBySso: Boolean,
val email: String, val email: String,
val emailConfirmed: Boolean, val emailConfirmed: Boolean,
val isEnabled: Boolean, val enabled: Boolean,
val hasAvatar: Boolean, val hasAvatar: Boolean,
val avatarId: Long? = null, val avatarId: Long? = null,
var roles: List<Role> var roles: List<Role>
@@ -1,5 +1,8 @@
package org.gameyfin.plugins.metadata.steamgriddb package org.gameyfin.plugins.metadata.steamgriddb
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.gameyfin.pluginapi.core.config.* import org.gameyfin.pluginapi.core.config.*
import org.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin 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<GameMetadata> { override fun fetchByTitle(gameTitle: String, maxResults: Int): List<GameMetadata> {
return runBlocking { return runBlocking {
val results = searchSteamGridDb(gameTitle) val results = searchSteamGridDb(gameTitle)
coroutineScope {
results.map { game -> results.map { game ->
val grids = getGridsForGame(game.id) async {
val heroes = getHeroesForGame(game.id) val grids = getGridsForGame(game.id)
GameMetadata( val heroes = getHeroesForGame(game.id)
originalId = game.id.toString(), GameMetadata(
title = game.name, originalId = game.id.toString(),
release = game.releaseDate, title = game.name,
coverUrls = grids?.map { URI(it.url) }, release = game.releaseDate,
headerUrls = heroes?.map { URI(it.url) } coverUrls = grids?.map { URI(it.url) },
) headerUrls = heroes?.map { URI(it.url) }
}.take(maxResults) )
}
}.awaitAll().take(maxResults)
}
} }
} }