feat: Implemented download page for downloading roms from various sources using plugins. Added support for internet archive external plugin. feat: Added tasks page to track running tasks/downloads feat: Added tanstack caching feat: Added quick play action Fixes #6 feat: Added quick emulator launch action fix: Made task queue only support 1 task per group and task ID should now be unique
124 lines
No EOL
4.7 KiB
TypeScript
124 lines
No EOL
4.7 KiB
TypeScript
import { IJob, JobContext } from "@simeonradivoev/gameflow-sdk/task-queue";
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { config, events, plugins } from "../app";
|
|
import { simulateProgress } from "@/bun/utils";
|
|
import z from "zod";
|
|
import { checkFiles, createLocalGame, downloadGame } from "../games/services/utils";
|
|
import { ensureDir } from "fs-extra";
|
|
import { DownloadInfo, DownloadJobData } from "@simeonradivoev/gameflow-sdk/shared";
|
|
|
|
interface JobConfig
|
|
{
|
|
dryRun?: boolean;
|
|
dryDownload?: boolean;
|
|
downloadId?: string;
|
|
}
|
|
|
|
export type InstallJobStates = 'download' | 'extract';
|
|
|
|
export class InstallJob implements IJob<DownloadJobData, InstallJobStates>
|
|
{
|
|
static id = "install-job" as const;
|
|
static query = (q: { source: string; id: string; }) => `${InstallJob.id}-${q.source}-${q.id}`;
|
|
static dataSchema = z.never();
|
|
public gameId: string;
|
|
public source: string;
|
|
public config?: JobConfig;
|
|
// The local game ID of newly created entry, if successful
|
|
public localGameId?: number;
|
|
public group = InstallJob.id;
|
|
public localPath?: string;
|
|
data: DownloadJobData = {
|
|
name: "Install Game"
|
|
};
|
|
|
|
constructor(id: string, source: string, config?: JobConfig)
|
|
{
|
|
this.gameId = id;
|
|
this.config = config;
|
|
this.source = source;
|
|
}
|
|
|
|
public async start (cx: JobContext<InstallJob, DownloadJobData, InstallJobStates>)
|
|
{
|
|
cx.setProgress(0, 'download');
|
|
await fs.mkdir(config.get('downloadPath'), { recursive: true });
|
|
|
|
const downloadPath = config.get('downloadPath');
|
|
const finalFiles: string[] = [];
|
|
let info: DownloadInfo | undefined;
|
|
|
|
if (this.config?.dryRun !== true)
|
|
{
|
|
const allDownloads = await plugins.hooks.games.fetchDownloads.promise({ source: this.source, id: this.gameId, downloadId: this.config?.downloadId });
|
|
info = allDownloads?.[0];
|
|
|
|
if (!info) throw new Error(`Could not find downloader for source ${this.source}`);
|
|
|
|
this.data.name = info.name;
|
|
this.data.preview_url = info.coverUrl;
|
|
|
|
const files = await checkFiles(info.files, !!info.extract_path);
|
|
|
|
if (this.config?.dryDownload !== true && files.some(f => !f.exists || !f.matches))
|
|
{
|
|
const downloadedFiles = await downloadGame({
|
|
downloads: files.filter(f => !f.exists || !f.matches),
|
|
extract_path: info.extract_path,
|
|
path_fs: info.path_fs,
|
|
abortSignal: cx.abortSignal,
|
|
auth: info.auth,
|
|
id: `game-${this.source}-${this.gameId}`,
|
|
setProgress: (process, state, info) =>
|
|
{
|
|
cx.setProgress(process, state);
|
|
this.data.downloaded = info.downloaded;
|
|
this.data.speed = info.speed;
|
|
this.data.total = info.total;
|
|
},
|
|
});
|
|
|
|
if (downloadedFiles)
|
|
finalFiles.push(...downloadedFiles);
|
|
}
|
|
|
|
if (this.config?.dryDownload === true && info.extract_path)
|
|
{
|
|
await ensureDir(path.join(downloadPath, info.extract_path));
|
|
}
|
|
|
|
const coverResponse = await fetch(info.coverUrl);
|
|
const cover = Buffer.from(await coverResponse.arrayBuffer());
|
|
|
|
cx.abortSignal.throwIfAborted();
|
|
|
|
this.localGameId = await createLocalGame({
|
|
cover,
|
|
coverType: coverResponse.headers.get('content-type'),
|
|
system_slug: info.system_slug,
|
|
source_id: info.source_id,
|
|
source: this.source,
|
|
slug: info.slug,
|
|
path_fs: info.path_fs ?? (info.extract_path ? path.join(downloadPath, info.extract_path) : undefined),
|
|
summary: info.summary,
|
|
igdb_id: info.igdb_id,
|
|
ra_id: info.ra_id,
|
|
name: info.name,
|
|
main_glob: info.main_glob,
|
|
version: info.version,
|
|
version_source: info.version_source,
|
|
screenshotUrls: info.screenshotUrls,
|
|
version_system: info.version_system,
|
|
metadata: info.metadata,
|
|
platform: info.platform
|
|
});
|
|
|
|
if (this.source && this.gameId) await plugins.hooks.games.postInstall.promise({ source: this.source, id: this.gameId, files: finalFiles, info });
|
|
events.emit('notification', { message: `${info.name}: Installed`, type: 'success', duration: 8000 });
|
|
} else
|
|
{
|
|
await simulateProgress(p => cx.setProgress(p, "download"), cx.abortSignal);
|
|
}
|
|
}
|
|
} |