Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"lint:fix": "eslint . --fix && prettier --write --list-different ."
},
"dependencies": {
"@drop-oss/droplet": "^0.7.2",
"@drop-oss/droplet": "^1.3.1",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@lobomfz/prismark": "0.0.3",
Expand Down
6 changes: 2 additions & 4 deletions pages/admin/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@
</NuxtLink>
</p>

<div
v-if="libraryState.unimportedGames.length > 0"
class="mt-2 rounded-md bg-blue-600/10 p-4"
>
<div v-if="toImport" class="mt-2 rounded-md bg-blue-600/10 p-4">
<div class="flex">
<div class="flex-shrink-0">
<InformationCircleIcon
Expand Down Expand Up @@ -177,4 +174,5 @@ useHead({
});

const libraryState = await $dropFetch("/api/v1/admin/library");
const toImport = Object.entries(libraryState.unimportedGames).length > 0;
</script>
29 changes: 16 additions & 13 deletions pages/admin/library/import.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
class="relative w-full cursor-default rounded-md bg-zinc-950 py-1.5 pl-3 pr-10 text-left text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
>
<span v-if="currentlySelectedGame != -1" class="block truncate">{{
games.unimportedGames[currentlySelectedGame]
games.unimportedGames[currentlySelectedGame].game
}}</span>
<span v-else class="block truncate text-zinc-400"
>Please select a directory...</span
Expand All @@ -37,7 +37,7 @@
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-zinc-900 py-1 text-base shadow-lg ring-1 ring-zinc-800 focus:outline-none sm:text-sm"
>
<ListboxOption
v-for="(game, gameIdx) in games.unimportedGames"
v-for="({ game }, gameIdx) in games.unimportedGames"
:key="game"
v-slot="{ active, selected }"
as="template"
Expand Down Expand Up @@ -275,7 +275,6 @@ definePageMeta({
});

const games = await $dropFetch("/api/v1/admin/import/game");

const currentlySelectedGame = ref(-1);
const gameSearchResultsLoading = ref(false);
const gameSearchResultsError = ref<string | undefined>();
Expand All @@ -286,12 +285,12 @@ async function updateSelectedGame(value: number) {
if (currentlySelectedGame.value == value) return;
currentlySelectedGame.value = value;
if (currentlySelectedGame.value == -1) return;
const game = games.unimportedGames[currentlySelectedGame.value];
if (!game) return;
const option = games.unimportedGames[currentlySelectedGame.value];
if (!option) return;

metadataResults.value = undefined;
currentlySelectedMetadata.value = -1;
gameSearchTerm.value = game;
gameSearchTerm.value = option.game;

await searchGame();
}
Expand Down Expand Up @@ -324,17 +323,21 @@ const router = useRouter();

const importLoading = ref(false);
const importError = ref<string | undefined>();
async function importGame(metadata: boolean) {
if (!metadataResults.value && metadata) return;
async function importGame(useMetadata: boolean) {
if (!metadataResults.value && useMetadata) return;

const metadata =
useMetadata && metadataResults.value
? metadataResults.value[currentlySelectedMetadata.value]
: undefined;
const option = games.unimportedGames[currentlySelectedGame.value];

const game = await $dropFetch("/api/v1/admin/import/game", {
method: "POST",
body: {
path: games.unimportedGames[currentlySelectedGame.value],
metadata:
metadata && metadataResults.value
? metadataResults.value[currentlySelectedMetadata.value]
: undefined,
path: option.game,
library: option.library,
metadata,
},
});

Expand Down
9 changes: 5 additions & 4 deletions pages/admin/library/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@
version.
</p>
</div>
<div
v-if="libraryState.unimportedGames.length > 0"
class="rounded-md bg-blue-600/10 p-4"
>
<div v-if="toImport" class="rounded-md bg-blue-600/10 p-4">
<div class="flex">
<div class="flex-shrink-0">
<InformationCircleIcon
Expand Down Expand Up @@ -186,6 +183,9 @@ useHead({
const searchQuery = ref("");

const libraryState = await $dropFetch("/api/v1/admin/library");

const toImport = ref(Object.entries(libraryState.unimportedGames).length > 0);

const libraryGames = ref(
libraryState.games.map((e) => {
const noVersions = e.status.noVersions;
Expand Down Expand Up @@ -219,5 +219,6 @@ async function deleteGame(id: string) {
await $dropFetch(`/api/v1/admin/game?id=${id}`, { method: "DELETE" });
const index = libraryGames.value.findIndex((e) => e.id === id);
libraryGames.value.splice(index, 1);
toImport.value = true;
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
Warnings:

- You are about to drop the `ClientPeerAPIConfiguration` table. If the table is not empty, all the data it contains will be lost.

*/
-- CreateEnum
CREATE TYPE "LibraryBackend" AS ENUM ('Filesystem');

-- AlterEnum
ALTER TYPE "ClientCapabilities" ADD VALUE 'trackPlaytime';

-- DropForeignKey
ALTER TABLE "ClientPeerAPIConfiguration" DROP CONSTRAINT "ClientPeerAPIConfiguration_clientId_fkey";

-- AlterTable
ALTER TABLE "Screenshot" ALTER COLUMN "private" DROP DEFAULT;

-- DropTable
DROP TABLE "ClientPeerAPIConfiguration";

-- CreateTable
CREATE TABLE "Library" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"backend" "LibraryBackend" NOT NULL,
"options" JSONB NOT NULL,

CONSTRAINT "Library_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Playtime" (
"gameId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"seconds" INTEGER NOT NULL,
"updatedAt" TIMESTAMPTZ(6) NOT NULL,
"createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "Playtime_pkey" PRIMARY KEY ("gameId","userId")
);

-- CreateIndex
CREATE INDEX "Playtime_userId_idx" ON "Playtime"("userId");

-- CreateIndex
CREATE INDEX "Screenshot_userId_idx" ON "Screenshot"("userId");

-- AddForeignKey
ALTER TABLE "Playtime" ADD CONSTRAINT "Playtime_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Playtime" ADD CONSTRAINT "Playtime_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
Warnings:

- You are about to drop the column `libraryBasePath` on the `Game` table. All the data in the column will be lost.

*/
-- DropIndex
DROP INDEX "Game_libraryBasePath_key";

-- AlterTable
ALTER TABLE "Game" RENAME COLUMN "libraryBasePath" TO "libraryPath";

ALTER TABLE "Game" ADD COLUMN "libraryId" TEXT;

-- AddForeignKey
ALTER TABLE "Game"
ADD CONSTRAINT "Game_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "Library" ("id") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- A unique constraint covering the columns `[libraryId,libraryPath]` on the table `Game` will be added. If there are existing duplicate values, this will fail.

*/
-- CreateIndex
CREATE UNIQUE INDEX "Game_libraryId_libraryPath_key" ON "Game"("libraryId", "libraryPath");
16 changes: 15 additions & 1 deletion prisma/models/app.prisma
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
model ApplicationSettings {
timestamp DateTime @id @default(now())

metadataProviders String[]
metadataProviders String[]

saveSlotCountLimit Int @default(5)
saveSlotSizeLimit Float @default(10) // MB
Expand All @@ -13,3 +13,17 @@ enum Platform {
Linux @map("linux")
macOS @map("macos")
}

enum LibraryBackend {
Filesystem
}

model Library {
id String @id @default(uuid())
name String

backend LibraryBackend
options Json

games Game[]
}
10 changes: 8 additions & 2 deletions prisma/models/content.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ model Game {
mImageCarouselObjectIds String[] // linked to below array
mImageLibraryObjectIds String[] // linked to objects in s3

versions GameVersion[]
libraryBasePath String @unique // Base dir for all the game versions
versions GameVersion[]

// These fields will not be optional in the next version
// Any game without a library ID will be assigned one at startup, based on the defaults
libraryId String?
library Library? @relation(fields: [libraryId], references: [id])
libraryPath String

collections CollectionEntry[]
saves SaveSlot[]
Expand All @@ -42,6 +47,7 @@ model Game {
publishers Company[] @relation(name: "publishers")

@@unique([metadataSource, metadataId], name: "metadataKey")
@@unique([libraryId, libraryPath], name: "libraryKey")
}

model GameRating {
Expand Down
7 changes: 4 additions & 3 deletions server/api/v1/admin/game/index.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ export default defineEventHandler(async (h3) => {
},
});

if (!game)
if (!game || !game.libraryId)
throw createError({ statusCode: 404, statusMessage: "Game ID not found" });

const unimportedVersions = await libraryManager.fetchUnimportedVersions(
game.id,
const unimportedVersions = await libraryManager.fetchUnimportedGameVersions(
game.libraryId,
game.libraryPath,
);

return { game, unimportedVersions };
Expand Down
7 changes: 6 additions & 1 deletion server/api/v1/admin/import/game/index.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,10 @@ export default defineEventHandler(async (h3) => {
if (!allowed) throw createError({ statusCode: 403 });

const unimportedGames = await libraryManager.fetchAllUnimportedGames();
return { unimportedGames };
const iterableUnimportedGames = Object.entries(unimportedGames)
.map(([libraryId, gameArray]) =>
gameArray.map((e) => ({ game: e, library: libraryId })),
)
.flat();
return { unimportedGames: iterableUnimportedGames };
});
67 changes: 38 additions & 29 deletions server/api/v1/admin/import/game/index.post.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,46 @@
import { type } from "arktype";
import { throwingArktype } from "~/server/arktype";
import aclManager from "~/server/internal/acls";
import libraryManager from "~/server/internal/library";
import metadataHandler from "~/server/internal/metadata";
import type {
GameMetadataSearchResult,
GameMetadataSource,
} from "~/server/internal/metadata/types";

export default defineEventHandler(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:game:new"]);
if (!allowed) throw createError({ statusCode: 403 });
const ImportGameBody = type({
library: "string",
path: "string",
["metadata?"]: {
id: "string",
sourceId: "string",
name: "string",
},
}).configure(throwingArktype);

const body = await readBody(h3);
export default defineEventHandler<{ body: typeof ImportGameBody.infer }>(
async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["import:game:new"]);
if (!allowed) throw createError({ statusCode: 403 });

const path = body.path;
const metadata = body.metadata as GameMetadataSearchResult &
GameMetadataSource;
if (!path)
throw createError({
statusCode: 400,
statusMessage: "Path missing from body",
});
const { library, path, metadata } = await readValidatedBody(
h3,
ImportGameBody,
);

const validPath = await libraryManager.checkUnimportedGamePath(path);
if (!validPath)
throw createError({
statusCode: 400,
statusMessage: "Invalid unimported game path",
});
if (!path)
throw createError({
statusCode: 400,
statusMessage: "Path missing from body",
});

if (!metadata || !metadata.id || !metadata.sourceId) {
console.log(metadata);
return await metadataHandler.createGameWithoutMetadata(path);
} else {
return await metadataHandler.createGame(metadata, path);
}
});
const valid = await libraryManager.checkUnimportedGamePath(library, path);
if (!valid)
throw createError({
statusCode: 400,
statusMessage: "Invalid library or game.",
});

if (!metadata) {
return await metadataHandler.createGameWithoutMetadata(library, path);
} else {
return await metadataHandler.createGame(metadata, library, path);
}
},
);
14 changes: 12 additions & 2 deletions server/api/v1/admin/import/version/index.get.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
import libraryManager from "~/server/internal/library";

export default defineEventHandler(async (h3) => {
Expand All @@ -13,8 +14,17 @@ export default defineEventHandler(async (h3) => {
statusMessage: "Missing id in request params",
});

const unimportedVersions =
await libraryManager.fetchUnimportedVersions(gameId);
const game = await prisma.game.findUnique({
where: { id: gameId },
select: { libraryId: true, libraryPath: true },
});
if (!game || !game.libraryId)
throw createError({ statusCode: 404, statusMessage: "Game not found" });

const unimportedVersions = await libraryManager.fetchUnimportedGameVersions(
game.libraryId,
game.libraryPath,
);
if (!unimportedVersions)
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" });

Expand Down
Loading