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
7 changes: 7 additions & 0 deletions desktop/main/pages/library/[id]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
>
{{ game.mName }}
</h1>
<p v-if="game.playtime[0]" class="mt-2 text-sm text-zinc-300">
Playtime: {{ formatPlaytime(game.playtime[0].seconds) }}
</p>
<div class="relative" v-if="status.type === 'Installed' && status.install_type.type != InstalledType.PartiallyInstalled">
<div
v-if="!version?.userConfiguration?.enableUpdates"
Expand Down Expand Up @@ -686,6 +689,10 @@ const bannerUrl = await useObject(game.mBannerObjectId);

const htmlDescription = micromark(game.mDescription);

function formatPlaytime(seconds: number): string {
return `${(seconds / 3600).toFixed(1)}h`;
}

const installFlowOpen = ref(false);
const versionOptions = ref<undefined | Array<VersionOption>>();
const installDirs = ref<undefined | Array<string>>();
Expand Down
1 change: 1 addition & 0 deletions desktop/main/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type Game = {
mCoverObjectId: string;
mImageLibraryObjectIds: string[];
mImageCarouselObjectIds: string[];
playtime: Array<{ seconds: number }>;
};

export type Collection = {
Expand Down
25 changes: 24 additions & 1 deletion desktop/src-tauri/games/src/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use database::{
};
use log::{debug, error, warn};
use remote::{
auth::generate_authorization_header, error::RemoteAccessError, requests::generate_url,
auth::generate_authorization_header, error::{DropServerError, RemoteAccessError}, requests::{generate_url, make_authenticated_post},
utils::DROP_CLIENT_ASYNC,
};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -51,6 +51,17 @@ pub struct Game {
pub m_image_library_object_ids: Vec<String>,
pub m_image_carousel_object_ids: Vec<String>,
pub library_path: String,
#[serde(default)]
pub playtime: Vec<GamePlaytime>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, Encode, Decode)]
#[serde(rename_all = "camelCase")]
pub struct GamePlaytime {
pub seconds: i32,
}
#[derive(Serialize)]
struct PlaytimeUpdate {
seconds: u64,
}
impl Game {
pub fn id(&self) -> &String {
Expand Down Expand Up @@ -268,6 +279,18 @@ pub async fn on_game_complete(
Ok(())
}

pub async fn report_playtime(game_id: String, seconds: u64) -> Result<(), RemoteAccessError> {
let endpoint = generate_url(&["/api/v1/client/playtime", &game_id], &[])?;
let response = make_authenticated_post(endpoint, &PlaytimeUpdate { seconds }).await?;

if !response.status().is_success() {
let error: DropServerError = response.json().await?;
return Err(RemoteAccessError::InvalidResponse(error));
}

Ok(())
}

pub fn push_game_update(
app_handle: &AppHandle,
game_id: &String,
Expand Down
14 changes: 13 additions & 1 deletion desktop/src-tauri/process/src/process_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use database::{
};
use dynfmt::Format;
use dynfmt::SimpleCurlyFormat;
use games::{library::push_game_update, state::GameStatusManager};
use games::{library::{push_game_update, report_playtime}, state::GameStatusManager};
use log::{debug, info, warn};
use serde::Serialize;
use shared_child::SharedChild;
Expand Down Expand Up @@ -205,6 +205,18 @@ impl ProcessManager<'_> {
Some(version_data.clone()),
status,
);

let elapsed_seconds = elapsed.as_secs();
Comment thread
DecDuck marked this conversation as resolved.
if elapsed_seconds > 2 {
let playtime_game_id = game_id.clone();
tauri::async_runtime::spawn(async move {
if let Err(error) = report_playtime(playtime_game_id.clone(), elapsed_seconds).await
{
warn!("failed to report playtime for game {playtime_game_id}: {error}");
}
});
}

Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/remote/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
capabilities: HashMap::from([
("peerAPI".to_owned(), CapabilityConfiguration {}),
("cloudSaves".to_owned(), CapabilityConfiguration {}),
("trackPlaytime".to_owned(), CapabilityConfiguration {}),
]),
mode,
};
Expand Down
9 changes: 9 additions & 0 deletions desktop/src-tauri/remote/src/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,12 @@ pub async fn make_authenticated_get(url: Url) -> Result<reqwest::Response, Error
.send()
.await
}

pub async fn make_authenticated_post<T: serde::Serialize>(url: Url, body: &T) -> Result<reqwest::Response, Error> {
DROP_CLIENT_ASYNC
.post(url)
.header("Authorization", generate_authorization_header())
.json(body)
.send()
.await
}
1 change: 1 addition & 0 deletions server/i18n/locales/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@
"launcherOpen": "Open in Launcher",
"noGames": "No games in library",
"notFound": "Game not found",
"playtime": "Playtime: {time} hours",
"search": "Search library…",
"subheader": "Organize your games into collections for easy access, and access all your games."
},
Expand Down
11 changes: 11 additions & 0 deletions server/pages/library/game/[id]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
>
{{ game.mName }}
</h1>
<p v-if="game.playtime?.[0]" class="mt-2 text-sm text-zinc-300">
{{
$t("library.playtime", {
time: formatPlaytime(game.playtime[0].seconds),
})
}}
</p>
<p class="mt-4 mb-8 text-sm sm:text-lg text-zinc-400 max-w-3xl">
{{ game.mShortDescription }}
</p>
Expand Down Expand Up @@ -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() {
Expand Down
44 changes: 44 additions & 0 deletions server/server/api/v1/client/playtime/[gameid]/index.get.ts
Original file line number Diff line number Diff line change
@@ -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;
},
);
60 changes: 60 additions & 0 deletions server/server/api/v1/client/playtime/[gameid]/index.post.ts
Original file line number Diff line number Diff line change
@@ -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 },
},
});
},
);
23 changes: 23 additions & 0 deletions server/server/api/v1/client/playtime/index.get.ts
Original file line number Diff line number Diff line change
@@ -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;
},
);
3 changes: 3 additions & 0 deletions server/server/api/v1/games/[id]/index.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export default defineEventHandler(async (h3) => {
},
tags: true,
ageRatings: true,
playtime: {
where: { userId: user.id },
},
},
});

Expand Down
8 changes: 7 additions & 1 deletion server/server/internal/userlibrary/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ class UserLibraryManager {
include: {
entries: {
where: gameFilter ? { game: gameFilter } : undefined,
include: { game: true },
include: {
game: {
include: {
playtime: { where: { userId } },
},
},
},
},
},
});
Expand Down
Loading