Merge pull request #648 from zurdi15/multi-file-emu-support

Multi-file emulation support
This commit is contained in:
Zurdi
2024-02-09 09:43:31 +01:00
committed by GitHub
13 changed files with 71 additions and 51 deletions

View File

@@ -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}"

View File

@@ -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)

View File

@@ -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},
)

View File

@@ -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()
],

View File

@@ -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)

View File

@@ -32,6 +32,7 @@ services:
- 80:8080
depends_on:
- mariadb
- redis
mariadb:
image: mariadb:latest

View File

@@ -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>;

View File

@@ -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">

View File

@@ -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">

View File

@@ -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

View File

@@ -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 },

View File

@@ -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",

View File

@@ -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";