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,22 +23,13 @@ export default function LibraryCreationModal({
const [scanAfterCreation, setScanAfterCreation] = useState<boolean>(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 (
@@ -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) {}
}
}
@@ -215,7 +215,7 @@ class GameyfinPluginManager(
fun getPluginForExtension(extensionClass: Class<ExtensionPoint>): 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.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<PluginApiMetadata?> {
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
}
}
@@ -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 {
@@ -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 {
@@ -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<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>
}
interface GameRepository : JpaRepository<Game, Long>
@@ -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,
@@ -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<Library, Long> {
@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<String>): List<Pair<String, Library>>
}
@@ -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()
@@ -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<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
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
}