v2 download API and Admin UI fixes (#177)

* fix: small ui fixes

* feat: #171

* fix: improvements to library scanning on admin UI

* feat: v2 download API

* fix: add download context cleanup

* fix: lint
This commit is contained in:
DecDuck
2025-08-09 15:45:39 +10:00
committed by GitHub
parent 359e1412a8
commit 3c5fe2e651
17 changed files with 504 additions and 44 deletions
@@ -1,9 +1,68 @@
/*
The download co-ordinator's job is to keep track of all the currently online clients.
import prisma from "../db/database";
import type { DropManifest } from "./manifest";
When a client signs on and registers itself as a peer
const TIMEOUT = 1000 * 60 * 60 * 1; // 1 hour
*/
class DownloadContextManager {
private contexts: Map<
string,
{
timeout: Date;
manifest: DropManifest;
versionName: string;
libraryId: string;
libraryPath: string;
}
> = new Map();
// eslint-disable-next-line @typescript-eslint/no-extraneous-class, @typescript-eslint/no-unused-vars
class DownloadCoordinator {}
async createContext(game: string, versionName: string) {
const version = await prisma.gameVersion.findUnique({
where: {
gameId_versionName: {
gameId: game,
versionName,
},
},
include: {
game: {
select: {
libraryId: true,
libraryPath: true,
},
},
},
});
if (!version) return undefined;
const contextId = crypto.randomUUID();
this.contexts.set(contextId, {
timeout: new Date(),
manifest: JSON.parse(version.dropletManifest as string) as DropManifest,
versionName,
libraryId: version.game.libraryId!,
libraryPath: version.game.libraryPath,
});
return contextId;
}
async fetchContext(contextId: string) {
const context = this.contexts.get(contextId);
if (!context) return undefined;
context.timeout = new Date();
this.contexts.set(contextId, context);
return context;
}
async cleanup() {
for (const key of this.contexts.keys()) {
const context = this.contexts.get(key)!;
if (context.timeout.getDate() + TIMEOUT < Date.now()) {
this.contexts.delete(key);
}
}
}
}
export const contextManager = new DownloadContextManager();
export default contextManager;
+1 -1
View File
@@ -5,7 +5,7 @@ export type DropChunk = {
permissions: number;
ids: string[];
checksums: string[];
lengths: string[];
lengths: number[];
};
export type DropManifest = {
+23 -14
View File
@@ -13,6 +13,7 @@ import { parsePlatform } from "../utils/parseplatform";
import notificationSystem from "../notifications";
import { GameNotFoundError, type LibraryProvider } from "./provider";
import { logger } from "../logging";
import type { GameModel } from "~/prisma/client/models";
class LibraryManager {
private libraries: Map<string, LibraryProvider<unknown>> = new Map();
@@ -37,24 +38,32 @@ class LibraryManager {
return libraryWithMetadata;
}
async fetchGamesByLibrary() {
const results: { [key: string]: { [key: string]: GameModel } } = {};
const games = await prisma.game.findMany({});
for (const game of games) {
const libraryId = game.libraryId!;
const libraryPath = game.libraryPath!;
results[libraryId] ??= {};
results[libraryId][libraryPath] = game;
}
return results;
}
async fetchUnimportedGames() {
const unimportedGames: { [key: string]: string[] } = {};
const instanceGames = await this.fetchGamesByLibrary();
for (const [id, library] of this.libraries.entries()) {
const games = await library.listGames();
const validGames = await prisma.game.findMany({
where: {
libraryId: id,
libraryPath: { in: games },
},
select: {
libraryPath: true,
},
});
const providerUnimportedGames = games.filter(
(e) =>
validGames.findIndex((v) => v.libraryPath == e) == -1 &&
!(this.gameImportLocks.get(id) ?? []).includes(e),
const providerGames = await library.listGames();
const locks = this.gameImportLocks.get(id) ?? [];
const providerUnimportedGames = providerGames.filter(
(libraryPath) =>
instanceGames[id] &&
!instanceGames[id][libraryPath] &&
!locks.includes(libraryPath),
);
unimportedGames[id] = providerUnimportedGames;
}