From 95a66a182e7dc57954dfe34aa117e459dab65926 Mon Sep 17 00:00:00 2001 From: Michael Manganiello Date: Wed, 7 Aug 2024 21:15:52 -0300 Subject: [PATCH] misc: Improve typing for feed schemas * Add typing for nested objects in Tinfoil and Webrcade feed schemas. * Do not send Rom background and thumbnail, if not available. * Correctly build URLs using `starlette` utils. * Deprecate the `ROMM_HOST` setting, no longer needed. * Fix FastAPI custom router to prefer routes without trailing slash. * Fix Webrcade background URL pointing to gallery screenshot. --- backend/config/__init__.py | 1 - backend/endpoints/feeds.py | 129 ++++++++++++++++++--------- backend/endpoints/responses/feeds.py | 121 ++++++++++++++++--------- backend/utils/router.py | 4 +- env.template | 1 - 5 files changed, 172 insertions(+), 84 deletions(-) diff --git a/backend/config/__init__.py b/backend/config/__init__.py index c28218d9a..af698d32e 100644 --- a/backend/config/__init__.py +++ b/backend/config/__init__.py @@ -9,7 +9,6 @@ # GUNICORN DEV_PORT: Final = int(os.environ.get("VITE_BACKEND_DEV_PORT", "5000")) DEV_HOST: Final = "127.0.0.1" -ROMM_HOST: Final = os.environ.get("ROMM_HOST", DEV_HOST) GUNICORN_WORKERS: Final = int(os.environ.get("GUNICORN_WORKERS", 2)) # PATHS diff --git a/backend/endpoints/feeds.py b/backend/endpoints/feeds.py index 561dc6589..07487ad87 100644 --- a/backend/endpoints/feeds.py +++ b/backend/endpoints/feeds.py @@ -1,14 +1,19 @@ -from config import DISABLE_DOWNLOAD_ENDPOINT_AUTH, ROMM_HOST +from config import DISABLE_DOWNLOAD_ENDPOINT_AUTH from decorators.auth import protected_route from endpoints.responses.feeds import ( WEBRCADE_SLUG_TO_TYPE_MAP, WEBRCADE_SUPPORTED_PLATFORM_SLUGS, + TinfoilFeedFileSchema, TinfoilFeedSchema, + WebrcadeFeedCategorySchema, + WebrcadeFeedItemPropsSchema, + WebrcadeFeedItemSchema, WebrcadeFeedSchema, ) from fastapi import Request from handler.database import db_platform_handler, db_rom_handler from models.rom import Rom +from starlette.datastructures import URLPath from utils.router import APIRouter router = APIRouter() @@ -21,6 +26,7 @@ ) def platforms_webrcade_feed(request: Request) -> WebrcadeFeedSchema: """Get webrcade feed endpoint + https://docs.webrcade.com/feeds/format/ Args: request (Request): Fastapi Request object @@ -31,37 +37,67 @@ def platforms_webrcade_feed(request: Request) -> WebrcadeFeedSchema: platforms = db_platform_handler.get_platforms() - return { - "title": "RomM Feed", - "longTitle": "Custom RomM Feed", - "description": "Custom feed from your RomM library", - "thumbnail": "https://github.com/raw/rommapp/romm/f2dd425d87ad8e21bf47f8258ae5dcf90f56fbc2/frontend/assets/isotipo.svg", - "background": "https://github.com/raw/rommapp/romm/release/.github/screenshots/gallery.png", - "categories": [ - { - "title": p.name, - "longTitle": f"{p.name} Games", - "background": f"{ROMM_HOST}/assets/webrcade/feed/{p.slug.lower()}-background.png", - "thumbnail": f"{ROMM_HOST}/assets/webrcade/feed/{p.slug.lower()}-thumb.png", - "description": "", - "items": [ - { - "title": rom.name, - "description": rom.summary, - "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}/api/roms/{rom.id}/content/{rom.file_name}" - }, - } - for rom in db_rom_handler.get_roms(platform_id=p.id) - ], - } - for p in platforms - if p.slug in WEBRCADE_SUPPORTED_PLATFORM_SLUGS - ], - } + categories = [] + for p in platforms: + if p.slug not in WEBRCADE_SUPPORTED_PLATFORM_SLUGS: + continue + + category_items = [] + for rom in db_rom_handler.get_roms(platform_id=p.id): + category_item = WebrcadeFeedItemSchema( + title=rom.name or "", + description=rom.summary or "", + type=WEBRCADE_SLUG_TO_TYPE_MAP.get(p.slug, p.slug), + props=WebrcadeFeedItemPropsSchema( + rom=str( + request.url_for( + "get_rom_content", + id=rom.id, + file_name=rom.file_name, + ) + ), + ), + ) + if rom.path_cover_s: + category_item["thumbnail"] = str( + URLPath( + f"/assets/romm/resources/{rom.path_cover_s}" + ).make_absolute_url(request.base_url) + ) + if rom.path_cover_l: + category_item["background"] = str( + URLPath( + f"/assets/romm/resources/{rom.path_cover_l}" + ).make_absolute_url(request.base_url) + ) + category_items.append(category_item) + + categories.append( + WebrcadeFeedCategorySchema( + title=p.name, + longTitle=f"{p.name} Games", + background=str( + URLPath( + f"/assets/webrcade/feed/{p.slug.lower()}-background.png" + ).make_absolute_url(request.base_url) + ), + thumbnail=str( + URLPath( + f"/assets/webrcade/feed/{p.slug.lower()}-thumb.png" + ).make_absolute_url(request.base_url) + ), + items=category_items, + ) + ) + + return WebrcadeFeedSchema( + title="RomM Feed", + longTitle="Custom RomM Feed", + description="Custom feed from your RomM library", + thumbnail="https://github.com/raw/rommapp/romm/f2dd425d87ad8e21bf47f8258ae5dcf90f56fbc2/frontend/assets/isotipo.svg", + background="https://github.com/raw/rommapp/romm/release/.github/resources/screenshots/gallery.png", + categories=categories, + ) @protected_route(router.get, "/tinfoil/feed", ["roms.read"]) @@ -77,16 +113,27 @@ def tinfoil_index_feed(request: Request, slug: str = "switch") -> TinfoilFeedSch TinfoilFeedSchema: Tinfoil feed object schema """ switch = db_platform_handler.get_platform_by_fs_slug(slug) + if not switch: + return TinfoilFeedSchema( + files=[], + directories=[], + error="Nintendo Switch platform not found", + ) + files: list[Rom] = db_rom_handler.get_roms(platform_id=switch.id) - return { - "files": [ - { - "url": f"{ROMM_HOST}/api/roms/{file.id}/content/{file.file_name}", - "size": file.file_size_bytes, - } + return TinfoilFeedSchema( + files=[ + TinfoilFeedFileSchema( + url=str( + request.url_for( + "get_rom_content", id=file.id, file_name=file.file_name + ) + ), + size=file.file_size_bytes, + ) for file in files ], - "directories": [], - "success": "RomM Switch Library", - } + directories=[], + success="RomM Switch Library", + ) diff --git a/backend/endpoints/responses/feeds.py b/backend/endpoints/responses/feeds.py index c87c14017..81c327453 100644 --- a/backend/endpoints/responses/feeds.py +++ b/backend/endpoints/responses/feeds.py @@ -1,38 +1,42 @@ +from typing import NotRequired + from typing_extensions import TypedDict -WEBRCADE_SUPPORTED_PLATFORM_SLUGS = [ - "3do", - "arcade", - "atari2600", - "atari5200", - "atari7800", - "lynx", - "wonderswan", - "wonderswan-color", - "colecovision", - "turbografx16--1", - "turbografx-16-slash-pc-engine-cd", - "supergrafx", - "pc-fx", - "nes", - "n64", - "snes", - "gb", - "gba", - "gbc", - "virtualboy", - "sg1000", - "sms", - "genesis-slash-megadrive", - "segacd", - "gamegear", - "neo-geo-cd", - "neogeoaes", - "neogeomvs", - "neo-geo-pocket", - "neo-geo-pocket-color", - "ps", -] +WEBRCADE_SUPPORTED_PLATFORM_SLUGS = frozenset( + ( + "3do", + "arcade", + "atari2600", + "atari5200", + "atari7800", + "colecovision", + "gamegear", + "gb", + "gba", + "gbc", + "genesis-slash-megadrive", + "lynx", + "n64", + "neo-geo-cd", + "neo-geo-pocket", + "neo-geo-pocket-color", + "neogeoaes", + "neogeomvs", + "nes", + "pc-fx", + "ps", + "segacd", + "sg1000", + "sms", + "snes", + "supergrafx", + "turbografx-16-slash-pc-engine-cd", + "turbografx16--1", + "virtualboy", + "wonderswan", + "wonderswan-color", + ) +) WEBRCADE_SLUG_TO_TYPE_MAP = { "atari2600": "2600", @@ -55,16 +59,53 @@ } +# Webrcade feed format +# Source: https://docs.webrcade.com/feeds/format/ + + +class WebrcadeFeedItemPropsSchema(TypedDict): + rom: str + + +class WebrcadeFeedItemSchema(TypedDict): + title: str + longTitle: NotRequired[str] + description: NotRequired[str] + type: str + thumbnail: NotRequired[str] + background: NotRequired[str] + props: WebrcadeFeedItemPropsSchema + + +class WebrcadeFeedCategorySchema(TypedDict): + title: str + longTitle: NotRequired[str] + background: NotRequired[str] + thumbnail: NotRequired[str] + description: NotRequired[str] + items: list[WebrcadeFeedItemSchema] + + class WebrcadeFeedSchema(TypedDict): title: str - longTitle: str - description: str - thumbnail: str - background: str - categories: list[dict] + longTitle: NotRequired[str] + description: NotRequired[str] + thumbnail: NotRequired[str] + background: NotRequired[str] + categories: list[WebrcadeFeedCategorySchema] + + +# Tinfoil feed format +# Source: https://blawar.github.io/tinfoil/custom_index/ + + +class TinfoilFeedFileSchema(TypedDict): + url: str + size: int class TinfoilFeedSchema(TypedDict): - files: list[dict] + files: list[TinfoilFeedFileSchema] directories: list[str] - success: str + success: NotRequired[str] + error: NotRequired[str] diff --git a/backend/utils/router.py b/backend/utils/router.py index 9abb9bf45..0c88a6591 100644 --- a/backend/utils/router.py +++ b/backend/utils/router.py @@ -29,7 +29,9 @@ def api_route( ) def decorator(func: DecoratedCallable) -> DecoratedCallable: + # Path without trailing slash is registered first, for router's `url_path_for` to prefer it. + result = add_path(func) add_alternate_path(func) - return add_path(func) + return result return decorator diff --git a/env.template b/env.template index 63035ca63..5dc71d43f 100644 --- a/env.template +++ b/env.template @@ -2,7 +2,6 @@ ROMM_BASE_PATH=/path/to/romm_mock VITE_BACKEND_DEV_PORT=5000 # Gunicorn (optional) -ROMM_HOST=localhost GUNICORN_WORKERS=4 # (2 × CPU cores) + 1 # IGDB credentials