mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 00:30:02 +00:00
Implement different DTOs for users and admins (#644)
* Implement different DTOs for users and admins * Fix performance by not creating unnecessary websocket connections
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<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" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
||||||
@@ -40,13 +40,17 @@ export default function App() {
|
|||||||
function ViewWithAuth() {
|
function ViewWithAuth() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
|
||||||
initializeLibraryState();
|
useEffect(() => {
|
||||||
initializeGameState();
|
if (auth.state.initializing || auth.state.loading) return;
|
||||||
|
|
||||||
if (isAdmin(auth)) {
|
initializeLibraryState();
|
||||||
initializeScanState();
|
initializeGameState();
|
||||||
initializePluginState();
|
|
||||||
}
|
if (isAdmin(auth)) {
|
||||||
|
initializeScanState();
|
||||||
|
initializePluginState();
|
||||||
|
}
|
||||||
|
}, [auth]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<IconContext.Provider value={{size: 20}}>
|
<IconContext.Provider value={{size: 20}}>
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getConfig(key: string): ConfigEntryDto | undefined {
|
function getConfig(key: string): ConfigEntryDto | undefined {
|
||||||
// @ts-ignore
|
return state.state[key] as ConfigEntryDto | undefined;
|
||||||
return state.state[key];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChangedValues(initial: NestedConfig, current: NestedConfig): Record<string, any> {
|
function getChangedValues(initial: NestedConfig, current: NestedConfig): Record<string, any> {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function SearchBar() {
|
|||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const state = useSnapshot(gameState);
|
const state = useSnapshot(gameState);
|
||||||
const games = state.recentlyUpdated as GameDto[];
|
const games = state.games as GameDto[];
|
||||||
|
|
||||||
return <Autocomplete
|
return <Autocomplete
|
||||||
aria-label="Search for games"
|
aria-label="Search for games"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import {Button, Card, Chip, Tooltip} from "@heroui/react";
|
import {Button, Card, Chip, Tooltip} from "@heroui/react";
|
||||||
import LibraryDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryDto";
|
|
||||||
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||||
@@ -10,9 +9,10 @@ import {useNavigate} from "react-router";
|
|||||||
import {useSnapshot} from "valtio/react";
|
import {useSnapshot} from "valtio/react";
|
||||||
import {gameState} from "Frontend/state/GameState";
|
import {gameState} from "Frontend/state/GameState";
|
||||||
import IconBackgroundPattern from "Frontend/components/general/IconBackgroundPattern";
|
import IconBackgroundPattern from "Frontend/components/general/IconBackgroundPattern";
|
||||||
|
import LibraryAdminDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryAdminDto";
|
||||||
|
|
||||||
interface LibraryOverviewCardProps {
|
interface LibraryOverviewCardProps {
|
||||||
library: LibraryDto;
|
library: LibraryAdminDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LibraryOverviewCard({library}: LibraryOverviewCardProps) {
|
export function LibraryOverviewCard({library}: LibraryOverviewCardProps) {
|
||||||
@@ -28,7 +28,7 @@ export function LibraryOverviewCard({library}: LibraryOverviewCardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function triggerScan(scanType: ScanType) {
|
async function triggerScan(scanType: ScanType) {
|
||||||
await LibraryEndpoint.triggerScan(scanType, [library]);
|
await LibraryEndpoint.triggerScan(scanType, [library.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import GameUpdateDto from "Frontend/generated/org/gameyfin/app/games/dto/GameUpd
|
|||||||
import {useMemo, useState} from "react";
|
import {useMemo, useState} from "react";
|
||||||
import EditGameMetadataModal from "Frontend/components/general/modals/EditGameMetadataModal";
|
import EditGameMetadataModal from "Frontend/components/general/modals/EditGameMetadataModal";
|
||||||
import MatchGameModal from "Frontend/components/general/modals/MatchGameModal";
|
import MatchGameModal from "Frontend/components/general/modals/MatchGameModal";
|
||||||
|
import {GameAdminDto} from "Frontend/dtos/GameDtos";
|
||||||
|
|
||||||
interface LibraryManagementGamesProps {
|
interface LibraryManagementGamesProps {
|
||||||
library: LibraryDto;
|
library: LibraryDto;
|
||||||
@@ -34,12 +35,12 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
|||||||
const rowsPerPage = 25;
|
const rowsPerPage = 25;
|
||||||
|
|
||||||
const state = useSnapshot(gameState);
|
const state = useSnapshot(gameState);
|
||||||
const games = state.gamesByLibraryId[library.id] ? state.gamesByLibraryId[library.id] as GameDto[] : [];
|
const games = state.gamesByLibraryId[library.id] ? state.gamesByLibraryId[library.id] as GameAdminDto[] : [];
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [filter, setFilter] = useState<"all" | "confirmed" | "nonConfirmed">("all");
|
const [filter, setFilter] = useState<"all" | "confirmed" | "nonConfirmed">("all");
|
||||||
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({column: "title", direction: "ascending"});
|
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({column: "title", direction: "ascending"});
|
||||||
|
|
||||||
const [selectedGame, setSelectedGame] = useState<GameDto>(games[0]);
|
const [selectedGame, setSelectedGame] = useState<GameAdminDto>(games[0]);
|
||||||
const editGameModal = useDisclosure();
|
const editGameModal = useDisclosure();
|
||||||
const matchGameModal = useDisclosure();
|
const matchGameModal = useDisclosure();
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
|||||||
}, [games, filter, searchTerm]);
|
}, [games, filter, searchTerm]);
|
||||||
|
|
||||||
const sortedItems = useMemo(() => {
|
const sortedItems = useMemo(() => {
|
||||||
return filteredItems.slice().sort((a, b) => {
|
return (filteredItems as GameAdminDto[]).slice().sort((a, b) => {
|
||||||
let cmp: number;
|
let cmp: number;
|
||||||
|
|
||||||
switch (sortDescriptor.column) {
|
switch (sortDescriptor.column) {
|
||||||
@@ -86,7 +87,7 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
|||||||
|
|
||||||
|
|
||||||
function getFilteredGames() {
|
function getFilteredGames() {
|
||||||
let filteredGames = games.filter((game) =>
|
let filteredGames = (games as GameAdminDto[]).filter((game) =>
|
||||||
game.metadata.path!!.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
game.metadata.path!!.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
game.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
game.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
game.publishers?.some(publisher => publisher.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
game.publishers?.some(publisher => publisher.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
||||||
@@ -102,7 +103,7 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
|||||||
return filteredGames;
|
return filteredGames;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleMatchConfirmed(game: GameDto) {
|
async function toggleMatchConfirmed(game: GameAdminDto) {
|
||||||
await GameEndpoint.updateGame(
|
await GameEndpoint.updateGame(
|
||||||
{
|
{
|
||||||
id: game.id,
|
id: game.id,
|
||||||
@@ -163,13 +164,13 @@ export default function LibraryManagementGames({library}: LibraryManagementGames
|
|||||||
<TableColumn width={1}>Actions</TableColumn>
|
<TableColumn width={1}>Actions</TableColumn>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody emptyContent="Your filter did not match any games." items={pagedItems}>
|
<TableBody emptyContent="Your filter did not match any games." items={pagedItems}>
|
||||||
{(item) => (
|
{(item: GameAdminDto) => (
|
||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Link href={`/game/${item.id}`}
|
<Link href={`/game/${item.id}`}
|
||||||
color="foreground"
|
color="foreground"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
underline="hover">{item.title} ({item.release !== undefined ? new Date(item.release).getFullYear() : "unknown"})
|
underline="hover">{item.title} ({item.release ? new Date(item.release).getFullYear() : "unknown"})
|
||||||
</Link>
|
</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,3 @@
|
|||||||
import LibraryDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryDto";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
@@ -19,9 +18,10 @@ import {useMemo, useState} from "react";
|
|||||||
import LibraryUpdateDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryUpdateDto";
|
import LibraryUpdateDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryUpdateDto";
|
||||||
import {fileNameFromPath, hashCode} from "Frontend/util/utils";
|
import {fileNameFromPath, hashCode} from "Frontend/util/utils";
|
||||||
import MatchGameModal from "Frontend/components/general/modals/MatchGameModal";
|
import MatchGameModal from "Frontend/components/general/modals/MatchGameModal";
|
||||||
|
import LibraryAdminDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryAdminDto";
|
||||||
|
|
||||||
interface LibraryManagementUnmatchedPathsProps {
|
interface LibraryManagementUnmatchedPathsProps {
|
||||||
library: LibraryDto;
|
library: LibraryAdminDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LibraryManagementUnmatchedPaths({library}: LibraryManagementUnmatchedPathsProps) {
|
export default function LibraryManagementUnmatchedPaths({library}: LibraryManagementUnmatchedPathsProps) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
|||||||
import Input from "Frontend/components/general/input/Input";
|
import Input from "Frontend/components/general/input/Input";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import DirectoryMappingInput from "Frontend/components/general/input/DirectoryMappingInput";
|
import DirectoryMappingInput from "Frontend/components/general/input/DirectoryMappingInput";
|
||||||
|
import LibraryAdminDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryAdminDto";
|
||||||
|
|
||||||
interface LibraryCreationModalProps {
|
interface LibraryCreationModalProps {
|
||||||
libraries: LibraryDto[];
|
libraries: LibraryDto[];
|
||||||
@@ -23,7 +24,7 @@ export default function LibraryCreationModal({
|
|||||||
const [scanAfterCreation, setScanAfterCreation] = useState<boolean>(true);
|
const [scanAfterCreation, setScanAfterCreation] = useState<boolean>(true);
|
||||||
|
|
||||||
async function createLibrary(library: LibraryDto) {
|
async function createLibrary(library: LibraryDto) {
|
||||||
await LibraryEndpoint.createLibrary(library as LibraryDto, scanAfterCreation);
|
await LibraryEndpoint.createLibrary(library as LibraryAdminDto, scanAfterCreation);
|
||||||
|
|
||||||
addToast({
|
addToast({
|
||||||
title: "New library created",
|
title: "New library created",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export default function PluginIcon({
|
|||||||
blurred = false,
|
blurred = false,
|
||||||
showTooltip = true
|
showTooltip = true
|
||||||
}: PluginIconProps) {
|
}: PluginIconProps) {
|
||||||
|
|
||||||
const icon = plugin.hasLogo
|
const icon = plugin.hasLogo
|
||||||
?
|
?
|
||||||
<Image isBlurred={blurred} src={`/images/plugins/${plugin.id}/logo`} width={size} height={size} radius="none"/>
|
<Image isBlurred={blurred} src={`/images/plugins/${plugin.id}/logo`} width={size} height={size} radius="none"/>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
||||||
|
|
||||||
|
export interface GameAdminDto extends GameDto {
|
||||||
|
metadata: GameMetadataAdminDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameMetadataAdminDto {
|
||||||
|
path?: string | null;
|
||||||
|
fileSize: number;
|
||||||
|
fields?: { [key: string]: GameFieldMetadataDto } | null;
|
||||||
|
originalIds?: { [key: string]: string } | null;
|
||||||
|
downloadCount: number;
|
||||||
|
matchConfirmed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GameFieldMetadataDto {
|
||||||
|
type: GameFieldMetadataType;
|
||||||
|
source: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GameFieldMetadataType {
|
||||||
|
PLUGIN = 'PLUGIN',
|
||||||
|
USER = 'USER',
|
||||||
|
UNKNOWN = 'UNKNOWN'
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import {Subscription} from "@vaadin/hilla-frontend";
|
import {Subscription} from "@vaadin/hilla-frontend";
|
||||||
import {proxy} from "valtio/index";
|
import {proxy} from "valtio/index";
|
||||||
import {GameEndpoint} from "Frontend/generated/endpoints";
|
import {GameEndpoint} from "Frontend/generated/endpoints";
|
||||||
import GameEvent from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryEvent";
|
|
||||||
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
||||||
import Rand from "rand-seed";
|
import Rand from "rand-seed";
|
||||||
|
import GameEvent from "Frontend/generated/org/gameyfin/app/games/dto/GameEvent";
|
||||||
|
|
||||||
type GameState = {
|
type GameState = {
|
||||||
subscription?: Subscription<GameEvent[]>;
|
subscription?: Subscription<GameEvent[]>;
|
||||||
@@ -116,7 +116,7 @@ export const gameState = proxy<GameState>({
|
|||||||
|
|
||||||
/** Subscribe to and process state updates from backend **/
|
/** Subscribe to and process state updates from backend **/
|
||||||
export async function initializeGameState() {
|
export async function initializeGameState() {
|
||||||
if (gameState.isLoaded) return gameState;
|
if (gameState.isLoaded) return;
|
||||||
|
|
||||||
// Fetch initial library list
|
// Fetch initial library list
|
||||||
const initialEntries = await GameEndpoint.getAll();
|
const initialEntries = await GameEndpoint.getAll();
|
||||||
@@ -140,6 +140,4 @@ export async function initializeGameState() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
return gameState;
|
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ export const libraryState = proxy<LibraryState>({
|
|||||||
|
|
||||||
/** Subscribe to and process state updates from backend **/
|
/** Subscribe to and process state updates from backend **/
|
||||||
export async function initializeLibraryState() {
|
export async function initializeLibraryState() {
|
||||||
if (libraryState.isLoaded) return libraryState;
|
if (libraryState.isLoaded) return;
|
||||||
|
|
||||||
// Fetch initial library list
|
// Fetch initial library list
|
||||||
const initialEntries = await LibraryEndpoint.getAll();
|
const initialEntries = await LibraryEndpoint.getAll();
|
||||||
@@ -57,6 +57,4 @@ export async function initializeLibraryState() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
return libraryState;
|
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,8 @@ import ImageCarousel from "Frontend/components/general/covers/ImageCarousel";
|
|||||||
import {Accordion, AccordionItem, addToast, Button, Chip, Link, Tooltip, useDisclosure} from "@heroui/react";
|
import {Accordion, AccordionItem, addToast, Button, Chip, Link, Tooltip, useDisclosure} from "@heroui/react";
|
||||||
import {humanFileSize, isAdmin, toTitleCase} from "Frontend/util/utils";
|
import {humanFileSize, isAdmin, toTitleCase} from "Frontend/util/utils";
|
||||||
import {DownloadEndpoint} from "Frontend/endpoints/endpoints";
|
import {DownloadEndpoint} from "Frontend/endpoints/endpoints";
|
||||||
import {gameState, initializeGameState} from "Frontend/state/GameState";
|
import {gameState} from "Frontend/state/GameState";
|
||||||
import {useSnapshot} from "valtio/react";
|
import {useSnapshot} from "valtio/react";
|
||||||
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
|
||||||
import {CheckCircle, Info, MagnifyingGlass, Pencil, Trash, TriangleDashed} from "@phosphor-icons/react";
|
import {CheckCircle, Info, MagnifyingGlass, Pencil, Trash, TriangleDashed} from "@phosphor-icons/react";
|
||||||
import {useAuth} from "Frontend/util/auth";
|
import {useAuth} from "Frontend/util/auth";
|
||||||
import MatchGameModal from "Frontend/components/general/modals/MatchGameModal";
|
import MatchGameModal from "Frontend/components/general/modals/MatchGameModal";
|
||||||
@@ -17,6 +16,7 @@ import EditGameMetadataModal from "Frontend/components/general/modals/EditGameMe
|
|||||||
import GameUpdateDto from "Frontend/generated/org/gameyfin/app/games/dto/GameUpdateDto";
|
import GameUpdateDto from "Frontend/generated/org/gameyfin/app/games/dto/GameUpdateDto";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import remarkBreaks from "remark-breaks";
|
import remarkBreaks from "remark-breaks";
|
||||||
|
import {GameAdminDto} from "Frontend/dtos/GameDtos";
|
||||||
|
|
||||||
export default function GameView() {
|
export default function GameView() {
|
||||||
const {gameId} = useParams();
|
const {gameId} = useParams();
|
||||||
@@ -28,7 +28,7 @@ export default function GameView() {
|
|||||||
const matchGameModal = useDisclosure();
|
const matchGameModal = useDisclosure();
|
||||||
|
|
||||||
const state = useSnapshot(gameState);
|
const state = useSnapshot(gameState);
|
||||||
const game = gameId ? state.state[parseInt(gameId)] as GameDto : undefined;
|
const game = gameId ? state.state[parseInt(gameId)] as GameAdminDto : undefined;
|
||||||
|
|
||||||
const [downloadOptions, setDownloadOptions] = useState<Record<string, ComboButtonOption>>();
|
const [downloadOptions, setDownloadOptions] = useState<Record<string, ComboButtonOption>>();
|
||||||
|
|
||||||
@@ -49,13 +49,11 @@ export default function GameView() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeGameState().then((state) => {
|
if (state.isLoaded && (!gameId || !state.state[parseInt(gameId)])) {
|
||||||
if (!gameId || !state.state[parseInt(gameId)]) {
|
navigate("/", {replace: true});
|
||||||
navigate("/", {replace: true});
|
}
|
||||||
}
|
document.title = game ? game.title : "Gameyfin";
|
||||||
document.title = game ? game.title : "Gameyfin";
|
}, [gameId, state]);
|
||||||
});
|
|
||||||
}, [gameId]);
|
|
||||||
|
|
||||||
async function toggleMatchConfirmed() {
|
async function toggleMatchConfirmed() {
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import {ArrowLeft} from "@phosphor-icons/react";
|
|||||||
import LibraryManagementDetails from "Frontend/components/general/library/LibraryManagementDetails";
|
import LibraryManagementDetails from "Frontend/components/general/library/LibraryManagementDetails";
|
||||||
import LibraryManagementGames from "Frontend/components/general/library/LibraryManagementGames";
|
import LibraryManagementGames from "Frontend/components/general/library/LibraryManagementGames";
|
||||||
import {useSnapshot} from "valtio/react";
|
import {useSnapshot} from "valtio/react";
|
||||||
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
import {libraryState} from "Frontend/state/LibraryState";
|
||||||
import LibraryManagementUnmatchedPaths from "Frontend/components/general/library/LibraryManagementUnmatchedPaths";
|
import LibraryManagementUnmatchedPaths from "Frontend/components/general/library/LibraryManagementUnmatchedPaths";
|
||||||
|
import LibraryAdminDto from "Frontend/generated/org/gameyfin/app/libraries/dto/LibraryAdminDto";
|
||||||
|
|
||||||
|
|
||||||
export default function LibraryManagementView() {
|
export default function LibraryManagementView() {
|
||||||
@@ -17,12 +18,10 @@ export default function LibraryManagementView() {
|
|||||||
const state = useSnapshot(libraryState);
|
const state = useSnapshot(libraryState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeLibraryState().then((state) => {
|
if (state.isLoaded && (!libraryId || !state.state[parseInt(libraryId)])) {
|
||||||
if (!libraryId || !state.state[parseInt(libraryId)]) {
|
navigate("/administration/libraries");
|
||||||
navigate("/administration/libraries");
|
}
|
||||||
}
|
}, [state, libraryId]);
|
||||||
});
|
|
||||||
}, [libraryId]);
|
|
||||||
|
|
||||||
return libraryId && state.state[parseInt(libraryId)] && <div className="flex flex-col gap-4">
|
return libraryId && state.state[parseInt(libraryId)] && <div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-row gap-4 items-center">
|
<div className="flex flex-row gap-4 items-center">
|
||||||
@@ -31,23 +30,18 @@ export default function LibraryManagementView() {
|
|||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-2xl font-bold">Manage library</h1>
|
<h1 className="text-2xl font-bold">Manage library</h1>
|
||||||
</div>
|
</div>
|
||||||
{/* @ts-ignore */}
|
<LibraryHeader library={state.state[parseInt(libraryId)] as LibraryAdminDto} className="h-32"/>
|
||||||
<LibraryHeader library={state.state[libraryId]} className="h-32"/>
|
|
||||||
{/* @ts-ignore */}
|
|
||||||
<Tabs color="primary" fullWidth
|
<Tabs color="primary" fullWidth
|
||||||
selectedKey={hash.length > 0 ? hash : "#details"}
|
selectedKey={hash.length > 0 ? hash : "#details"}
|
||||||
onSelectionChange={(newKey) => navigate(newKey.toString(), {replace: true})}>
|
onSelectionChange={(newKey) => navigate(newKey.toString(), {replace: true})}>
|
||||||
<Tab key="#details" title="Details">
|
<Tab key="#details" title="Details">
|
||||||
{/* @ts-ignore */}
|
<LibraryManagementDetails library={state.state[parseInt(libraryId)] as LibraryAdminDto}/>
|
||||||
<LibraryManagementDetails library={state.state[libraryId]}/>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key="#games" title="Games">
|
<Tab key="#games" title="Games">
|
||||||
{/* @ts-ignore */}
|
<LibraryManagementGames library={state.state[parseInt(libraryId)] as LibraryAdminDto}/>
|
||||||
<LibraryManagementGames library={state.state[libraryId]}/>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key="#unmatched-paths" title="Unmatched paths">
|
<Tab key="#unmatched-paths" title="Unmatched paths">
|
||||||
{/* @ts-ignore */}
|
<LibraryManagementUnmatchedPaths library={state.state[parseInt(libraryId)] as LibraryAdminDto}/>
|
||||||
<LibraryManagementUnmatchedPaths library={state.state[libraryId]}/>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useSnapshot} from "valtio/react";
|
import {useSnapshot} from "valtio/react";
|
||||||
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
import {libraryState} from "Frontend/state/LibraryState";
|
||||||
import {gameState} from "Frontend/state/GameState";
|
import {gameState} from "Frontend/state/GameState";
|
||||||
import React, {useEffect} from "react";
|
import React, {useEffect} from "react";
|
||||||
import {useNavigate, useParams} from "react-router";
|
import {useNavigate, useParams} from "react-router";
|
||||||
@@ -13,13 +13,11 @@ export default function LibraryView() {
|
|||||||
const games = (libraryId ? useSnapshot(gameState).gamesByLibraryId[parseInt(libraryId!!)] || [] : []) as GameDto[];
|
const games = (libraryId ? useSnapshot(gameState).gamesByLibraryId[parseInt(libraryId!!)] || [] : []) as GameDto[];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeLibraryState().then((state) => {
|
if (libraries.isLoaded && (!libraryId || !libraries.state[parseInt(libraryId)])) {
|
||||||
if (!libraryId || !state.state[parseInt(libraryId)]) {
|
navigate("/", {replace: true});
|
||||||
navigate("/", {replace: true});
|
}
|
||||||
}
|
document.title = libraries.state[parseInt(libraryId!!)]?.name || "Gameyfin";
|
||||||
document.title = state.state[parseInt(libraryId!!)]?.name || "Gameyfin";
|
}, [libraryId, libraries]);
|
||||||
});
|
|
||||||
}, [libraryId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import org.gameyfin.app.config.dto.ConfigEntryDto
|
|||||||
import org.gameyfin.app.config.dto.ConfigUpdateDto
|
import org.gameyfin.app.config.dto.ConfigUpdateDto
|
||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
import org.gameyfin.app.core.annotations.DynamicPublicAccess
|
import org.gameyfin.app.core.annotations.DynamicPublicAccess
|
||||||
import org.gameyfin.app.users.UserService
|
import org.gameyfin.app.core.security.isCurrentUserAdmin
|
||||||
import org.gameyfin.app.users.util.isAdmin
|
|
||||||
import org.springframework.scheduling.support.CronExpression
|
import org.springframework.scheduling.support.CronExpression
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
@@ -18,7 +17,6 @@ import reactor.core.publisher.Flux
|
|||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
class ConfigEndpoint(
|
class ConfigEndpoint(
|
||||||
private val configService: ConfigService,
|
private val configService: ConfigService,
|
||||||
private val userService: UserService,
|
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
val log = KotlinLogging.logger { }
|
val log = KotlinLogging.logger { }
|
||||||
@@ -28,8 +26,7 @@ class ConfigEndpoint(
|
|||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun subscribe(): Flux<List<ConfigUpdateDto>> {
|
fun subscribe(): Flux<List<ConfigUpdateDto>> {
|
||||||
val user = userService.getCurrentUser()
|
return if (isCurrentUserAdmin()) ConfigService.subscribe()
|
||||||
return if (user.isAdmin()) ConfigService.subscribe()
|
|
||||||
else Flux.empty()
|
else Flux.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class ConfigService(
|
|||||||
*/
|
*/
|
||||||
fun <T : Serializable> get(configProperty: ConfigProperties<T>): T? {
|
fun <T : Serializable> get(configProperty: ConfigProperties<T>): T? {
|
||||||
|
|
||||||
log.debug { "Getting config value '${configProperty.key}'" }
|
log.trace { "Getting config value '${configProperty.key}'" }
|
||||||
|
|
||||||
val appConfig = appConfigRepository.findByIdOrNull(configProperty.key)
|
val appConfig = appConfigRepository.findByIdOrNull(configProperty.key)
|
||||||
return if (appConfig != null) {
|
return if (appConfig != null) {
|
||||||
@@ -65,7 +65,7 @@ class ConfigService(
|
|||||||
*/
|
*/
|
||||||
fun get(key: String): Serializable? {
|
fun get(key: String): Serializable? {
|
||||||
|
|
||||||
log.debug { "Getting config value '$key'" }
|
log.trace { "Getting config value '$key'" }
|
||||||
|
|
||||||
val configProperty = findConfigProperty(key)
|
val configProperty = findConfigProperty(key)
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ import com.vaadin.hilla.Endpoint
|
|||||||
import jakarta.annotation.security.PermitAll
|
import jakarta.annotation.security.PermitAll
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
import org.gameyfin.app.users.UserService
|
import org.gameyfin.app.core.security.isCurrentUserAdmin
|
||||||
import org.gameyfin.app.users.util.isAdmin
|
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
@Endpoint
|
@Endpoint
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
class LogEndpoint(
|
class LogEndpoint(
|
||||||
private val logService: LogService,
|
private val logService: LogService,
|
||||||
private val userService: UserService,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun reloadLogConfig() {
|
fun reloadLogConfig() {
|
||||||
@@ -21,8 +19,7 @@ class LogEndpoint(
|
|||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun getApplicationLogs(): Flux<String> {
|
fun getApplicationLogs(): Flux<String> {
|
||||||
val user = userService.getCurrentUser()
|
return if (isCurrentUserAdmin()) logService.streamLogs()
|
||||||
return if (user.isAdmin()) logService.streamLogs()
|
|
||||||
else Flux.empty()
|
else Flux.empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,7 @@ import jakarta.annotation.security.PermitAll
|
|||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
import org.gameyfin.app.core.plugins.dto.PluginUpdateDto
|
import org.gameyfin.app.core.plugins.dto.PluginUpdateDto
|
||||||
import org.gameyfin.app.users.UserService
|
import org.gameyfin.app.core.security.isCurrentUserAdmin
|
||||||
import org.gameyfin.app.users.util.isAdmin
|
|
||||||
import org.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
import org.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
@@ -14,13 +13,11 @@ import reactor.core.publisher.Flux
|
|||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
class PluginEndpoint(
|
class PluginEndpoint(
|
||||||
private val pluginService: PluginService,
|
private val pluginService: PluginService,
|
||||||
private val userService: UserService,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun subscribe(): Flux<List<PluginUpdateDto>> {
|
fun subscribe(): Flux<List<PluginUpdateDto>> {
|
||||||
val user = userService.getCurrentUser()
|
return if (isCurrentUserAdmin()) PluginService.subscribe()
|
||||||
return if (user.isAdmin()) PluginService.subscribe()
|
|
||||||
else Flux.empty()
|
else Flux.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.gameyfin.app.core.security
|
||||||
|
|
||||||
|
import org.gameyfin.app.core.Role
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
|
||||||
|
fun isCurrentUserAdmin(): Boolean {
|
||||||
|
return SecurityContextHolder.getContext().authentication?.authorities?.any { it.authority == Role.Names.ADMIN || it.authority == Role.Names.SUPERADMIN }
|
||||||
|
?: false
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import com.vaadin.hilla.Endpoint
|
|||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
import org.gameyfin.app.core.annotations.DynamicPublicAccess
|
import org.gameyfin.app.core.annotations.DynamicPublicAccess
|
||||||
|
import org.gameyfin.app.core.security.isCurrentUserAdmin
|
||||||
import org.gameyfin.app.games.dto.*
|
import org.gameyfin.app.games.dto.*
|
||||||
import org.gameyfin.app.libraries.LibraryCoreService
|
import org.gameyfin.app.libraries.LibraryCoreService
|
||||||
import org.gameyfin.app.libraries.LibraryService
|
import org.gameyfin.app.libraries.LibraryService
|
||||||
@@ -19,8 +20,12 @@ class GameEndpoint(
|
|||||||
private val libraryService: LibraryService,
|
private val libraryService: LibraryService,
|
||||||
private val libraryCoreService: LibraryCoreService
|
private val libraryCoreService: LibraryCoreService
|
||||||
) {
|
) {
|
||||||
fun subscribe(): Flux<List<GameEvent>> {
|
fun subscribe(): Flux<out List<GameEvent>> {
|
||||||
return GameService.subscribe()
|
return if (isCurrentUserAdmin()) {
|
||||||
|
GameService.subscribeAdmin()
|
||||||
|
} else {
|
||||||
|
GameService.subscribeUser()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAll(): List<GameDto> = gameService.getAll()
|
fun getAll(): List<GameDto> = gameService.getAll()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import org.gameyfin.app.core.plugins.management.PluginManagementEntry
|
|||||||
import org.gameyfin.app.core.replaceRomanNumerals
|
import org.gameyfin.app.core.replaceRomanNumerals
|
||||||
import org.gameyfin.app.games.dto.*
|
import org.gameyfin.app.games.dto.*
|
||||||
import org.gameyfin.app.games.entities.*
|
import org.gameyfin.app.games.entities.*
|
||||||
import org.gameyfin.app.games.entities.GameMetadata
|
import org.gameyfin.app.games.extensions.toDtos
|
||||||
import org.gameyfin.app.games.repositories.GameRepository
|
import org.gameyfin.app.games.repositories.GameRepository
|
||||||
import org.gameyfin.app.libraries.Library
|
import org.gameyfin.app.libraries.Library
|
||||||
import org.gameyfin.app.media.ImageService
|
import org.gameyfin.app.media.ImageService
|
||||||
@@ -57,22 +57,39 @@ class GameService(
|
|||||||
private val log = KotlinLogging.logger {}
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
/* Websockets */
|
/* Websockets */
|
||||||
private val gameEvents = Sinks.many().multicast().onBackpressureBuffer<GameEvent>(1024, false)
|
private val gameUserEvents = Sinks.many().multicast().onBackpressureBuffer<GameUserEvent>(1024, false)
|
||||||
|
private val gameAdminEvents = Sinks.many().multicast().onBackpressureBuffer<GameAdminEvent>(1024, false)
|
||||||
|
|
||||||
fun subscribe(): Flux<List<GameEvent>> {
|
fun subscribeUser(): Flux<List<GameUserEvent>> {
|
||||||
log.debug { "New subscription for gameUpdates" }
|
log.debug { "New user subscription for gameUpdates" }
|
||||||
return gameEvents.asFlux()
|
return gameUserEvents.asFlux()
|
||||||
.buffer(100.milliseconds.toJavaDuration())
|
.buffer(100.milliseconds.toJavaDuration())
|
||||||
.doOnSubscribe {
|
.doOnSubscribe {
|
||||||
log.debug { "Subscriber added to gameEvents [${gameEvents.currentSubscriberCount()}]" }
|
log.debug { "Subscriber added to gameUserEvents [${gameUserEvents.currentSubscriberCount()}]" }
|
||||||
}
|
}
|
||||||
.doFinally {
|
.doFinally {
|
||||||
log.debug { "Subscriber removed from gameEvents with signal type $it [${gameEvents.currentSubscriberCount()}]" }
|
log.debug { "Subscriber removed from gameUserEvents with signal type $it [${gameUserEvents.currentSubscriberCount()}]" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emit(event: GameEvent) {
|
fun subscribeAdmin(): Flux<List<GameAdminEvent>> {
|
||||||
gameEvents.tryEmitNext(event)
|
log.debug { "New admin subscription for gameUpdates" }
|
||||||
|
return gameAdminEvents.asFlux()
|
||||||
|
.buffer(100.milliseconds.toJavaDuration())
|
||||||
|
.doOnSubscribe {
|
||||||
|
log.debug { "Subscriber added to gameAdminEvents [${gameAdminEvents.currentSubscriberCount()}]" }
|
||||||
|
}
|
||||||
|
.doFinally {
|
||||||
|
log.debug { "Subscriber removed from gameAdminEvents with signal type $it [${gameAdminEvents.currentSubscriberCount()}]" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitUser(event: GameUserEvent) {
|
||||||
|
gameUserEvents.tryEmitNext(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitAdmin(event: GameAdminEvent) {
|
||||||
|
gameAdminEvents.tryEmitNext(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val executor = Executors.newVirtualThreadPerTaskExecutor()
|
private val executor = Executors.newVirtualThreadPerTaskExecutor()
|
||||||
@@ -84,7 +101,7 @@ class GameService(
|
|||||||
|
|
||||||
fun getAll(): List<GameDto> {
|
fun getAll(): List<GameDto> {
|
||||||
val entities = gameRepository.findAll()
|
val entities = gameRepository.findAll()
|
||||||
return entities.map { it.toDto() }
|
return entities.toDtos()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -853,74 +870,4 @@ class GameService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun String.normalizeGameTitle(): String = this.alphaNumeric().replaceRomanNumerals()
|
fun String.normalizeGameTitle(): String = this.alphaNumeric().replaceRomanNumerals()
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun Game.toDto(): GameDto {
|
|
||||||
// Helper functions
|
|
||||||
fun toDto(fieldMetadata: GameFieldMetadata): GameFieldMetadataDto {
|
|
||||||
val source = fieldMetadata.source
|
|
||||||
|
|
||||||
return when (source) {
|
|
||||||
is GameFieldPluginSource -> {
|
|
||||||
GameFieldMetadataDto(
|
|
||||||
type = GameFieldMetadataType.PLUGIN,
|
|
||||||
source = source.plugin.pluginId,
|
|
||||||
updatedAt = fieldMetadata.updatedAt!!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is GameFieldUserSource -> {
|
|
||||||
GameFieldMetadataDto(
|
|
||||||
type = GameFieldMetadataType.USER,
|
|
||||||
source = source.user.username,
|
|
||||||
updatedAt = fieldMetadata.updatedAt!!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
GameFieldMetadataDto(
|
|
||||||
type = GameFieldMetadataType.UNKNOWN,
|
|
||||||
source = "unknown source",
|
|
||||||
updatedAt = fieldMetadata.updatedAt!!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toDto(metadata: GameMetadata): GameMetadataDto {
|
|
||||||
return GameMetadataDto(
|
|
||||||
fileSize = metadata.fileSize ?: 0L,
|
|
||||||
downloadCount = metadata.downloadCount,
|
|
||||||
path = metadata.path,
|
|
||||||
fields = metadata.fields.mapValues { toDto(it.value) },
|
|
||||||
originalIds = metadata.originalIds.mapKeys { it.key.pluginId },
|
|
||||||
matchConfirmed = metadata.matchConfirmed
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return GameDto(
|
|
||||||
id = id!!,
|
|
||||||
createdAt = createdAt!!,
|
|
||||||
updatedAt = updatedAt!!,
|
|
||||||
libraryId = this.library.id!!,
|
|
||||||
title = title!!,
|
|
||||||
coverId = this.coverImage?.id,
|
|
||||||
headerId = this.headerImage?.id,
|
|
||||||
comment = this.comment,
|
|
||||||
summary = this.summary,
|
|
||||||
release = this.release?.atZone(ZoneOffset.UTC)?.toLocalDate(),
|
|
||||||
userRating = this.userRating,
|
|
||||||
criticRating = this.criticRating,
|
|
||||||
publishers = this.publishers.map { it.name },
|
|
||||||
developers = this.developers.map { it.name },
|
|
||||||
genres = this.genres.map { it.name },
|
|
||||||
themes = this.themes.map { it.name },
|
|
||||||
keywords = this.keywords.toList(),
|
|
||||||
features = this.features.map { it.name },
|
|
||||||
perspectives = this.perspectives?.map { it.name },
|
|
||||||
imageIds = this.images.mapNotNull { it.id },
|
|
||||||
videoUrls = this.videoUrls.map { it.toString() },
|
|
||||||
metadata = toDto(this.metadata)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -4,28 +4,79 @@ import com.fasterxml.jackson.annotation.JsonInclude
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
sealed interface GameDto {
|
||||||
class GameDto(
|
val id: Long
|
||||||
val id: Long,
|
val createdAt: Instant
|
||||||
val createdAt: Instant,
|
val updatedAt: Instant
|
||||||
val updatedAt: Instant,
|
val libraryId: Long
|
||||||
val libraryId: Long,
|
val title: String
|
||||||
val title: String,
|
val coverId: Long?
|
||||||
val coverId: Long?,
|
val headerId: Long?
|
||||||
val headerId: Long?,
|
val comment: String?
|
||||||
val comment: String?,
|
val summary: String?
|
||||||
val summary: String?,
|
val release: LocalDate?
|
||||||
val release: LocalDate?,
|
val userRating: Int?
|
||||||
val userRating: Int?,
|
val criticRating: Int?
|
||||||
val criticRating: Int?,
|
val publishers: List<String>?
|
||||||
val publishers: List<String>?,
|
val developers: List<String>?
|
||||||
val developers: List<String>?,
|
val genres: List<String>?
|
||||||
val genres: List<String>?,
|
val themes: List<String>?
|
||||||
val themes: List<String>?,
|
val keywords: List<String>?
|
||||||
val keywords: List<String>?,
|
val features: List<String>?
|
||||||
val features: List<String>?,
|
val perspectives: List<String>?
|
||||||
val perspectives: List<String>?,
|
val imageIds: List<Long>?
|
||||||
val imageIds: List<Long>?,
|
val videoUrls: List<String>?
|
||||||
val videoUrls: List<String>?,
|
|
||||||
val metadata: GameMetadataDto
|
val metadata: GameMetadataDto
|
||||||
)
|
}
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
data class GameUserDto(
|
||||||
|
override val id: Long,
|
||||||
|
override val createdAt: Instant,
|
||||||
|
override val updatedAt: Instant,
|
||||||
|
override val libraryId: Long,
|
||||||
|
override val title: String,
|
||||||
|
override val coverId: Long?,
|
||||||
|
override val headerId: Long?,
|
||||||
|
override val comment: String?,
|
||||||
|
override val summary: String?,
|
||||||
|
override val release: LocalDate?,
|
||||||
|
override val userRating: Int?,
|
||||||
|
override val criticRating: Int?,
|
||||||
|
override val publishers: List<String>?,
|
||||||
|
override val developers: List<String>?,
|
||||||
|
override val genres: List<String>?,
|
||||||
|
override val themes: List<String>?,
|
||||||
|
override val keywords: List<String>?,
|
||||||
|
override val features: List<String>?,
|
||||||
|
override val perspectives: List<String>?,
|
||||||
|
override val imageIds: List<Long>?,
|
||||||
|
override val videoUrls: List<String>?,
|
||||||
|
override val metadata: GameMetadataUserDto
|
||||||
|
) : GameDto
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
data class GameAdminDto(
|
||||||
|
override val id: Long,
|
||||||
|
override val createdAt: Instant,
|
||||||
|
override val updatedAt: Instant,
|
||||||
|
override val libraryId: Long,
|
||||||
|
override val title: String,
|
||||||
|
override val coverId: Long?,
|
||||||
|
override val headerId: Long?,
|
||||||
|
override val comment: String?,
|
||||||
|
override val summary: String?,
|
||||||
|
override val release: LocalDate?,
|
||||||
|
override val userRating: Int?,
|
||||||
|
override val criticRating: Int?,
|
||||||
|
override val publishers: List<String>?,
|
||||||
|
override val developers: List<String>?,
|
||||||
|
override val genres: List<String>?,
|
||||||
|
override val themes: List<String>?,
|
||||||
|
override val keywords: List<String>?,
|
||||||
|
override val features: List<String>?,
|
||||||
|
override val perspectives: List<String>?,
|
||||||
|
override val imageIds: List<Long>?,
|
||||||
|
override val videoUrls: List<String>?,
|
||||||
|
override val metadata: GameMetadataAdminDto
|
||||||
|
) : GameDto
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.gameyfin.app.games.dto
|
|
||||||
|
|
||||||
sealed class GameEvent {
|
|
||||||
abstract val type: String
|
|
||||||
|
|
||||||
data class Created(val game: GameDto, override val type: String = "created") : GameEvent()
|
|
||||||
data class Updated(val game: GameDto, override val type: String = "updated") : GameEvent()
|
|
||||||
data class Deleted(val gameId: Long, override val type: String = "deleted") : GameEvent()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.gameyfin.app.games.dto
|
||||||
|
|
||||||
|
|
||||||
|
sealed interface GameEvent {
|
||||||
|
val type: String
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class GameUserEvent : GameEvent {
|
||||||
|
data class Created(val game: GameUserDto, override val type: String = "created") : GameUserEvent()
|
||||||
|
data class Updated(val game: GameUserDto, override val type: String = "updated") : GameUserEvent()
|
||||||
|
data class Deleted(val gameId: Long, override val type: String = "deleted") : GameUserEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class GameAdminEvent : GameEvent {
|
||||||
|
data class Created(val game: GameAdminDto, override val type: String = "created") : GameAdminEvent()
|
||||||
|
data class Updated(val game: GameAdminDto, override val type: String = "updated") : GameAdminEvent()
|
||||||
|
data class Deleted(val gameId: Long, override val type: String = "deleted") : GameAdminEvent()
|
||||||
|
}
|
||||||
@@ -2,12 +2,21 @@ package org.gameyfin.app.games.dto
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
|
|
||||||
|
interface GameMetadataDto {
|
||||||
|
val fileSize: Long
|
||||||
|
}
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
class GameMetadataDto(
|
data class GameMetadataUserDto(
|
||||||
|
override val fileSize: Long
|
||||||
|
) : GameMetadataDto
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
data class GameMetadataAdminDto(
|
||||||
val path: String?,
|
val path: String?,
|
||||||
val fileSize: Long,
|
override val fileSize: Long,
|
||||||
val fields: Map<String, GameFieldMetadataDto>?,
|
val fields: Map<String, GameFieldMetadataDto>?,
|
||||||
val originalIds: Map<String, String>?,
|
val originalIds: Map<String, String>?,
|
||||||
val downloadCount: Int,
|
val downloadCount: Int,
|
||||||
val matchConfirmed: Boolean
|
val matchConfirmed: Boolean
|
||||||
)
|
) : GameMetadataDto
|
||||||
|
|||||||
@@ -4,25 +4,27 @@ import jakarta.persistence.PostPersist
|
|||||||
import jakarta.persistence.PostRemove
|
import jakarta.persistence.PostRemove
|
||||||
import jakarta.persistence.PostUpdate
|
import jakarta.persistence.PostUpdate
|
||||||
import org.gameyfin.app.games.GameService
|
import org.gameyfin.app.games.GameService
|
||||||
import org.gameyfin.app.games.dto.GameEvent
|
import org.gameyfin.app.games.dto.GameAdminEvent
|
||||||
import org.gameyfin.app.games.toDto
|
import org.gameyfin.app.games.dto.GameUserEvent
|
||||||
|
import org.gameyfin.app.games.extensions.toAdminDto
|
||||||
|
import org.gameyfin.app.games.extensions.toUserDto
|
||||||
|
|
||||||
class GameEntityListener {
|
class GameEntityListener {
|
||||||
@PostPersist
|
@PostPersist
|
||||||
fun created(game: Game) {
|
fun created(game: Game) {
|
||||||
val event = GameEvent.Created(game.toDto())
|
GameService.Companion.emitUser(GameUserEvent.Created(game.toUserDto()))
|
||||||
GameService.Companion.emit(event)
|
GameService.Companion.emitAdmin(GameAdminEvent.Created(game.toAdminDto()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostUpdate
|
@PostUpdate
|
||||||
fun updated(game: Game) {
|
fun updated(game: Game) {
|
||||||
val event = GameEvent.Updated(game.toDto())
|
GameService.Companion.emitUser(GameUserEvent.Updated(game.toUserDto()))
|
||||||
GameService.Companion.emit(event)
|
GameService.Companion.emitAdmin(GameAdminEvent.Updated(game.toAdminDto()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostRemove
|
@PostRemove
|
||||||
fun deleted(game: Game) {
|
fun deleted(game: Game) {
|
||||||
val event = GameEvent.Deleted(game.id!!)
|
GameService.Companion.emitUser(GameUserEvent.Deleted(game.id!!))
|
||||||
GameService.Companion.emit(event)
|
GameService.Companion.emitAdmin(GameAdminEvent.Deleted(game.id!!))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,31 @@
|
|||||||
package org.gameyfin.app.games.entities
|
package org.gameyfin.app.games.entities
|
||||||
|
|
||||||
import org.gameyfin.app.libraries.dto.LibraryEvent
|
|
||||||
import jakarta.persistence.PostPersist
|
import jakarta.persistence.PostPersist
|
||||||
import jakarta.persistence.PostRemove
|
import jakarta.persistence.PostRemove
|
||||||
import jakarta.persistence.PostUpdate
|
import jakarta.persistence.PostUpdate
|
||||||
import org.gameyfin.app.libraries.Library
|
import org.gameyfin.app.libraries.Library
|
||||||
import org.gameyfin.app.libraries.LibraryService
|
import org.gameyfin.app.libraries.LibraryService
|
||||||
import org.gameyfin.app.libraries.toDto
|
import org.gameyfin.app.libraries.dto.LibraryAdminEvent
|
||||||
|
import org.gameyfin.app.libraries.dto.LibraryUserEvent
|
||||||
|
import org.gameyfin.app.libraries.extensions.toAdminDto
|
||||||
|
import org.gameyfin.app.libraries.extensions.toUserDto
|
||||||
|
|
||||||
class LibraryEntityListener {
|
class LibraryEntityListener {
|
||||||
@PostPersist
|
@PostPersist
|
||||||
fun created(library: Library) {
|
fun created(library: Library) {
|
||||||
val event = LibraryEvent.Created(library.toDto())
|
LibraryService.emitUser(LibraryUserEvent.Created(library.toUserDto()))
|
||||||
LibraryService.Companion.emit(event)
|
LibraryService.emitAdmin(LibraryAdminEvent.Created(library.toAdminDto()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostUpdate
|
@PostUpdate
|
||||||
fun updated(library: Library) {
|
fun updated(library: Library) {
|
||||||
val event = LibraryEvent.Updated(library.toDto())
|
LibraryService.emitUser(LibraryUserEvent.Updated(library.toUserDto()))
|
||||||
LibraryService.Companion.emit(event)
|
LibraryService.emitAdmin(LibraryAdminEvent.Updated(library.toAdminDto()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostRemove
|
@PostRemove
|
||||||
fun deleted(library: Library) {
|
fun deleted(library: Library) {
|
||||||
val event = LibraryEvent.Deleted(library.id!!)
|
LibraryService.emitUser(LibraryUserEvent.Deleted(library.id!!))
|
||||||
LibraryService.Companion.emit(event)
|
LibraryService.emitAdmin(LibraryAdminEvent.Deleted(library.id!!))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package org.gameyfin.app.games.extensions
|
||||||
|
|
||||||
|
import org.gameyfin.app.core.security.isCurrentUserAdmin
|
||||||
|
import org.gameyfin.app.games.dto.*
|
||||||
|
import org.gameyfin.app.games.entities.*
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
|
|
||||||
|
fun Game.toDto(): GameDto {
|
||||||
|
return if (isCurrentUserAdmin()) {
|
||||||
|
this.toAdminDto()
|
||||||
|
} else {
|
||||||
|
this.toUserDto()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Collection<Game>.toDtos(): List<GameDto> {
|
||||||
|
return if (isCurrentUserAdmin()) {
|
||||||
|
this.map { it.toAdminDto() }
|
||||||
|
} else {
|
||||||
|
this.map { it.toUserDto() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Game.toAdminDto(): GameAdminDto {
|
||||||
|
return GameAdminDto(
|
||||||
|
id = id!!,
|
||||||
|
createdAt = createdAt!!,
|
||||||
|
updatedAt = updatedAt!!,
|
||||||
|
libraryId = this.library.id!!,
|
||||||
|
title = title!!,
|
||||||
|
coverId = this.coverImage?.id,
|
||||||
|
headerId = this.headerImage?.id,
|
||||||
|
comment = this.comment,
|
||||||
|
summary = this.summary,
|
||||||
|
release = this.release?.atZone(ZoneOffset.UTC)?.toLocalDate(),
|
||||||
|
userRating = this.userRating,
|
||||||
|
criticRating = this.criticRating,
|
||||||
|
publishers = this.publishers.map { it.name },
|
||||||
|
developers = this.developers.map { it.name },
|
||||||
|
genres = this.genres.map { it.name },
|
||||||
|
themes = this.themes.map { it.name },
|
||||||
|
keywords = this.keywords.toList(),
|
||||||
|
features = this.features.map { it.name },
|
||||||
|
perspectives = this.perspectives.map { it.name },
|
||||||
|
imageIds = this.images.mapNotNull { it.id },
|
||||||
|
videoUrls = this.videoUrls.map { it.toString() },
|
||||||
|
metadata = this.metadata.toAdminDto()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Game.toUserDto(): GameUserDto {
|
||||||
|
return GameUserDto(
|
||||||
|
id = id!!,
|
||||||
|
createdAt = createdAt!!,
|
||||||
|
updatedAt = updatedAt!!,
|
||||||
|
libraryId = this.library.id!!,
|
||||||
|
title = title!!,
|
||||||
|
coverId = this.coverImage?.id,
|
||||||
|
headerId = this.headerImage?.id,
|
||||||
|
comment = this.comment,
|
||||||
|
summary = this.summary,
|
||||||
|
release = this.release?.atZone(ZoneOffset.UTC)?.toLocalDate(),
|
||||||
|
userRating = this.userRating,
|
||||||
|
criticRating = this.criticRating,
|
||||||
|
publishers = this.publishers.map { it.name },
|
||||||
|
developers = this.developers.map { it.name },
|
||||||
|
genres = this.genres.map { it.name },
|
||||||
|
themes = this.themes.map { it.name },
|
||||||
|
keywords = this.keywords.toList(),
|
||||||
|
features = this.features.map { it.name },
|
||||||
|
perspectives = this.perspectives.map { it.name },
|
||||||
|
imageIds = this.images.mapNotNull { it.id },
|
||||||
|
videoUrls = this.videoUrls.map { it.toString() },
|
||||||
|
metadata = this.metadata.toUserDto()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GameMetadata.toAdminDto(): GameMetadataAdminDto {
|
||||||
|
return GameMetadataAdminDto(
|
||||||
|
fileSize = this.fileSize ?: 0L,
|
||||||
|
downloadCount = this.downloadCount,
|
||||||
|
path = this.path,
|
||||||
|
fields = this.fields.mapValues { it.value.toDto() },
|
||||||
|
originalIds = this.originalIds.mapKeys { it.key.pluginId },
|
||||||
|
matchConfirmed = this.matchConfirmed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GameMetadata.toUserDto(): GameMetadataUserDto {
|
||||||
|
return GameMetadataUserDto(
|
||||||
|
fileSize = this.fileSize ?: 0L
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GameFieldMetadata.toDto(): GameFieldMetadataDto {
|
||||||
|
val source = this.source
|
||||||
|
|
||||||
|
return when (source) {
|
||||||
|
is GameFieldPluginSource -> {
|
||||||
|
GameFieldMetadataDto(
|
||||||
|
type = GameFieldMetadataType.PLUGIN,
|
||||||
|
source = source.plugin.pluginId,
|
||||||
|
updatedAt = this.updatedAt!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is GameFieldUserSource -> {
|
||||||
|
GameFieldMetadataDto(
|
||||||
|
type = GameFieldMetadataType.USER,
|
||||||
|
source = source.user.username,
|
||||||
|
updatedAt = this.updatedAt!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
GameFieldMetadataDto(
|
||||||
|
type = GameFieldMetadataType.UNKNOWN,
|
||||||
|
source = "unknown source",
|
||||||
|
updatedAt = this.updatedAt!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,6 @@ package org.gameyfin.app.libraries
|
|||||||
|
|
||||||
import org.gameyfin.app.games.GameService
|
import org.gameyfin.app.games.GameService
|
||||||
import org.gameyfin.app.games.entities.Game
|
import org.gameyfin.app.games.entities.Game
|
||||||
import org.gameyfin.app.libraries.dto.DirectoryMappingDto
|
|
||||||
import org.gameyfin.app.libraries.dto.LibraryDto
|
|
||||||
import org.gameyfin.app.libraries.dto.LibraryStatsDto
|
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@@ -54,36 +50,4 @@ class LibraryCoreService(
|
|||||||
library.updatedAt = Instant.now() // Force the EntityListener to trigger an update and update the timestamp
|
library.updatedAt = Instant.now() // Force the EntityListener to trigger an update and update the timestamp
|
||||||
libraryRepository.save(library)
|
libraryRepository.save(library)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a LibraryDto to a Library entity.
|
|
||||||
*
|
|
||||||
* @param library: The LibraryDto to convert.
|
|
||||||
* @return The converted Library entity.
|
|
||||||
*/
|
|
||||||
fun toEntity(library: LibraryDto): Library {
|
|
||||||
return libraryRepository.findByIdOrNull(library.id) ?: Library(
|
|
||||||
name = library.name,
|
|
||||||
directories = library.directories.distinctBy { it.internalPath }.map {
|
|
||||||
DirectoryMapping(internalPath = it.internalPath, externalPath = it.externalPath)
|
|
||||||
}.toMutableList(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Library.toDto(): LibraryDto {
|
|
||||||
val statsDto = LibraryStatsDto(
|
|
||||||
gamesCount = this.games.size,
|
|
||||||
downloadedGamesCount = this.games.sumOf { it.metadata.downloadCount }
|
|
||||||
)
|
|
||||||
|
|
||||||
return LibraryDto(
|
|
||||||
id = this.id!!,
|
|
||||||
name = this.name,
|
|
||||||
directories = this.directories.map { DirectoryMappingDto(it.internalPath, it.externalPath) },
|
|
||||||
games = this.games.mapNotNull { it.id },
|
|
||||||
stats = statsDto,
|
|
||||||
unmatchedPaths = this.unmatchedPaths
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -5,13 +5,12 @@ import com.vaadin.hilla.Endpoint
|
|||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
import org.gameyfin.app.core.annotations.DynamicPublicAccess
|
import org.gameyfin.app.core.annotations.DynamicPublicAccess
|
||||||
import org.gameyfin.app.libraries.dto.LibraryDto
|
import org.gameyfin.app.core.security.isCurrentUserAdmin
|
||||||
|
import org.gameyfin.app.libraries.dto.LibraryAdminDto
|
||||||
import org.gameyfin.app.libraries.dto.LibraryEvent
|
import org.gameyfin.app.libraries.dto.LibraryEvent
|
||||||
import org.gameyfin.app.libraries.dto.LibraryScanProgress
|
import org.gameyfin.app.libraries.dto.LibraryScanProgress
|
||||||
import org.gameyfin.app.libraries.dto.LibraryUpdateDto
|
import org.gameyfin.app.libraries.dto.LibraryUpdateDto
|
||||||
import org.gameyfin.app.libraries.enums.ScanType
|
import org.gameyfin.app.libraries.enums.ScanType
|
||||||
import org.gameyfin.app.users.UserService
|
|
||||||
import org.gameyfin.app.users.util.isAdmin
|
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
@Endpoint
|
@Endpoint
|
||||||
@@ -19,27 +18,29 @@ import reactor.core.publisher.Flux
|
|||||||
@AnonymousAllowed
|
@AnonymousAllowed
|
||||||
class LibraryEndpoint(
|
class LibraryEndpoint(
|
||||||
private val libraryService: LibraryService,
|
private val libraryService: LibraryService,
|
||||||
private val userService: UserService,
|
|
||||||
private val libraryScanService: LibraryScanService,
|
private val libraryScanService: LibraryScanService,
|
||||||
) {
|
) {
|
||||||
fun subscribeToLibraryEvents(): Flux<List<LibraryEvent>> {
|
fun subscribeToLibraryEvents(): Flux<out List<LibraryEvent>> {
|
||||||
return LibraryService.subscribeToLibraryEvents()
|
return if (isCurrentUserAdmin()) {
|
||||||
|
LibraryService.subscribeAdmin()
|
||||||
|
} else {
|
||||||
|
LibraryService.subscribeUser()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAll() = libraryService.getAll()
|
fun getAll() = libraryService.getAll()
|
||||||
|
|
||||||
fun subscribeToScanProgressEvents(): Flux<List<LibraryScanProgress>> {
|
fun subscribeToScanProgressEvents(): Flux<List<LibraryScanProgress>> {
|
||||||
val user = userService.getCurrentUser()
|
return if (isCurrentUserAdmin()) LibraryScanService.subscribeToScanProgressEvents()
|
||||||
return if (user.isAdmin()) LibraryScanService.subscribeToScanProgressEvents()
|
|
||||||
else Flux.empty()
|
else Flux.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun triggerScan(scanType: ScanType = ScanType.QUICK, libraries: Collection<LibraryDto>?) =
|
fun triggerScan(scanType: ScanType = ScanType.QUICK, libraryIds: Collection<Long>?) =
|
||||||
libraryScanService.triggerScan(scanType, libraries)
|
libraryScanService.triggerScan(scanType, libraryIds)
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun createLibrary(library: LibraryDto, scanAfterCreation: Boolean = true) =
|
fun createLibrary(library: LibraryAdminDto, scanAfterCreation: Boolean = true) =
|
||||||
libraryService.create(library, scanAfterCreation)
|
libraryService.create(library, scanAfterCreation)
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||||||
import org.gameyfin.app.core.filesystem.FilesystemService
|
import org.gameyfin.app.core.filesystem.FilesystemService
|
||||||
import org.gameyfin.app.games.GameService
|
import org.gameyfin.app.games.GameService
|
||||||
import org.gameyfin.app.games.entities.Game
|
import org.gameyfin.app.games.entities.Game
|
||||||
import org.gameyfin.app.libraries.dto.LibraryDto
|
|
||||||
import org.gameyfin.app.libraries.dto.LibraryScanProgress
|
import org.gameyfin.app.libraries.dto.LibraryScanProgress
|
||||||
import org.gameyfin.app.libraries.dto.LibraryScanStatus
|
import org.gameyfin.app.libraries.dto.LibraryScanStatus
|
||||||
import org.gameyfin.app.libraries.dto.LibraryScanStep
|
import org.gameyfin.app.libraries.dto.LibraryScanStep
|
||||||
@@ -62,8 +61,8 @@ class LibraryScanService(
|
|||||||
/**
|
/**
|
||||||
* Wrapper function to trigger a scan for a list of libraries.
|
* Wrapper function to trigger a scan for a list of libraries.
|
||||||
*/
|
*/
|
||||||
fun triggerScan(scanType: ScanType, libraryDtos: Collection<LibraryDto>?) {
|
fun triggerScan(scanType: ScanType, libraryIds: Collection<Long>?) {
|
||||||
val libraries = libraryDtos?.map { libraryCoreService.toEntity(it) } ?: libraryRepository.findAll()
|
val libraries = libraryIds?.let { libraryRepository.findAllById(libraryIds) } ?: libraryRepository.findAll()
|
||||||
libraries.forEach { library ->
|
libraries.forEach { library ->
|
||||||
val libraryId = library.id!!
|
val libraryId = library.id!!
|
||||||
if (scansInProgress.putIfAbsent(libraryId, true) == null) {
|
if (scansInProgress.putIfAbsent(libraryId, true) == null) {
|
||||||
@@ -84,30 +83,6 @@ class LibraryScanService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Triggers a quick scan for a list of libraries.
|
|
||||||
* A quick scan will only scan for new games and deleted games, but will not touch existing games.
|
|
||||||
* If no list is provided, all libraries will be scanned.
|
|
||||||
*
|
|
||||||
* @param libraryDtos: List of LibraryDto objects to scan.
|
|
||||||
*/
|
|
||||||
fun quickScan(libraryDtos: Collection<LibraryDto>?) {
|
|
||||||
val libraries = libraryDtos?.map { libraryCoreService.toEntity(it) } ?: libraryRepository.findAll()
|
|
||||||
libraries.forEach { executor.submit { quickScan(it) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Triggers a full scan for a list of libraries.
|
|
||||||
* A full scan will rescan all games in the library, including metadata and images.
|
|
||||||
* If no list is provided, all libraries will be scanned.
|
|
||||||
*
|
|
||||||
* @param libraryDtos: List of LibraryDto objects to scan.
|
|
||||||
*/
|
|
||||||
fun fullScan(libraryDtos: Collection<LibraryDto>?) {
|
|
||||||
val libraries = libraryDtos?.map { libraryCoreService.toEntity(it) } ?: libraryRepository.findAll()
|
|
||||||
libraries.forEach { executor.submit { fullScan(it, false) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun quickScan(library: Library) {
|
private fun quickScan(library: Library) {
|
||||||
val progress = LibraryScanProgress(
|
val progress = LibraryScanProgress(
|
||||||
libraryId = library.id!!,
|
libraryId = library.id!!,
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ package org.gameyfin.app.libraries
|
|||||||
|
|
||||||
import com.vaadin.hilla.exception.EndpointException
|
import com.vaadin.hilla.exception.EndpointException
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.gameyfin.app.games.GameService
|
import org.gameyfin.app.libraries.dto.*
|
||||||
import org.gameyfin.app.libraries.dto.LibraryDto
|
|
||||||
import org.gameyfin.app.libraries.dto.LibraryEvent
|
|
||||||
import org.gameyfin.app.libraries.dto.LibraryUpdateDto
|
|
||||||
import org.gameyfin.app.libraries.enums.ScanType
|
import org.gameyfin.app.libraries.enums.ScanType
|
||||||
|
import org.gameyfin.app.libraries.extensions.toDtos
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
@@ -18,31 +16,46 @@ import kotlin.time.toJavaDuration
|
|||||||
@Service
|
@Service
|
||||||
class LibraryService(
|
class LibraryService(
|
||||||
private val libraryRepository: LibraryRepository,
|
private val libraryRepository: LibraryRepository,
|
||||||
private val libraryCoreService: LibraryCoreService,
|
|
||||||
private val libraryScanService: LibraryScanService,
|
private val libraryScanService: LibraryScanService,
|
||||||
private val gameService: GameService
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = KotlinLogging.logger {}
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
/* Websockets */
|
/* Websockets */
|
||||||
private val libraryEvents = Sinks.many().multicast().onBackpressureBuffer<LibraryEvent>(1024, false)
|
private val libraryUserEvents = Sinks.many().multicast().onBackpressureBuffer<LibraryUserEvent>(1024, false)
|
||||||
|
private val libraryAdminEvents = Sinks.many().multicast().onBackpressureBuffer<LibraryAdminEvent>(1024, false)
|
||||||
|
|
||||||
fun subscribeToLibraryEvents(): Flux<List<LibraryEvent>> {
|
fun subscribeUser(): Flux<List<LibraryUserEvent>> {
|
||||||
log.debug { "New subscription for libraryEvents" }
|
log.debug { "New user subscription for libraryEvents" }
|
||||||
return libraryEvents.asFlux()
|
return libraryUserEvents.asFlux()
|
||||||
.buffer(100.milliseconds.toJavaDuration())
|
.buffer(100.milliseconds.toJavaDuration())
|
||||||
.doOnSubscribe {
|
.doOnSubscribe {
|
||||||
log.debug { "Subscriber added to libraryEvents [${libraryEvents.currentSubscriberCount()}]" }
|
log.debug { "Subscriber added to user libraryUserEvents [${libraryUserEvents.currentSubscriberCount()}]" }
|
||||||
}
|
}
|
||||||
.doFinally {
|
.doFinally {
|
||||||
log.debug { "Subscriber removed from libraryEvents with signal type $it [${libraryEvents.currentSubscriberCount()}]" }
|
log.debug { "Subscriber removed from user libraryUserEvents with signal type $it [${libraryUserEvents.currentSubscriberCount()}]" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emit(event: LibraryEvent) {
|
fun subscribeAdmin(): Flux<List<LibraryAdminEvent>> {
|
||||||
libraryEvents.tryEmitNext(event)
|
log.debug { "New admin subscription for libraryEvents" }
|
||||||
|
return libraryAdminEvents.asFlux()
|
||||||
|
.buffer(100.milliseconds.toJavaDuration())
|
||||||
|
.doOnSubscribe {
|
||||||
|
log.debug { "Subscriber added to admin libraryAdminEvents [${libraryAdminEvents.currentSubscriberCount()}]" }
|
||||||
|
}
|
||||||
|
.doFinally {
|
||||||
|
log.debug { "Subscriber removed from admin libraryAdminEvents with signal type $it [${libraryAdminEvents.currentSubscriberCount()}]" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitUser(event: LibraryUserEvent) {
|
||||||
|
libraryUserEvents.tryEmitNext(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitAdmin(event: LibraryAdminEvent) {
|
||||||
|
libraryAdminEvents.tryEmitNext(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +65,7 @@ class LibraryService(
|
|||||||
*/
|
*/
|
||||||
fun getAll(): List<LibraryDto> {
|
fun getAll(): List<LibraryDto> {
|
||||||
val entities = libraryRepository.findAll()
|
val entities = libraryRepository.findAll()
|
||||||
return entities.map { it.toDto() }
|
return entities.toDtos()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,14 +83,21 @@ class LibraryService(
|
|||||||
* @param library: The library to create or update.
|
* @param library: The library to create or update.
|
||||||
* @return The created or updated LibraryDto object.
|
* @return The created or updated LibraryDto object.
|
||||||
*/
|
*/
|
||||||
fun create(library: LibraryDto, scanAfterCreation: Boolean) {
|
fun create(library: LibraryAdminDto, scanAfterCreation: Boolean) {
|
||||||
// Check for duplicate directories before creating a new library
|
// Check for duplicate directories before creating a new library
|
||||||
checkForDuplicateDirectories(library.directories.map { it.internalPath })
|
checkForDuplicateDirectories(library.directories.map { it.internalPath })
|
||||||
|
|
||||||
val newLibrary = libraryRepository.save(libraryCoreService.toEntity(library))
|
var newLibrary = Library(
|
||||||
|
name = library.name,
|
||||||
|
directories = library.directories.distinctBy { it.internalPath }.map {
|
||||||
|
DirectoryMapping(internalPath = it.internalPath, externalPath = it.externalPath)
|
||||||
|
}.toMutableList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
newLibrary = libraryRepository.save(newLibrary)
|
||||||
|
|
||||||
if (scanAfterCreation) {
|
if (scanAfterCreation) {
|
||||||
libraryScanService.triggerScan(ScanType.QUICK, listOf(newLibrary.toDto()))
|
libraryScanService.triggerScan(ScanType.QUICK, listOf(newLibrary.id!!))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
package org.gameyfin.app.libraries.dto
|
package org.gameyfin.app.libraries.dto
|
||||||
|
|
||||||
data class LibraryDto(
|
import com.fasterxml.jackson.annotation.JsonInclude
|
||||||
val id: Long,
|
|
||||||
val name: String,
|
interface LibraryDto {
|
||||||
|
val id: Long
|
||||||
|
val name: String
|
||||||
|
val games: List<Long>?
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
data class LibraryUserDto(
|
||||||
|
override val id: Long,
|
||||||
|
override val name: String,
|
||||||
|
override val games: List<Long>?
|
||||||
|
) : LibraryDto
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
data class LibraryAdminDto(
|
||||||
|
override val id: Long,
|
||||||
|
override val name: String,
|
||||||
val directories: List<DirectoryMappingDto>,
|
val directories: List<DirectoryMappingDto>,
|
||||||
val games: List<Long>?,
|
override val games: List<Long>?,
|
||||||
val stats: LibraryStatsDto?,
|
val stats: LibraryStatsDto?,
|
||||||
val unmatchedPaths: List<String>? = emptyList()
|
val unmatchedPaths: List<String> = emptyList()
|
||||||
)
|
) : LibraryDto
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.gameyfin.app.libraries.dto
|
|
||||||
|
|
||||||
sealed class LibraryEvent {
|
|
||||||
abstract val type: String
|
|
||||||
|
|
||||||
data class Created(val library: LibraryDto, override val type: String = "created") : LibraryEvent()
|
|
||||||
data class Updated(val library: LibraryDto, override val type: String = "updated") : LibraryEvent()
|
|
||||||
data class Deleted(val libraryId: Long, override val type: String = "deleted") : LibraryEvent()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.gameyfin.app.libraries.dto
|
||||||
|
|
||||||
|
sealed interface LibraryEvent {
|
||||||
|
val type: String
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class LibraryUserEvent : LibraryEvent {
|
||||||
|
data class Created(val library: LibraryUserDto, override val type: String = "created") : LibraryUserEvent()
|
||||||
|
data class Updated(val library: LibraryUserDto, override val type: String = "updated") : LibraryUserEvent()
|
||||||
|
data class Deleted(val libraryId: Long, override val type: String = "deleted") : LibraryUserEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class LibraryAdminEvent : LibraryEvent {
|
||||||
|
data class Created(val library: LibraryAdminDto, override val type: String = "created") : LibraryAdminEvent()
|
||||||
|
data class Updated(val library: LibraryAdminDto, override val type: String = "updated") : LibraryAdminEvent()
|
||||||
|
data class Deleted(val libraryId: Long, override val type: String = "deleted") : LibraryAdminEvent()
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package org.gameyfin.app.libraries.extensions
|
||||||
|
|
||||||
|
import org.gameyfin.app.core.security.isCurrentUserAdmin
|
||||||
|
import org.gameyfin.app.libraries.Library
|
||||||
|
import org.gameyfin.app.libraries.dto.*
|
||||||
|
|
||||||
|
|
||||||
|
fun Library.toDto(): LibraryDto {
|
||||||
|
return if (isCurrentUserAdmin()) {
|
||||||
|
this.toAdminDto()
|
||||||
|
} else {
|
||||||
|
this.toUserDto()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Collection<Library>.toDtos(): List<LibraryDto> {
|
||||||
|
return if (isCurrentUserAdmin()) {
|
||||||
|
this.map { it.toAdminDto() }
|
||||||
|
} else {
|
||||||
|
this.map { it.toUserDto() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Library.toUserDto(): LibraryUserDto {
|
||||||
|
return LibraryUserDto(
|
||||||
|
id = this.id!!,
|
||||||
|
name = this.name,
|
||||||
|
games = this.games.mapNotNull { it.id }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Library.toAdminDto(): LibraryAdminDto {
|
||||||
|
return LibraryAdminDto(
|
||||||
|
id = this.id!!,
|
||||||
|
name = this.name,
|
||||||
|
directories = this.directories.map { DirectoryMappingDto(it.internalPath, it.externalPath) },
|
||||||
|
games = this.games.mapNotNull { it.id },
|
||||||
|
stats = LibraryStatsDto(
|
||||||
|
gamesCount = this.games.size,
|
||||||
|
downloadedGamesCount = this.games.sumOf { it.metadata.downloadCount }
|
||||||
|
),
|
||||||
|
unmatchedPaths = this.unmatchedPaths
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -109,15 +109,6 @@ class UserService(
|
|||||||
return toUserInfo(user)
|
return toUserInfo(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentUser(): org.gameyfin.app.users.entities.User {
|
|
||||||
val auth = SecurityContextHolder.getContext().authentication
|
|
||||||
if (auth.principal is OidcUser) {
|
|
||||||
return userRepository.findByOidcProviderId((auth.principal as OidcUser).subject)
|
|
||||||
?: throw UsernameNotFoundException("OIDC user not found")
|
|
||||||
}
|
|
||||||
return getByUsernameNonNull(auth.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAvatar(username: String): Image? {
|
fun getAvatar(username: String): Image? {
|
||||||
val user = getByUsernameNonNull(username)
|
val user = getByUsernameNonNull(username)
|
||||||
return user.avatar
|
return user.avatar
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
package org.gameyfin.app.users.util
|
|
||||||
|
|
||||||
import org.gameyfin.app.core.Role
|
|
||||||
import org.gameyfin.app.users.entities.User
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails
|
|
||||||
|
|
||||||
fun User.hasRole(role: Role): Boolean {
|
|
||||||
return role.roleName in this.roles.map { r -> r.roleName }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun UserDetails.hasRole(role: Role): Boolean {
|
|
||||||
return role.roleName in this.authorities.map { a -> a.authority }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun UserDetails.isAdmin(): Boolean {
|
|
||||||
return hasRole(Role.SUPERADMIN) || hasRole(Role.ADMIN)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun User.isAdmin(): Boolean {
|
|
||||||
return hasRole(Role.SUPERADMIN) || hasRole(Role.ADMIN)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user