diff --git a/desktop/main/pages/library/[id]/index.vue b/desktop/main/pages/library/[id]/index.vue index 498078739..f86cd6282 100644 --- a/desktop/main/pages/library/[id]/index.vue +++ b/desktop/main/pages/library/[id]/index.vue @@ -22,6 +22,9 @@ > {{ game.mName }} +
+ Playtime: {{ formatPlaytime(game.playtime[0].seconds) }} +
+ {{ + $t("library.playtime", { + time: formatPlaytime(game.playtime[0].seconds), + }) + }} +
{{ game.mShortDescription }}
@@ -139,6 +146,10 @@ const descriptionHTML = computed(() => micromark(game.value.mDescription ?? ""), ); +function formatPlaytime(seconds: number): string { + return `${(seconds / 3600).toFixed(1)}`; +} + // const currentImageIndex = ref(0); // function nextImage() { diff --git a/server/server/api/v1/client/playtime/[gameid]/index.get.ts b/server/server/api/v1/client/playtime/[gameid]/index.get.ts new file mode 100644 index 000000000..440974170 --- /dev/null +++ b/server/server/api/v1/client/playtime/[gameid]/index.get.ts @@ -0,0 +1,44 @@ +import { ClientCapabilities } from "~/prisma/client/enums"; +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import prisma from "~/server/internal/db/database"; + +export default defineClientEventHandler( + async (h3, { fetchClient, fetchUser }) => { + const client = await fetchClient(); + if (!client.capabilities.includes(ClientCapabilities.TrackPlaytime)) + throw createError({ + statusCode: 403, + statusMessage: "Capability not allowed.", + }); + const user = await fetchUser(); + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ + statusCode: 400, + statusMessage: "No gameID in route params", + }); + + const game = await prisma.game.findUnique({ + where: { id: gameId }, + select: { id: true }, + }); + if (!game) + throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + + const playtime = await prisma.playtime.findUnique({ + where: { + gameId_userId: { + gameId, + userId: user.id, + }, + }, + }); + if (!playtime) + throw createError({ + statusCode: 404, + statusMessage: "Playtime not found", + }); + + return playtime; + }, +); diff --git a/server/server/api/v1/client/playtime/[gameid]/index.post.ts b/server/server/api/v1/client/playtime/[gameid]/index.post.ts new file mode 100644 index 000000000..eb8eba24a --- /dev/null +++ b/server/server/api/v1/client/playtime/[gameid]/index.post.ts @@ -0,0 +1,60 @@ +import { type } from "arktype"; +import { ClientCapabilities } from "~/prisma/client/enums"; +import { readDropValidatedBody, throwingArktype } from "~/server/arktype"; +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import prisma from "~/server/internal/db/database"; + +const maxElapsedSeconds = 31536000; +const PlaytimeBody = type({ + seconds: "number.integer", +}).configure(throwingArktype); + +export default defineClientEventHandler( + async (h3, { fetchClient, fetchUser }) => { + const client = await fetchClient(); + if (!client.capabilities.includes(ClientCapabilities.TrackPlaytime)) + throw createError({ + statusCode: 403, + statusMessage: "Capability not allowed.", + }); + const user = await fetchUser(); + const gameId = getRouterParam(h3, "gameid"); + if (!gameId) + throw createError({ + statusCode: 400, + statusMessage: "No gameID in route params", + }); + + const game = await prisma.game.findUnique({ + where: { id: gameId }, + select: { id: true }, + }); + if (!game) + throw createError({ statusCode: 400, statusMessage: "Invalid game ID" }); + + const body = await readDropValidatedBody(h3, PlaytimeBody); + const elapsedSeconds = body.seconds; + if (elapsedSeconds <= 0 || elapsedSeconds > maxElapsedSeconds) + throw createError({ + statusCode: 400, + statusMessage: `seconds must be between 1 and ${maxElapsedSeconds}.`, + }); + + return prisma.playtime.upsert({ + where: { + gameId_userId: { + gameId, + userId: user.id, + }, + }, + create: { + gameId, + userId: user.id, + seconds: elapsedSeconds, + }, + update: { + seconds: { increment: elapsedSeconds }, + }, + }); + }, +); diff --git a/server/server/api/v1/client/playtime/index.get.ts b/server/server/api/v1/client/playtime/index.get.ts new file mode 100644 index 000000000..1f7867daa --- /dev/null +++ b/server/server/api/v1/client/playtime/index.get.ts @@ -0,0 +1,23 @@ +import { ClientCapabilities } from "~/prisma/client/enums"; +import { defineClientEventHandler } from "~/server/internal/clients/event-handler"; +import prisma from "~/server/internal/db/database"; + +export default defineClientEventHandler( + async (h3, { fetchClient, fetchUser }) => { + const client = await fetchClient(); + if (!client.capabilities.includes(ClientCapabilities.TrackPlaytime)) + throw createError({ + statusCode: 403, + statusMessage: "Capability not allowed.", + }); + const user = await fetchUser(); + + const playtimes = await prisma.playtime.findMany({ + where: { + userId: user.id, + }, + }); + + return playtimes; + }, +); diff --git a/server/server/api/v1/games/[id]/index.get.ts b/server/server/api/v1/games/[id]/index.get.ts index 61293e331..88471491b 100644 --- a/server/server/api/v1/games/[id]/index.get.ts +++ b/server/server/api/v1/games/[id]/index.get.ts @@ -57,6 +57,9 @@ export default defineEventHandler(async (h3) => { }, tags: true, ageRatings: true, + playtime: { + where: { userId: user.id }, + }, }, }); diff --git a/server/server/internal/userlibrary/index.ts b/server/server/internal/userlibrary/index.ts index 6991b8a77..07aea579a 100644 --- a/server/server/internal/userlibrary/index.ts +++ b/server/server/internal/userlibrary/index.ts @@ -53,7 +53,13 @@ class UserLibraryManager { include: { entries: { where: gameFilter ? { game: gameFilter } : undefined, - include: { game: true }, + include: { + game: { + include: { + playtime: { where: { userId } }, + }, + }, + }, }, }, });