From 16759da5d3157193d2e3917f73068968f85fabe7 Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Fri, 18 Apr 2025 13:36:50 +0200
Subject: [PATCH] WIP: Implement library scans
---
.../general/cards/LibraryOverviewCard.tsx | 13 +-
.../de/grimsi/gameyfin/GameyfinApplication.kt | 2 +
.../grimsi/gameyfin/config/ConfigService.kt | 2 -
.../grimsi/gameyfin/core/SetupDataLoader.kt | 2 -
.../gameyfin/core/events/AsyncConfig.kt | 8 -
.../core/filesystem/FilesystemEndpoint.kt | 10 +-
.../core/filesystem/FilesystemService.kt | 54 ++++++-
.../de/grimsi/gameyfin/games/GameService.kt | 6 +-
.../de/grimsi/gameyfin/games/entities/Game.kt | 8 +-
.../de/grimsi/gameyfin/libraries/Library.kt | 10 +-
.../gameyfin/libraries/LibraryEndpoint.kt | 5 +-
.../gameyfin/libraries/LibraryService.kt | 140 +++++++++++-------
.../gameyfin/messages/MessageService.kt | 2 -
.../de/grimsi/gameyfin/users/RoleService.kt | 2 -
.../de/grimsi/gameyfin/users/UserService.kt | 2 -
.../preferences/UserPreferencesService.kt | 2 -
16 files changed, 162 insertions(+), 106 deletions(-)
delete mode 100644 gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/events/AsyncConfig.kt
diff --git a/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx b/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx
index 0d430c2..fa4bd91 100644
--- a/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx
+++ b/gameyfin/src/main/frontend/components/general/cards/LibraryOverviewCard.tsx
@@ -1,7 +1,7 @@
-import {Card, Chip} from "@heroui/react";
+import {Button, Card, Chip, Tooltip} from "@heroui/react";
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/LibraryDto";
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
-import {useEffect, useState} from "react";
+import React, {useEffect, useState} from "react";
import {LibraryEndpoint} from "Frontend/generated/endpoints";
import {GameCover} from "Frontend/components/general/GameCover";
import Rand from "rand-seed";
@@ -12,6 +12,7 @@ import {
Ghost,
Joystick,
Lego,
+ MagnifyingGlass,
Skull,
SoccerBall,
Strategy,
@@ -72,6 +73,14 @@ export function LibraryOverviewCard({library}: { library: LibraryDto }) {
{library.name}
+
+
+
+
+
+
{!!library.stats &&
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/GameyfinApplication.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/GameyfinApplication.kt
index 944d6b9..d1174e3 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/GameyfinApplication.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/GameyfinApplication.kt
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
+import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.transaction.annotation.EnableTransactionManagement
@@ -9,6 +10,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement
@SpringBootApplication
@EnableScheduling
@EnableTransactionManagement
+@EnableAsync
class GameyfinApplication
fun main(args: Array) {
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/config/ConfigService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/config/ConfigService.kt
index 0cc3346..95c1813 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/config/ConfigService.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/config/ConfigService.kt
@@ -5,12 +5,10 @@ import de.grimsi.gameyfin.config.dto.ConfigValuePairDto
import de.grimsi.gameyfin.config.entities.ConfigEntry
import de.grimsi.gameyfin.config.persistence.ConfigRepository
import io.github.oshai.kotlinlogging.KotlinLogging
-import jakarta.transaction.Transactional
import org.springframework.stereotype.Service
import java.io.Serializable
@Service
-@Transactional
class ConfigService(
private val appConfigRepository: ConfigRepository
) {
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/SetupDataLoader.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/SetupDataLoader.kt
index 5294d05..51fbdb9 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/SetupDataLoader.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/SetupDataLoader.kt
@@ -4,7 +4,6 @@ import de.grimsi.gameyfin.setup.SetupService
import de.grimsi.gameyfin.users.UserService
import de.grimsi.gameyfin.users.entities.User
import io.github.oshai.kotlinlogging.KotlinLogging
-import jakarta.transaction.Transactional
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.event.EventListener
import org.springframework.core.env.Environment
@@ -13,7 +12,6 @@ import java.net.InetAddress
@Service
-@Transactional
class SetupDataLoader(
private val userService: UserService,
private val setupService: SetupService,
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/events/AsyncConfig.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/events/AsyncConfig.kt
deleted file mode 100644
index 6859acd..0000000
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/events/AsyncConfig.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package de.grimsi.gameyfin.core.events
-
-import org.springframework.context.annotation.Configuration
-import org.springframework.scheduling.annotation.EnableAsync
-
-@Configuration
-@EnableAsync
-class AsyncConfig
\ No newline at end of file
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FilesystemEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FilesystemEndpoint.kt
index da8fb70..c1cfbe5 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FilesystemEndpoint.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FilesystemEndpoint.kt
@@ -6,11 +6,13 @@ import jakarta.annotation.security.RolesAllowed
@Endpoint
@RolesAllowed(Role.Names.ADMIN)
-class FilesystemEndpoint {
+class FilesystemEndpoint(
+ private val filesystemService: FilesystemService
+) {
- fun listContents(path: String) = FilesystemService().listContents(path)
+ fun listContents(path: String) = filesystemService.listContents(path)
- fun listSubDirectories(path: String) = FilesystemService().listSubDirectories(path)
+ fun listSubDirectories(path: String) = filesystemService.listSubDirectories(path)
- fun getHostOperatingSystem() = FilesystemService().getHostOperatingSystem()
+ fun getHostOperatingSystem() = filesystemService.getHostOperatingSystem()
}
\ No newline at end of file
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FilesystemService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FilesystemService.kt
index 462183e..a506163 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FilesystemService.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/filesystem/FilesystemService.kt
@@ -1,17 +1,27 @@
package de.grimsi.gameyfin.core.filesystem
+import de.grimsi.gameyfin.config.ConfigProperties
+import de.grimsi.gameyfin.config.ConfigService
+import de.grimsi.gameyfin.libraries.Library
import io.github.oshai.kotlinlogging.KotlinLogging
import org.apache.commons.io.FilenameUtils
import org.springframework.stereotype.Service
import java.nio.file.FileSystems
-import kotlin.io.path.Path
-import kotlin.io.path.isDirectory
+import java.nio.file.Path
+import kotlin.io.path.*
@Service
-class FilesystemService {
+class FilesystemService(
+ private val config: ConfigService
+) {
private val log = KotlinLogging.logger {}
+ private val gameFileExtensions
+ get() = config.get(ConfigProperties.Libraries.Scan.GameFileExtensions)!!
+ .split(",")
+ .map { it.trim().lowercase() }
+
/**
* Lists all files and directories in the given path.
* If the path is null or empty, it lists all root directories.
@@ -61,11 +71,43 @@ class FilesystemService {
}
}
+ /**
+ * Scans the given library for files and directories potentially containing games.
+ *
+ * @param library The library to scan.
+ * @return A list of paths representing game files and directories.
+ */
+ fun scanLibraryForGamefiles(library: Library): List {
+ // Cache the game file extensions to avoid reading them multiple times in the same scan
+ val gamefileExtensions = gameFileExtensions
+
+ // Filter out invalid directories (directories could have been changed externally after the library was created)
+ val validDirectories = library.directories.map { Path(it) }
+ .filter { path ->
+ if (!path.isDirectory()) {
+ log.warn { "Invalid directory '$path' in library '${library.name}'" }
+ false
+ } else {
+ true
+ }
+ }
+
+ // Return all paths that are directories or match the game file extensions
+ return validDirectories.flatMap {
+ safeReadDirectoryContents(it)
+ .filter { it.isDirectory() || it.extension.lowercase() in gamefileExtensions }
+ }
+ }
+
private fun safeReadDirectoryContents(path: String): List {
+ return safeReadDirectoryContents(Path(path))
+ .map { FileDto(it.name, if (it.isDirectory()) FileType.DIRECTORY else FileType.FILE, it.hashCode()) }
+ }
+
+ private fun safeReadDirectoryContents(path: Path): List {
return try {
- Path(path).toFile().listFiles()
- .filter { !it.isHidden }
- .map { FileDto(it.name, if (it.isDirectory) FileType.DIRECTORY else FileType.FILE, it.hashCode()) }
+ path.listDirectoryEntries()
+ .filter { !it.isHidden() }
} catch (_: Exception) {
log.warn { "Error reading directory contents of $path" }
emptyList()
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt
index e865dc3..ea313cb 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt
@@ -21,7 +21,6 @@ import me.xdrop.fuzzywuzzy.FuzzySearch
import org.pf4j.PluginManager
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
-import org.springframework.transaction.annotation.Transactional
import java.net.URI
import java.net.URLConnection
import java.nio.file.Path
@@ -41,9 +40,7 @@ class GameService(
get() = pluginManager.getExtensions(GameMetadataProvider::class.java)
fun createOrUpdate(game: Game): Game {
- gameRepository.findByPath(game.path)?.let {
- game.id = it.id
- }
+ gameRepository.findByPath(game.path)?.let { game.id = it.id }
return gameRepository.save(game)
}
@@ -74,7 +71,6 @@ class GameService(
return createOrUpdate(mergedGame)
}
- @Transactional(readOnly = true)
fun getAllGames(): Collection {
val entities = gameRepository.findAll()
return entities.map { toDto(it) }
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt
index 5aedfe0..be87a3b 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt
@@ -17,7 +17,7 @@ class Game(
var title: String? = null,
- @OneToOne(cascade = [CascadeType.MERGE])
+ @OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true)
var coverImage: Image? = null,
@Lob
@@ -34,10 +34,10 @@ class Game(
var criticRating: Int? = null,
- @ManyToMany(cascade = [CascadeType.MERGE])
+ @ManyToMany
var publishers: Set? = null,
- @ManyToMany(cascade = [CascadeType.MERGE])
+ @ManyToMany
var developers: Set? = null,
@ElementCollection(targetClass = Genre::class)
@@ -55,7 +55,7 @@ class Game(
@ElementCollection(targetClass = PlayerPerspective::class)
var perspectives: Set? = null,
- @OneToMany(cascade = [CascadeType.MERGE])
+ @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
var images: Set? = null,
@ElementCollection
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt
index d9e8fdf..5bd016e 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt
@@ -7,13 +7,13 @@ import jakarta.persistence.*
class Library(
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
- val id: Long? = null,
+ var id: Long? = null,
- val name: String,
+ var name: String,
@ElementCollection(fetch = FetchType.EAGER)
- val directories: Set,
+ var directories: MutableSet = HashSet(),
- @ManyToMany(cascade = [CascadeType.ALL])
- val games: MutableSet = mutableSetOf()
+ @ManyToMany(fetch = FetchType.EAGER)
+ var games: MutableSet = HashSet()
)
\ No newline at end of file
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt
index 37daafd..8a3b7aa 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt
@@ -21,10 +21,9 @@ class LibraryEndpoint(
return libraryService.getGamesInLibrary(libraryId)
}
- // FIXME: Just for testing
@RolesAllowed(Role.Names.ADMIN)
- fun test(testString: String): GameDto {
- return libraryService.test(testString)
+ fun triggerScan(libraries: Collection?) {
+ return libraryService.triggerScan(libraries)
}
@RolesAllowed(Role.Names.ADMIN)
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt
index f17235e..b4e3a2a 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt
@@ -1,60 +1,65 @@
package de.grimsi.gameyfin.libraries
-import de.grimsi.gameyfin.config.ConfigProperties
-import de.grimsi.gameyfin.config.ConfigService
+import de.grimsi.gameyfin.core.filesystem.FilesystemService
import de.grimsi.gameyfin.games.GameService
import de.grimsi.gameyfin.games.dto.GameDto
+import de.grimsi.gameyfin.games.entities.Game
import io.github.oshai.kotlinlogging.KotlinLogging
+import kotlinx.coroutines.runBlocking
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
-import org.springframework.transaction.annotation.Transactional
-import java.nio.file.Path
-import kotlin.io.path.Path
-import kotlin.io.path.extension
-import kotlin.io.path.isDirectory
-import kotlin.io.path.listDirectoryEntries
@Service
class LibraryService(
private val libraryRepository: LibraryRepository,
private val gameService: GameService,
- private val config: ConfigService
+ private val filesystemService: FilesystemService
) {
private val log = KotlinLogging.logger {}
- fun test(testString: String): GameDto {
- val game = gameService.createFromFile(Path(testString))
-
- val randomLibrary = libraryRepository.findRandomLibrary() ?: throw IllegalArgumentException("No library found")
-
- randomLibrary.games.add(game)
- libraryRepository.save(randomLibrary)
-
- return gameService.toDto(game)
- }
-
+ /**
+ * Creates or updates a library in the repository.
+ *
+ * @param library: The library to create or update.
+ * @return The created or updated LibraryDto object.
+ */
fun createOrUpdate(library: LibraryDto): LibraryDto {
val entity = libraryRepository.save(toEntity(library))
return toDto(entity)
}
- @Transactional(readOnly = true)
+ /**
+ * Retrieves all libraries from the repository.
+ */
fun getAllLibraries(): Collection {
val entities = libraryRepository.findAll()
return entities.map { toDto(it) }
}
+ /**
+ * Deletes a library from the repository.
+ *
+ * @param library: The library to delete.
+ */
fun deleteLibrary(library: LibraryDto) {
val entity = toEntity(library)
libraryRepository.delete(entity)
}
+ /**
+ * Deletes all libraries from the repository.
+ */
fun deleteAllLibraries() {
libraryRepository.deleteAll()
}
- @Transactional(readOnly = true)
+ /**
+ * Retrieves all games in a library.
+ *
+ * @param libraryId: The ID of the library to retrieve games from.
+ * @return A collection of GameDto objects representing the games in the library.
+ */
fun getGamesInLibrary(libraryId: Long): Collection {
val library = libraryRepository.findByIdOrNull(libraryId)
?: throw IllegalArgumentException("Library with ID $libraryId not found")
@@ -65,48 +70,63 @@ class LibraryService(
}
/**
- * Triggers a scan for a list of libraries. If no list is provided, all libraries will be scanned.
+ * Adds a game to the library.
+ *
+ * @param game: The game to add.
+ * @param library: The library to add the game to.
+ * @return The updated library.
*/
- fun scan(libraryDtos: Collection?) {
+ fun addGameToLibrary(game: Game, library: Library): Library {
+ if (library.games.any { it.id == game.id }) return library
+
+ library.games.add(game)
+ return libraryRepository.save(library)
+ }
+
+ /**
+ * Adds a collection of games to the library.
+ *
+ * @param games: The collection of games to add.
+ * @param library: The library to add the games to.
+ * @return The updated library.
+ */
+ fun addGamesToLibrary(games: Collection, library: Library): Library {
+ val newGames = games.filter { game -> library.games.none { it.id == game.id } }
+ library.games = library.games.toMutableSet().apply { addAll(newGames) }
+ return libraryRepository.save(library)
+ }
+
+
+ /**
+ * Wrapper function to trigger a scan for a list of libraries.
+ */
+ fun triggerScan(libraryDtos: Collection?) = runBlocking {
+ scan(libraryDtos)
+ }
+
+ /**
+ * Triggers a scan for a list of libraries.
+ * If no list is provided, all libraries will be scanned.
+ *
+ * @param libraryDtos: List of LibraryDto objects to scan.
+ */
+ suspend fun scan(libraryDtos: Collection?) {
val libraries = libraryDtos?.map { toEntity(it) } ?: libraryRepository.findAll()
libraries.forEach { library ->
- val games = scan(library)
- games.forEach(gameService::createFromFile)
+ val gamePaths = filesystemService.scanLibraryForGamefiles(library)
+ val newGames = gamePaths.map { gameService.createFromFile(it) }
+ addGamesToLibrary(newGames, library)
}
}
/**
- * Return a list of all subfolders and game files in the provided library
+ * Converts a Library entity to a LibraryDto.
+ *
+ * @param library: The Library entity to convert.
+ * @return The converted LibraryDto.
*/
- fun scan(library: Library): List {
- val validDirectories = library.directories.map { Path(it) }
- .filter { path ->
- if (!path.isDirectory()) {
- log.warn { "Invalid directory '$path' in library '${library.name}'" }
- false
- } else {
- true
- }
- }
-
- return validDirectories.flatMap { directory ->
- directory.listDirectoryEntries()
- .filter { it.isDirectory() || it.isGameFile() }
- .map { it.fileName }
- }
- }
-
- private fun Path.isGameFile(): Boolean {
- val gameFileExtensions = config.get(ConfigProperties.Libraries.Scan.GameFileExtensions)!!
- .split(",")
- .map { it.trim().lowercase() }
- return extension.lowercase() in gameFileExtensions
- }
-
private fun toDto(library: Library): LibraryDto {
- if (library.id == null) {
- throw IllegalArgumentException("Library ID is null")
- }
+ val libraryId = library.id ?: throw IllegalArgumentException("Library ID is null")
val statsDto = LibraryStatsDto(
gamesCount = library.games.size,
@@ -114,17 +134,23 @@ class LibraryService(
)
return LibraryDto(
- id = library.id,
+ id = libraryId,
name = library.name,
directories = library.directories,
stats = statsDto
)
}
+ /**
+ * Converts a LibraryDto to a Library entity.
+ *
+ * @param library: The LibraryDto to convert.
+ * @return The converted Library entity.
+ */
private fun toEntity(library: LibraryDto): Library {
return libraryRepository.findByIdOrNull(library.id) ?: Library(
name = library.name,
- directories = library.directories
+ directories = library.directories.toMutableSet()
)
}
}
\ No newline at end of file
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt
index 21576fe..77edaab 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/messages/MessageService.kt
@@ -10,13 +10,11 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.context.ApplicationContext
import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async
-import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Service
import java.util.*
-@EnableAsync
@Service
class MessageService(
private val applicationContext: ApplicationContext,
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt
index d7d93b5..71d1c3b 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/RoleService.kt
@@ -3,7 +3,6 @@ package de.grimsi.gameyfin.users
import de.grimsi.gameyfin.core.Role
import de.grimsi.gameyfin.users.entities.User
import de.grimsi.gameyfin.users.persistence.UserRepository
-import jakarta.transaction.Transactional
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
@@ -11,7 +10,6 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority
import org.springframework.stereotype.Service
@Service
-@Transactional
class RoleService(
private val userRepository: UserRepository
) {
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt
index 5f5ede5..1dc178f 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/UserService.kt
@@ -15,7 +15,6 @@ import de.grimsi.gameyfin.users.entities.User
import de.grimsi.gameyfin.users.enums.RoleAssignmentResult
import de.grimsi.gameyfin.users.persistence.UserRepository
import io.github.oshai.kotlinlogging.KotlinLogging
-import jakarta.transaction.Transactional
import org.springframework.context.ApplicationEventPublisher
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
@@ -30,7 +29,6 @@ import org.springframework.stereotype.Service
@Service
-@Transactional
class UserService(
private val userRepository: UserRepository,
private val imageService: ImageService,
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/preferences/UserPreferencesService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/preferences/UserPreferencesService.kt
index 48745c5..af88c35 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/preferences/UserPreferencesService.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/users/preferences/UserPreferencesService.kt
@@ -2,13 +2,11 @@ package de.grimsi.gameyfin.users.preferences
import de.grimsi.gameyfin.users.UserService
import io.github.oshai.kotlinlogging.KotlinLogging
-import jakarta.transaction.Transactional
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Service
import java.io.Serializable
@Service
-@Transactional
class UserPreferencesService(
private val userPreferenceRepository: UserPreferenceRepository,
private val userService: UserService