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