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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 69 additions and 45 deletions

View File

@ -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");
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);

View File

@ -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: <App/>,
handle: {requiresLogin: false},
children: [
{
element: <MainLayout/>,
handle: {requiresLogin: !ConfigEndpoint.isPublicAccessEnabled()},
children: [
{
index: true, element: <HomeView/>
index: true,
element: <HomeView/>
},
{
path: 'search',
@ -65,7 +63,6 @@ export const {router, routes} = new RouterConfigurationBuilder()
{
path: 'administration',
element: <AdministrationView/>,
handle: {requiresLogin: true},
children: [
{
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/>
},
]
}

View File

@ -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 ->

View File

@ -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<GameMetadataProvider>
@ -237,15 +241,18 @@ class GameService(
fun getPotentialMatches(searchTerm: String): List<GameSearchResultDto> {
// 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<Future<List<Pair<GameMetadataProvider, PluginApiMetadata>>>> = metadataPlugins.map { plugin ->
executor.submit<List<Pair<GameMetadataProvider, PluginApiMetadata>>> {
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<GameMetadataProvider, PluginApiMetadata?> {
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<PluginApiMetadata?> {
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() }
}
/**

View File

@ -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,

View File

@ -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<Role>

View File

@ -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<GameMetadata> {
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)
}
}
}