mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Merge pull request #648 from zurdi15/multi-file-emu-support
Multi-file emulation support
This commit is contained in:
@@ -1,32 +1,11 @@
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import FileResponse
|
||||
from decorators.auth import protected_route
|
||||
from config import LIBRARY_BASE_PATH, ASSETS_BASE_PATH
|
||||
from config import ASSETS_BASE_PATH
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@protected_route(router.head, "/raw/roms/{path:path}", ["roms.read"])
|
||||
def head_raw_rom(request: Request, path: str):
|
||||
rom_path = f"{LIBRARY_BASE_PATH}/{path}"
|
||||
return FileResponse(path=rom_path, filename=path.split("/")[-1])
|
||||
|
||||
|
||||
@protected_route(router.get, "/raw/roms/{path:path}", ["roms.read"])
|
||||
def get_raw_rom(request: Request, path: str):
|
||||
"""Download a single rom file
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
|
||||
Returns:
|
||||
FileResponse: Returns a single rom file
|
||||
"""
|
||||
|
||||
rom_path = f"{LIBRARY_BASE_PATH}/{path}"
|
||||
return FileResponse(path=rom_path, filename=path.split("/")[-1])
|
||||
|
||||
|
||||
@protected_route(router.head, "/raw/assets/{path:path}", ["assets.read"])
|
||||
def head_raw_asset(request: Request, path: str):
|
||||
asset_path = f"{ASSETS_BASE_PATH}/{path}"
|
||||
|
||||
@@ -71,7 +71,6 @@ class RomSchema(BaseModel):
|
||||
url_screenshots: list[str]
|
||||
merged_screenshots: list[str]
|
||||
full_path: str
|
||||
download_path: str
|
||||
|
||||
sibling_roms: list["RomSchema"] = Field(default_factory=list)
|
||||
user_saves: list[SaveSchema] = Field(default_factory=list)
|
||||
|
||||
@@ -129,6 +129,32 @@ def get_rom(request: Request, id: int) -> RomSchema:
|
||||
return RomSchema.from_orm_with_request(db_rom_handler.get_roms(id), request)
|
||||
|
||||
|
||||
@protected_route(router.head, "/roms/{id}/content", ["roms.read"])
|
||||
def head_rom_content(request: Request, id: int):
|
||||
"""Head rom content endpoint
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
id (int): Rom internal id
|
||||
|
||||
Returns:
|
||||
FileResponse: Returns the response with headers
|
||||
"""
|
||||
|
||||
rom = db_rom_handler.get_roms(id)
|
||||
rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}"
|
||||
|
||||
return FileResponse(
|
||||
path=rom_path if not rom.multi else f"{rom_path}/{rom.files[0]}",
|
||||
filename=rom.file_name,
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={rom.name}.zip",
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Length": str(rom.file_size_bytes),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@protected_route(router.get, "/roms/{id}/content", ["roms.read"])
|
||||
def get_rom_content(
|
||||
request: Request, id: int, files: Annotated[list[str] | None, Query()] = None
|
||||
@@ -152,7 +178,7 @@ def get_rom_content(
|
||||
|
||||
if not rom.multi:
|
||||
return FileResponse(path=rom_path, filename=rom.file_name)
|
||||
|
||||
|
||||
# Builds a generator of tuples for each member file
|
||||
def local_files():
|
||||
def contents(file_name):
|
||||
@@ -165,7 +191,15 @@ def get_rom_content(
|
||||
|
||||
return [
|
||||
(file_name, datetime.now(), S_IFREG | 0o600, ZIP_64, contents(file_name))
|
||||
for file_name in files
|
||||
for file_name in rom.files
|
||||
] + [
|
||||
(
|
||||
f"{rom.file_name}.m3u",
|
||||
datetime.now(),
|
||||
S_IFREG | 0o600,
|
||||
ZIP_64,
|
||||
[str.encode(f"{rom.files[i]}\n") for i in range(len(rom.files))],
|
||||
)
|
||||
]
|
||||
|
||||
zipped_chunks = stream_zip(local_files())
|
||||
@@ -174,7 +208,7 @@ def get_rom_content(
|
||||
return CustomStreamingResponse(
|
||||
zipped_chunks,
|
||||
media_type="application/zip",
|
||||
headers={"Content-Disposition": f"attachment; filename={rom.name}.zip"},
|
||||
headers={"Content-Disposition": f"attachment; filename={rom.file_name}.zip"},
|
||||
emit_body={"id": rom.id},
|
||||
)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ def platforms_webrcade_feed(request: Request) -> WebrcadeFeedSchema:
|
||||
"type": WEBRCADE_SLUG_TO_TYPE_MAP.get(p.slug, p.slug),
|
||||
"thumbnail": f"{ROMM_HOST}/assets/romm/resources/{rom.path_cover_s}",
|
||||
"background": f"{ROMM_HOST}/assets/romm/resources/{rom.path_cover_l}",
|
||||
"props": {"rom": f"{ROMM_HOST}{rom.download_path}"},
|
||||
"props": {"rom": f"{ROMM_HOST}/api/roms/{rom.id}/content"},
|
||||
}
|
||||
for rom in session.scalars(db_rom_handler.get_roms(platform_id=p.id)).all()
|
||||
],
|
||||
|
||||
@@ -88,10 +88,6 @@ class Rom(BaseModel):
|
||||
def full_path(self) -> str:
|
||||
return f"{self.file_path}/{self.file_name}"
|
||||
|
||||
@cached_property
|
||||
def download_path(self) -> str:
|
||||
return f"/api/raw/roms/{self.full_path}"
|
||||
|
||||
@cached_property
|
||||
def has_cover(self) -> bool:
|
||||
return bool(self.path_cover_s or self.path_cover_l)
|
||||
|
||||
@@ -32,6 +32,7 @@ services:
|
||||
- 80:8080
|
||||
depends_on:
|
||||
- mariadb
|
||||
- redis
|
||||
|
||||
mariadb:
|
||||
image: mariadb:latest
|
||||
|
||||
1
frontend/src/__generated__/models/RomSchema.ts
generated
1
frontend/src/__generated__/models/RomSchema.ts
generated
@@ -47,7 +47,6 @@ export type RomSchema = {
|
||||
url_screenshots: Array<string>;
|
||||
merged_screenshots: Array<string>;
|
||||
full_path: string;
|
||||
download_path: string;
|
||||
sibling_roms?: Array<RomSchema>;
|
||||
user_saves?: Array<SaveSchema>;
|
||||
user_states?: Array<StateSchema>;
|
||||
|
||||
@@ -15,6 +15,7 @@ const emitter = inject<Emitter<Events>>("emitter");
|
||||
const auth = storeAuth();
|
||||
const emulation = ref(false);
|
||||
const playInfoIcon = ref("mdi-play");
|
||||
const emulationSupported = props.rom.platform_slug in platformSlugEJSCoreMap;
|
||||
|
||||
function toggleEmulation() {
|
||||
emulation.value = !emulation.value;
|
||||
@@ -42,14 +43,24 @@ function toggleEmulation() {
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
:disabled="!(rom.platform_slug in platformSlugEJSCoreMap)"
|
||||
rounded="0"
|
||||
block
|
||||
@click="toggleEmulation"
|
||||
<v-tooltip
|
||||
text="Emulation not currently supported"
|
||||
location="bottom"
|
||||
:disabled="emulationSupported"
|
||||
>
|
||||
<v-icon :icon="playInfoIcon" size="large" />
|
||||
</v-btn>
|
||||
<template v-slot:activator="{ props }">
|
||||
<div v-bind="props">
|
||||
<v-btn
|
||||
rounded="0"
|
||||
block
|
||||
@click="toggleEmulation"
|
||||
:disabled="!emulationSupported"
|
||||
>
|
||||
<v-icon :icon="playInfoIcon" size="large" />
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-menu location="bottom">
|
||||
|
||||
@@ -25,13 +25,13 @@ const downloadStore = storeDownload();
|
||||
variant="text"
|
||||
/>
|
||||
<v-btn
|
||||
v-if="rom.platform_slug in platformSlugEJSCoreMap"
|
||||
class="action-bar-btn"
|
||||
:href="`/play/${rom.id}`"
|
||||
icon="mdi-play"
|
||||
size="x-small"
|
||||
rounded="0"
|
||||
variant="text"
|
||||
:disabled="!(rom.platform_slug in platformSlugEJSCoreMap)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-menu location="bottom">
|
||||
|
||||
@@ -148,14 +148,15 @@ function rowClick(_: Event, row: any) {
|
||||
<v-icon>mdi-download</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="item.raw.platform_slug in platformSlugEJSCoreMap"
|
||||
size="small"
|
||||
variant="text"
|
||||
:href="`/play/${item.raw.id}`"
|
||||
class="my-1 bg-terciary"
|
||||
rounded="0"
|
||||
:disabled="!(item.raw.platform_slug in platformSlugEJSCoreMap)"
|
||||
><v-icon>mdi-play</v-icon></v-btn
|
||||
>
|
||||
<v-icon>mdi-play</v-icon>
|
||||
</v-btn>
|
||||
<v-menu location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
|
||||
@@ -7,7 +7,7 @@ const api = axios.create({ baseURL: "/api", timeout: 120000 });
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response.status === 403) {
|
||||
if (error.response?.status === 403) {
|
||||
router.push({
|
||||
name: "login",
|
||||
params: { next: router.currentRoute.value.path },
|
||||
|
||||
@@ -243,9 +243,9 @@ export function languageToEmoji(language: string) {
|
||||
}
|
||||
|
||||
export const platformSlugEJSCoreMap: Record<string, string> = {
|
||||
"3do": "opera",
|
||||
// "3do": "opera", Disabled until BIOS file support is added
|
||||
amiga: "puae",
|
||||
arcade: "mame2003_plus", // fbneo
|
||||
// arcade: "mame2003_plus", Disabled until BIOS file support is added
|
||||
atari2600: "stella2014",
|
||||
atari5200: "a5200",
|
||||
atari7800: "prosystem",
|
||||
@@ -256,17 +256,17 @@ export const platformSlugEJSCoreMap: Record<string, string> = {
|
||||
"neo-geo-pocket": "mednafen_ngp",
|
||||
"neo-geo-pocket-color": "mednafen_ngp",
|
||||
nes: "fceumm",
|
||||
"famicom": "fceumm",
|
||||
famicom: "fceumm",
|
||||
n64: "mupen64plus_next",
|
||||
nds: "melonds",
|
||||
gba: "mgba",
|
||||
gb: "gambatte",
|
||||
gbc: "gambatte",
|
||||
"pc-fx": "mednafen_pcfx",
|
||||
ps: "pcsx_rearmed",
|
||||
psp: "ppsspp",
|
||||
segacd: "genesis_plus_gx",
|
||||
sega32: "picodrive",
|
||||
// ps: "pcsx_rearmed", Disabled until BIOS file support is added
|
||||
// psp: "ppsspp", Disabled until BIOS file support is added
|
||||
// segacd: "genesis_plus_gx", Disabled until BIOS file support is added
|
||||
// sega32: "picodrive", // Broken: https://github.com/EmulatorJS/EmulatorJS/issues/579
|
||||
gamegear: "genesis_plus_gx",
|
||||
sms: "genesis_plus_gx",
|
||||
"genesis-slash-megadrive": "genesis_plus_gx",
|
||||
|
||||
@@ -49,7 +49,7 @@ declare global {
|
||||
|
||||
window.EJS_core = platformSlugEJSCoreMap[props.rom.platform_slug];
|
||||
window.EJS_gameID = props.rom.id;
|
||||
window.EJS_gameUrl = props.rom.download_path;
|
||||
window.EJS_gameUrl = `/api/roms/${props.rom.id}/content`;
|
||||
window.EJS_player = "#game";
|
||||
window.EJS_pathtodata = "/assets/emulatorjs/";
|
||||
window.EJS_color = "#A453FF";
|
||||
|
||||
Reference in New Issue
Block a user