fix: Fixed a bunch of issues on linux
fix: Removed archive when unzipping with stream zip fallback
This commit is contained in:
parent
7065e64722
commit
6aacec2c0d
22 changed files with 236 additions and 83 deletions
|
|
@ -451,7 +451,7 @@ export default new Elysia()
|
|||
}, {
|
||||
params: z.object({ id: z.string(), source: z.string() }),
|
||||
})
|
||||
.post('/game/:source/:id/install', async ({ params: { id, source }, query: { downloadId } }) =>
|
||||
.post('/game/:source/:id/install', async ({ params: { id, source }, body: { downloadId } }) =>
|
||||
{
|
||||
if (!taskQueue.findJob(InstallJob.query({ source, id }), InstallJob))
|
||||
{
|
||||
|
|
@ -462,7 +462,7 @@ export default new Elysia()
|
|||
}
|
||||
}, {
|
||||
params: z.object({ id: z.string(), source: z.string() }),
|
||||
query: z.object({ downloadId: z.string().optional() }),
|
||||
body: z.object({ downloadId: z.string().optional() }),
|
||||
response: z.any()
|
||||
})
|
||||
.delete('/game/:source/:id/install', async ({ params: { id, source } }) =>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import { getErrorMessage } from "@/bun/utils";
|
|||
import { checkFiles, getLocalGameMatch, getSourceGameDetailed } from "./utils";
|
||||
import fs from 'node:fs/promises';
|
||||
import Elysia from "elysia";
|
||||
import z from "zod";
|
||||
import z, { string } from "zod";
|
||||
import { InstallJob, InstallJobStates } from "../../jobs/install-job";
|
||||
import { LaunchGameJob } from "../../jobs/launch-game-job";
|
||||
import * as appSchema from "@schema/app";
|
||||
import { RPC_URL } from "@/shared/constants";
|
||||
import { DownloadSourceSchema, RPC_URL } from "@/shared/constants";
|
||||
import { host } from "@/bun/utils/host";
|
||||
|
||||
export class CommandSearchError extends Error
|
||||
|
|
@ -205,7 +205,7 @@ export default function buildStatusResponse ()
|
|||
z.object({ status: z.literal('refresh'), localId: z.number().optional() }),
|
||||
z.object({ status: z.literal(['queued']) }),
|
||||
z.object({ status: z.literal('playing'), details: z.string() }),
|
||||
z.object({ status: z.literal('install'), details: z.string() }),
|
||||
z.object({ status: z.literal('install'), details: z.string(), sources: DownloadSourceSchema.array() }),
|
||||
z.object({ status: z.literal('present'), details: z.string() }),
|
||||
z.object({ status: z.literal(['download', 'extract']), progress: z.number() }),
|
||||
]),
|
||||
|
|
@ -261,6 +261,8 @@ export default function buildStatusResponse ()
|
|||
|
||||
} else if (!localGame && ws.data.params.source === 'store')
|
||||
{
|
||||
const downloads = await plugins.hooks.games.fetchDownloads.promise({ source: ws.data.params.source, id: ws.data.params.id });
|
||||
const sources = downloads?.map(d => ({ id: d.id, name: d.id })) ?? [];
|
||||
/*const storeGame = await getStoreGame(ws.data.params.id);
|
||||
const fileResponse = await fetch(storeGame.file, { method: 'HEAD' });
|
||||
const size = Number(fileResponse.headers.get('content-length'));
|
||||
|
|
@ -274,19 +276,20 @@ export default function buildStatusResponse ()
|
|||
ws.send({ status: 'install', details: 'Install' });
|
||||
}*/
|
||||
|
||||
ws.send({ status: 'install', details: 'Install' });
|
||||
ws.send({ status: 'install', details: 'Install', sources });
|
||||
} else if (!localGame)
|
||||
{
|
||||
const files = await plugins.hooks.games.fetchDownloads.promise({
|
||||
source: ws.data.params.source,
|
||||
id: ws.data.params.id
|
||||
});
|
||||
const sources = files?.map(d => ({ id: d.id, name: d.id })) ?? [];
|
||||
|
||||
let filesChecked: LocalDownloadFileEntry[] | undefined;
|
||||
|
||||
if (files)
|
||||
if (files && files.length)
|
||||
{
|
||||
filesChecked = await checkFiles(files.files, !!files.extract_path);
|
||||
filesChecked = await checkFiles(files[0].files, !!files[0].extract_path);
|
||||
}
|
||||
|
||||
if (filesChecked && !filesChecked.some(f => f.exists === false || f.matches === false))
|
||||
|
|
@ -301,11 +304,11 @@ export default function buildStatusResponse ()
|
|||
ws.send({ status: 'error', error: "Not Enough Free Space" });
|
||||
} else if (filesChecked?.some(f => f.exists === true && f.matches === false))
|
||||
{
|
||||
ws.send({ status: 'install', details: 'Some Files Present, Install' });
|
||||
ws.send({ status: 'install', details: 'Some Files Present, Install', sources });
|
||||
}
|
||||
else
|
||||
{
|
||||
ws.send({ status: 'install', details: 'Install' });
|
||||
ws.send({ status: 'install', details: 'Install', sources });
|
||||
}
|
||||
}
|
||||
} else
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export class GameHooks
|
|||
source: string;
|
||||
id: string;
|
||||
downloadId?: string;
|
||||
}], DownloadInfo | undefined>(['ctx']);
|
||||
}], DownloadInfo[] | undefined>(['ctx']);
|
||||
fetchRomFiles = new AsyncSeriesBailHook<[ctx: {
|
||||
source: string;
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ export class InstallJob implements IJob<never, InstallJobStates>
|
|||
const downloadPath = config.get('downloadPath');
|
||||
let info: DownloadInfo | undefined;
|
||||
|
||||
info = await plugins.hooks.games.fetchDownloads.promise({ source: this.source, id: this.gameId, downloadId: this.config?.downloadId });
|
||||
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}`);
|
||||
|
||||
|
|
@ -137,12 +138,21 @@ export class InstallJob implements IJob<never, InstallJobStates>
|
|||
{
|
||||
if (filePath.endsWith('.zip'))
|
||||
{
|
||||
cx.setProgress(0, "extract");
|
||||
console.warn("Could not extract", filePath, "with 7zip trying zip extractor");
|
||||
await ensureDir(extractPath);
|
||||
const zip = new StreamZip.async({ file: filePath });
|
||||
let entryCount = await zip.entriesCount;
|
||||
let entryCounter = entryCount;
|
||||
zip.on('extract', (entry, outPath) =>
|
||||
{
|
||||
entryCounter--;
|
||||
cx.setProgress(progress + (1 - (entryCounter / entryCount)) * 100 * progressDelta, "extract");
|
||||
});
|
||||
const count = await zip.extract(null, extractPath);
|
||||
console.log(`Extracted ${count} entries`);
|
||||
await zip.close();
|
||||
await fs.rm(filePath);
|
||||
} else
|
||||
{
|
||||
throw e;
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
|
|||
|
||||
// ES-DE commands require shell execution. Some emulators fail otherwise.
|
||||
const spawnGame = spawn(this.validCommand.command, {
|
||||
shell: true,
|
||||
shell: this.validCommand.shell ?? true,
|
||||
cwd: this.validCommand.startDir,
|
||||
signal: context.abortSignal,
|
||||
env: {
|
||||
|
|
@ -151,6 +151,7 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
|
|||
await this.prePlay(context.setProgress.bind(context), { platformSlug: gameInfo?.platformSlug });
|
||||
|
||||
// We have full control over launching integrated emulators better to use bun spawn
|
||||
await fs.chmod(this.validCommand.metadata.emulatorBin, 0o755);
|
||||
const bunGame = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs.args], {
|
||||
cwd: this.validCommand.startDir,
|
||||
signal: context.abortSignal,
|
||||
|
|
|
|||
|
|
@ -3,16 +3,13 @@ import desc from './package.json';
|
|||
import { config, events } from "@/bun/api/app";
|
||||
import path, { dirname } from 'node:path';
|
||||
import unzip from 'unzip-stream';
|
||||
import { ensureDir } from "fs-extra";
|
||||
import { chmodSync, ensureDir } from "fs-extra";
|
||||
import { Readable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import fs from 'node:fs/promises';
|
||||
import { randomUUIDv7, sleep } from "bun";
|
||||
import z from "zod";
|
||||
import { createInterface } from "node:readline";
|
||||
import { redirect } from "elysia";
|
||||
import { getErrorMessage } from "@/bun/utils";
|
||||
import { id } from "zod/v4/locales";
|
||||
|
||||
const SettingsSchema = z.object({
|
||||
runWebGui: z.boolean()
|
||||
|
|
@ -75,7 +72,13 @@ export default class RcloneIntegration implements PluginType<SettingsType>
|
|||
ctx.zodRegistry.add(SettingsSchema.shape.globalConfig, { requiresRestart: true });
|
||||
|
||||
const toolsPath = path.join(config.get('downloadPath'), "tools");
|
||||
const existingRclones = await Array.fromAsync(fs.glob('**/rclone.exe', { cwd: toolsPath }));
|
||||
await ensureDir(toolsPath);
|
||||
const binaryMap: Record<string, string> = {
|
||||
win32: '**/rclone.exe',
|
||||
linux: '**/rclone',
|
||||
darwin: '**/rclone'
|
||||
};
|
||||
const existingRclones = await Array.fromAsync(fs.glob(binaryMap[process.platform], { cwd: toolsPath }));
|
||||
if (existingRclones[0])
|
||||
{
|
||||
this.rclonePath = path.join(toolsPath, existingRclones[0]);
|
||||
|
|
@ -83,13 +86,19 @@ export default class RcloneIntegration implements PluginType<SettingsType>
|
|||
return;
|
||||
}
|
||||
|
||||
if (await fs.exists(path.join(toolsPath, 'rclone-current-windows-amd64')))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.setProgress(0.5, "Downloading RClone");
|
||||
const rcCloseZip = await fetch(`https://downloads.rclone.org/rclone-current-windows-amd64.zip`);
|
||||
const platformMap: Record<string, string> = {
|
||||
linux: "linux",
|
||||
win32: "windows",
|
||||
darwin: "osx"
|
||||
};
|
||||
const archMap: Record<string, string> = {
|
||||
x64: "amd64",
|
||||
arm64: "arm64"
|
||||
};
|
||||
const downloadUrl = `https://downloads.rclone.org/rclone-current-${platformMap[process.platform]}-${archMap[process.arch]}.zip`;
|
||||
console.log("Starting Download", downloadUrl);
|
||||
const rcCloseZip = await fetch(downloadUrl);
|
||||
|
||||
await ensureDir(toolsPath);
|
||||
await pipeline(Readable.fromWeb(rcCloseZip.body as any), unzip.Extract({ path: toolsPath }));
|
||||
|
|
@ -97,6 +106,7 @@ export default class RcloneIntegration implements PluginType<SettingsType>
|
|||
if (dests[0])
|
||||
{
|
||||
this.rclonePath = path.join(toolsPath, dests[0]);
|
||||
await fs.chmod(this.rclonePath, 0o755);
|
||||
await this.startServer(ctx);
|
||||
return;
|
||||
}
|
||||
|
|
@ -139,7 +149,12 @@ export default class RcloneIntegration implements PluginType<SettingsType>
|
|||
if (data.level === 'error')
|
||||
{
|
||||
console.error(data.msg);
|
||||
} else
|
||||
} else if (data.level === 'critical')
|
||||
{
|
||||
console.error(data.msg);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
console.log(e);
|
||||
if (loginTokenUrlRegex.test(data.msg))
|
||||
|
|
@ -150,7 +165,7 @@ export default class RcloneIntegration implements PluginType<SettingsType>
|
|||
|
||||
});
|
||||
|
||||
await new Promise((resolve) =>
|
||||
await new Promise((resolve, reject) =>
|
||||
{
|
||||
const handleResolve = (line: string) =>
|
||||
{
|
||||
|
|
@ -160,6 +175,7 @@ export default class RcloneIntegration implements PluginType<SettingsType>
|
|||
resolve(data);
|
||||
};
|
||||
rl.on('line', handleResolve);
|
||||
setTimeout(() => { reject("Timeout"); }, 5000);
|
||||
});
|
||||
|
||||
await this.refresh();
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
release: "metadatum.first_release_date"
|
||||
};
|
||||
|
||||
async checkRemote ()
|
||||
{
|
||||
if (!config.has('rommAddress')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async updateClient ()
|
||||
{
|
||||
client.setConfig({
|
||||
|
|
@ -141,6 +147,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.fetchGames.tapPromise(desc.name, async ({ query, games }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
if (((!query.platform_source || query.platform_source === 'romm') || !!query.collection_id) && (!query.source || query.source === 'romm'))
|
||||
{
|
||||
|
||||
|
|
@ -173,6 +180,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.fetchFilters.tapPromise(desc.name, async ({ filters, source }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
if (source && source !== 'romm') return;
|
||||
|
||||
const rommFilters = await getRomFiltersApiRomsFiltersGet({ throwOnError: true });
|
||||
|
|
@ -185,12 +193,14 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.auth.loginComplete.tapPromise(desc.name, async ({ service }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
if (service !== 'romm') return;
|
||||
await this.updateClient();
|
||||
});
|
||||
|
||||
ctx.hooks.games.fetchGame.tapPromise(desc.name, async ({ source, id }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
if (source !== 'romm') return;
|
||||
|
||||
const rom = await getRomApiRomsIdGet({ path: { id: Number(id) } });
|
||||
|
|
@ -205,6 +215,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.fetchDownloads.tapPromise(desc.name, async ({ source, id }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
if (source !== 'romm') return;
|
||||
|
||||
const rom = (await getRomApiRomsIdGet({ path: { id: Number(id) }, throwOnError: true })).data;
|
||||
|
|
@ -260,12 +271,13 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
extract_path
|
||||
};
|
||||
|
||||
return info;
|
||||
return [info];
|
||||
|
||||
});
|
||||
|
||||
ctx.hooks.emulators.fetchBiosDownload.tapPromise(desc.name, async ({ systems, biosFolder }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
const files: DownloadFileEntry[] = [];
|
||||
const allRommPlatforms = await this.getAllRommPlatforms();
|
||||
|
||||
|
|
@ -296,6 +308,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.fetchRecommendedGamesForGame.tapPromise(desc.name, async ({ game, games }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
const rommPlatforms = await this.getAllRommPlatforms();
|
||||
if (rommPlatforms)
|
||||
{
|
||||
|
|
@ -313,7 +326,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.fetchRecommendedGamesForEmulator.tapPromise(desc.name, async ({ emulator, games, systems }) =>
|
||||
{
|
||||
|
||||
if (!await this.checkRemote()) return;
|
||||
const rommPlatforms = await this.getAllRommPlatforms();
|
||||
const systemsRommSlugSet = new Set(systems.filter(s => s.romm_slug).map(s => s.romm_slug!));
|
||||
if (rommPlatforms)
|
||||
|
|
@ -343,6 +356,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.fetchPlatform.tapPromise(desc.name, async ({ source, id }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
if (source !== 'romm') return;
|
||||
const { data: rommPlatform } = await getPlatformApiPlatformsIdGet({ path: { id: Number(id) } });
|
||||
if (rommPlatform)
|
||||
|
|
@ -365,7 +379,13 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.fetchPlatforms.tapPromise(desc.name, async ({ platforms }) =>
|
||||
{
|
||||
const rommPlatforms = await this.getAllRommPlatforms();
|
||||
if (!await this.checkRemote()) return;
|
||||
const rommPlatforms = await this.getAllRommPlatforms().catch(e =>
|
||||
{
|
||||
console.error(e);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (rommPlatforms)
|
||||
{
|
||||
const frontEndPlatforms = await Promise.all(rommPlatforms.map(async p =>
|
||||
|
|
@ -401,6 +421,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.prePlay.tapPromise(desc.name, async ({ source, id, saveFolderSlots, setProgress }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
if (source !== 'romm' || !ctx.config.get('savesSync')) return;
|
||||
if (!saveFolderSlots) return;
|
||||
|
||||
|
|
@ -445,6 +466,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
// Should run after emulators decide on saves
|
||||
ctx.hooks.games.postPlay.tapPromise({ name: desc.name, stage: 10 }, async ({ source, id, validChangedSaveFiles, command }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
if (source !== 'romm' || !ctx.config.get('savesSync')) return;
|
||||
|
||||
const sourceValidation = await validateGameSource(source, id);
|
||||
|
|
@ -529,6 +551,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.fetchCollections.tapPromise(desc.name, async ({ collections }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
const rommCollections = await getCollectionsApiCollectionsGet();
|
||||
if (rommCollections.response.ok && rommCollections.data)
|
||||
{
|
||||
|
|
@ -549,6 +572,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.fetchCollection.tapPromise(desc.name, async ({ source, id }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
if (source !== 'romm') return;
|
||||
const collection = await getCollectionApiCollectionsIdGet({ path: { id: Number(id) } });
|
||||
if (collection.data)
|
||||
|
|
@ -567,6 +591,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.platformLookup.tapPromise(desc.name, async ({ source, id, slug }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
let platform: PlatformSchema | undefined = undefined;
|
||||
|
||||
if (id && source)
|
||||
|
|
@ -587,6 +612,7 @@ export default class RommIntegration implements PluginType<SettingsType>
|
|||
|
||||
ctx.hooks.games.searchGame.tapPromise(desc.name, async ({ source, igdb_id, ra_id }) =>
|
||||
{
|
||||
if (!await this.checkRemote()) return;
|
||||
if (source !== 'romm') return;
|
||||
const roms = await getRomByMetadataProviderApiRomsByMetadataProviderGet({ query: { igdb_id, ra_id } });
|
||||
if (roms.error) throw roms.error;
|
||||
|
|
|
|||
|
|
@ -50,16 +50,16 @@ function convertStoreMediaToPath (c: string)
|
|||
|
||||
export async function convertStoreToFrontend (id: string, storeGame: StoreGameType): Promise<FrontEndGameType>
|
||||
{
|
||||
const validDownload = getValidDownload(storeGame);
|
||||
const validDownloads = getValidDownloads(storeGame);
|
||||
|
||||
let platform_slug: string | null = null;
|
||||
let platform_id: number | null = null;
|
||||
let platform_display_name: string | null = null;
|
||||
let path_platform_cover: string | null = null;
|
||||
|
||||
if (validDownload?.system)
|
||||
if (validDownloads.length > 0 && validDownloads[0].system)
|
||||
{
|
||||
let system = validDownload.system.split(':')[0];
|
||||
let system = validDownloads[0].system.split(':')[0];
|
||||
if (system === 'win32') system = 'win';
|
||||
|
||||
const localPlatform = await db.query.platforms.findFirst({ where: eq(appSchema.platforms.slug, system), columns: { id: true, slug: true, name: true } });
|
||||
|
|
@ -130,13 +130,13 @@ export async function convertStoreToFrontend (id: string, storeGame: StoreGameTy
|
|||
|
||||
export async function convertStoreToFrontendDetailed (id: string, storeGame: StoreGameType): Promise<FrontEndGameTypeDetailed>
|
||||
{
|
||||
const validDownload = getValidDownload(storeGame);
|
||||
const validDownloads = getValidDownloads(storeGame);
|
||||
let size: number | null = null;
|
||||
if (validDownload?.url)
|
||||
if (validDownloads.length > 0 && validDownloads[0].url)
|
||||
{
|
||||
try
|
||||
{
|
||||
const fileResponse = await fetch(validDownload?.url, { method: 'HEAD' });
|
||||
const fileResponse = await fetch(validDownloads[0]?.url, { method: 'HEAD' });
|
||||
size = Number(fileResponse.headers.get('content-length'));
|
||||
} catch (error)
|
||||
{
|
||||
|
|
@ -167,25 +167,32 @@ export async function convertStoreToFrontendDetailed (id: string, storeGame: Sto
|
|||
return detailed;
|
||||
}
|
||||
|
||||
export function getValidDownload (game: StoreGameType, downloadId?: string)
|
||||
export function getValidDownloads (game: StoreGameType, downloadId?: string)
|
||||
{
|
||||
const downloads = Object.entries(game.downloads).map(([k, d]) => ({ id: k, ...d }));
|
||||
const supportedDownloads = downloads.filter(d => d.type === 'direct');
|
||||
|
||||
if (downloadId)
|
||||
{
|
||||
return supportedDownloads.find(d => d.id === downloadId);
|
||||
return supportedDownloads.filter(d => d.id === downloadId);
|
||||
} else
|
||||
{
|
||||
return supportedDownloads.find(d => d.system === `${process.platform}:${process.arch}`)
|
||||
?? supportedDownloads.find(d =>
|
||||
{
|
||||
// Linux supports proton, can run windows games
|
||||
if (process.platform === 'linux') return d.system === `win32:${process.arch}`;
|
||||
return false;
|
||||
})
|
||||
// Fallback to emulator platforms
|
||||
?? supportedDownloads.find(d => !d.system.includes(':'));
|
||||
return supportedDownloads.filter(d =>
|
||||
{
|
||||
if (d.system === `${process.platform}:${process.arch}`) return true;
|
||||
|
||||
// TODO: Add linux proton support
|
||||
//if (process.platform === 'linux' && d.system === `win32:${process.arch}`) return true;
|
||||
|
||||
// emulator fallback
|
||||
return !d.system.includes(':');
|
||||
}).toSorted((a, b) =>
|
||||
{
|
||||
const bScore = b.system.includes(':') ? 0 : 1;
|
||||
const aScore = a.system.includes(':') ? 0 : 1;
|
||||
|
||||
return bScore - aScore;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -283,7 +290,8 @@ export async function convertStoreEmulatorToFrontend (emulator: EmulatorPackageT
|
|||
systems,
|
||||
gameCount: 0,
|
||||
validSources: execPaths,
|
||||
integrations: []
|
||||
integrations: [],
|
||||
source: "store"
|
||||
};
|
||||
|
||||
return em;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,17 @@
|
|||
import { PluginLoadingContextType, PluginType } from "@/bun/types/typesc.schema";
|
||||
import desc from './package.json';
|
||||
import path, { basename, dirname } from 'node:path';
|
||||
import { StoreDownloadType, StoreGameSchema, StoreGameType } from "@/shared/constants";
|
||||
import { buildStoreFrontendEmulatorSystems, getAllStoreEmulatorPackages, getStoreEmulatorPackage, getStoreFolder } from "@/bun/api/store/services/gamesService";
|
||||
import { Glob, pathToFileURL } from "bun";
|
||||
import { getOrCached } from "@/bun/api/cache";
|
||||
import { shuffleInPlace } from "@/bun/utils";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import * as emulatorSchema from '@schema/emulators';
|
||||
|
||||
import { config, db, emulatorsDb, plugins, taskQueue } from "@/bun/api/app";
|
||||
import { config, emulatorsDb, taskQueue } from "@/bun/api/app";
|
||||
import fs from "node:fs/promises";
|
||||
import { getSourceGameDetailed } from "@/bun/api/games/services/utils";
|
||||
import mustache from "mustache";
|
||||
import os from 'node:os';
|
||||
import UpdateStoreJob from "@/bun/api/jobs/update-store";
|
||||
import { getEmulatorDownload } from "@/bun/api/store/services/emulatorsService";
|
||||
import { buildFilters, buildSaves, convertStoreEmulatorToFrontend, convertStoreToFrontend, convertStoreToFrontendDetailed, getExistingStoreEmulatorDownload, getShuffledStoreGames, getStoreGame, getValidDownload } from "./services";
|
||||
import { buildFilters, buildSaves, convertStoreEmulatorToFrontend, convertStoreToFrontend, convertStoreToFrontendDetailed, getExistingStoreEmulatorDownload, getShuffledStoreGames, getStoreGame, getValidDownloads } from "./services";
|
||||
|
||||
export default class RommIntegration implements PluginType
|
||||
{
|
||||
|
|
@ -29,11 +24,13 @@ export default class RommIntegration implements PluginType
|
|||
|
||||
async load (ctx: PluginLoadingContextType)
|
||||
{
|
||||
await this.setup(ctx);
|
||||
|
||||
ctx.hooks.store.fetchDownload.tapPromise(desc.name, async ({ id }) =>
|
||||
{
|
||||
const emulatorPackage = await getStoreEmulatorPackage(id);
|
||||
const downloadInfo = await getExistingStoreEmulatorDownload(emulatorPackage!);
|
||||
if (!emulatorPackage) return;
|
||||
const downloadInfo = await getExistingStoreEmulatorDownload(emulatorPackage);
|
||||
return downloadInfo;
|
||||
});
|
||||
|
||||
|
|
@ -131,7 +128,7 @@ export default class RommIntegration implements PluginType
|
|||
|
||||
ctx.hooks.games.buildLaunchCommands.tapPromise({ name: desc.name, before: 'com.simeonradivoev.gameflow.es' }, async ({ gamePath, source, sourceId, systemSlug, mainGlob }) =>
|
||||
{
|
||||
if (source !== 'store' || !gamePath || systemSlug !== 'win') return;
|
||||
if (source !== 'store' || !gamePath) return;
|
||||
const downloadPath = config.get('downloadPath');
|
||||
const gamePathAbsolute = path.join(downloadPath, gamePath);
|
||||
if (!(await fs.exists(gamePathAbsolute))) return;
|
||||
|
|
@ -139,13 +136,15 @@ export default class RommIntegration implements PluginType
|
|||
|
||||
if (gamePathStat.isDirectory())
|
||||
{
|
||||
if (!mainGlob && systemSlug !== 'win') return;
|
||||
const fileGlob = new Glob(mainGlob ?? '**/*.exe');
|
||||
for await (const file of fileGlob.scan({ cwd: path.join(downloadPath, gamePath) }))
|
||||
{
|
||||
return [{
|
||||
startDir: path.join(downloadPath, gamePath, dirname(file)),
|
||||
command: basename(file),
|
||||
id: 'store-win',
|
||||
command: `./${basename(file)}`,
|
||||
id: `store-${process.platform}`,
|
||||
shell: false,
|
||||
valid: true,
|
||||
env: {
|
||||
XDG_DATA_HOME: path.join(config.get('downloadPath'), 'save', source, sourceId ?? '')
|
||||
|
|
@ -160,12 +159,13 @@ export default class RommIntegration implements PluginType
|
|||
{
|
||||
return [{
|
||||
startDir: path.join(downloadPath, dirname(gamePath)),
|
||||
command: basename(gamePath),
|
||||
command: `./${basename(gamePath)}`,
|
||||
env: {
|
||||
XDG_DATA_HOME: path.join(config.get('downloadPath'), 'save', source, sourceId ?? '')
|
||||
},
|
||||
id: 'store-win',
|
||||
id: `store-${process.platform}`,
|
||||
valid: true,
|
||||
shell: false,
|
||||
metadata: {
|
||||
romPath: path.join(downloadPath, gamePath)
|
||||
}
|
||||
|
|
@ -272,14 +272,15 @@ export default class RommIntegration implements PluginType
|
|||
const game = await getStoreGame(id);
|
||||
if (!game) throw new Error("Missing Store Game");
|
||||
|
||||
const validDownload = getValidDownload(game, downloadId);
|
||||
const validDownloads = getValidDownloads(game, downloadId);
|
||||
|
||||
if (validDownload)
|
||||
return validDownloads.map(validDownload =>
|
||||
{
|
||||
let system = validDownload.system.split(":")[0];
|
||||
if (system === 'win32') system = 'win';
|
||||
|
||||
const info: DownloadInfo = {
|
||||
id: validDownload.id,
|
||||
coverUrl: game.covers?.[0] ? game.covers[0].startsWith('http') ? game.covers[0] : pathToFileURL(path.join(getStoreFolder(), game.covers[0])).href : "",
|
||||
screenshotUrls: game.screenshots ?? [],
|
||||
files: [{
|
||||
|
|
@ -306,7 +307,7 @@ export default class RommIntegration implements PluginType
|
|||
};
|
||||
|
||||
return info;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -126,7 +126,14 @@ export const store = new Elysia({ prefix: '/api/store' })
|
|||
})
|
||||
.get('/emulator/:id', async ({ params: { id } }) =>
|
||||
{
|
||||
return plugins.hooks.store.fetchEmulator.promise({ id });
|
||||
const emulator = await plugins.hooks.store.fetchEmulator.promise({ id });
|
||||
if (!emulator) return status("Not Found");
|
||||
const sources: EmulatorSourceEntryType[] = [];
|
||||
await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: emulator.name, sources });
|
||||
const integrations = findEmulatorPluginIntegration(emulator.name, sources);
|
||||
emulator.validSources = sources;
|
||||
emulator.integrations = integrations;
|
||||
return emulator;
|
||||
}, { params: z.object({ id: z.string() }) })
|
||||
.post('/install/emulator/:id/:source', async ({ params: { source, id }, body: { isUpdate } }) =>
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue