Update dependenicies

Update Plugin-API to allow multiple results per plugin
Add LibraryDetailsModal
Minor refactorings & bugfixes
This commit is contained in:
grimsi
2025-05-01 22:07:55 +02:00
parent 410172fee2
commit 5c0e06c1d7
19 changed files with 1329 additions and 1985 deletions
+1009 -1823
View File
File diff suppressed because it is too large Load Diff
+67 -67
View File
@@ -9,22 +9,22 @@
"@polymer/polymer": "3.5.2",
"@react-stately/data": "^3.12.2",
"@react-types/shared": "^3.28.0",
"@vaadin/bundles": "24.7.1",
"@vaadin/bundles": "24.7.5",
"@vaadin/common-frontend": "0.0.19",
"@vaadin/hilla-file-router": "24.7.1",
"@vaadin/hilla-frontend": "24.7.1",
"@vaadin/hilla-lit-form": "24.7.1",
"@vaadin/hilla-react-auth": "24.7.1",
"@vaadin/hilla-react-crud": "24.7.1",
"@vaadin/hilla-react-form": "24.7.1",
"@vaadin/hilla-react-i18n": "24.7.1",
"@vaadin/hilla-react-signals": "24.7.1",
"@vaadin/polymer-legacy-adapter": "24.7.1",
"@vaadin/react-components": "24.7.1",
"@vaadin/hilla-file-router": "24.7.3",
"@vaadin/hilla-frontend": "24.7.3",
"@vaadin/hilla-lit-form": "24.7.3",
"@vaadin/hilla-react-auth": "24.7.3",
"@vaadin/hilla-react-crud": "24.7.3",
"@vaadin/hilla-react-form": "24.7.3",
"@vaadin/hilla-react-i18n": "24.7.3",
"@vaadin/hilla-react-signals": "24.7.3",
"@vaadin/polymer-legacy-adapter": "24.7.5",
"@vaadin/react-components": "24.7.5",
"@vaadin/vaadin-development-mode-detector": "2.0.7",
"@vaadin/vaadin-lumo-styles": "24.7.1",
"@vaadin/vaadin-material-styles": "24.7.1",
"@vaadin/vaadin-themable-mixin": "24.7.1",
"@vaadin/vaadin-lumo-styles": "24.7.5",
"@vaadin/vaadin-material-styles": "24.7.5",
"@vaadin/vaadin-themable-mixin": "24.7.5",
"@vaadin/vaadin-usage-statistics": "2.1.3",
"classnames": "^2.5.1",
"construct-style-sheets-polyfill": "3.1.0",
@@ -33,7 +33,7 @@
"formik": "^2.4.6",
"framer-motion": "^12.5.0",
"http-status-codes": "^2.3.0",
"lit": "3.2.1",
"lit": "3.3.0",
"moment": "^2.30.1",
"moment-timezone": "^0.5.47",
"next-themes": "^0.4.6",
@@ -43,7 +43,7 @@
"react-aria-components": "^1.7.1",
"react-confetti-boom": "^1.0.0",
"react-dom": "18.3.1",
"react-router": "7.2.0",
"react-router": "7.5.2",
"yup": "^1.6.1"
},
"devDependencies": {
@@ -53,24 +53,24 @@
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@types/node": "^22.4.0",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@vaadin/hilla-generator-cli": "24.7.1",
"@vaadin/hilla-generator-core": "24.7.1",
"@vaadin/hilla-generator-plugin-backbone": "24.7.1",
"@vaadin/hilla-generator-plugin-barrel": "24.7.1",
"@vaadin/hilla-generator-plugin-client": "24.7.1",
"@vaadin/hilla-generator-plugin-model": "24.7.1",
"@vaadin/hilla-generator-plugin-push": "24.7.1",
"@vaadin/hilla-generator-plugin-signals": "24.7.1",
"@vaadin/hilla-generator-plugin-subtypes": "24.7.1",
"@vaadin/hilla-generator-plugin-transfertypes": "24.7.1",
"@vaadin/hilla-generator-utils": "24.7.1",
"@vitejs/plugin-react": "4.3.4",
"@types/react": "18.3.20",
"@types/react-dom": "18.3.6",
"@vaadin/hilla-generator-cli": "24.7.3",
"@vaadin/hilla-generator-core": "24.7.3",
"@vaadin/hilla-generator-plugin-backbone": "24.7.3",
"@vaadin/hilla-generator-plugin-barrel": "24.7.3",
"@vaadin/hilla-generator-plugin-client": "24.7.3",
"@vaadin/hilla-generator-plugin-model": "24.7.3",
"@vaadin/hilla-generator-plugin-push": "24.7.3",
"@vaadin/hilla-generator-plugin-signals": "24.7.3",
"@vaadin/hilla-generator-plugin-subtypes": "24.7.3",
"@vaadin/hilla-generator-plugin-transfertypes": "24.7.3",
"@vaadin/hilla-generator-utils": "24.7.3",
"@vitejs/plugin-react": "4.4.1",
"@vitejs/plugin-react-swc": "^3.7.0",
"async": "3.2.6",
"autoprefixer": "^10.4.20",
"glob": "11.0.1",
"glob": "11.0.2",
"postcss": "^8.4.41",
"postcss-import": "^16.1.0",
"rollup-plugin-brotli": "3.1.0",
@@ -79,8 +79,8 @@
"tailwindcss": "^3.4.13",
"transform-ast": "2.4.4",
"typescript": "5.7.3",
"vite": "6.2.3",
"vite-plugin-checker": "0.8.0",
"vite": "6.3.3",
"vite-plugin-checker": "0.9.1",
"workbox-build": "7.3.0",
"workbox-core": "7.3.0",
"workbox-precaching": "7.3.0"
@@ -133,62 +133,62 @@
"vaadin": {
"dependencies": {
"@polymer/polymer": "3.5.2",
"@vaadin/bundles": "24.7.1",
"@vaadin/bundles": "24.7.5",
"@vaadin/common-frontend": "0.0.19",
"@vaadin/hilla-file-router": "24.7.1",
"@vaadin/hilla-frontend": "24.7.1",
"@vaadin/hilla-lit-form": "24.7.1",
"@vaadin/hilla-react-auth": "24.7.1",
"@vaadin/hilla-react-crud": "24.7.1",
"@vaadin/hilla-react-form": "24.7.1",
"@vaadin/hilla-react-i18n": "24.7.1",
"@vaadin/hilla-react-signals": "24.7.1",
"@vaadin/polymer-legacy-adapter": "24.7.1",
"@vaadin/react-components": "24.7.1",
"@vaadin/hilla-file-router": "24.7.3",
"@vaadin/hilla-frontend": "24.7.3",
"@vaadin/hilla-lit-form": "24.7.3",
"@vaadin/hilla-react-auth": "24.7.3",
"@vaadin/hilla-react-crud": "24.7.3",
"@vaadin/hilla-react-form": "24.7.3",
"@vaadin/hilla-react-i18n": "24.7.3",
"@vaadin/hilla-react-signals": "24.7.3",
"@vaadin/polymer-legacy-adapter": "24.7.5",
"@vaadin/react-components": "24.7.5",
"@vaadin/vaadin-development-mode-detector": "2.0.7",
"@vaadin/vaadin-lumo-styles": "24.7.1",
"@vaadin/vaadin-material-styles": "24.7.1",
"@vaadin/vaadin-themable-mixin": "24.7.1",
"@vaadin/vaadin-lumo-styles": "24.7.5",
"@vaadin/vaadin-material-styles": "24.7.5",
"@vaadin/vaadin-themable-mixin": "24.7.5",
"@vaadin/vaadin-usage-statistics": "2.1.3",
"construct-style-sheets-polyfill": "3.1.0",
"date-fns": "2.29.3",
"lit": "3.2.1",
"lit": "3.3.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-router": "7.2.0"
"react-router": "7.5.2"
},
"devDependencies": {
"@babel/preset-react": "7.26.3",
"@preact/signals-react-transform": "0.5.1",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@vaadin/hilla-generator-cli": "24.7.1",
"@vaadin/hilla-generator-core": "24.7.1",
"@vaadin/hilla-generator-plugin-backbone": "24.7.1",
"@vaadin/hilla-generator-plugin-barrel": "24.7.1",
"@vaadin/hilla-generator-plugin-client": "24.7.1",
"@vaadin/hilla-generator-plugin-model": "24.7.1",
"@vaadin/hilla-generator-plugin-push": "24.7.1",
"@vaadin/hilla-generator-plugin-signals": "24.7.1",
"@vaadin/hilla-generator-plugin-subtypes": "24.7.1",
"@vaadin/hilla-generator-plugin-transfertypes": "24.7.1",
"@vaadin/hilla-generator-utils": "24.7.1",
"@vitejs/plugin-react": "4.3.4",
"@types/react": "18.3.20",
"@types/react-dom": "18.3.6",
"@vaadin/hilla-generator-cli": "24.7.3",
"@vaadin/hilla-generator-core": "24.7.3",
"@vaadin/hilla-generator-plugin-backbone": "24.7.3",
"@vaadin/hilla-generator-plugin-barrel": "24.7.3",
"@vaadin/hilla-generator-plugin-client": "24.7.3",
"@vaadin/hilla-generator-plugin-model": "24.7.3",
"@vaadin/hilla-generator-plugin-push": "24.7.3",
"@vaadin/hilla-generator-plugin-signals": "24.7.3",
"@vaadin/hilla-generator-plugin-subtypes": "24.7.3",
"@vaadin/hilla-generator-plugin-transfertypes": "24.7.3",
"@vaadin/hilla-generator-utils": "24.7.3",
"@vitejs/plugin-react": "4.4.1",
"async": "3.2.6",
"glob": "11.0.1",
"glob": "11.0.2",
"rollup-plugin-brotli": "3.1.0",
"rollup-plugin-visualizer": "5.14.0",
"strip-css-comments": "5.0.0",
"transform-ast": "2.4.4",
"typescript": "5.7.3",
"vite": "6.2.3",
"vite-plugin-checker": "0.8.0",
"vite": "6.3.3",
"vite-plugin-checker": "0.9.1",
"workbox-build": "7.3.0",
"workbox-core": "7.3.0",
"workbox-precaching": "7.3.0"
},
"hash": "26d6e466339c0b4c7dfffb8fe616e725dd60e800bba0169742bfabede9023961"
"hash": "4507ade910f8d37b606e1a8cff078e31d3a90314f1c7426a4aea761dc563f02a"
}
}
@@ -6,9 +6,10 @@ import * as Yup from 'yup';
import {Button, Divider, Tooltip, useDisclosure} from "@heroui/react";
import {Plus} from "@phosphor-icons/react";
import {LibraryEndpoint} from "Frontend/generated/endpoints";
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/LibraryDto";
import {LibraryOverviewCard} from "Frontend/components/general/cards/LibraryOverviewCard";
import LibraryCreationModal from "Frontend/components/general/modals/LibraryCreationModal";
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
function LibraryManagementLayout({getConfig, formik}: any) {
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
@@ -27,6 +28,21 @@ function LibraryManagementLayout({getConfig, formik}: any) {
});
}, []);
async function updateLibrary(library: LibraryUpdateDto) {
let updatedLibrary = await LibraryEndpoint.updateLibrary(library);
if (updatedLibrary === undefined) return;
setLibraries((prevLibraries) => {
const index = prevLibraries.findIndex((l) => l.id === updatedLibrary.id);
if (index !== -1) {
const updatedLibraries = [...prevLibraries];
updatedLibraries[index] = updatedLibrary;
return updatedLibraries;
}
return [...prevLibraries, updatedLibrary];
});
}
return (
<div className="flex flex-col">
<Section title="Permissions"/>
@@ -54,7 +70,9 @@ function LibraryManagementLayout({getConfig, formik}: any) {
{libraries.length > 0 ?
// Aspect ratio of cover = 12/17 -> 5 covers = 60/17 -> 353px * 100px
<div id="library-cards" className="grid gap-4 grid-cols-[repeat(auto-fill,minmax(353px,1fr))]">
{libraries.map((library) => <LibraryOverviewCard library={library} key={library.name}/>)}
{libraries.map((library) =>
<LibraryOverviewCard library={library} updateLibrary={updateLibrary} key={library.name}/>
)}
</div> :
"No libraries configured. Add your first library!"
}
@@ -1,5 +1,5 @@
import {Button, Card, Chip, Tooltip} from "@heroui/react";
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/LibraryDto";
import {Button, Card, Chip, Tooltip, useDisclosure} from "@heroui/react";
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
import React, {useEffect, useState} from "react";
import {LibraryEndpoint} from "Frontend/generated/endpoints";
@@ -14,18 +14,25 @@ import {
Lego,
MagnifyingGlass,
Skull,
SlidersHorizontal,
SoccerBall,
Strategy,
Sword,
TreasureChest,
Trophy
} from "@phosphor-icons/react";
import LibraryDetailsModal from "Frontend/components/general/modals/LibraryDetailsModal";
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
export function LibraryOverviewCard({library}: { library: LibraryDto }) {
export function LibraryOverviewCard({library, updateLibrary}: {
library: LibraryDto,
updateLibrary: (library: LibraryUpdateDto) => void
}) {
const MAX_COVER_COUNT = 5;
const rand = new Rand(library.id.toString());
const [randomGamesFromLibrary, setRandomGamesFromLibrary] = useState<GameDto[]>([]);
const libraryDetailsModal = useDisclosure();
useEffect(() => {
LibraryEndpoint.getGamesInLibrary(library.id).then(
@@ -35,6 +42,7 @@ export function LibraryOverviewCard({library}: { library: LibraryDto }) {
let gamesFromLibrary: GameDto[] = response
.filter(g => !!g)
.sort((a: GameDto, b: GameDto) => a.id - b.id)
.sort(() => rand.next() - 0.5)
.slice(0, count)
@@ -44,55 +52,68 @@ export function LibraryOverviewCard({library}: { library: LibraryDto }) {
}, []);
return (
<Card className="flex flex-col justify-between w-[353px]">
<div className="flex flex-1 justify-center items-center">
<div className="flex flex-1 opacity-10 min-h-[100px]">
<div className="absolute w-full h-full opacity-50">
<GameController size={32}
className="absolute fill-primary top-[10%] left-[10%] rotate-[350deg]"/>
<SoccerBall size={34}
className="absolute fill-primary top-[50%] left-[35%] rotate-[60deg]"/>
<Joystick size={40} className="absolute top-[30%] left-[50%] rotate-[90deg]"/>
<Strategy size={36} className="absolute fill-primary top-[50%] left-[70%] rotate-[30deg]"/>
<Sword size={28} className="absolute top-[70%] left-[10%] rotate-[60deg]"/>
<Alien size={34} className="absolute fill-primary top-[10%] left-[85%] rotate-[15deg]"/>
<CastleTurret size={30} className="absolute top-[5%] left-[40%] rotate-[320deg]"/>
<Ghost size={38} className="absolute fill-primary top-[40%] left-[5%] rotate-[300deg]"/>
<Skull size={32} className="absolute top-[80%] left-[30%] rotate-[90deg]"/>
<Trophy size={36} className="absolute fill-primary top-[10%] left-[60%] rotate-[45deg]"/>
<Lego size={28} className="absolute top-[30%] left-[20%] rotate-[30deg]"/>
<TreasureChest size={40} className="absolute top-[70%] left-[50%] rotate-[75deg]"/>
</div>
{randomGamesFromLibrary.length > 0 &&
<div className="absolute flex flex-row">
{randomGamesFromLibrary.map((game) => (
<GameCover game={game} size={100} radius="none" key={game.coverId}/>
))}
<>
<Card className="flex flex-col justify-between w-[353px]">
<div className="flex flex-1 justify-center items-center">
<div className="flex flex-1 opacity-10 min-h-[100px]">
<div className="absolute w-full h-full opacity-50">
<GameController size={32}
className="absolute fill-primary top-[10%] left-[10%] rotate-[350deg]"/>
<SoccerBall size={34}
className="absolute fill-primary top-[50%] left-[35%] rotate-[60deg]"/>
<Joystick size={40} className="absolute top-[30%] left-[50%] rotate-[90deg]"/>
<Strategy size={36} className="absolute fill-primary top-[50%] left-[70%] rotate-[30deg]"/>
<Sword size={28} className="absolute top-[70%] left-[10%] rotate-[60deg]"/>
<Alien size={34} className="absolute fill-primary top-[10%] left-[85%] rotate-[15deg]"/>
<CastleTurret size={30} className="absolute top-[5%] left-[40%] rotate-[320deg]"/>
<Ghost size={38} className="absolute fill-primary top-[40%] left-[5%] rotate-[300deg]"/>
<Skull size={32} className="absolute top-[80%] left-[30%] rotate-[90deg]"/>
<Trophy size={36} className="absolute fill-primary top-[10%] left-[60%] rotate-[45deg]"/>
<Lego size={28} className="absolute top-[30%] left-[20%] rotate-[30deg]"/>
<TreasureChest size={40} className="absolute top-[70%] left-[50%] rotate-[75deg]"/>
</div>
}
{randomGamesFromLibrary.length > 0 &&
<div className="absolute flex flex-row">
{randomGamesFromLibrary.map((game) => (
<GameCover game={game} size={100} radius="none" key={game.coverId}/>
))}
</div>
}
</div>
<p className="absolute text-2xl font-bold">{library.name}</p>
<div className="absolute right-0 top-0 flex flex-row">
<Tooltip content="Scan library" placement="bottom" color="foreground">
<Button isIconOnly variant="light" onPress={() => LibraryEndpoint.triggerScan([library])}>
<MagnifyingGlass/>
</Button>
</Tooltip>
<Tooltip content="Configuration" placement="bottom" color="foreground">
<Button isIconOnly variant="light" onPress={libraryDetailsModal.onOpen}>
<SlidersHorizontal/>
</Button>
</Tooltip>
</div>
</div>
<p className="absolute text-2xl font-bold">{library.name}</p>
<div className="absolute right-0 top-0 flex flex-row">
<Tooltip content="Scan library" placement="bottom" color="foreground">
<Button isIconOnly variant="light" onPress={() => LibraryEndpoint.triggerScan([library])}>
<MagnifyingGlass/>
</Button>
</Tooltip>
</div>
</div>
{!!library.stats &&
<div className="grid grid-rows-2 grid-cols-3 justify-items-center items-center p-2 pt-4">
<p>Games</p>
<p>Downloads</p>
<p>Platforms</p>
<p className="font-bold">{library.stats.gamesCount}</p>
<p className="font-bold">{library.stats.downloadedGamesCount}</p>
<Chip size="sm">PC</Chip>
</div>
}
</Card>
{library.stats &&
<div className="grid grid-rows-2 grid-cols-3 justify-items-center items-center p-2 pt-4">
<p>Games</p>
<p>Downloads</p>
<p>Platforms</p>
<p className="font-bold">{library.stats.gamesCount}</p>
<p className="font-bold">{library.stats.downloadedGamesCount}</p>
<Chip size="sm">PC</Chip>
</div>
}
</Card>
<LibraryDetailsModal
library={library}
isOpen={libraryDetailsModal.isOpen}
onOpenChange={libraryDetailsModal.onOpenChange}
updateLibrary={updateLibrary}
/>
</>
);
}
@@ -11,7 +11,7 @@ import {
useDisclosure
} from "@heroui/react";
import {Form, Formik} from "formik";
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/LibraryDto";
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
import {LibraryEndpoint} from "Frontend/generated/endpoints";
import Input from "Frontend/components/general/input/Input";
import PathPickerModal from "Frontend/components/general/modals/PathPickerModal";
@@ -0,0 +1,58 @@
import React from "react";
import {Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
import {Form, Formik} from "formik";
import Input from "Frontend/components/general/input/Input";
import {LibraryEndpoint} from "Frontend/generated/endpoints";
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
interface LibraryDetailsModalProps {
library: LibraryDto;
isOpen: boolean;
onOpenChange: () => void;
updateLibrary: (library: LibraryUpdateDto) => void;
}
export default function LibraryDetailsModal({library, isOpen, onOpenChange, updateLibrary}: LibraryDetailsModalProps) {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
<ModalContent>
{(onClose) => (
<Formik initialValues={library}
enableReinitialize={true}
onSubmit={async (values: LibraryUpdateDto) => {
updateLibrary(values);
onClose();
}}
>
{(formik: { isSubmitting: any; }) => (
<Form>
<ModalHeader className="flex flex-col gap-1">
Edit library
</ModalHeader>
<ModalBody>
<Input key="name" name="name" label="Name"/>
<Button onPress={() => LibraryEndpoint.removeLibrary(library.id)}
color="danger">Delete</Button>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onClose}>
Cancel
</Button>
<Button
color="primary"
isLoading={formik.isSubmitting}
disabled={formik.isSubmitting}
type="submit"
>
{formik.isSubmitting ? "" : "Save"}
</Button>
</ModalFooter>
</Form>
)}
</Formik>
)}
</ModalContent>
</Modal>
);
}
@@ -55,7 +55,8 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange, update
{(formik: { isSubmitting: any; }) => (
<Form>
<ModalHeader className="flex flex-col gap-1">
Plugin configuration for {plugin.name}</ModalHeader>
Plugin configuration for {plugin.name}
</ModalHeader>
<ModalBody>
<h4 className="text-l font-bold">Details</h4>
<div className="flex flex-row gap-8">
@@ -18,6 +18,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import me.xdrop.fuzzywuzzy.FuzzySearch
import org.apache.commons.io.FilenameUtils
import org.pf4j.PluginManager
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
@@ -44,8 +45,8 @@ class GameService(
return gameRepository.save(game)
}
fun createFromFile(path: Path): Game {
val query = path.fileName.toString()
fun createFromFile(path: Path): Game? {
val query = FilenameUtils.removeExtension(path.fileName.toString())
// Step 0: Query all metadata plugins for metadata on the provided game title
val metadataResults = queryPlugins(query)
@@ -53,7 +54,8 @@ class GameService(
// Step 1: Filter out invalid (empty) results
val validResults = metadataResults.filterValuesNotNull()
if (validResults.isEmpty()) {
throw NoMatchException("Could not match game at $path")
log.error { "Could not identify game at path '$path'" }
return null
}
// Step 2: Filter results to find the best matching title
@@ -99,9 +101,9 @@ class GameService(
metadataPlugins.associateWith {
async {
try {
it.fetchMetadata(gameTitle)
it.fetchMetadata(gameTitle).firstOrNull()
} catch (e: Exception) {
log.error(e) { "Error fetching metadata with plugin ${it.javaClass.name}" }
log.error(e) { "Error fetching metadata for game with plugin ${it.javaClass.name}" }
null
}
}.await()
@@ -4,6 +4,8 @@ import com.vaadin.hilla.Endpoint
import de.grimsi.gameyfin.core.Role
import de.grimsi.gameyfin.games.GameService
import de.grimsi.gameyfin.games.dto.GameDto
import de.grimsi.gameyfin.libraries.dto.LibraryDto
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
import jakarta.annotation.security.PermitAll
import jakarta.annotation.security.RolesAllowed
@@ -28,7 +30,17 @@ class LibraryEndpoint(
@RolesAllowed(Role.Names.ADMIN)
fun createLibrary(library: LibraryDto): LibraryDto {
return libraryService.createOrUpdate(library)
return libraryService.create(library)
}
@RolesAllowed(Role.Names.ADMIN)
fun updateLibrary(library: LibraryUpdateDto): LibraryDto {
return libraryService.update(library)
}
@RolesAllowed(Role.Names.ADMIN)
fun removeLibrary(libraryId: Long) {
return libraryService.deleteLibrary(libraryId)
}
@RolesAllowed(Role.Names.ADMIN)
@@ -4,6 +4,9 @@ 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 de.grimsi.gameyfin.libraries.dto.LibraryDto
import de.grimsi.gameyfin.libraries.dto.LibraryStatsDto
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.runBlocking
import org.springframework.data.repository.findByIdOrNull
@@ -24,11 +27,30 @@ class LibraryService(
* @param library: The library to create or update.
* @return The created or updated LibraryDto object.
*/
fun createOrUpdate(library: LibraryDto): LibraryDto {
fun create(library: LibraryDto): LibraryDto {
val entity = libraryRepository.save(toEntity(library))
return toDto(entity)
}
/**
* Updates a library entity with the non-null fields from a LibraryUpdateDto.
*
* @param libraryDto: The LibraryUpdateDto containing the fields to update.
* @return The updated LibraryDto.
* @throws IllegalArgumentException if the library ID is null or the library is not found.
*/
fun update(libraryDto: LibraryUpdateDto): LibraryDto {
val existingLibrary = libraryRepository.findByIdOrNull(libraryDto.id)
?: throw IllegalArgumentException("Library with ID $libraryDto.id not found")
// Update only non-null fields
libraryDto.name?.let { existingLibrary.name = it }
libraryDto.directories?.let { existingLibrary.directories = it.toMutableSet() }
val updatedLibrary = libraryRepository.save(existingLibrary)
return toDto(updatedLibrary)
}
/**
* Retrieves all libraries from the repository.
*/
@@ -40,11 +62,10 @@ class LibraryService(
/**
* Deletes a library from the repository.
*
* @param library: The library to delete.
* @param libraryId: ID of the library to delete.
*/
fun deleteLibrary(library: LibraryDto) {
val entity = toEntity(library)
libraryRepository.delete(entity)
fun deleteLibrary(libraryId: Long) {
libraryRepository.deleteById(libraryId)
}
/**
@@ -114,7 +135,10 @@ class LibraryService(
val libraries = libraryDtos?.map { toEntity(it) } ?: libraryRepository.findAll()
libraries.forEach { library ->
val gamePaths = filesystemService.scanLibraryForGamefiles(library)
val newGames = gamePaths.map { gameService.createFromFile(it) }
val newGames = gamePaths.mapNotNull {
gameService.createFromFile(it)
}
addGamesToLibrary(newGames, library)
}
}
@@ -1,4 +1,4 @@
package de.grimsi.gameyfin.libraries
package de.grimsi.gameyfin.libraries.dto
data class LibraryDto(
val id: Long,
@@ -1,4 +1,4 @@
package de.grimsi.gameyfin.libraries
package de.grimsi.gameyfin.libraries.dto
data class LibraryStatsDto(
val gamesCount: Int,
@@ -0,0 +1,7 @@
package de.grimsi.gameyfin.libraries.dto
data class LibraryUpdateDto(
val id: Long,
val name: String? = null,
val directories: Set<String>? = null,
)
@@ -1,2 +1,3 @@
logging.level.de.grimsi.gameyfin: DEBUG
logging.level.org.hibernate.SQL: DEBUG
logging.level.org.hibernate.type: TRACE