mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-17 08:15:44 +00:00
Update dependenicies
Update Plugin-API to allow multiple results per plugin Add LibraryDetailsModal Minor refactorings & bugfixes
This commit is contained in:
Generated
+1009
-1823
File diff suppressed because it is too large
Load Diff
+67
-67
@@ -9,22 +9,22 @@
|
|||||||
"@polymer/polymer": "3.5.2",
|
"@polymer/polymer": "3.5.2",
|
||||||
"@react-stately/data": "^3.12.2",
|
"@react-stately/data": "^3.12.2",
|
||||||
"@react-types/shared": "^3.28.0",
|
"@react-types/shared": "^3.28.0",
|
||||||
"@vaadin/bundles": "24.7.1",
|
"@vaadin/bundles": "24.7.5",
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
"@vaadin/common-frontend": "0.0.19",
|
||||||
"@vaadin/hilla-file-router": "24.7.1",
|
"@vaadin/hilla-file-router": "24.7.3",
|
||||||
"@vaadin/hilla-frontend": "24.7.1",
|
"@vaadin/hilla-frontend": "24.7.3",
|
||||||
"@vaadin/hilla-lit-form": "24.7.1",
|
"@vaadin/hilla-lit-form": "24.7.3",
|
||||||
"@vaadin/hilla-react-auth": "24.7.1",
|
"@vaadin/hilla-react-auth": "24.7.3",
|
||||||
"@vaadin/hilla-react-crud": "24.7.1",
|
"@vaadin/hilla-react-crud": "24.7.3",
|
||||||
"@vaadin/hilla-react-form": "24.7.1",
|
"@vaadin/hilla-react-form": "24.7.3",
|
||||||
"@vaadin/hilla-react-i18n": "24.7.1",
|
"@vaadin/hilla-react-i18n": "24.7.3",
|
||||||
"@vaadin/hilla-react-signals": "24.7.1",
|
"@vaadin/hilla-react-signals": "24.7.3",
|
||||||
"@vaadin/polymer-legacy-adapter": "24.7.1",
|
"@vaadin/polymer-legacy-adapter": "24.7.5",
|
||||||
"@vaadin/react-components": "24.7.1",
|
"@vaadin/react-components": "24.7.5",
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||||
"@vaadin/vaadin-lumo-styles": "24.7.1",
|
"@vaadin/vaadin-lumo-styles": "24.7.5",
|
||||||
"@vaadin/vaadin-material-styles": "24.7.1",
|
"@vaadin/vaadin-material-styles": "24.7.5",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.7.1",
|
"@vaadin/vaadin-themable-mixin": "24.7.5",
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
"construct-style-sheets-polyfill": "3.1.0",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"framer-motion": "^12.5.0",
|
"framer-motion": "^12.5.0",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"lit": "3.2.1",
|
"lit": "3.3.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.5.47",
|
"moment-timezone": "^0.5.47",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"react-aria-components": "^1.7.1",
|
"react-aria-components": "^1.7.1",
|
||||||
"react-confetti-boom": "^1.0.0",
|
"react-confetti-boom": "^1.0.0",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-router": "7.2.0",
|
"react-router": "7.5.2",
|
||||||
"yup": "^1.6.1"
|
"yup": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -53,24 +53,24 @@
|
|||||||
"@rollup/plugin-replace": "6.0.2",
|
"@rollup/plugin-replace": "6.0.2",
|
||||||
"@rollup/pluginutils": "5.1.4",
|
"@rollup/pluginutils": "5.1.4",
|
||||||
"@types/node": "^22.4.0",
|
"@types/node": "^22.4.0",
|
||||||
"@types/react": "18.3.18",
|
"@types/react": "18.3.20",
|
||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.6",
|
||||||
"@vaadin/hilla-generator-cli": "24.7.1",
|
"@vaadin/hilla-generator-cli": "24.7.3",
|
||||||
"@vaadin/hilla-generator-core": "24.7.1",
|
"@vaadin/hilla-generator-core": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-backbone": "24.7.1",
|
"@vaadin/hilla-generator-plugin-backbone": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-barrel": "24.7.1",
|
"@vaadin/hilla-generator-plugin-barrel": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-client": "24.7.1",
|
"@vaadin/hilla-generator-plugin-client": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-model": "24.7.1",
|
"@vaadin/hilla-generator-plugin-model": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-push": "24.7.1",
|
"@vaadin/hilla-generator-plugin-push": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-signals": "24.7.1",
|
"@vaadin/hilla-generator-plugin-signals": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-subtypes": "24.7.1",
|
"@vaadin/hilla-generator-plugin-subtypes": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.7.1",
|
"@vaadin/hilla-generator-plugin-transfertypes": "24.7.3",
|
||||||
"@vaadin/hilla-generator-utils": "24.7.1",
|
"@vaadin/hilla-generator-utils": "24.7.3",
|
||||||
"@vitejs/plugin-react": "4.3.4",
|
"@vitejs/plugin-react": "4.4.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||||
"async": "3.2.6",
|
"async": "3.2.6",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"glob": "11.0.1",
|
"glob": "11.0.2",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"rollup-plugin-brotli": "3.1.0",
|
"rollup-plugin-brotli": "3.1.0",
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"transform-ast": "2.4.4",
|
"transform-ast": "2.4.4",
|
||||||
"typescript": "5.7.3",
|
"typescript": "5.7.3",
|
||||||
"vite": "6.2.3",
|
"vite": "6.3.3",
|
||||||
"vite-plugin-checker": "0.8.0",
|
"vite-plugin-checker": "0.9.1",
|
||||||
"workbox-build": "7.3.0",
|
"workbox-build": "7.3.0",
|
||||||
"workbox-core": "7.3.0",
|
"workbox-core": "7.3.0",
|
||||||
"workbox-precaching": "7.3.0"
|
"workbox-precaching": "7.3.0"
|
||||||
@@ -133,62 +133,62 @@
|
|||||||
"vaadin": {
|
"vaadin": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@polymer/polymer": "3.5.2",
|
"@polymer/polymer": "3.5.2",
|
||||||
"@vaadin/bundles": "24.7.1",
|
"@vaadin/bundles": "24.7.5",
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
"@vaadin/common-frontend": "0.0.19",
|
||||||
"@vaadin/hilla-file-router": "24.7.1",
|
"@vaadin/hilla-file-router": "24.7.3",
|
||||||
"@vaadin/hilla-frontend": "24.7.1",
|
"@vaadin/hilla-frontend": "24.7.3",
|
||||||
"@vaadin/hilla-lit-form": "24.7.1",
|
"@vaadin/hilla-lit-form": "24.7.3",
|
||||||
"@vaadin/hilla-react-auth": "24.7.1",
|
"@vaadin/hilla-react-auth": "24.7.3",
|
||||||
"@vaadin/hilla-react-crud": "24.7.1",
|
"@vaadin/hilla-react-crud": "24.7.3",
|
||||||
"@vaadin/hilla-react-form": "24.7.1",
|
"@vaadin/hilla-react-form": "24.7.3",
|
||||||
"@vaadin/hilla-react-i18n": "24.7.1",
|
"@vaadin/hilla-react-i18n": "24.7.3",
|
||||||
"@vaadin/hilla-react-signals": "24.7.1",
|
"@vaadin/hilla-react-signals": "24.7.3",
|
||||||
"@vaadin/polymer-legacy-adapter": "24.7.1",
|
"@vaadin/polymer-legacy-adapter": "24.7.5",
|
||||||
"@vaadin/react-components": "24.7.1",
|
"@vaadin/react-components": "24.7.5",
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||||
"@vaadin/vaadin-lumo-styles": "24.7.1",
|
"@vaadin/vaadin-lumo-styles": "24.7.5",
|
||||||
"@vaadin/vaadin-material-styles": "24.7.1",
|
"@vaadin/vaadin-material-styles": "24.7.5",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.7.1",
|
"@vaadin/vaadin-themable-mixin": "24.7.5",
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
"construct-style-sheets-polyfill": "3.1.0",
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"lit": "3.2.1",
|
"lit": "3.3.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-router": "7.2.0"
|
"react-router": "7.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-react": "7.26.3",
|
"@babel/preset-react": "7.26.3",
|
||||||
"@preact/signals-react-transform": "0.5.1",
|
"@preact/signals-react-transform": "0.5.1",
|
||||||
"@rollup/plugin-replace": "6.0.2",
|
"@rollup/plugin-replace": "6.0.2",
|
||||||
"@rollup/pluginutils": "5.1.4",
|
"@rollup/pluginutils": "5.1.4",
|
||||||
"@types/react": "18.3.18",
|
"@types/react": "18.3.20",
|
||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.6",
|
||||||
"@vaadin/hilla-generator-cli": "24.7.1",
|
"@vaadin/hilla-generator-cli": "24.7.3",
|
||||||
"@vaadin/hilla-generator-core": "24.7.1",
|
"@vaadin/hilla-generator-core": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-backbone": "24.7.1",
|
"@vaadin/hilla-generator-plugin-backbone": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-barrel": "24.7.1",
|
"@vaadin/hilla-generator-plugin-barrel": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-client": "24.7.1",
|
"@vaadin/hilla-generator-plugin-client": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-model": "24.7.1",
|
"@vaadin/hilla-generator-plugin-model": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-push": "24.7.1",
|
"@vaadin/hilla-generator-plugin-push": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-signals": "24.7.1",
|
"@vaadin/hilla-generator-plugin-signals": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-subtypes": "24.7.1",
|
"@vaadin/hilla-generator-plugin-subtypes": "24.7.3",
|
||||||
"@vaadin/hilla-generator-plugin-transfertypes": "24.7.1",
|
"@vaadin/hilla-generator-plugin-transfertypes": "24.7.3",
|
||||||
"@vaadin/hilla-generator-utils": "24.7.1",
|
"@vaadin/hilla-generator-utils": "24.7.3",
|
||||||
"@vitejs/plugin-react": "4.3.4",
|
"@vitejs/plugin-react": "4.4.1",
|
||||||
"async": "3.2.6",
|
"async": "3.2.6",
|
||||||
"glob": "11.0.1",
|
"glob": "11.0.2",
|
||||||
"rollup-plugin-brotli": "3.1.0",
|
"rollup-plugin-brotli": "3.1.0",
|
||||||
"rollup-plugin-visualizer": "5.14.0",
|
"rollup-plugin-visualizer": "5.14.0",
|
||||||
"strip-css-comments": "5.0.0",
|
"strip-css-comments": "5.0.0",
|
||||||
"transform-ast": "2.4.4",
|
"transform-ast": "2.4.4",
|
||||||
"typescript": "5.7.3",
|
"typescript": "5.7.3",
|
||||||
"vite": "6.2.3",
|
"vite": "6.3.3",
|
||||||
"vite-plugin-checker": "0.8.0",
|
"vite-plugin-checker": "0.9.1",
|
||||||
"workbox-build": "7.3.0",
|
"workbox-build": "7.3.0",
|
||||||
"workbox-core": "7.3.0",
|
"workbox-core": "7.3.0",
|
||||||
"workbox-precaching": "7.3.0"
|
"workbox-precaching": "7.3.0"
|
||||||
},
|
},
|
||||||
"hash": "26d6e466339c0b4c7dfffb8fe616e725dd60e800bba0169742bfabede9023961"
|
"hash": "4507ade910f8d37b606e1a8cff078e31d3a90314f1c7426a4aea761dc563f02a"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,10 @@ import * as Yup from 'yup';
|
|||||||
import {Button, Divider, Tooltip, useDisclosure} from "@heroui/react";
|
import {Button, Divider, Tooltip, useDisclosure} from "@heroui/react";
|
||||||
import {Plus} from "@phosphor-icons/react";
|
import {Plus} from "@phosphor-icons/react";
|
||||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
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 {LibraryOverviewCard} from "Frontend/components/general/cards/LibraryOverviewCard";
|
||||||
import LibraryCreationModal from "Frontend/components/general/modals/LibraryCreationModal";
|
import LibraryCreationModal from "Frontend/components/general/modals/LibraryCreationModal";
|
||||||
|
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
||||||
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||||
|
|
||||||
function LibraryManagementLayout({getConfig, formik}: any) {
|
function LibraryManagementLayout({getConfig, formik}: any) {
|
||||||
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
|
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
|
||||||
@@ -27,6 +28,21 @@ function LibraryManagementLayout({getConfig, formik}: any) {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
async function updateLibrary(library: LibraryUpdateDto) {
|
||||||
|
let updatedLibrary = await LibraryEndpoint.updateLibrary(library);
|
||||||
|
if (updatedLibrary === undefined) return;
|
||||||
|
|
||||||
|
setLibraries((prevLibraries) => {
|
||||||
|
const index = prevLibraries.findIndex((l) => l.id === updatedLibrary.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
const updatedLibraries = [...prevLibraries];
|
||||||
|
updatedLibraries[index] = updatedLibrary;
|
||||||
|
return updatedLibraries;
|
||||||
|
}
|
||||||
|
return [...prevLibraries, updatedLibrary];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Section title="Permissions"/>
|
<Section title="Permissions"/>
|
||||||
@@ -54,7 +70,9 @@ function LibraryManagementLayout({getConfig, formik}: any) {
|
|||||||
{libraries.length > 0 ?
|
{libraries.length > 0 ?
|
||||||
// Aspect ratio of cover = 12/17 -> 5 covers = 60/17 -> 353px * 100px
|
// 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))]">
|
<div id="library-cards" className="grid gap-4 grid-cols-[repeat(auto-fill,minmax(353px,1fr))]">
|
||||||
{libraries.map((library) => <LibraryOverviewCard library={library} key={library.name}/>)}
|
{libraries.map((library) =>
|
||||||
|
<LibraryOverviewCard library={library} updateLibrary={updateLibrary} key={library.name}/>
|
||||||
|
)}
|
||||||
</div> :
|
</div> :
|
||||||
"No libraries configured. Add your first library!"
|
"No libraries configured. Add your first library!"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Button, Card, Chip, Tooltip} from "@heroui/react";
|
import {Button, Card, Chip, Tooltip, useDisclosure} from "@heroui/react";
|
||||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/LibraryDto";
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||||
@@ -14,18 +14,25 @@ import {
|
|||||||
Lego,
|
Lego,
|
||||||
MagnifyingGlass,
|
MagnifyingGlass,
|
||||||
Skull,
|
Skull,
|
||||||
|
SlidersHorizontal,
|
||||||
SoccerBall,
|
SoccerBall,
|
||||||
Strategy,
|
Strategy,
|
||||||
Sword,
|
Sword,
|
||||||
TreasureChest,
|
TreasureChest,
|
||||||
Trophy
|
Trophy
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
|
import LibraryDetailsModal from "Frontend/components/general/modals/LibraryDetailsModal";
|
||||||
|
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
||||||
|
|
||||||
export function LibraryOverviewCard({library}: { library: LibraryDto }) {
|
export function LibraryOverviewCard({library, updateLibrary}: {
|
||||||
|
library: LibraryDto,
|
||||||
|
updateLibrary: (library: LibraryUpdateDto) => void
|
||||||
|
}) {
|
||||||
const MAX_COVER_COUNT = 5;
|
const MAX_COVER_COUNT = 5;
|
||||||
const rand = new Rand(library.id.toString());
|
const rand = new Rand(library.id.toString());
|
||||||
|
|
||||||
const [randomGamesFromLibrary, setRandomGamesFromLibrary] = useState<GameDto[]>([]);
|
const [randomGamesFromLibrary, setRandomGamesFromLibrary] = useState<GameDto[]>([]);
|
||||||
|
const libraryDetailsModal = useDisclosure();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
LibraryEndpoint.getGamesInLibrary(library.id).then(
|
LibraryEndpoint.getGamesInLibrary(library.id).then(
|
||||||
@@ -35,6 +42,7 @@ export function LibraryOverviewCard({library}: { library: LibraryDto }) {
|
|||||||
|
|
||||||
let gamesFromLibrary: GameDto[] = response
|
let gamesFromLibrary: GameDto[] = response
|
||||||
.filter(g => !!g)
|
.filter(g => !!g)
|
||||||
|
.sort((a: GameDto, b: GameDto) => a.id - b.id)
|
||||||
.sort(() => rand.next() - 0.5)
|
.sort(() => rand.next() - 0.5)
|
||||||
.slice(0, count)
|
.slice(0, count)
|
||||||
|
|
||||||
@@ -44,6 +52,7 @@ export function LibraryOverviewCard({library}: { library: LibraryDto }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Card className="flex flex-col justify-between w-[353px]">
|
<Card className="flex flex-col justify-between w-[353px]">
|
||||||
<div className="flex flex-1 justify-center items-center">
|
<div className="flex flex-1 justify-center items-center">
|
||||||
<div className="flex flex-1 opacity-10 min-h-[100px]">
|
<div className="flex flex-1 opacity-10 min-h-[100px]">
|
||||||
@@ -80,10 +89,15 @@ export function LibraryOverviewCard({library}: { library: LibraryDto }) {
|
|||||||
<MagnifyingGlass/>
|
<MagnifyingGlass/>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip content="Configuration" placement="bottom" color="foreground">
|
||||||
|
<Button isIconOnly variant="light" onPress={libraryDetailsModal.onOpen}>
|
||||||
|
<SlidersHorizontal/>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!!library.stats &&
|
{library.stats &&
|
||||||
<div className="grid grid-rows-2 grid-cols-3 justify-items-center items-center p-2 pt-4">
|
<div className="grid grid-rows-2 grid-cols-3 justify-items-center items-center p-2 pt-4">
|
||||||
<p>Games</p>
|
<p>Games</p>
|
||||||
<p>Downloads</p>
|
<p>Downloads</p>
|
||||||
@@ -94,5 +108,12 @@ export function LibraryOverviewCard({library}: { library: LibraryDto }) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</Card>
|
</Card>
|
||||||
|
<LibraryDetailsModal
|
||||||
|
library={library}
|
||||||
|
isOpen={libraryDetailsModal.isOpen}
|
||||||
|
onOpenChange={libraryDetailsModal.onOpenChange}
|
||||||
|
updateLibrary={updateLibrary}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
useDisclosure
|
useDisclosure
|
||||||
} from "@heroui/react";
|
} from "@heroui/react";
|
||||||
import {Form, Formik} from "formik";
|
import {Form, Formik} from "formik";
|
||||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/LibraryDto";
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||||
import Input from "Frontend/components/general/input/Input";
|
import Input from "Frontend/components/general/input/Input";
|
||||||
import PathPickerModal from "Frontend/components/general/modals/PathPickerModal";
|
import PathPickerModal from "Frontend/components/general/modals/PathPickerModal";
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
|
||||||
|
import {Form, Formik} from "formik";
|
||||||
|
import Input from "Frontend/components/general/input/Input";
|
||||||
|
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||||
|
import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
||||||
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||||
|
|
||||||
|
interface LibraryDetailsModalProps {
|
||||||
|
library: LibraryDto;
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: () => void;
|
||||||
|
updateLibrary: (library: LibraryUpdateDto) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LibraryDetailsModal({library, isOpen, onOpenChange, updateLibrary}: LibraryDetailsModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
|
||||||
|
<ModalContent>
|
||||||
|
{(onClose) => (
|
||||||
|
<Formik initialValues={library}
|
||||||
|
enableReinitialize={true}
|
||||||
|
onSubmit={async (values: LibraryUpdateDto) => {
|
||||||
|
updateLibrary(values);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(formik: { isSubmitting: any; }) => (
|
||||||
|
<Form>
|
||||||
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
|
Edit library
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<Input key="name" name="name" label="Name"/>
|
||||||
|
<Button onPress={() => LibraryEndpoint.removeLibrary(library.id)}
|
||||||
|
color="danger">Delete</Button>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button variant="light" onPress={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
isLoading={formik.isSubmitting}
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{formik.isSubmitting ? "" : "Save"}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -55,7 +55,8 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange, update
|
|||||||
{(formik: { isSubmitting: any; }) => (
|
{(formik: { isSubmitting: any; }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
Plugin configuration for {plugin.name}</ModalHeader>
|
Plugin configuration for {plugin.name}
|
||||||
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<h4 className="text-l font-bold">Details</h4>
|
<h4 className="text-l font-bold">Details</h4>
|
||||||
<div className="flex flex-row gap-8">
|
<div className="flex flex-row gap-8">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import kotlinx.coroutines.async
|
|||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
|
import org.apache.commons.io.FilenameUtils
|
||||||
import org.pf4j.PluginManager
|
import org.pf4j.PluginManager
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -44,8 +45,8 @@ class GameService(
|
|||||||
return gameRepository.save(game)
|
return gameRepository.save(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createFromFile(path: Path): Game {
|
fun createFromFile(path: Path): Game? {
|
||||||
val query = path.fileName.toString()
|
val query = FilenameUtils.removeExtension(path.fileName.toString())
|
||||||
|
|
||||||
// Step 0: Query all metadata plugins for metadata on the provided game title
|
// Step 0: Query all metadata plugins for metadata on the provided game title
|
||||||
val metadataResults = queryPlugins(query)
|
val metadataResults = queryPlugins(query)
|
||||||
@@ -53,7 +54,8 @@ class GameService(
|
|||||||
// Step 1: Filter out invalid (empty) results
|
// Step 1: Filter out invalid (empty) results
|
||||||
val validResults = metadataResults.filterValuesNotNull()
|
val validResults = metadataResults.filterValuesNotNull()
|
||||||
if (validResults.isEmpty()) {
|
if (validResults.isEmpty()) {
|
||||||
throw NoMatchException("Could not match game at $path")
|
log.error { "Could not identify game at path '$path'" }
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Filter results to find the best matching title
|
// Step 2: Filter results to find the best matching title
|
||||||
@@ -99,9 +101,9 @@ class GameService(
|
|||||||
metadataPlugins.associateWith {
|
metadataPlugins.associateWith {
|
||||||
async {
|
async {
|
||||||
try {
|
try {
|
||||||
it.fetchMetadata(gameTitle)
|
it.fetchMetadata(gameTitle).firstOrNull()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error fetching metadata with plugin ${it.javaClass.name}" }
|
log.error(e) { "Error fetching metadata for game with plugin ${it.javaClass.name}" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}.await()
|
}.await()
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import com.vaadin.hilla.Endpoint
|
|||||||
import de.grimsi.gameyfin.core.Role
|
import de.grimsi.gameyfin.core.Role
|
||||||
import de.grimsi.gameyfin.games.GameService
|
import de.grimsi.gameyfin.games.GameService
|
||||||
import de.grimsi.gameyfin.games.dto.GameDto
|
import de.grimsi.gameyfin.games.dto.GameDto
|
||||||
|
import de.grimsi.gameyfin.libraries.dto.LibraryDto
|
||||||
|
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
|
||||||
import jakarta.annotation.security.PermitAll
|
import jakarta.annotation.security.PermitAll
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
|
|
||||||
@@ -28,7 +30,17 @@ class LibraryEndpoint(
|
|||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun createLibrary(library: LibraryDto): LibraryDto {
|
fun createLibrary(library: LibraryDto): LibraryDto {
|
||||||
return libraryService.createOrUpdate(library)
|
return libraryService.create(library)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
|
fun updateLibrary(library: LibraryUpdateDto): LibraryDto {
|
||||||
|
return libraryService.update(library)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
|
fun removeLibrary(libraryId: Long) {
|
||||||
|
return libraryService.deleteLibrary(libraryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import de.grimsi.gameyfin.core.filesystem.FilesystemService
|
|||||||
import de.grimsi.gameyfin.games.GameService
|
import de.grimsi.gameyfin.games.GameService
|
||||||
import de.grimsi.gameyfin.games.dto.GameDto
|
import de.grimsi.gameyfin.games.dto.GameDto
|
||||||
import de.grimsi.gameyfin.games.entities.Game
|
import de.grimsi.gameyfin.games.entities.Game
|
||||||
|
import de.grimsi.gameyfin.libraries.dto.LibraryDto
|
||||||
|
import de.grimsi.gameyfin.libraries.dto.LibraryStatsDto
|
||||||
|
import de.grimsi.gameyfin.libraries.dto.LibraryUpdateDto
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
@@ -24,11 +27,30 @@ 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 createOrUpdate(library: LibraryDto): LibraryDto {
|
fun create(library: LibraryDto): LibraryDto {
|
||||||
val entity = libraryRepository.save(toEntity(library))
|
val entity = libraryRepository.save(toEntity(library))
|
||||||
return toDto(entity)
|
return toDto(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a library entity with the non-null fields from a LibraryUpdateDto.
|
||||||
|
*
|
||||||
|
* @param libraryDto: The LibraryUpdateDto containing the fields to update.
|
||||||
|
* @return The updated LibraryDto.
|
||||||
|
* @throws IllegalArgumentException if the library ID is null or the library is not found.
|
||||||
|
*/
|
||||||
|
fun update(libraryDto: LibraryUpdateDto): LibraryDto {
|
||||||
|
val existingLibrary = libraryRepository.findByIdOrNull(libraryDto.id)
|
||||||
|
?: throw IllegalArgumentException("Library with ID $libraryDto.id not found")
|
||||||
|
|
||||||
|
// Update only non-null fields
|
||||||
|
libraryDto.name?.let { existingLibrary.name = it }
|
||||||
|
libraryDto.directories?.let { existingLibrary.directories = it.toMutableSet() }
|
||||||
|
|
||||||
|
val updatedLibrary = libraryRepository.save(existingLibrary)
|
||||||
|
return toDto(updatedLibrary)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all libraries from the repository.
|
* Retrieves all libraries from the repository.
|
||||||
*/
|
*/
|
||||||
@@ -40,11 +62,10 @@ class LibraryService(
|
|||||||
/**
|
/**
|
||||||
* Deletes a library from the repository.
|
* Deletes a library from the repository.
|
||||||
*
|
*
|
||||||
* @param library: The library to delete.
|
* @param libraryId: ID of the library to delete.
|
||||||
*/
|
*/
|
||||||
fun deleteLibrary(library: LibraryDto) {
|
fun deleteLibrary(libraryId: Long) {
|
||||||
val entity = toEntity(library)
|
libraryRepository.deleteById(libraryId)
|
||||||
libraryRepository.delete(entity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,7 +135,10 @@ class LibraryService(
|
|||||||
val libraries = libraryDtos?.map { toEntity(it) } ?: libraryRepository.findAll()
|
val libraries = libraryDtos?.map { toEntity(it) } ?: libraryRepository.findAll()
|
||||||
libraries.forEach { library ->
|
libraries.forEach { library ->
|
||||||
val gamePaths = filesystemService.scanLibraryForGamefiles(library)
|
val gamePaths = filesystemService.scanLibraryForGamefiles(library)
|
||||||
val newGames = gamePaths.map { gameService.createFromFile(it) }
|
val newGames = gamePaths.mapNotNull {
|
||||||
|
gameService.createFromFile(it)
|
||||||
|
}
|
||||||
|
|
||||||
addGamesToLibrary(newGames, library)
|
addGamesToLibrary(newGames, library)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package de.grimsi.gameyfin.libraries
|
package de.grimsi.gameyfin.libraries.dto
|
||||||
|
|
||||||
data class LibraryDto(
|
data class LibraryDto(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package de.grimsi.gameyfin.libraries
|
package de.grimsi.gameyfin.libraries.dto
|
||||||
|
|
||||||
data class LibraryStatsDto(
|
data class LibraryStatsDto(
|
||||||
val gamesCount: Int,
|
val gamesCount: Int,
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.grimsi.gameyfin.libraries.dto
|
||||||
|
|
||||||
|
data class LibraryUpdateDto(
|
||||||
|
val id: Long,
|
||||||
|
val name: String? = null,
|
||||||
|
val directories: Set<String>? = null,
|
||||||
|
)
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
|
logging.level.de.grimsi.gameyfin: DEBUG
|
||||||
logging.level.org.hibernate.SQL: DEBUG
|
logging.level.org.hibernate.SQL: DEBUG
|
||||||
logging.level.org.hibernate.type: TRACE
|
logging.level.org.hibernate.type: TRACE
|
||||||
+4
-7
@@ -1,13 +1,10 @@
|
|||||||
# Plugin versions
|
# Plugin versions
|
||||||
kotlinVersion=2.1.20
|
kotlinVersion=2.1.20
|
||||||
kspVersion=2.1.20-1.0.32
|
kspVersion=2.1.20-2.0.1
|
||||||
vaadinVersion=24.7.1
|
vaadinVersion=24.7.3
|
||||||
springBootVersion=3.4.4
|
springBootVersion=3.4.5
|
||||||
springCloudVersion=2024.0.0
|
springCloudVersion=2024.0.1
|
||||||
springDependencyManagementVersion=1.1.7
|
springDependencyManagementVersion=1.1.7
|
||||||
# Dependency versions
|
# Dependency versions
|
||||||
pf4jVersion=3.13.0
|
pf4jVersion=3.13.0
|
||||||
pf4jKspVersion=2.1.20-1.0.2
|
pf4jKspVersion=2.1.20-1.0.2
|
||||||
# Annotation processor settings
|
|
||||||
kapt.use.k2=true
|
|
||||||
ksp.useKSP2=true
|
|
||||||
+1
-1
@@ -3,5 +3,5 @@ package de.grimsi.gameyfin.pluginapi.gamemetadata
|
|||||||
import org.pf4j.ExtensionPoint
|
import org.pf4j.ExtensionPoint
|
||||||
|
|
||||||
interface GameMetadataProvider : ExtensionPoint {
|
interface GameMetadataProvider : ExtensionPoint {
|
||||||
fun fetchMetadata(gameId: String): GameMetadata?
|
fun fetchMetadata(gameId: String, maxResults: Int = 1): List<GameMetadata>
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
|||||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
import org.pf4j.Extension
|
import org.pf4j.Extension
|
||||||
import org.pf4j.PluginWrapper
|
import org.pf4j.PluginWrapper
|
||||||
|
import proto.Game
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
||||||
@@ -93,32 +94,40 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
"platforms.platform_logo.image_id"
|
"platforms.platform_logo.image_id"
|
||||||
).joinToString(",")
|
).joinToString(",")
|
||||||
|
|
||||||
override fun fetchMetadata(gameId: String): GameMetadata? {
|
override fun fetchMetadata(gameId: String, maxResults: Int): List<GameMetadata> {
|
||||||
val findBySlugQuery = APICalypse()
|
val findBySlugQuery = APICalypse()
|
||||||
.fields(QUERY_FIELDS)
|
.fields(QUERY_FIELDS)
|
||||||
.where("slug = \"${guessSlug(gameId)}\"")
|
.where("slug = \"${guessSlug(gameId)}\"")
|
||||||
|
|
||||||
// First step: Try to find the game by guessing the slug
|
// First step: Try to find the game by guessing the slug
|
||||||
var game = IGDBWrapper.games(findBySlugQuery).firstOrNull()
|
var games = IGDBWrapper.games(findBySlugQuery)
|
||||||
|
|
||||||
// Second step: Try a fuzzy search
|
// Second step: Try a fuzzy search
|
||||||
if (game == null) {
|
// Note: Limit is intentionally set high because IGDBs ranking algorithm is not very good
|
||||||
|
if (games.isEmpty()) {
|
||||||
val searchByNameQuery = APICalypse()
|
val searchByNameQuery = APICalypse()
|
||||||
.fields(QUERY_FIELDS)
|
.fields(QUERY_FIELDS)
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.search(gameId)
|
.search(gameId)
|
||||||
|
|
||||||
// Use IGDBs search function to get a list of games that match the search query
|
// Use IGDBs search function to get a list of games that match the search query
|
||||||
val games = IGDBWrapper.games(searchByNameQuery)
|
games = IGDBWrapper.games(searchByNameQuery)
|
||||||
|
|
||||||
if (games.isEmpty()) return null
|
if (games.isEmpty()) return emptyList()
|
||||||
|
|
||||||
// Use fuzzy search to find the best matching game name
|
// Use fuzzy search to find the best matching game name
|
||||||
val bestMatchingName = FuzzySearch.extractOne(gameId, games.map { it.name }).string
|
val bestMatchingTitles = FuzzySearch.extractTop(gameId, games.map { it.name }, maxResults)
|
||||||
|
games = bestMatchingTitles.mapNotNull { title -> games.find { it.name == title.string } }
|
||||||
game = games.find { it.name == bestMatchingName } ?: return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return games.map { toGameMetadata(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun guessSlug(gameId: String): String {
|
||||||
|
return gameId.replace(" ", "-").lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toGameMetadata(game: Game): GameMetadata {
|
||||||
return GameMetadata(
|
return GameMetadata(
|
||||||
originalId = game.slug,
|
originalId = game.slug,
|
||||||
title = game.name,
|
title = game.name,
|
||||||
@@ -138,9 +147,5 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
perspectives = game.playerPerspectivesList.map { Mapper.playerPerspective(it) }.toSet()
|
perspectives = game.playerPerspectivesList.map { Mapper.playerPerspective(it) }.toSet()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun guessSlug(gameId: String): String {
|
|
||||||
return gameId.replace(" ", "-").lowercase()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +52,7 @@ class Mapper {
|
|||||||
"fantasy" -> Theme.FANTASY
|
"fantasy" -> Theme.FANTASY
|
||||||
"horror" -> Theme.HORROR
|
"horror" -> Theme.HORROR
|
||||||
"sci-fi" -> Theme.SCIENCE_FICTION
|
"sci-fi" -> Theme.SCIENCE_FICTION
|
||||||
|
"science-fiction" -> Theme.SCIENCE_FICTION
|
||||||
"mystery" -> Theme.MYSTERY
|
"mystery" -> Theme.MYSTERY
|
||||||
"thriller" -> Theme.THRILLER
|
"thriller" -> Theme.THRILLER
|
||||||
"survival" -> Theme.SURVIVAL
|
"survival" -> Theme.SURVIVAL
|
||||||
@@ -82,6 +83,7 @@ class Mapper {
|
|||||||
"first-person" -> PlayerPerspective.FIRST_PERSON
|
"first-person" -> PlayerPerspective.FIRST_PERSON
|
||||||
"third-person" -> PlayerPerspective.THIRD_PERSON
|
"third-person" -> PlayerPerspective.THIRD_PERSON
|
||||||
"bird-view-isometric" -> PlayerPerspective.BIRD_VIEW_ISOMETRIC
|
"bird-view-isometric" -> PlayerPerspective.BIRD_VIEW_ISOMETRIC
|
||||||
|
"bird-view-slash-isometric" -> PlayerPerspective.BIRD_VIEW_ISOMETRIC
|
||||||
"side-view" -> PlayerPerspective.SIDE_VIEW
|
"side-view" -> PlayerPerspective.SIDE_VIEW
|
||||||
"text" -> PlayerPerspective.TEXT
|
"text" -> PlayerPerspective.TEXT
|
||||||
"auditory" -> PlayerPerspective.AUDITORY
|
"auditory" -> PlayerPerspective.AUDITORY
|
||||||
|
|||||||
@@ -57,14 +57,14 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
* The Steam Store API I am using provides far less info than IGDB for example
|
* The Steam Store API I am using provides far less info than IGDB for example
|
||||||
* See it more as a proof of concept than a fully functional plugin
|
* See it more as a proof of concept than a fully functional plugin
|
||||||
**/
|
**/
|
||||||
override fun fetchMetadata(gameId: String): GameMetadata? {
|
override fun fetchMetadata(gameId: String, maxResults: Int): List<GameMetadata> {
|
||||||
val searchResult: List<SteamGame> = runBlocking { searchStore(gameId) }
|
val searchResult: List<SteamGame> = runBlocking { searchStore(gameId) }
|
||||||
if (searchResult.isEmpty()) return null
|
if (searchResult.isEmpty()) return emptyList()
|
||||||
|
|
||||||
val bestMatchingTitle = FuzzySearch.extractOne(gameId, searchResult.map { it.name }).string
|
val bestMatchingTitles = FuzzySearch.extractTop(gameId, searchResult.map { it.name }, maxResults)
|
||||||
val bestMatch = searchResult.find { it.name == bestMatchingTitle } ?: return null
|
val bestMatches = bestMatchingTitles.mapNotNull { title -> searchResult.find { it.name == title.string } }
|
||||||
|
|
||||||
return runBlocking { getGameDetails(bestMatch.id) }
|
return runBlocking { bestMatches.map { getGameDetails(it.id) } }.filterNotNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun searchStore(title: String): List<SteamGame> {
|
private suspend fun searchStore(title: String): List<SteamGame> {
|
||||||
@@ -105,7 +105,7 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
// This is as much as I can get from the Steam Store API
|
// This is as much as I can get from the Steam Store API
|
||||||
val metadata = GameMetadata(
|
val metadata = GameMetadata(
|
||||||
originalId = id.toString(),
|
originalId = id.toString(),
|
||||||
title = game.name,
|
title = sanitizeTitle(game.name),
|
||||||
description = game.detailedDescription,
|
description = game.detailedDescription,
|
||||||
coverUrl = game.headerImage?.let { URI(it) },
|
coverUrl = game.headerImage?.let { URI(it) },
|
||||||
release = game.releaseDate?.date,
|
release = game.releaseDate?.date,
|
||||||
@@ -119,5 +119,15 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Often titles on Steam copyright symbols which makes matching between different providers harder
|
||||||
|
* This method removes those symbols
|
||||||
|
*/
|
||||||
|
private fun sanitizeTitle(originalTitle: String): String {
|
||||||
|
val unwantedChars = setOf('™', '©', '®')
|
||||||
|
return originalTitle.filter { it !in unwantedChars }.trim()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user