191 lines
No EOL
6.6 KiB
TypeScript
191 lines
No EOL
6.6 KiB
TypeScript
import { GameInstallProgress, GameStatusType, } from "@shared/constants";
|
|
import { activeGame, config, customEmulators, db, events, taskQueue } from "../../app";
|
|
import { getValidLaunchCommands } from "./launchGameService";
|
|
import * as schema from '../../schema/app';
|
|
import { eq } from "drizzle-orm";
|
|
import { getErrorMessage } from "@/bun/utils";
|
|
import { getLocalGameMatch } from "./utils";
|
|
import { getRomApiRomsIdGet } from "@/clients/romm";
|
|
import fs from 'node:fs/promises';
|
|
import { ErrorLike } from "elysia/universal";
|
|
|
|
class CommandSearchError extends Error
|
|
{
|
|
constructor(status: GameStatusType, message: string)
|
|
{
|
|
super(message);
|
|
this.name = status;
|
|
}
|
|
}
|
|
|
|
export async function getLocalGame (source: string, id: number)
|
|
{
|
|
const localGames = await db.select({ id: schema.games.id, path_fs: schema.games.path_fs, platform_slug: schema.platforms.es_slug })
|
|
.from(schema.games)
|
|
.where(getLocalGameMatch(id, source))
|
|
.leftJoin(schema.platforms, eq(schema.games.platform_id, schema.platforms.id));
|
|
|
|
if (localGames.length > 0)
|
|
{
|
|
return localGames[0];
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export async function getValidLaunchCommandsForGame (source: string, id: number)
|
|
{
|
|
const localGame = await getLocalGame(source, id);
|
|
if (localGame)
|
|
{
|
|
if (localGame.platform_slug)
|
|
{
|
|
if (localGame.path_fs)
|
|
{
|
|
try
|
|
{
|
|
const commands = await getValidLaunchCommands({ systemSlug: localGame.platform_slug, customEmulatorConfig: customEmulators, gamePath: localGame.path_fs });
|
|
const validCommand = commands.find(c => c.valid);
|
|
if (validCommand)
|
|
{
|
|
return { command: validCommand, gameId: localGame.id, source: source, sourceId: id };
|
|
|
|
}
|
|
else
|
|
{
|
|
return new CommandSearchError('missing-emulator', `Missing One Of Emulators: ${commands.filter(e => e.emulator && e.emulator !== "OS-SHELL").map(e => e.emulator).join(',')}`);
|
|
}
|
|
} catch (error)
|
|
{
|
|
console.error(error);
|
|
return new CommandSearchError('error', getErrorMessage(error));
|
|
}
|
|
|
|
} else
|
|
{
|
|
return new CommandSearchError('error', 'Missing Path');
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return new CommandSearchError('error', 'Missing Platform');
|
|
}
|
|
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export default async function buildStatusResponse (source: string, id: number)
|
|
{
|
|
let cleanup: (() => void) | undefined;
|
|
return new Response(new ReadableStream({
|
|
async start (controller)
|
|
{
|
|
function enqueue (data: GameInstallProgress, event?: 'error' | 'refresh')
|
|
{
|
|
const evntString = event ? `event: ${event}\n` : '';
|
|
controller.enqueue(`${evntString}data: ${JSON.stringify(data)}\n\n`);
|
|
}
|
|
|
|
const sourceId = `${source}-${id}`;
|
|
|
|
async function sendLatests ()
|
|
{
|
|
const localGame = await db.query.games.findFirst({ where: getLocalGameMatch(id, source), columns: { id: true } });
|
|
const activeTask = taskQueue.findJob(`install-rom-${source}-${id}`);
|
|
if (activeTask)
|
|
{
|
|
enqueue({
|
|
progress: activeTask.progress,
|
|
status: activeTask.state as any
|
|
});
|
|
|
|
} else if (activeGame && activeGame.gameId === localGame?.id)
|
|
{
|
|
enqueue({ status: 'playing' as GameStatusType, details: 'Playing' });
|
|
}
|
|
else
|
|
{
|
|
const validCommand = await getValidLaunchCommandsForGame(source, id);
|
|
if (validCommand)
|
|
{
|
|
if (validCommand instanceof Error)
|
|
{
|
|
enqueue({ status: validCommand.name as GameStatusType, error: validCommand.message });
|
|
}
|
|
else
|
|
{
|
|
enqueue({ status: 'installed', details: validCommand.command.label });
|
|
}
|
|
|
|
} else if (source === 'romm')
|
|
{
|
|
// TODO: Add Caching
|
|
const remoteGame = await getRomApiRomsIdGet({ path: { id } });
|
|
const stats = await fs.statfs(config.get('downloadPath'));
|
|
if (remoteGame.data?.fs_size_bytes && remoteGame.data?.fs_size_bytes > stats.bsize * stats.bavail)
|
|
{
|
|
enqueue({ status: 'error', error: "Not Enough Free Space" });
|
|
} else
|
|
{
|
|
enqueue({ status: 'install', details: 'Install' });
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
await sendLatests();
|
|
|
|
const dispose: Function[] = [];
|
|
const handleActiveExit = async (data: { error?: ErrorLike; }) =>
|
|
{
|
|
if (data.error)
|
|
{
|
|
enqueue({
|
|
status: 'error',
|
|
error: data.error
|
|
}, 'error');
|
|
}
|
|
await sendLatests();
|
|
};
|
|
events.on('activegameexit', handleActiveExit);
|
|
dispose.push(() => events.off('activegameexit', handleActiveExit));
|
|
dispose.push(taskQueue.on('progress', ({ id, progress, state }) =>
|
|
{
|
|
if (id.endsWith(sourceId))
|
|
{
|
|
enqueue({ progress, status: state as any });
|
|
}
|
|
}));
|
|
dispose.push(taskQueue.on('completed', ({ id }) =>
|
|
{
|
|
if (id.endsWith(sourceId))
|
|
{
|
|
enqueue({}, 'refresh');
|
|
}
|
|
}));
|
|
dispose.push(taskQueue.on('error', ({ id, error }) =>
|
|
{
|
|
if (id.endsWith(sourceId))
|
|
{
|
|
enqueue({
|
|
status: 'error',
|
|
error: error
|
|
}, 'error');
|
|
}
|
|
}));
|
|
|
|
cleanup = () =>
|
|
{
|
|
dispose.forEach(f => f());
|
|
};
|
|
},
|
|
cancel (reason)
|
|
{
|
|
cleanup?.();
|
|
cleanup = undefined;
|
|
},
|
|
}));
|
|
} |