-
{library.name}
-
{library.path}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {randomGamesFromLibrary.length > 0 &&
+
+ {randomGamesFromLibrary.map((game) => (
+
+ ))}
+
+ }
+
+
+
{library.name}
+
+ {!!library.stats &&
+
+
Games
+
Downloads
+
Platforms
+
{library.stats.gamesCount}
+
{library.stats.downloadedGamesCount}
+
PC
+
+ }
);
}
\ No newline at end of file
diff --git a/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx b/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx
new file mode 100644
index 0000000..79f480e
--- /dev/null
+++ b/gameyfin/src/main/frontend/components/general/modals/LibraryCreationModal.tsx
@@ -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 (
+
+
+ {(onClose) => (
+ {
+ await createLibrary(values);
+ onClose();
+ }}
+ >
+ {(formik) => (
+
+ )}
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/gameyfin/src/main/frontend/views/TestView.tsx b/gameyfin/src/main/frontend/views/TestView.tsx
index 47852cf..8de0cf0 100644
--- a/gameyfin/src/main/frontend/views/TestView.tsx
+++ b/gameyfin/src/main/frontend/views/TestView.tsx
@@ -27,6 +27,16 @@ export default function TestView() {
});
}
+ function removeLibraries() {
+ LibraryEndpoint.removeLibraries().then(() => {
+ addToast({
+ title: "Success",
+ description: "Libraries removed",
+ color: "success"
+ })
+ });
+ }
+
return (
@@ -58,6 +68,7 @@ export default function TestView() {
+
{game &&
}
{game && <>{JSON.stringify(game, null, 2)}>}
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 dc5b8cf..e865dc3 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt
@@ -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
{
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(
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 435d467..5aedfe0 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
@@ -68,5 +68,7 @@ class Game(
var metadata: Map = emptyMap(),
@ElementCollection
- var originalIds: Map = emptyMap()
+ var originalIds: Map = emptyMap(),
+
+ var downloadCount: Int = 0
)
\ No newline at end of file
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 8ef3407..88514b1 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/Library.kt
@@ -14,6 +14,6 @@ class Library(
@Column(unique = true)
val path: String,
- @OneToMany
- val games: Set = emptySet()
+ @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
+ val games: MutableSet = mutableSetOf()
)
\ No newline at end of file
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryDto.kt
index cfdb3cb..9f5aa82 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryDto.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryDto.kt
@@ -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?
)
\ 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 44fc0d4..37daafd 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryEndpoint.kt
@@ -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 {
return libraryService.getAllLibraries()
}
+ fun getGamesInLibrary(libraryId: Long): Collection {
+ 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)
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryRepository.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryRepository.kt
index 066ff5d..47fd84e 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryRepository.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryRepository.kt
@@ -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
\ No newline at end of file
+interface LibraryRepository : JpaRepository {
+ @EntityGraph(attributePaths = ["games"])
+ @Query("SELECT l FROM Library l ORDER BY function('RAND') LIMIT 1")
+ fun findRandomLibrary(): Library?
+}
\ 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 0406e47..71c060b 100644
--- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryService.kt
@@ -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 {
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 {
+ 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
)
diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryStatsDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryStatsDto.kt
new file mode 100644
index 0000000..953b13e
--- /dev/null
+++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/libraries/LibraryStatsDto.kt
@@ -0,0 +1,6 @@
+package de.grimsi.gameyfin.libraries
+
+data class LibraryStatsDto(
+ val gamesCount: Int,
+ val downloadedGamesCount: Int,
+)
\ No newline at end of file