From c01222d5218ffe0d3ff5f855379d1925bd59020b Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:11:40 +0200 Subject: [PATCH] Various bugfixes and minor improvements --- .../general/modals/LibraryCreationModal.tsx | 21 ++----- .../management/GameyfinExtensionFinder.kt | 6 +- .../management/GameyfinPluginManager.kt | 2 +- .../org/gameyfin/app/games/GameService.kt | 18 ++++-- .../gameyfin/app/games/entities/Company.kt | 7 +++ .../org/gameyfin/app/games/entities/Image.kt | 6 ++ .../app/games/repositories/GameRepository.kt | 8 +-- .../app/libraries/DirectoryMapping.kt | 6 +- .../app/libraries/LibraryRepository.kt | 6 +- .../app/libraries/LibraryScanService.kt | 15 +++-- .../gameyfin/app/libraries/LibraryService.kt | 60 ++++++++++++++++--- .../gameyfin/app/messages/MessageService.kt | 15 ++--- 12 files changed, 110 insertions(+), 60 deletions(-) diff --git a/app/src/main/frontend/components/general/modals/LibraryCreationModal.tsx b/app/src/main/frontend/components/general/modals/LibraryCreationModal.tsx index bc4f2d1..3f2fb0d 100644 --- a/app/src/main/frontend/components/general/modals/LibraryCreationModal.tsx +++ b/app/src/main/frontend/components/general/modals/LibraryCreationModal.tsx @@ -23,22 +23,13 @@ export default function LibraryCreationModal({ const [scanAfterCreation, setScanAfterCreation] = useState(true); async function createLibrary(library: LibraryDto) { - try { - await LibraryEndpoint.createLibrary(library as LibraryDto, scanAfterCreation); + await LibraryEndpoint.createLibrary(library as LibraryDto, scanAfterCreation); - addToast({ - title: "New library created", - description: `Library ${library.name} created!`, - color: "success" - }); - } catch (e) { - addToast({ - title: "Error creating library", - description: `Library ${library.name} could not be created!`, - color: "warning" - }); - throw "Error creating library: " + e; - } + addToast({ + title: "New library created", + description: `Library ${library.name} created!`, + color: "success" + }); } return ( diff --git a/app/src/main/kotlin/org/gameyfin/app/core/plugins/management/GameyfinExtensionFinder.kt b/app/src/main/kotlin/org/gameyfin/app/core/plugins/management/GameyfinExtensionFinder.kt index c2cf86d..36dceb6 100644 --- a/app/src/main/kotlin/org/gameyfin/app/core/plugins/management/GameyfinExtensionFinder.kt +++ b/app/src/main/kotlin/org/gameyfin/app/core/plugins/management/GameyfinExtensionFinder.kt @@ -38,9 +38,11 @@ class GameyfinExtensionFinder(pluginManager: PluginManager) : LegacyExtensionFin result.add(extensionWrapper) log.debug { "Added extension '$className' with ordinal ${extensionWrapper.ordinal}" } } catch (e: ClassNotFoundException) { - log.error(e) { e.message } + log.error { "Error loading plugin: ${e.message}" } + log.debug(e) {} } catch (e: NoClassDefFoundError) { - log.error(e) { e.message } + log.error { "Error loading plugin: ${e.message}" } + log.debug(e) {} } } diff --git a/app/src/main/kotlin/org/gameyfin/app/core/plugins/management/GameyfinPluginManager.kt b/app/src/main/kotlin/org/gameyfin/app/core/plugins/management/GameyfinPluginManager.kt index 7eb8c3a..ad0ea3f 100644 --- a/app/src/main/kotlin/org/gameyfin/app/core/plugins/management/GameyfinPluginManager.kt +++ b/app/src/main/kotlin/org/gameyfin/app/core/plugins/management/GameyfinPluginManager.kt @@ -215,7 +215,7 @@ class GameyfinPluginManager( fun getPluginForExtension(extensionClass: Class): PluginWrapper? { return getPlugins().firstOrNull { pluginWrapper -> - getExtensionTypeClasses(pluginWrapper.pluginId).any { it == extensionClass.javaClass } + getExtensionClasses(pluginWrapper.pluginId).any { it == extensionClass } } } 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 9440478..88cbef1 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/GameService.kt @@ -12,6 +12,7 @@ import org.gameyfin.app.core.alphaNumeric import org.gameyfin.app.core.filesystem.FilesystemService import org.gameyfin.app.core.filterValuesNotNull 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.PluginManagementEntry import org.gameyfin.app.core.replaceRomanNumerals @@ -104,7 +105,8 @@ class GameService( imageService.downloadIfNew(it) } } 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 } @@ -449,7 +451,9 @@ class GameService( 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}" } + 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() } } @@ -561,7 +565,9 @@ class GameService( try { return@async plugin.fetchById(originalId) } 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 } }.await() @@ -638,8 +644,10 @@ class GameService( executor.submit { try { plugin.fetchByTitle(gameTitle).firstOrNull() - } catch (_: Exception) { - log.error { "Error fetching metadata for game title '$gameTitle' with plugin ${plugin.javaClass.name}" } + } catch (e: Exception) { + 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 } } diff --git a/app/src/main/kotlin/org/gameyfin/app/games/entities/Company.kt b/app/src/main/kotlin/org/gameyfin/app/games/entities/Company.kt index f45ef80..5a96883 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/entities/Company.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/entities/Company.kt @@ -16,6 +16,13 @@ class Company( if (other !is Company) return false 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 { diff --git a/app/src/main/kotlin/org/gameyfin/app/games/entities/Image.kt b/app/src/main/kotlin/org/gameyfin/app/games/entities/Image.kt index 91c6b55..4a1dc05 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/entities/Image.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/entities/Image.kt @@ -33,6 +33,12 @@ class Image( if (other !is Image) return false 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 { diff --git a/app/src/main/kotlin/org/gameyfin/app/games/repositories/GameRepository.kt b/app/src/main/kotlin/org/gameyfin/app/games/repositories/GameRepository.kt index e2b3819..bbea808 100644 --- a/app/src/main/kotlin/org/gameyfin/app/games/repositories/GameRepository.kt +++ b/app/src/main/kotlin/org/gameyfin/app/games/repositories/GameRepository.kt @@ -1,12 +1,6 @@ package org.gameyfin.app.games.repositories import org.gameyfin.app.games.entities.Game -import org.springframework.data.domain.Limit import org.springframework.data.jpa.repository.JpaRepository -interface GameRepository : JpaRepository { - fun findByMetadata_Path(path: String): Game? - fun findAllByMetadata_PathIn(paths: List): List - fun findByOrderByCreatedAtDesc(limit: Limit): List - fun findByOrderByUpdatedAtDesc(limit: Limit): List -} \ No newline at end of file +interface GameRepository : JpaRepository \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/libraries/DirectoryMapping.kt b/app/src/main/kotlin/org/gameyfin/app/libraries/DirectoryMapping.kt index 4df31ce..48e0d51 100644 --- a/app/src/main/kotlin/org/gameyfin/app/libraries/DirectoryMapping.kt +++ b/app/src/main/kotlin/org/gameyfin/app/libraries/DirectoryMapping.kt @@ -1,9 +1,6 @@ package org.gameyfin.app.libraries -import jakarta.persistence.Entity -import jakarta.persistence.GeneratedValue -import jakarta.persistence.GenerationType -import jakarta.persistence.Id +import jakarta.persistence.* @Entity class DirectoryMapping( @@ -12,6 +9,7 @@ class DirectoryMapping( @GeneratedValue(strategy = GenerationType.AUTO) var id: Long? = null, + @Column(unique = true) var internalPath: String, var externalPath: String? = null, diff --git a/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryRepository.kt b/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryRepository.kt index c9faebc..b60aefc 100644 --- a/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryRepository.kt +++ b/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryRepository.kt @@ -1,11 +1,9 @@ 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.Query interface LibraryRepository : JpaRepository { - @EntityGraph(attributePaths = ["games"]) - @Query("SELECT l FROM Library l ORDER BY function('RAND') LIMIT 1") - fun findRandomLibrary(): Library? + @Query("SELECT d.internalPath, l FROM Library l JOIN l.directories d WHERE d.internalPath IN :paths") + fun findByPaths(paths: List): List> } \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryScanService.kt b/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryScanService.kt index 2087619..2f0e382 100644 --- a/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryScanService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryScanService.kt @@ -183,7 +183,8 @@ class LibraryScanService( ) emit(progress) } 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.finishedAt = Instant.now() emit(progress) @@ -282,7 +283,8 @@ class LibraryScanService( ) emit(progress) } 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.finishedAt = Instant.now() emit(progress) @@ -312,7 +314,8 @@ class LibraryScanService( return@Callable game } 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()) return@Callable null @@ -372,7 +375,8 @@ class LibraryScanService( game } 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 } finally { progress.currentStep.current = completedImageDownload.get() @@ -437,7 +441,8 @@ class LibraryScanService( val game = gameService.update(game) return@Callable game } 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 } finally { progress.currentStep.current = completedUpdates.incrementAndGet() diff --git a/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryService.kt b/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryService.kt index 0099a00..6b5f343 100644 --- a/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/libraries/LibraryService.kt @@ -1,5 +1,6 @@ package org.gameyfin.app.libraries +import com.vaadin.hilla.exception.EndpointException import io.github.oshai.kotlinlogging.KotlinLogging import org.gameyfin.app.games.GameService import org.gameyfin.app.libraries.dto.LibraryDto @@ -19,7 +20,7 @@ class LibraryService( private val libraryRepository: LibraryRepository, private val libraryCoreService: LibraryCoreService, private val libraryScanService: LibraryScanService, - private val gameService: GameService, + private val gameService: GameService ) { companion object { @@ -70,6 +71,9 @@ class LibraryService( * @return The created or updated LibraryDto object. */ 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)) if (scanAfterCreation) { @@ -88,20 +92,49 @@ class LibraryService( val library = libraryRepository.findByIdOrNull(libraryUpdateDto.id) ?: throw IllegalArgumentException("Library with ID $libraryUpdateDto.id not found") - // Update only non-null fields libraryUpdateDto.name?.let { library.name = it } - libraryUpdateDto.directories?.let { - library.directories.clear() - library.directories.addAll( - it.map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) } + libraryUpdateDto.directories?.let { updatedDirs -> + checkForDuplicateDirectories( + updatedDirs.map { it.internalPath }, + 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 { library.unmatchedPaths.clear() 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) } @@ -113,4 +146,17 @@ class LibraryService( fun delete(libraryId: Long) { libraryRepository.deleteById(libraryId) } + + private fun checkForDuplicateDirectories(newLibraryFolders: List, 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})" } + ) + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt b/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt index a7c1902..68a1605 100644 --- a/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt +++ b/app/src/main/kotlin/org/gameyfin/app/messages/MessageService.kt @@ -1,18 +1,12 @@ 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.KotlinLogging -import org.gameyfin.app.core.events.AccountDeletedEvent -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.core.events.* import org.gameyfin.app.messages.providers.AbstractMessageProvider 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.event.EventListener import org.springframework.scheduling.annotation.Async @@ -72,7 +66,8 @@ class MessageService( val template = templateService.getMessageTemplate(templateKey) sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders) } 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 }