mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
First prototype of game libraries
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="UI debug" type="JavascriptDebugType" engineId="37cae5b9-e8b2-4949-9172-aafa37fbc09c" uri="http://localhost:8080">
|
||||
<configuration default="false" name="UI debug" type="JavascriptDebugType" uri="http://localhost:8080">
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
Generated
+20
@@ -42,7 +42,9 @@
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"next-themes": "^0.4.6",
|
||||
"rand-seed": "^2.1.7",
|
||||
"react": "18.3.1",
|
||||
"react-accessible-treeview": "^2.11.1",
|
||||
"react-aria-components": "^1.7.1",
|
||||
"react-confetti-boom": "^1.0.0",
|
||||
"react-dom": "18.3.1",
|
||||
@@ -15550,6 +15552,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/rand-seed": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/rand-seed/-/rand-seed-2.1.7.tgz",
|
||||
"integrity": "sha512-Yaz75D2fTWtIr69iDd+PGwtQkFkqOIMQZl+W8U3NMR6F2a1UEk2FmnQkNd6Z1eNtL96Z5nznw5PKYk1Z9m6lxw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@@ -15572,6 +15580,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-accessible-treeview": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/react-accessible-treeview/-/react-accessible-treeview-2.11.1.tgz",
|
||||
"integrity": "sha512-lFegHjFJp2OvtoHMtbIqjby7N3MGDRASlbJsMLqElxQHwZ97xIYho2S4QvXKK7l3/nII0IKDQFJXZNBj6ecG3g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-aria": {
|
||||
"version": "3.38.1",
|
||||
"resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.38.1.tgz",
|
||||
|
||||
@@ -37,7 +37,9 @@
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"next-themes": "^0.4.6",
|
||||
"rand-seed": "^2.1.7",
|
||||
"react": "18.3.1",
|
||||
"react-accessible-treeview": "^2.11.1",
|
||||
"react-aria-components": "^1.7.1",
|
||||
"react-confetti-boom": "^1.0.0",
|
||||
"react-dom": "18.3.1",
|
||||
@@ -123,7 +125,9 @@
|
||||
"@vaadin/vaadin-material-styles": "$@vaadin/vaadin-material-styles",
|
||||
"@react-types/shared": "$@react-types/shared",
|
||||
"@react-stately/data": "$@react-stately/data",
|
||||
"react-aria-components": "$react-aria-components"
|
||||
"react-aria-components": "$react-aria-components",
|
||||
"react-accessible-treeview": "$react-accessible-treeview",
|
||||
"rand-seed": "$rand-seed"
|
||||
},
|
||||
"vaadin": {
|
||||
"dependencies": {
|
||||
@@ -183,6 +187,6 @@
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-precaching": "7.3.0"
|
||||
},
|
||||
"hash": "081663e5e6c1316ae1baa055271a5fe7aebc8015d2310320fd58e3e20c1dd342"
|
||||
"hash": "a951d6d827dd2a43a7d0fa6330d5030cc19c1077490488a24cfd7908bd43aac3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
import React, {useEffect} from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||
import Section from "Frontend/components/general/Section";
|
||||
import * as Yup from 'yup';
|
||||
import {Button, Divider, Tooltip} from "@heroui/react";
|
||||
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 {ListData, useListData} from "@react-stately/data";
|
||||
import LibraryCreationModal from "Frontend/components/general/modals/LibraryCreationModal";
|
||||
|
||||
function LibraryManagementLayout({getConfig, formik}: any) {
|
||||
const libraries: ListData<LibraryDto> = useListData({});
|
||||
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
|
||||
const libraryCreationModal = useDisclosure();
|
||||
|
||||
useEffect(() => {
|
||||
LibraryEndpoint.getAllLibraries().then(
|
||||
(response) => libraries.items = response as LibraryDto[]
|
||||
);
|
||||
LibraryEndpoint.getAllLibraries().then((response) => {
|
||||
if (response === undefined) return;
|
||||
let sortedLibraries: LibraryDto[] = response
|
||||
.filter(l => !!l)
|
||||
.sort((a: LibraryDto, b: LibraryDto) => {
|
||||
if (a.name === undefined || b.name === undefined) return 0;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
setLibraries(sortedLibraries);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
|
||||
<Section title="Permissions"/>
|
||||
<ConfigFormField configElement={getConfig("library.allow-public-access")}/>
|
||||
|
||||
@@ -38,20 +45,26 @@ function LibraryManagementLayout({getConfig, formik}: any) {
|
||||
<div className="flex flex-row items-baseline justify-between">
|
||||
<h2 className={"text-xl font-bold mt-8 mb-1"}>Libraries</h2>
|
||||
<Tooltip content="Add new library">
|
||||
<Button isIconOnly variant="flat" onPress={() => {
|
||||
libraries.append({id: 1, name: "Library", path: "/path/to/library"} as LibraryDto)
|
||||
}}>
|
||||
<Button isIconOnly variant="flat" onPress={libraryCreationModal.onOpen}>
|
||||
<Plus/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Divider className="mb-4"/>
|
||||
{libraries.items.length > 0 ?
|
||||
<div className="grid grid-cols-300px gap-4">
|
||||
{libraries.items.map((library) => <LibraryOverviewCard library={library} key={library.name}/>)}
|
||||
{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}/>)}
|
||||
</div> :
|
||||
"No libraries configured. Add your first library!"
|
||||
}
|
||||
|
||||
<LibraryCreationModal
|
||||
libraries={libraries}
|
||||
setLibraries={setLibraries}
|
||||
isOpen={libraryCreationModal.isOpen}
|
||||
onOpenChange={libraryCreationModal.onOpenChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
import {Image} from "@heroui/react";
|
||||
|
||||
interface GameCoverProps {
|
||||
game: GameDto;
|
||||
size?: number;
|
||||
radius?: "none" | "sm" | "md" | "lg" | "full";
|
||||
}
|
||||
|
||||
export function GameCover({game, size = 300, radius}: GameCoverProps) {
|
||||
return (
|
||||
<Image
|
||||
alt={game.title}
|
||||
className="z-0 w-full h-full object-cover aspect-[12/17]"
|
||||
src={`images/cover/${game.coverId}`}
|
||||
radius={radius}
|
||||
height={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,8 @@
|
||||
import {GameCover} from "Frontend/components/general/GameCover";
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
import {Card, Image} from "@heroui/react";
|
||||
|
||||
export function GameOverviewCard({game}: { game: GameDto }) {
|
||||
return (
|
||||
<Card className="h-80 aspect-[12/17]">
|
||||
<Image
|
||||
removeWrapper
|
||||
alt={game.title}
|
||||
className="z-0 w-full h-full object-cover"
|
||||
src={`images/cover/${game.coverId}`}
|
||||
/>
|
||||
</Card>
|
||||
<GameCover game={game} radius="sm"></GameCover>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,89 @@
|
||||
import {Card} from "@heroui/react";
|
||||
import {Card, Chip} 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 {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||
import {GameCover} from "Frontend/components/general/GameCover";
|
||||
import Rand from "rand-seed";
|
||||
import {
|
||||
Alien,
|
||||
CastleTurret,
|
||||
GameController,
|
||||
Ghost,
|
||||
Joystick,
|
||||
Lego,
|
||||
Skull,
|
||||
SoccerBall,
|
||||
Strategy,
|
||||
Sword,
|
||||
TreasureChest,
|
||||
Trophy
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export function LibraryOverviewCard({library}: { library: LibraryDto }) {
|
||||
const MAX_COVER_COUNT = 5;
|
||||
const rand = new Rand(library.id.toString());
|
||||
|
||||
const [randomGamesFromLibrary, setRandomGamesFromLibrary] = useState<GameDto[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
LibraryEndpoint.getGamesInLibrary(library.id).then(
|
||||
(response) => {
|
||||
if (response === undefined) return;
|
||||
const count = Math.min(response.length, MAX_COVER_COUNT)
|
||||
|
||||
let gamesFromLibrary: GameDto[] = response
|
||||
.filter(g => !!g)
|
||||
.sort(() => rand.next() - 0.5)
|
||||
.slice(0, count)
|
||||
|
||||
setRandomGamesFromLibrary(gamesFromLibrary);
|
||||
}
|
||||
)
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className="flex flex-row justify-between p-2">
|
||||
<div className="flex flex-col flex-1 items-center gap-4">
|
||||
<p className="text-2xl font-bold">{library.name}</p>
|
||||
<p>{library.path}</p>
|
||||
<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>
|
||||
|
||||
{!!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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from "react";
|
||||
import {addToast, Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
|
||||
import {Form, Formik} from "formik";
|
||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/LibraryDto";
|
||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||
import Input from "Frontend/components/general/input/Input";
|
||||
|
||||
interface LibraryCreationModalProps {
|
||||
libraries: LibraryDto[];
|
||||
setLibraries: (libraries: LibraryDto[]) => void;
|
||||
isOpen: boolean;
|
||||
onOpenChange: () => void;
|
||||
}
|
||||
|
||||
export default function LibraryCreationModal({
|
||||
libraries,
|
||||
setLibraries,
|
||||
isOpen,
|
||||
onOpenChange
|
||||
}: LibraryCreationModalProps) {
|
||||
async function createLibrary(library: LibraryDto) {
|
||||
try {
|
||||
const newLibrary = await LibraryEndpoint.createLibrary(library as LibraryDto);
|
||||
if (newLibrary === undefined) return;
|
||||
setLibraries([...libraries, newLibrary]);
|
||||
} catch (e) {
|
||||
addToast({
|
||||
title: "Error creating library",
|
||||
description: `Library ${library.name} could not be created!`,
|
||||
color: "warning"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
addToast({
|
||||
title: "New library created",
|
||||
description: `Library ${library.name} created!`,
|
||||
color: "success"
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<Formik initialValues={{name: "", path: ""}}
|
||||
onSubmit={async (values: any) => {
|
||||
await createLibrary(values);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form>
|
||||
<ModalHeader className="flex flex-col gap-1">Add a new library</ModalHeader>
|
||||
<ModalBody>
|
||||
<h4 className="text-l font-bold">Details</h4>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Input
|
||||
name="name"
|
||||
label="Library Name"
|
||||
placeholder="Enter library name"
|
||||
value={formik.values.name}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
name="path"
|
||||
label="path"
|
||||
placeholder="Enter library path"
|
||||
value={formik.values.path}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : "Add"}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,16 @@ export default function TestView() {
|
||||
});
|
||||
}
|
||||
|
||||
function removeLibraries() {
|
||||
LibraryEndpoint.removeLibraries().then(() => {
|
||||
addToast({
|
||||
title: "Success",
|
||||
description: "Libraries removed",
|
||||
color: "success"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grow justify-center mt-12">
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
@@ -58,6 +68,7 @@ export default function TestView() {
|
||||
<Input label="Game title" onValueChange={setGameTitle}/>
|
||||
<Button onPress={getGame} size="lg">Match</Button>
|
||||
<Button onPress={removeGames} size="lg">Clear DB</Button>
|
||||
<Button onPress={removeLibraries} size="lg">Clear Libs</Button>
|
||||
</div>
|
||||
{game && <GameOverviewCard game={game}></GameOverviewCard>}
|
||||
{game && <>{JSON.stringify(game, null, 2)}</>}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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
|
||||
@@ -46,7 +47,7 @@ class GameService(
|
||||
return gameRepository.save(game)
|
||||
}
|
||||
|
||||
fun createFromFile(path: Path): GameDto {
|
||||
fun createFromFile(path: Path): Game {
|
||||
val query = path.fileName.toString()
|
||||
|
||||
// Step 0: Query all metadata plugins for metadata on the provided game title
|
||||
@@ -70,11 +71,10 @@ class GameService(
|
||||
val mergedGame = mergeResults(sortedResults, path)
|
||||
|
||||
// Step 5: Save the new game
|
||||
val savedGame = createOrUpdate(mergedGame)
|
||||
|
||||
return toDto(savedGame)
|
||||
return createOrUpdate(mergedGame)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getAllGames(): Collection<GameDto> {
|
||||
val entities = gameRepository.findAll()
|
||||
return entities.map { toDto(it) }
|
||||
@@ -252,7 +252,7 @@ class GameService(
|
||||
return mergedGame
|
||||
}
|
||||
|
||||
private fun toDto(game: Game): GameDto {
|
||||
fun toDto(game: Game): GameDto {
|
||||
val gameId = game.id ?: throw IllegalArgumentException("Game ID is null")
|
||||
|
||||
return GameDto(
|
||||
|
||||
@@ -68,5 +68,7 @@ class Game(
|
||||
var metadata: Map<String, FieldMetadata> = emptyMap(),
|
||||
|
||||
@ElementCollection
|
||||
var originalIds: Map<PluginManagementEntry, String> = emptyMap()
|
||||
var originalIds: Map<PluginManagementEntry, String> = emptyMap(),
|
||||
|
||||
var downloadCount: Int = 0
|
||||
)
|
||||
@@ -14,6 +14,6 @@ class Library(
|
||||
@Column(unique = true)
|
||||
val path: String,
|
||||
|
||||
@OneToMany
|
||||
val games: Set<Game> = emptySet()
|
||||
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||
val games: MutableSet<Game> = mutableSetOf()
|
||||
)
|
||||
@@ -3,5 +3,6 @@ package de.grimsi.gameyfin.libraries
|
||||
data class LibraryDto(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val path: String
|
||||
val path: String,
|
||||
val stats: LibraryStatsDto?
|
||||
)
|
||||
@@ -4,26 +4,37 @@ 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 jakarta.annotation.security.PermitAll
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
|
||||
@Endpoint
|
||||
@PermitAll
|
||||
class LibraryEndpoint(
|
||||
private val libraryService: LibraryService,
|
||||
private val gameService: GameService
|
||||
) {
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
fun getAllLibraries(): Collection<LibraryDto> {
|
||||
return libraryService.getAllLibraries()
|
||||
}
|
||||
|
||||
fun getGamesInLibrary(libraryId: Long): Collection<GameDto> {
|
||||
return libraryService.getGamesInLibrary(libraryId)
|
||||
}
|
||||
|
||||
// FIXME: Just for testing
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
fun test(testString: String): GameDto {
|
||||
return libraryService.test(testString)
|
||||
}
|
||||
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
fun createLibrary(library: LibraryDto): LibraryDto {
|
||||
return libraryService.createOrUpdate(library)
|
||||
}
|
||||
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
fun test(testString: String): GameDto {
|
||||
return libraryService.test(testString)
|
||||
fun removeLibraries() {
|
||||
return libraryService.deleteAllLibraries()
|
||||
}
|
||||
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package de.grimsi.gameyfin.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>
|
||||
interface LibraryRepository : JpaRepository<Library, Long> {
|
||||
@EntityGraph(attributePaths = ["games"])
|
||||
@Query("SELECT l FROM Library l ORDER BY function('RAND') LIMIT 1")
|
||||
fun findRandomLibrary(): Library?
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import de.grimsi.gameyfin.games.GameService
|
||||
import de.grimsi.gameyfin.games.dto.GameDto
|
||||
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
|
||||
@@ -18,8 +19,16 @@ class LibraryService(
|
||||
private val gameService: GameService,
|
||||
private val config: ConfigService
|
||||
) {
|
||||
|
||||
fun test(testString: String): GameDto {
|
||||
return gameService.createFromFile(Path(testString))
|
||||
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)
|
||||
}
|
||||
|
||||
fun createOrUpdate(library: LibraryDto): LibraryDto {
|
||||
@@ -27,6 +36,7 @@ class LibraryService(
|
||||
return toDto(entity)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getAllLibraries(): Collection<LibraryDto> {
|
||||
val entities = libraryRepository.findAll()
|
||||
return entities.map { toDto(it) }
|
||||
@@ -37,6 +47,20 @@ class LibraryService(
|
||||
libraryRepository.delete(entity)
|
||||
}
|
||||
|
||||
fun deleteAllLibraries() {
|
||||
libraryRepository.deleteAll()
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getGamesInLibrary(libraryId: Long): Collection<GameDto> {
|
||||
val library = libraryRepository.findByIdOrNull(libraryId)
|
||||
?: throw IllegalArgumentException("Library with ID $libraryId not found")
|
||||
|
||||
val games = library.games.map { gameService.toDto(it) }
|
||||
|
||||
return games
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a scan for a list of libraries. If no list is provided, all libraries will be scanned.
|
||||
*/
|
||||
@@ -72,16 +96,21 @@ class LibraryService(
|
||||
throw IllegalArgumentException("Library ID is null")
|
||||
}
|
||||
|
||||
val statsDto = LibraryStatsDto(
|
||||
gamesCount = library.games.size,
|
||||
downloadedGamesCount = library.games.sumOf { it.downloadCount }
|
||||
)
|
||||
|
||||
return LibraryDto(
|
||||
id = library.id,
|
||||
name = library.name,
|
||||
path = library.path
|
||||
path = library.path,
|
||||
stats = statsDto
|
||||
)
|
||||
}
|
||||
|
||||
private fun toEntity(library: LibraryDto): Library {
|
||||
return libraryRepository.findByIdOrNull(library.id) ?: Library(
|
||||
id = library.id,
|
||||
name = library.name,
|
||||
path = library.path
|
||||
)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package de.grimsi.gameyfin.libraries
|
||||
|
||||
data class LibraryStatsDto(
|
||||
val gamesCount: Int,
|
||||
val downloadedGamesCount: Int,
|
||||
)
|
||||
Reference in New Issue
Block a user