From 517c5b489061be50a76a66f5a06445b5b8a42d78 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 8 Feb 2024 20:34:33 -0500 Subject: [PATCH 1/2] Multi-file emulation support --- backend/endpoints/raw.py | 23 +---------- backend/endpoints/responses/rom.py | 1 - backend/endpoints/rom.py | 40 +++++++++++++++++-- backend/endpoints/webrcade.py | 2 +- backend/models/rom.py | 4 -- .../src/__generated__/models/RomSchema.ts | 1 - frontend/src/components/Details/ActionBar.vue | 25 ++++++++---- .../src/components/Game/Card/ActionBar.vue | 2 +- .../src/components/Game/DataTable/Base.vue | 5 ++- frontend/src/services/api/index.ts | 2 +- frontend/src/utils/index.ts | 14 +++---- frontend/src/views/Play/Player.vue | 2 +- 12 files changed, 70 insertions(+), 51 deletions(-) diff --git a/backend/endpoints/raw.py b/backend/endpoints/raw.py index c65fc7bc8..ed1880c25 100644 --- a/backend/endpoints/raw.py +++ b/backend/endpoints/raw.py @@ -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}" diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index c03048a37..25d1e67c2 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -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) diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 0e85beb26..fb8adf358 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -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}, ) diff --git a/backend/endpoints/webrcade.py b/backend/endpoints/webrcade.py index e0671406a..b5ade2254 100644 --- a/backend/endpoints/webrcade.py +++ b/backend/endpoints/webrcade.py @@ -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() ], diff --git a/backend/models/rom.py b/backend/models/rom.py index 59b29938b..ccfb81118 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -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) diff --git a/frontend/src/__generated__/models/RomSchema.ts b/frontend/src/__generated__/models/RomSchema.ts index f0d49efa5..57eee2824 100644 --- a/frontend/src/__generated__/models/RomSchema.ts +++ b/frontend/src/__generated__/models/RomSchema.ts @@ -47,7 +47,6 @@ export type RomSchema = { url_screenshots: Array; merged_screenshots: Array; full_path: string; - download_path: string; sibling_roms?: Array; user_saves?: Array; user_states?: Array; diff --git a/frontend/src/components/Details/ActionBar.vue b/frontend/src/components/Details/ActionBar.vue index cf917d83d..72d5f0ea9 100644 --- a/frontend/src/components/Details/ActionBar.vue +++ b/frontend/src/components/Details/ActionBar.vue @@ -15,6 +15,7 @@ const emitter = inject>("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() { - - - + + diff --git a/frontend/src/components/Game/Card/ActionBar.vue b/frontend/src/components/Game/Card/ActionBar.vue index a6c58d2da..3521dfeba 100644 --- a/frontend/src/components/Game/Card/ActionBar.vue +++ b/frontend/src/components/Game/Card/ActionBar.vue @@ -25,13 +25,13 @@ const downloadStore = storeDownload(); variant="text" /> diff --git a/frontend/src/components/Game/DataTable/Base.vue b/frontend/src/components/Game/DataTable/Base.vue index 4b3bd84e5..61c80226d 100644 --- a/frontend/src/components/Game/DataTable/Base.vue +++ b/frontend/src/components/Game/DataTable/Base.vue @@ -148,14 +148,15 @@ function rowClick(_: Event, row: any) { mdi-download mdi-play + mdi-play +