Various bugfixes and minor improvements

This commit is contained in:
grimsi
2025-07-19 15:11:40 +02:00
parent 577f901e85
commit c01222d521
12 changed files with 110 additions and 60 deletions
@@ -23,7 +23,6 @@ export default function LibraryCreationModal({
const [scanAfterCreation, setScanAfterCreation] = useState<boolean>(true); const [scanAfterCreation, setScanAfterCreation] = useState<boolean>(true);
async function createLibrary(library: LibraryDto) { async function createLibrary(library: LibraryDto) {
try {
await LibraryEndpoint.createLibrary(library as LibraryDto, scanAfterCreation); await LibraryEndpoint.createLibrary(library as LibraryDto, scanAfterCreation);
addToast({ addToast({
@@ -31,14 +30,6 @@ export default function LibraryCreationModal({
description: `Library ${library.name} created!`, description: `Library ${library.name} created!`,
color: "success" color: "success"
}); });
} catch (e) {
addToast({
title: "Error creating library",
description: `Library ${library.name} could not be created!`,
color: "warning"
});
throw "Error creating library: " + e;
}
} }
return ( return (
@@ -38,9 +38,11 @@ class GameyfinExtensionFinder(pluginManager: PluginManager) : LegacyExtensionFin
result.add(extensionWrapper) result.add(extensionWrapper)
log.debug { "Added extension '$className' with ordinal ${extensionWrapper.ordinal}" } log.debug { "Added extension '$className' with ordinal ${extensionWrapper.ordinal}" }
} catch (e: ClassNotFoundException) { } catch (e: ClassNotFoundException) {
log.error(e) { e.message } log.error { "Error loading plugin: ${e.message}" }
log.debug(e) {}
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
log.error(e) { e.message } log.error { "Error loading plugin: ${e.message}" }
log.debug(e) {}
} }
} }
@@ -215,7 +215,7 @@ class GameyfinPluginManager(
fun getPluginForExtension(extensionClass: Class<ExtensionPoint>): PluginWrapper? { fun getPluginForExtension(extensionClass: Class<ExtensionPoint>): PluginWrapper? {
return getPlugins().firstOrNull { pluginWrapper -> return getPlugins().firstOrNull { pluginWrapper ->
getExtensionTypeClasses(pluginWrapper.pluginId).any { it == extensionClass.javaClass } getExtensionClasses(pluginWrapper.pluginId).any { it == extensionClass }
} }
} }
@@ -12,6 +12,7 @@ import org.gameyfin.app.core.alphaNumeric
import org.gameyfin.app.core.filesystem.FilesystemService import org.gameyfin.app.core.filesystem.FilesystemService
import org.gameyfin.app.core.filterValuesNotNull import org.gameyfin.app.core.filterValuesNotNull
import org.gameyfin.app.core.plugins.PluginService import org.gameyfin.app.core.plugins.PluginService
import org.gameyfin.app.core.plugins.management.GameyfinPluginDescriptor
import org.gameyfin.app.core.plugins.management.GameyfinPluginManager import org.gameyfin.app.core.plugins.management.GameyfinPluginManager
import org.gameyfin.app.core.plugins.management.PluginManagementEntry import org.gameyfin.app.core.plugins.management.PluginManagementEntry
import org.gameyfin.app.core.replaceRomanNumerals import org.gameyfin.app.core.replaceRomanNumerals
@@ -104,7 +105,8 @@ class GameService(
imageService.downloadIfNew(it) imageService.downloadIfNew(it)
} }
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Error downloading images for game: ${e.message}" } log.error { "Error downloading images for game: ${e.message}" }
log.debug(e) {}
null null
} }
@@ -449,7 +451,9 @@ class GameService(
try { try {
plugin.fetchByTitle(searchTerm, 10).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 searchterm '$searchTerm' with plugin ${plugin.javaClass.name}" } val pluginWrapper = pluginManager.getPluginForExtension(plugin.javaClass)
log.warn { "Error fetching metadata for searchterm '$searchTerm' with plugin '${(pluginWrapper?.descriptor as GameyfinPluginDescriptor?)?.pluginName ?: pluginWrapper?.pluginId ?: plugin.javaClass.name}': ${e.message}" }
log.debug(e) {}
emptyList() emptyList()
} }
} }
@@ -561,7 +565,9 @@ class GameService(
try { try {
return@async plugin.fetchById(originalId) return@async plugin.fetchById(originalId)
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Error fetching metadata for game [id: $originalId] with plugin ${plugin.javaClass.name}" } val pluginWrapper = pluginManager.getPluginForExtension(plugin.javaClass)
log.warn { "Error fetching metadata for game [id: $originalId] with plugin '${(pluginWrapper?.descriptor as GameyfinPluginDescriptor?)?.pluginName ?: pluginWrapper?.pluginId ?: plugin.javaClass.name}': ${e.message}" }
log.debug(e) {}
null null
} }
}.await() }.await()
@@ -638,8 +644,10 @@ class GameService(
executor.submit<PluginApiMetadata?> { executor.submit<PluginApiMetadata?> {
try { try {
plugin.fetchByTitle(gameTitle).firstOrNull() plugin.fetchByTitle(gameTitle).firstOrNull()
} catch (_: Exception) { } catch (e: Exception) {
log.error { "Error fetching metadata for game title '$gameTitle' with plugin ${plugin.javaClass.name}" } val pluginWrapper = pluginManager.getPluginForExtension(plugin.javaClass)
log.warn { "Error fetching metadata for game title '$gameTitle' with plugin '${(pluginWrapper?.descriptor as GameyfinPluginDescriptor?)?.pluginName ?: pluginWrapper?.pluginId ?: plugin.javaClass.name}': ${e.message}" }
log.debug(e) {}
null null
} }
} }
@@ -16,6 +16,13 @@ class Company(
if (other !is Company) return false if (other !is Company) return false
return name == other.name && type == other.type return name == other.name && type == other.type
} }
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + name.hashCode()
result = 31 * result + type.hashCode()
return result
}
} }
enum class CompanyType { enum class CompanyType {
@@ -33,6 +33,12 @@ class Image(
if (other !is Image) return false if (other !is Image) return false
return originalUrl.toString() == other.originalUrl.toString() return originalUrl.toString() == other.originalUrl.toString()
} }
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + originalUrl?.toString().hashCode()
return result
}
} }
enum class ImageType { enum class ImageType {
@@ -1,12 +1,6 @@
package org.gameyfin.app.games.repositories package org.gameyfin.app.games.repositories
import org.gameyfin.app.games.entities.Game import org.gameyfin.app.games.entities.Game
import org.springframework.data.domain.Limit
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
interface GameRepository : JpaRepository<Game, Long> { interface GameRepository : JpaRepository<Game, Long>
fun findByMetadata_Path(path: String): Game?
fun findAllByMetadata_PathIn(paths: List<String>): List<Game>
fun findByOrderByCreatedAtDesc(limit: Limit): List<Game>
fun findByOrderByUpdatedAtDesc(limit: Limit): List<Game>
}
@@ -1,9 +1,6 @@
package org.gameyfin.app.libraries package org.gameyfin.app.libraries
import jakarta.persistence.Entity import jakarta.persistence.*
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
@Entity @Entity
class DirectoryMapping( class DirectoryMapping(
@@ -12,6 +9,7 @@ class DirectoryMapping(
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
var id: Long? = null, var id: Long? = null,
@Column(unique = true)
var internalPath: String, var internalPath: String,
var externalPath: String? = null, var externalPath: String? = null,
@@ -1,11 +1,9 @@
package org.gameyfin.app.libraries package org.gameyfin.app.libraries
import org.springframework.data.jpa.repository.EntityGraph
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
interface LibraryRepository : JpaRepository<Library, Long> { interface LibraryRepository : JpaRepository<Library, Long> {
@EntityGraph(attributePaths = ["games"]) @Query("SELECT d.internalPath, l FROM Library l JOIN l.directories d WHERE d.internalPath IN :paths")
@Query("SELECT l FROM Library l ORDER BY function('RAND') LIMIT 1") fun findByPaths(paths: List<String>): List<Pair<String, Library>>
fun findRandomLibrary(): Library?
} }
@@ -183,7 +183,8 @@ class LibraryScanService(
) )
emit(progress) emit(progress)
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Error during quick scan for library ${library.id}: ${e.message}" } log.error { "Error during quick scan for library ${library.id}: ${e.message}" }
log.debug(e) {}
progress.status = LibraryScanStatus.FAILED progress.status = LibraryScanStatus.FAILED
progress.finishedAt = Instant.now() progress.finishedAt = Instant.now()
emit(progress) emit(progress)
@@ -282,7 +283,8 @@ class LibraryScanService(
) )
emit(progress) emit(progress)
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Error during full scan for library ${library.id}: ${e.message}" } log.error { "Error during full scan for library ${library.id}: ${e.message}" }
log.debug(e) {}
progress.status = LibraryScanStatus.FAILED progress.status = LibraryScanStatus.FAILED
progress.finishedAt = Instant.now() progress.finishedAt = Instant.now()
emit(progress) emit(progress)
@@ -312,7 +314,8 @@ class LibraryScanService(
return@Callable game return@Callable game
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Error processing game: ${e.message}" } log.error { "Error processing game: ${e.message}" }
log.debug(e) {}
newUnmatchedPaths.add(path.toString()) newUnmatchedPaths.add(path.toString())
return@Callable null return@Callable null
@@ -372,7 +375,8 @@ class LibraryScanService(
game game
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Error downloading images for game: ${e.message}" } log.error { "Error downloading images for game: ${e.message}" }
log.debug(e) {}
null null
} finally { } finally {
progress.currentStep.current = completedImageDownload.get() progress.currentStep.current = completedImageDownload.get()
@@ -437,7 +441,8 @@ class LibraryScanService(
val game = gameService.update(game) val game = gameService.update(game)
return@Callable game return@Callable game
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Error updating game with id '${game.id}': ${e.message}" } log.error { "Error updating game with id '${game.id}': ${e.message}" }
log.debug(e) {}
return@Callable null return@Callable null
} finally { } finally {
progress.currentStep.current = completedUpdates.incrementAndGet() progress.currentStep.current = completedUpdates.incrementAndGet()
@@ -1,5 +1,6 @@
package org.gameyfin.app.libraries package org.gameyfin.app.libraries
import com.vaadin.hilla.exception.EndpointException
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.gameyfin.app.games.GameService import org.gameyfin.app.games.GameService
import org.gameyfin.app.libraries.dto.LibraryDto import org.gameyfin.app.libraries.dto.LibraryDto
@@ -19,7 +20,7 @@ class LibraryService(
private val libraryRepository: LibraryRepository, private val libraryRepository: LibraryRepository,
private val libraryCoreService: LibraryCoreService, private val libraryCoreService: LibraryCoreService,
private val libraryScanService: LibraryScanService, private val libraryScanService: LibraryScanService,
private val gameService: GameService, private val gameService: GameService
) { ) {
companion object { companion object {
@@ -70,6 +71,9 @@ class LibraryService(
* @return The created or updated LibraryDto object. * @return The created or updated LibraryDto object.
*/ */
fun create(library: LibraryDto, scanAfterCreation: Boolean) { fun create(library: LibraryDto, scanAfterCreation: Boolean) {
// Check for duplicate directories before creating a new library
checkForDuplicateDirectories(library.directories.map { it.internalPath })
val newLibrary = libraryRepository.save(libraryCoreService.toEntity(library)) val newLibrary = libraryRepository.save(libraryCoreService.toEntity(library))
if (scanAfterCreation) { if (scanAfterCreation) {
@@ -88,20 +92,49 @@ class LibraryService(
val library = libraryRepository.findByIdOrNull(libraryUpdateDto.id) val library = libraryRepository.findByIdOrNull(libraryUpdateDto.id)
?: throw IllegalArgumentException("Library with ID $libraryUpdateDto.id not found") ?: throw IllegalArgumentException("Library with ID $libraryUpdateDto.id not found")
// Update only non-null fields
libraryUpdateDto.name?.let { library.name = it } libraryUpdateDto.name?.let { library.name = it }
libraryUpdateDto.directories?.let { libraryUpdateDto.directories?.let { updatedDirs ->
library.directories.clear() checkForDuplicateDirectories(
library.directories.addAll( updatedDirs.map { it.internalPath },
it.map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) } excludeLibraryId = library.id
) )
val existingMappings = library.directories.associateBy { it.internalPath }
val updatedInternalPaths = updatedDirs.map { it.internalPath }.toSet()
// Remove mappings not present in the update
val removedDirs = library.directories.filter { it.internalPath !in updatedInternalPaths }
library.directories.removeAll(removedDirs)
// Remove all games within removed directories
val removedDirPaths = removedDirs.map { it.internalPath }
library.games.removeIf { game ->
removedDirPaths.any { removedPath ->
game.metadata.path.startsWith(removedPath)
}
}
// Update existing or add new directory mappings
updatedDirs.forEach { dto ->
val mapping = existingMappings[dto.internalPath]
if (mapping != null) {
mapping.externalPath = dto.externalPath // update fields
} else {
library.directories.add(
DirectoryMapping(
internalPath = dto.internalPath,
externalPath = dto.externalPath
)
)
}
}
} }
libraryUpdateDto.unmatchedPaths?.let { libraryUpdateDto.unmatchedPaths?.let {
library.unmatchedPaths.clear() library.unmatchedPaths.clear()
library.unmatchedPaths.addAll(it) library.unmatchedPaths.addAll(it)
} }
library.updatedAt = Instant.now() // Force the EntityListener to trigger an update and update the timestamp library.updatedAt = Instant.now()
libraryRepository.save(library) libraryRepository.save(library)
} }
@@ -113,4 +146,17 @@ class LibraryService(
fun delete(libraryId: Long) { fun delete(libraryId: Long) {
libraryRepository.deleteById(libraryId) libraryRepository.deleteById(libraryId)
} }
private fun checkForDuplicateDirectories(newLibraryFolders: List<String>, excludeLibraryId: Long? = null) {
val alreadyConfiguredFolders = libraryRepository.findByPaths(newLibraryFolders)
.filter { it.second.id != excludeLibraryId } // Exclude the current library if updating
.map { Pair(it.first, it.second) } // Convert to Pair for easier error message formatting
if (alreadyConfiguredFolders.isNotEmpty()) {
throw EndpointException(
"The following directories are already mapped to another library: " +
alreadyConfiguredFolders.joinToString(", ") { "${it.first} (${it.second.name})" }
)
}
}
} }
@@ -1,18 +1,12 @@
package org.gameyfin.app.messages package org.gameyfin.app.messages
import org.gameyfin.app.messages.templates.MessageTemplates
import org.gameyfin.app.users.UserService
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.gameyfin.app.core.events.AccountDeletedEvent import org.gameyfin.app.core.events.*
import org.gameyfin.app.core.events.AccountStatusChangedEvent
import org.gameyfin.app.core.events.EmailNeedsConfirmationEvent
import org.gameyfin.app.core.events.PasswordResetRequestEvent
import org.gameyfin.app.core.events.RegistrationAttemptWithExistingEmailEvent
import org.gameyfin.app.core.events.UserInvitationEvent
import org.gameyfin.app.core.events.UserRegistrationWaitingForApprovalEvent
import org.gameyfin.app.messages.providers.AbstractMessageProvider import org.gameyfin.app.messages.providers.AbstractMessageProvider
import org.gameyfin.app.messages.templates.MessageTemplateService import org.gameyfin.app.messages.templates.MessageTemplateService
import org.gameyfin.app.messages.templates.MessageTemplates
import org.gameyfin.app.users.UserService
import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContext
import org.springframework.context.event.EventListener import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async import org.springframework.scheduling.annotation.Async
@@ -72,7 +66,8 @@ class MessageService(
val template = templateService.getMessageTemplate(templateKey) val template = templateService.getMessageTemplate(templateKey)
sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders) sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders)
} catch (e: Exception) { } catch (e: Exception) {
log.error(e) { "Failed to send test message" } log.error { "Failed to send test message: ${e.message}" }
log.debug(e) {}
return false return false
} }