diff --git a/gameyfin/src/main/frontend/components/administration/LibraryManagement.tsx b/gameyfin/src/main/frontend/components/administration/LibraryManagement.tsx index 7fb4e0c..5f67a4e 100644 --- a/gameyfin/src/main/frontend/components/administration/LibraryManagement.tsx +++ b/gameyfin/src/main/frontend/components/administration/LibraryManagement.tsx @@ -68,6 +68,7 @@ function LibraryManagementLayout({getConfig, formik}: any) {
+
diff --git a/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx b/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx index 6e86fde..53fc469 100644 --- a/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx +++ b/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx @@ -15,9 +15,10 @@ import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/Libr import {LibraryEndpoint} from "Frontend/generated/endpoints"; import Input from "Frontend/components/general/input/Input"; import PathPickerModal from "Frontend/components/general/modals/PathPickerModal"; -import {Minus, Plus, XCircle} from "@phosphor-icons/react"; +import {ArrowRight, Minus, Plus, XCircle} from "@phosphor-icons/react"; import * as Yup from "yup"; import {SmallInfoField} from "Frontend/components/general/SmallInfoField"; +import DirectoryMappingDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/DirectoryMappingDto"; interface LibraryCreationModalProps { libraries: LibraryDto[]; @@ -57,7 +58,7 @@ export default function LibraryCreationModal({ return ( <> - + {(onClose) => ( {(formik) => { - function addDirectory(directory: string) { + function addDirectoryMapping(directory: DirectoryMappingDto) { formik.setFieldValue("directories", [...formik.values.directories, directory]); } - function removeDirectory(directory: string) { - formik.setFieldValue("directories", formik.values.directories.filter((d: string) => d !== directory)); + function removeDirectoryMapping(directory: DirectoryMappingDto) { + formik.setFieldValue("directories", formik.values.directories.filter((d: DirectoryMappingDto) => d.internalPath !== directory.internalPath)); } return ( @@ -103,11 +104,38 @@ export default function LibraryCreationModal({ - {formik.values.directories.map((directory: string) => ( - - {directory} - @@ -134,7 +162,7 @@ export default function LibraryCreationModal({ {formik.isSubmitting ? "" : "Add"} - diff --git a/gameyfin/src/main/frontend/components/general/modals/PathPickerModal.tsx b/gameyfin/src/main/frontend/components/general/modals/PathPickerModal.tsx index 7b87631..2529a95 100644 --- a/gameyfin/src/main/frontend/components/general/modals/PathPickerModal.tsx +++ b/gameyfin/src/main/frontend/components/general/modals/PathPickerModal.tsx @@ -3,45 +3,58 @@ import {Form, Formik} from "formik"; import React, {useEffect, useState} from "react"; import Input from "Frontend/components/general/input/Input"; import FileTreeView from "Frontend/components/general/input/FileTreeView"; +import DirectoryMappingDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/DirectoryMappingDto"; +import {ArrowRight} from "@phosphor-icons/react"; interface PathPickerModalProps { - returnSelectedPath: (path: string) => void; + returnSelectedPath: (path: DirectoryMappingDto) => void; isOpen: boolean; onOpenChange: () => void; } export default function PathPickerModal({returnSelectedPath, isOpen, onOpenChange}: PathPickerModalProps) { - const [currentlySelectedPath, setCurrentlySelectedPath] = useState(""); + const [internalPath, setInternalPath] = useState(""); + const [externalPath, setExternalPath] = useState(""); return ( - + {(onClose) => ( - { - returnSelectedPath(values.path); - setCurrentlySelectedPath(""); + { + returnSelectedPath(values); + setInternalPath(""); + setExternalPath(""); onClose(); }}> {(formik) => { useEffect(() => { - formik.setFieldValue("path", currentlySelectedPath); - }, [currentlySelectedPath]); + formik.setFieldValue("internalPath", internalPath); + }, [internalPath]); return (
- Add a new library + Select a folder - +
+ + + +
- +
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt index 62e3c2d..8c9c3dd 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/config/ConfigProperties.kt @@ -29,6 +29,13 @@ sealed class ConfigProperties( true ) + data object ScanEmptyDirectories : ConfigProperties( + Boolean::class, + "library.scan.scan-empty-directories", + "Scan empty directories", + false + ) + data object GameFileExtensions : ConfigProperties>( Array::class, "library.scan.game-file-extensions", @@ -237,7 +244,7 @@ sealed class ConfigProperties( LogLevel::class, "logs.level.root", "Log level (Root)", - LogLevel.INFO, + LogLevel.WARN, LogLevel.entries ) } 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 9389c2e..33b9576 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 @@ -81,7 +81,7 @@ class FilesystemService( 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) } + val validDirectories = library.directories.map { Path(it.internalPath) } .filter { path -> if (!path.isDirectory()) { log.warn { "Invalid directory '$path' in library '${library.name}'" } @@ -92,9 +92,19 @@ class FilesystemService( } // Get all paths that are directories or match the game file extensions + // Also check if the directory is empty and if empty directories should be included val currentFilesystemPaths = validDirectories.flatMap { validDirectory -> safeReadDirectoryContents(validDirectory) .filter { it.isDirectory() || it.extension.lowercase() in gamefileExtensions } + .filter { + val contents = safeReadDirectoryContents(it) + if (contents.isEmpty() && !config.get(ConfigProperties.Libraries.Scan.ScanEmptyDirectories)!!) { + log.debug { "Directory '$it' is empty and will be ignored" } + false + } else { + true + } + } } // Get all paths already in the library as game files or as unmatched paths diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/DirectoryMapping.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/DirectoryMapping.kt new file mode 100644 index 0000000..22553e1 --- /dev/null +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/DirectoryMapping.kt @@ -0,0 +1,18 @@ +package de.grimsi.gameyfin.libraries + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id + +@Entity +class DirectoryMapping( + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + var id: Long? = null, + + var internalPath: String, + + var externalPath: String? = null, +) 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 3c1c137..0c973c8 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt @@ -11,12 +11,12 @@ class Library( var name: String, - @ElementCollection(fetch = FetchType.EAGER) - var directories: MutableSet = HashSet(), + @OneToMany(fetch = FetchType.EAGER, orphanRemoval = true, cascade = [CascadeType.ALL]) + var directories: MutableSet = HashSet(), @OneToMany(fetch = FetchType.EAGER, orphanRemoval = true) - var games: MutableSet = HashSet(), + var games: MutableSet = HashSet(), @ElementCollection(fetch = FetchType.EAGER) - var unmatchedPaths: MutableSet = HashSet() + var unmatchedPaths: MutableSet = HashSet() ) \ No newline at end of file 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 1ac3630..46be1bf 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt @@ -5,6 +5,7 @@ import de.grimsi.gameyfin.games.GameService import de.grimsi.gameyfin.games.dto.GameDto import de.grimsi.gameyfin.games.entities.Game import de.grimsi.gameyfin.games.toDto +import de.grimsi.gameyfin.libraries.dto.DirectoryMappingDto import de.grimsi.gameyfin.libraries.dto.LibraryDto import de.grimsi.gameyfin.libraries.dto.LibraryStatsDto import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto @@ -56,7 +57,11 @@ class LibraryService( // Update only non-null fields libraryDto.name?.let { existingLibrary.name = it } - libraryDto.directories?.let { existingLibrary.directories = it.toMutableSet() } + libraryDto.directories?.let { + existingLibrary.directories = it + .map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) } + .toMutableSet() + } val updatedLibrary = libraryRepository.save(existingLibrary) return toDto(updatedLibrary) @@ -300,7 +305,7 @@ class LibraryService( return LibraryDto( id = libraryId, name = library.name, - directories = library.directories, + directories = library.directories.map { DirectoryMappingDto(it.internalPath, it.externalPath) }, stats = statsDto ) } @@ -314,7 +319,9 @@ class LibraryService( private fun toEntity(library: LibraryDto): Library { return libraryRepository.findByIdOrNull(library.id) ?: Library( name = library.name, - directories = library.directories.toMutableSet() + directories = library.directories.map { + DirectoryMapping(internalPath = it.internalPath, externalPath = it.externalPath) + }.toMutableSet() ) } } \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/DirectoryMappingDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/DirectoryMappingDto.kt new file mode 100644 index 0000000..dac4344 --- /dev/null +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/DirectoryMappingDto.kt @@ -0,0 +1,6 @@ +package de.grimsi.gameyfin.libraries.dto + +data class DirectoryMappingDto( + val internalPath: String, + val externalPath: String? = null, +) diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/LibraryDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/LibraryDto.kt index 604303a..55f5647 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/LibraryDto.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/LibraryDto.kt @@ -3,6 +3,6 @@ package de.grimsi.gameyfin.libraries.dto data class LibraryDto( val id: Long, val name: String, - val directories: Set, + val directories: List, val stats: LibraryStatsDto? ) \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto.kt index 09bf34e..c9bedb8 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto.kt @@ -3,5 +3,5 @@ package de.grimsi.gameyfin.libraries.dto data class LibraryUpdateDto( val id: Long, val name: String? = null, - val directories: Set? = null, + val directories: Set? = null, ) diff --git a/gameyfin/src/main/resources/application-dev.yml b/gameyfin/src/main/resources/application-dev.yml index a57c468..072d8f4 100644 --- a/gameyfin/src/main/resources/application-dev.yml +++ b/gameyfin/src/main/resources/application-dev.yml @@ -1,3 +1,7 @@ +logging.level: + root: info + de.grimsi.gameyfin.GameyfinApplicationKt: info + spring: datasource: url: jdbc:h2:file:./db/${spring.datasource.db-name};AUTO_SERVER=TRUE \ No newline at end of file diff --git a/gameyfin/src/main/resources/application.yml b/gameyfin/src/main/resources/application.yml index f8d1901..771864a 100644 --- a/gameyfin/src/main/resources/application.yml +++ b/gameyfin/src/main/resources/application.yml @@ -1,5 +1,8 @@ logging.level: + root: warn org.atmosphere: warn + de.grimsi.gameyfin: info + de.grimsi.gameyfin.GameyfinApplicationKt: warn server: port: 8080 @@ -8,6 +11,8 @@ server: tracking-modes: cookie management: + server: + port: 8081 endpoints: web: exposure: