diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e085c2ece..946a0733c 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -buy_me_a_coffee: zurdi15 +open_collective: romm diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d610a853..b3292e993 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,6 +47,7 @@ jobs: tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} labels: | org.opencontainers.image.version={{version}} org.opencontainers.image.title="rommapp/romm" diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index d681f2ebd..ddb70fca6 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -42,10 +42,10 @@ jobs: run: | pipx install poetry - - name: Set up Python 3.11 + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" cache: "poetry" - name: Install dependencies diff --git a/.gitignore b/.gitignore index f570fd1f7..d6cae8462 100644 --- a/.gitignore +++ b/.gitignore @@ -18,10 +18,13 @@ coverage /cypress/screenshots/ # Editor directories and files -.vscode +.vscode/* !.vscode/extensions.json !.vscode/settings.json !.vscode/tasks.json +.zed/* +!.zed/settings.json +pyrightconfig.json .idea *.suo *.ntvs* diff --git a/.python-version b/.python-version index 2c0733315..e4fba2183 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11 +3.12 diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 3fbf6ad2c..7b8d38035 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -2,53 +2,73 @@ # To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml version: 0.1 cli: - version: 1.22.2 + version: 1.22.3 # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins) plugins: sources: - id: trunk - ref: v1.6.0 + ref: v1.6.1 uri: https://github.com/trunk-io/plugins # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) runtimes: enabled: - go@1.21.0 - node@18.12.1 - - python@3.11.6 + - python@3.12.2 # This is the section where you manage your linters. (https://docs.trunk.io/check/configuration) lint: enabled: - markdownlint@0.41.0 - - eslint@9.6.0 + - eslint@9.9.0 - actionlint@1.7.1 - bandit@1.7.9 - - black@24.4.2 - - checkov@3.2.178 + - black@24.8.0 + - checkov@3.2.228 - git-diff-check - isort@5.13.2 - - mypy@1.10.1 - - osv-scanner@1.8.1 - - oxipng@9.1.1 - - prettier@3.3.2 - - ruff@0.5.1 + - mypy@1.11.1 + - osv-scanner@1.8.3 + - oxipng@9.1.2 + - prettier@3.3.3 + - ruff@0.6.0 - shellcheck@0.10.0 - shfmt@3.6.0 - svgo@3.3.2 - - taplo@0.8.1 - - trivy@0.52.2 - - trufflehog@3.79.0 + - taplo@0.9.3 + - trivy@0.54.1 + - trufflehog@3.81.9 - yamllint@1.35.1 ignore: - linters: [ALL] paths: - frontend/src/__generated__/** - docker/Dockerfile + - docker/nginx/js/** + files: + - name: vue + extensions: [vue] definitions: - name: eslint - files: [typescript, javascript] + files: + - javascript + - typescript commands: - name: lint run_from: ${root_or_parent_with_any_config} + - name: prettier + files: + - typescript + - yaml + - css + - postcss + - sass + - html + - markdown + - json + - javascript + - graphql + - vue + - prettier_supported_configs actions: disabled: - trunk-check-pre-push diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 000000000..c808db432 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,16 @@ +{ + "languages": { + "Python": { + "tab_size": 4 + }, + "Vue.js": { + "tab_size": 2, + "formatter": { + "external": { + "command": "prettier", + "arguments": ["--stdin-filepath", "{buffer_path}"] + } + } + } + } +} diff --git a/backend/alembic/versions/0024_sibling_roms_db_view.py b/backend/alembic/versions/0024_sibling_roms_db_view.py new file mode 100644 index 000000000..f6c438936 --- /dev/null +++ b/backend/alembic/versions/0024_sibling_roms_db_view.py @@ -0,0 +1,68 @@ +"""empty message + +Revision ID: 0024_sibling_roms_db_view +Revises: 0023_make_columns_non_nullable +Create Date: 2024-08-08 12:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "0024_sibling_roms_db_view" +down_revision = "0023_make_columns_non_nullable" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + with op.batch_alter_table("roms", schema=None) as batch_op: + batch_op.create_index("idx_roms_igdb_id", ["igdb_id"]) + batch_op.create_index("idx_roms_moby_id", ["moby_id"]) + + connection = op.get_bind() + + connection.execute( + sa.text( + """ + CREATE VIEW sibling_roms AS + SELECT + r1.id AS rom_id, + r2.id AS sibling_rom_id, + r1.platform_id AS platform_id, + NOW() AS created_at, + NOW() AS updated_at, + CASE WHEN r1.igdb_id <=> r2.igdb_id THEN r1.igdb_id END AS igdb_id, + CASE WHEN r1.moby_id <=> r2.moby_id THEN r1.moby_id END AS moby_id + FROM + roms r1 + JOIN + roms r2 + ON + r1.platform_id = r2.platform_id + AND r1.id != r2.id + AND ( + (r1.igdb_id = r2.igdb_id AND r1.igdb_id IS NOT NULL AND r1.igdb_id != '') + OR + (r1.moby_id = r2.moby_id AND r1.moby_id IS NOT NULL AND r1.moby_id != '') + ); + """ + ), + ) + + +def downgrade() -> None: + connection = op.get_bind() + + connection.execute( + sa.text( + """ + DROP VIEW sibling_roms; + """ + ), + ) + + with op.batch_alter_table("roms", schema=None) as batch_op: + batch_op.drop_index("idx_roms_igdb_id") + batch_op.drop_index("idx_roms_moby_id") diff --git a/backend/alembic/versions/0025_roms_hashes.py b/backend/alembic/versions/0025_roms_hashes.py new file mode 100644 index 000000000..0d14ade97 --- /dev/null +++ b/backend/alembic/versions/0025_roms_hashes.py @@ -0,0 +1,46 @@ +"""empty message + +Revision ID: 0025_roms_hashes +Revises: 0024_sibling_roms_db_view +Create Date: 2024-08-11 21:50:53.301352 + +""" + +import sqlalchemy as sa +from alembic import op +from config import IS_PYTEST_RUN, SCAN_TIMEOUT +from endpoints.sockets.scan import scan_platforms +from handler.redis_handler import high_prio_queue +from handler.scan_handler import ScanType + +# revision identifiers, used by Alembic. +revision = "0025_roms_hashes" +down_revision = "0024_sibling_roms_db_view" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + with op.batch_alter_table("roms", schema=None) as batch_op: + batch_op.add_column(sa.Column("crc_hash", sa.String(length=100), nullable=True)) + batch_op.add_column(sa.Column("md5_hash", sa.String(length=100), nullable=True)) + batch_op.add_column( + sa.Column("sha1_hash", sa.String(length=100), nullable=True) + ) + + # Run a no-scan in the background on migrate + if not IS_PYTEST_RUN: + high_prio_queue.enqueue( + scan_platforms, [], ScanType.QUICK, [], [], job_timeout=SCAN_TIMEOUT + ) + + high_prio_queue.enqueue( + scan_platforms, [], ScanType.HASHES, [], [], job_timeout=SCAN_TIMEOUT + ) + + +def downgrade() -> None: + with op.batch_alter_table("roms", schema=None) as batch_op: + batch_op.drop_column("sha1_hash") + batch_op.drop_column("md5_hash") + batch_op.drop_column("crc_hash") diff --git a/backend/config/__init__.py b/backend/config/__init__.py index c28218d9a..d48134a00 100644 --- a/backend/config/__init__.py +++ b/backend/config/__init__.py @@ -6,10 +6,14 @@ load_dotenv() + +def str_to_bool(value: str) -> bool: + return value.lower() in ("true", "1") + + # 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 @@ -54,35 +58,41 @@ ROMM_AUTH_SECRET_KEY: Final = os.environ.get( "ROMM_AUTH_SECRET_KEY", secrets.token_hex(32) ) -DISABLE_CSRF_PROTECTION = os.environ.get("DISABLE_CSRF_PROTECTION", "false") == "true" -DISABLE_DOWNLOAD_ENDPOINT_AUTH = ( - os.environ.get("DISABLE_DOWNLOAD_ENDPOINT_AUTH", "false") == "true" +DISABLE_CSRF_PROTECTION = str_to_bool( + os.environ.get("DISABLE_CSRF_PROTECTION", "false") +) +DISABLE_DOWNLOAD_ENDPOINT_AUTH = str_to_bool( + os.environ.get("DISABLE_DOWNLOAD_ENDPOINT_AUTH", "false") ) # SCANS SCAN_TIMEOUT: Final = int(os.environ.get("SCAN_TIMEOUT", 60 * 60 * 4)) # 4 hours # TASKS -ENABLE_RESCAN_ON_FILESYSTEM_CHANGE: Final = ( - os.environ.get("ENABLE_RESCAN_ON_FILESYSTEM_CHANGE", "false") == "true" +ENABLE_RESCAN_ON_FILESYSTEM_CHANGE: Final = str_to_bool( + os.environ.get("ENABLE_RESCAN_ON_FILESYSTEM_CHANGE", "false") ) RESCAN_ON_FILESYSTEM_CHANGE_DELAY: Final = int( os.environ.get("RESCAN_ON_FILESYSTEM_CHANGE_DELAY", 5) # 5 minutes ) -ENABLE_SCHEDULED_RESCAN: Final = ( - os.environ.get("ENABLE_SCHEDULED_RESCAN", "false") == "true" +ENABLE_SCHEDULED_RESCAN: Final = str_to_bool( + os.environ.get("ENABLE_SCHEDULED_RESCAN", "false") ) SCHEDULED_RESCAN_CRON: Final = os.environ.get( "SCHEDULED_RESCAN_CRON", "0 3 * * *", # At 3:00 AM every day ) -ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB: Final = ( - os.environ.get("ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB", "false") == "true" +ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB: Final = str_to_bool( + os.environ.get("ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB", "false") ) SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON: Final = os.environ.get( "SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON", "0 4 * * *", # At 4:00 AM every day ) +# EMULATION +DISABLE_EMULATOR_JS = str_to_bool(os.environ.get("DISABLE_EMULATOR_JS", "false")) +DISABLE_RUFFLE_RS = str_to_bool(os.environ.get("DISABLE_RUFFLE_RS", "false")) + # TESTING IS_PYTEST_RUN: Final = bool(os.environ.get("PYTEST_VERSION", False)) diff --git a/backend/config/config_manager.py b/backend/config/config_manager.py index 50339dedc..66c941bad 100644 --- a/backend/config/config_manager.py +++ b/backend/config/config_manager.py @@ -2,7 +2,6 @@ import sys from pathlib import Path from typing import Final -from urllib.parse import quote_plus import pydash import yaml @@ -21,6 +20,7 @@ ConfigNotWritableException, ) from logger.logger import log +from sqlalchemy import URL from yaml.loader import SafeLoader ROMM_USER_CONFIG_PATH: Final = f"{ROMM_BASE_PATH}/config" @@ -76,7 +76,7 @@ def __init__(self, config_file: str = ROMM_USER_CONFIG_FILE): sys.exit(5) @staticmethod - def get_db_engine() -> str: + def get_db_engine() -> URL: """Builds the database connection string depending on the defined database in the config.yml file Returns: @@ -90,9 +90,13 @@ def get_db_engine() -> str: ) sys.exit(3) - return ( - f"mariadb+mariadbconnector://{DB_USER}:%s@{DB_HOST}:{DB_PORT}/{DB_NAME}" - % quote_plus(DB_PASSWD) + return URL.create( + drivername="mariadb+mariadbconnector", + username=DB_USER, + password=DB_PASSWD, + host=DB_HOST, + port=DB_PORT, + database=DB_NAME, ) # DEPRECATED diff --git a/backend/decorators/database.py b/backend/decorators/database.py index 230e6ae13..4e226b1f3 100644 --- a/backend/decorators/database.py +++ b/backend/decorators/database.py @@ -1,6 +1,7 @@ import functools from fastapi import HTTPException, status +from handler.database.base_handler import sync_session from logger.logger import log from sqlalchemy.exc import ProgrammingError @@ -12,7 +13,7 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) try: - with args[0].session.begin() as s: + with sync_session.begin() as s: kwargs["session"] = s return func(*args, **kwargs) except ProgrammingError as exc: diff --git a/backend/endpoints/feeds.py b/backend/endpoints/feeds.py index 561dc6589..ac761b98a 100644 --- a/backend/endpoints/feeds.py +++ b/backend/endpoints/feeds.py @@ -1,14 +1,22 @@ -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, + TinfoilFeedTitleDBSchema, + WebrcadeFeedCategorySchema, + WebrcadeFeedItemPropsSchema, + WebrcadeFeedItemSchema, WebrcadeFeedSchema, ) from fastapi import Request from handler.database import db_platform_handler, db_rom_handler +from handler.metadata import meta_igdb_handler +from handler.metadata.base_hander import SWITCH_TITLEDB_REGEX from models.rom import Rom +from starlette.datastructures import URLPath from utils.router import APIRouter router = APIRouter() @@ -21,6 +29,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,41 +40,77 @@ 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://raw.githubusercontent.com/rommapp/romm/f2dd425d87ad8e21bf47f8258ae5dcf90f56fbc2/frontend/assets/isotipo.svg", - "background": "https://raw.githubusercontent.com/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://raw.githubusercontent.com/rommapp/romm/f2dd425d87ad8e21bf47f8258ae5dcf90f56fbc2/frontend/assets/isotipo.svg", + background="https://raw.githubusercontent.com/rommapp/romm/release/.github/resources/screenshots/gallery.png", + categories=categories, + ) -@protected_route(router.get, "/tinfoil/feed", ["roms.read"]) -def tinfoil_index_feed(request: Request, slug: str = "switch") -> TinfoilFeedSchema: + +@protected_route( + router.get, + "/tinfoil/feed", + [], +) +async def tinfoil_index_feed( + request: Request, slug: str = "switch" +) -> TinfoilFeedSchema: """Get tinfoil custom index feed endpoint https://blawar.github.io/tinfoil/custom_index/ @@ -77,16 +122,52 @@ 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) - 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, - } - for file in files + if not switch: + return TinfoilFeedSchema( + files=[], + directories=[], + error="Nintendo Switch platform not found", + ) + + roms: list[Rom] = db_rom_handler.get_roms(platform_id=switch.id) + + async def extract_titledb(roms: list[Rom]) -> dict[str, TinfoilFeedTitleDBSchema]: + titledb = {} + for rom in roms: + match = SWITCH_TITLEDB_REGEX.search(rom.file_name) + if match: + _search_term, index_entry = ( + await meta_igdb_handler._switch_titledb_format(match, rom.file_name) + ) + if index_entry: + titledb[str(index_entry["nsuId"])] = TinfoilFeedTitleDBSchema( + id=str(index_entry["nsuId"]), + name=index_entry["name"], + description=index_entry["description"], + size=index_entry["size"], + version=index_entry["version"] or 0, + region=index_entry["region"] or "US", + releaseDate=index_entry["releaseDate"] or 19700101, + rating=index_entry["rating"] or 0, + publisher=index_entry["publisher"] or "", + rank=0, + ) + + return titledb + + return TinfoilFeedSchema( + files=[ + TinfoilFeedFileSchema( + url=str( + request.url_for( + "get_rom_content", id=rom.id, file_name=rom.file_name + ) + ), + size=rom.file_size_bytes, + ) + for rom in roms ], - "directories": [], - "success": "RomM Switch Library", - } + directories=[], + success="RomM Switch Library", + titledb=await extract_titledb(roms), + ) diff --git a/backend/endpoints/heartbeat.py b/backend/endpoints/heartbeat.py index 7d1b37b26..e6e81f805 100644 --- a/backend/endpoints/heartbeat.py +++ b/backend/endpoints/heartbeat.py @@ -1,4 +1,6 @@ from config import ( + DISABLE_EMULATOR_JS, + DISABLE_RUFFLE_RS, ENABLE_RESCAN_ON_FILESYSTEM_CHANGE, ENABLE_SCHEDULED_RESCAN, ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB, @@ -55,4 +57,8 @@ def heartbeat() -> HeartbeatResponse: "MESSAGE": "Updates the Nintendo Switch TitleDB file", }, }, + "EMULATION": { + "DISABLE_EMULATOR_JS": DISABLE_EMULATOR_JS, + "DISABLE_RUFFLE_RS": DISABLE_RUFFLE_RS, + }, } diff --git a/backend/endpoints/responses/__init__.py b/backend/endpoints/responses/__init__.py index 29e7f685f..94a0a4bf4 100644 --- a/backend/endpoints/responses/__init__.py +++ b/backend/endpoints/responses/__init__.py @@ -1,4 +1,4 @@ -from typing_extensions import TypedDict +from typing import TypedDict class MessageResponse(TypedDict): diff --git a/backend/endpoints/responses/assets.py b/backend/endpoints/responses/assets.py index e274862c7..def6a753d 100644 --- a/backend/endpoints/responses/assets.py +++ b/backend/endpoints/responses/assets.py @@ -1,7 +1,7 @@ from datetime import datetime +from typing import TypedDict from pydantic import BaseModel -from typing_extensions import TypedDict class BaseAsset(BaseModel): diff --git a/backend/endpoints/responses/config.py b/backend/endpoints/responses/config.py index 4ff222c9d..4420a9354 100644 --- a/backend/endpoints/responses/config.py +++ b/backend/endpoints/responses/config.py @@ -1,4 +1,4 @@ -from typing_extensions import TypedDict +from typing import TypedDict class ConfigResponse(TypedDict): diff --git a/backend/endpoints/responses/feeds.py b/backend/endpoints/responses/feeds.py index c87c14017..00fc39b2e 100644 --- a/backend/endpoints/responses/feeds.py +++ b/backend/endpoints/responses/feeds.py @@ -1,38 +1,40 @@ -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", -] +from typing import NotRequired, TypedDict + +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 +57,67 @@ } +# 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 + 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 TinfoilFeedTitleDBSchema(TypedDict): + id: str + name: str + version: int + region: str + releaseDate: int + rating: int + publisher: str description: str - thumbnail: str - background: str - categories: list[dict] + size: int + rank: int class TinfoilFeedSchema(TypedDict): - files: list[dict] + files: list[TinfoilFeedFileSchema] directories: list[str] - success: str + titledb: NotRequired[dict[str, TinfoilFeedTitleDBSchema]] + success: NotRequired[str] + error: NotRequired[str] diff --git a/backend/endpoints/responses/firmware.py b/backend/endpoints/responses/firmware.py index 8faa3a306..b4a19cd60 100644 --- a/backend/endpoints/responses/firmware.py +++ b/backend/endpoints/responses/firmware.py @@ -1,7 +1,7 @@ from datetime import datetime +from typing import TypedDict from pydantic import BaseModel -from typing_extensions import TypedDict class FirmwareSchema(BaseModel): diff --git a/backend/endpoints/responses/heartbeat.py b/backend/endpoints/responses/heartbeat.py index ada1e424d..6a995dc16 100644 --- a/backend/endpoints/responses/heartbeat.py +++ b/backend/endpoints/responses/heartbeat.py @@ -1,4 +1,4 @@ -from typing_extensions import TypedDict +from typing import TypedDict class WatcherDict(TypedDict): @@ -22,6 +22,11 @@ class MetadataSourcesDict(TypedDict): STEAMGRIDDB_ENABLED: bool +class EmulationDict(TypedDict): + DISABLE_EMULATOR_JS: bool + DISABLE_RUFFLE_RS: bool + + class HeartbeatResponse(TypedDict): VERSION: str SHOW_SETUP_WIZARD: bool @@ -30,3 +35,4 @@ class HeartbeatResponse(TypedDict): ANY_SOURCE_ENABLED: bool METADATA_SOURCES: MetadataSourcesDict FS_PLATFORMS: list + EMULATION: EmulationDict diff --git a/backend/endpoints/responses/oauth.py b/backend/endpoints/responses/oauth.py index f9b126875..f70023a9b 100644 --- a/backend/endpoints/responses/oauth.py +++ b/backend/endpoints/responses/oauth.py @@ -1,6 +1,4 @@ -from typing import NotRequired - -from typing_extensions import TypedDict +from typing import NotRequired, TypedDict class TokenResponse(TypedDict): diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index 202d23707..200a9fef8 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -2,18 +2,15 @@ import re from datetime import datetime -from typing import NotRequired, get_type_hints +from typing import NotRequired, TypedDict, get_type_hints from endpoints.responses.assets import SaveSchema, ScreenshotSchema, StateSchema from endpoints.responses.collection import CollectionSchema from fastapi import Request -from fastapi.responses import StreamingResponse from handler.metadata.igdb_handler import IGDBMetadata from handler.metadata.moby_handler import MobyMetadata -from handler.socket_handler import socket_handler -from models.rom import Rom +from models.rom import Rom, RomFile from pydantic import BaseModel, Field, computed_field -from typing_extensions import TypedDict SORT_COMPARE_REGEX = re.compile(r"^([Tt]he|[Aa]|[Aa]nd)\s") @@ -108,29 +105,17 @@ class RomSchema(BaseModel): tags: list[str] multi: bool - files: list[str] + files: list[RomFile] + crc_hash: str | None + md5_hash: str | None + sha1_hash: str | None full_path: str created_at: datetime updated_at: datetime - rom_user: RomUserSchema | None = Field(default=None) - sibling_roms: list[RomSchema] = Field(default_factory=list) - class Config: from_attributes = True - @classmethod - def from_orm_with_request(cls, db_rom: Rom, request: Request) -> RomSchema: - rom = cls.model_validate(db_rom) - user_id = request.user.id - - rom.rom_user = RomUserSchema.for_user(user_id, db_rom) - rom.sibling_roms = [ - RomSchema.model_validate(r) for r in db_rom.get_sibling_roms() - ] - - return rom - @computed_field # type: ignore @property def sort_comparator(self) -> str: @@ -144,10 +129,24 @@ def sort_comparator(self) -> str: ) +class SimpleRomSchema(RomSchema): + sibling_roms: list[RomSchema] = Field(default_factory=list) + rom_user: RomUserSchema | None = Field(default=None) + + @classmethod + def from_orm_with_request(cls, db_rom: Rom, request: Request) -> SimpleRomSchema: + rom = cls.model_validate(db_rom) + user_id = request.user.id + + rom.rom_user = RomUserSchema.for_user(user_id, db_rom) + + return rom + + class DetailedRomSchema(RomSchema): merged_screenshots: list[str] - rom_user: RomUserSchema | None = Field(default=None) sibling_roms: list[RomSchema] = Field(default_factory=list) + rom_user: RomUserSchema | None = Field(default=None) user_saves: list[SaveSchema] = Field(default_factory=list) user_states: list[StateSchema] = Field(default_factory=list) user_screenshots: list[ScreenshotSchema] = Field(default_factory=list) @@ -161,9 +160,6 @@ def from_orm_with_request(cls, db_rom: Rom, request: Request) -> DetailedRomSche rom.rom_user = RomUserSchema.for_user(user_id, db_rom) rom.user_notes = RomUserSchema.notes_for_user(user_id, db_rom) - rom.sibling_roms = [ - RomSchema.model_validate(r) for r in db_rom.get_sibling_roms() - ] rom.user_saves = [ SaveSchema.model_validate(s) for s in db_rom.saves if s.user_id == user_id ] @@ -186,18 +182,3 @@ class UserNotesSchema(TypedDict): user_id: int username: str note_raw_markdown: str - - -class AddRomsResponse(TypedDict): - uploaded_roms: list[str] - skipped_roms: list[str] - - -class CustomStreamingResponse(StreamingResponse): - def __init__(self, *args, **kwargs) -> None: - self.emit_body = kwargs.pop("emit_body", None) - super().__init__(*args, **kwargs) - - async def stream_response(self, *args, **kwargs) -> None: - await super().stream_response(*args, **kwargs) - await socket_handler.socket_server.emit("download:complete", self.emit_body) diff --git a/backend/endpoints/responses/stats.py b/backend/endpoints/responses/stats.py index ee199ecaf..c7b4229be 100644 --- a/backend/endpoints/responses/stats.py +++ b/backend/endpoints/responses/stats.py @@ -1,4 +1,4 @@ -from typing_extensions import TypedDict +from typing import TypedDict class StatsReturn(TypedDict): diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 5fe7dd126..0168bc730 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -1,7 +1,6 @@ -from collections.abc import AsyncIterator -from datetime import datetime +import binascii +from base64 import b64encode from shutil import rmtree -from stat import S_IFREG from typing import Annotated from urllib.parse import quote @@ -13,84 +12,87 @@ ) from decorators.auth import protected_route from endpoints.responses import MessageResponse -from endpoints.responses.rom import ( - AddRomsResponse, - CustomStreamingResponse, - DetailedRomSchema, - RomSchema, - RomUserSchema, -) +from endpoints.responses.rom import DetailedRomSchema, RomUserSchema, SimpleRomSchema from exceptions.endpoint_exceptions import RomNotFoundInDatabaseException from exceptions.fs_exceptions import RomAlreadyExistsException -from fastapi import File, HTTPException, Query, Request, UploadFile, status -from fastapi.responses import FileResponse +from fastapi import HTTPException, Query, Request, UploadFile, status +from fastapi.responses import Response from handler.database import db_platform_handler, db_rom_handler from handler.filesystem import fs_resource_handler, fs_rom_handler from handler.filesystem.base_handler import CoverSize from handler.metadata import meta_igdb_handler, meta_moby_handler from logger.logger import log -from stream_zip import NO_COMPRESSION_32, ZIP_AUTO, AsyncMemberFile, async_stream_zip +from starlette.requests import ClientDisconnect +from streaming_form_data import StreamingFormDataParser +from streaming_form_data.targets import FileTarget, NullTarget +from utils.filesystem import sanitize_filename +from utils.hashing import crc32_to_hex +from utils.nginx import ZipContentLine, ZipResponse from utils.router import APIRouter router = APIRouter() @protected_route(router.post, "/roms", ["roms.write"]) -async def add_roms( - request: Request, - platform_id: int, - roms: list[UploadFile] = File(...), # noqa: B008 -) -> AddRomsResponse: - """Upload roms endpoint (one or more at the same time) +async def add_rom(request: Request): + """Upload single rom endpoint Args: request (Request): Fastapi Request object - platform_slug (str): Slug of the platform where to upload the roms - roms (list[UploadFile], optional): List of files to upload. Defaults to File(...). Raises: HTTPException: No files were uploaded - - Returns: - AddRomsResponse: Standard message response """ + platform_id = request.headers.get("x-upload-platform") + filename = request.headers.get("x-upload-filename") + if not platform_id or not filename: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No platform ID or filename provided", + ) from None - platform_fs_slug = db_platform_handler.get_platform(platform_id).fs_slug - log.info(f"Uploading roms to {platform_fs_slug}") - if roms is None: - log.error("No roms were uploaded") + db_platform = db_platform_handler.get_platform(int(platform_id)) + if not db_platform: raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="No roms were uploaded", - ) + status_code=status.HTTP_400_BAD_REQUEST, + detail="Platform not found", + ) from None + platform_fs_slug = db_platform.fs_slug roms_path = fs_rom_handler.build_upload_file_path(platform_fs_slug) + log.info(f"Uploading file to {platform_fs_slug}") - uploaded_roms = [] - skipped_roms = [] + file_location = Path(f"{roms_path}/{filename}") + parser = StreamingFormDataParser(headers=request.headers) + parser.register("x-upload-platform", NullTarget()) + parser.register(filename, FileTarget(str(file_location))) - for rom in roms: - if fs_rom_handler.file_exists(roms_path, rom.filename): - log.warning(f" - Skipping {rom.filename} since the file already exists") - skipped_roms.append(rom.filename) - continue - - log.info(f" - Uploading {rom.filename}") - file_location = f"{roms_path}/{rom.filename}" + if await file_location.exists(): + log.warning(f" - Skipping {filename} since the file already exists") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"File {filename} already exists", + ) from None - async with await open_file(file_location, "wb+") as f: - while True: - chunk = rom.file.read(1024) - if not chunk: - break - await f.write(chunk) + async def cleanup_partial_file(): + if await file_location.exists(): + await file_location.unlink() - uploaded_roms.append(rom.filename) + try: + async for chunk in request.stream(): + parser.data_received(chunk) + except ClientDisconnect: + log.error("Client disconnected during upload") + await cleanup_partial_file() + except Exception as exc: + log.error("Error uploading files", exc_info=exc) + await cleanup_partial_file() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="There was an error uploading the file(s)", + ) from exc - return { - "uploaded_roms": uploaded_roms, - "skipped_roms": skipped_roms, - } + return Response(status_code=status.HTTP_201_CREATED) @protected_route(router.get, "/roms", ["roms.read"]) @@ -102,7 +104,7 @@ def get_roms( limit: int | None = None, order_by: str = "name", order_dir: str = "asc", -) -> list[RomSchema]: +) -> list[SimpleRomSchema]: """Get roms endpoint Args: @@ -110,7 +112,7 @@ def get_roms( id (int, optional): Rom internal id Returns: - list[RomSchema]: List of roms stored in the database + list[SimpleRomSchema]: List of roms stored in the database """ roms = db_rom_handler.get_roms( @@ -122,7 +124,7 @@ def get_roms( limit=limit, ) - return [RomSchema.from_orm_with_request(rom, request) for rom in roms] + return [SimpleRomSchema.from_orm_with_request(rom, request) for rom in roms] @protected_route( @@ -154,7 +156,12 @@ def get_rom(request: Request, id: int) -> DetailedRomSchema: "/roms/{id}/content/{file_name}", [] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else ["roms.read"], ) -def head_rom_content(request: Request, id: int, file_name: str): +async def head_rom_content( + request: Request, + id: int, + file_name: str, + files: Annotated[list[str] | None, Query()] = None, +): """Head rom content endpoint Args: @@ -171,20 +178,39 @@ def head_rom_content(request: Request, id: int, file_name: str): if not rom: raise RomNotFoundInDatabaseException(id) - rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}" + files_to_check = files or [r["filename"] for r in rom.files] - return FileResponse( - path=rom_path if not rom.multi else f"{rom_path}/{rom.files[0]}", - filename=file_name, + if not rom.multi: + return Response( + media_type="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{quote(rom.file_name)}"', + "X-Accel-Redirect": f"/library/{rom.full_path}", + }, + ) + + if len(files_to_check) == 1: + return Response( + media_type="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{quote(files_to_check[0])}"', + "X-Accel-Redirect": f"/library/{rom.full_path}/{files_to_check[0]}", + }, + ) + + return Response( + media_type="application/zip", headers={ - "Content-Disposition": f'attachment; filename="{quote(rom.name)}.zip"', - "Content-Type": "application/zip", - "Content-Length": str(rom.file_size_bytes), + "Content-Disposition": f'attachment; filename="{quote(file_name)}.zip"', }, ) -@protected_route(router.get, "/roms/{id}/content/{file_name}", ["roms.read"]) +@protected_route( + router.get, + "/roms/{id}/content/{file_name}", + [] if DISABLE_DOWNLOAD_ENDPOINT_AUTH else ["roms.read"], +) async def get_rom_content( request: Request, id: int, @@ -202,7 +228,7 @@ async def get_rom_content( FileResponse: Returns one file for single file roms Yields: - CustomStreamingResponse: Streams a file for multi-part roms + ZipResponse: Returns a response for nginx to serve a Zip file for multi-part roms """ rom = db_rom_handler.get_rom(id) @@ -211,61 +237,49 @@ async def get_rom_content( raise RomNotFoundInDatabaseException(id) rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}" - files_to_download = files or rom.files or [] + files_to_download = sorted(files or [r["filename"] for r in rom.files]) if not rom.multi: - return FileResponse(path=rom_path, filename=rom.file_name) + return Response( + media_type="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{quote(rom.file_name)}"', + "X-Accel-Redirect": f"/library/{rom.full_path}", + }, + ) if len(files_to_download) == 1: - return FileResponse( - path=f"{rom_path}/{files_to_download[0]}", filename=files_to_download[0] + return Response( + media_type="application/octet-stream", + headers={ + "Content-Disposition": f'attachment; filename="{quote(files_to_download[0])}"', + "X-Accel-Redirect": f"/library/{rom.full_path}/{files_to_download[0]}", + }, ) - # Builds a generator of tuples for each member file - async def local_files() -> AsyncIterator[AsyncMemberFile]: - async def contents(filename: str) -> AsyncIterator[bytes]: - try: - async with await open_file(f"{rom_path}/{filename}", "rb") as f: - while chunk := await f.read(65536): - yield chunk - except FileNotFoundError: - log.error(f"File {rom_path}/{filename} not found!") - raise - - async def m3u_file() -> AsyncIterator[bytes]: - for file in files_to_download: - yield str.encode(f"{file}\n") - - now = datetime.now() - - for f in files_to_download: - file_size = (await Path(f"{rom_path}/{f}").stat()).st_size - yield ( - f, - now, - S_IFREG | 0o600, - ZIP_AUTO(file_size, level=0), - contents(f), - ) - - yield ( - f"{file_name}.m3u", - now, - S_IFREG | 0o600, - NO_COMPRESSION_32, - m3u_file(), + content_lines = [ + ZipContentLine( + # TODO: Use calculated CRC-32 if available. + crc32=None, + size_bytes=(await Path(f"{rom_path}/{f}").stat()).st_size, + encoded_location=quote(f"/library-zip/{rom.full_path}/{f}"), + filename=f, ) + for f in files_to_download + ] + + m3u_encoded_content = "\n".join([f for f in files_to_download]).encode() + m3u_base64_content = b64encode(m3u_encoded_content).decode() + m3u_line = ZipContentLine( + crc32=crc32_to_hex(binascii.crc32(m3u_encoded_content)), + size_bytes=len(m3u_encoded_content), + encoded_location=f"/decode?value={m3u_base64_content}", + filename=f"{file_name}.m3u", + ) - zipped_chunks = async_stream_zip(local_files()) - - # Streams the zip file to the client - return CustomStreamingResponse( - zipped_chunks, - media_type="application/zip", - headers={ - "Content-Disposition": f'attachment; filename="{quote(file_name)}.zip"' - }, - emit_body={"id": rom.id}, + return ZipResponse( + content_lines=content_lines + [m3u_line], + filename=f"{quote(file_name)}.zip", ) @@ -276,6 +290,7 @@ async def update_rom( rename_as_source: bool = False, remove_cover: bool = False, artwork: UploadFile | None = None, + unmatch_metadata: bool = False, ) -> DetailedRomSchema: """Update rom endpoint @@ -284,6 +299,7 @@ async def update_rom( id (Rom): Rom internal id rename_as_source (bool, optional): Flag to rename rom file as matched IGDB game. Defaults to False. artwork (UploadFile, optional): Custom artork to set as cover. Defaults to File(None). + unmatch_metadata: Remove the metadata matches for this game. Defaults to False. Raises: HTTPException: If a rom already have that name when enabling the rename_as_source flag @@ -299,6 +315,31 @@ async def update_rom( if not rom: raise RomNotFoundInDatabaseException(id) + if unmatch_metadata: + db_rom_handler.update_rom( + id, + { + "igdb_id": None, + "sgdb_id": None, + "moby_id": None, + "name": rom.file_name, + "summary": "", + "url_screenshots": [], + "path_screenshots": [], + "path_cover_s": "", + "path_cover_l": "", + "url_cover": "", + "slug": "", + "igdb_metadata": {}, + "moby_metadata": {}, + "revision": "", + }, + ) + + return DetailedRomSchema.from_orm_with_request( + db_rom_handler.get_rom(id), request + ) + cleaned_data = { "igdb_id": data.get("igdb_id", None), "moby_id": data.get("moby_id", None), @@ -335,19 +376,25 @@ async def update_rom( } ) - fs_safe_file_name = data.get("file_name", rom.file_name).strip().replace("/", "-") - fs_safe_name = cleaned_data["name"].strip().replace("/", "-") - - if rename_as_source: - fs_safe_file_name = rom.file_name.replace( - rom.file_name_no_tags or rom.file_name_no_ext, fs_safe_name - ) + new_file_name = data.get("file_name", rom.file_name) try: - if rom.file_name != fs_safe_file_name: + if rename_as_source: + new_file_name = rom.file_name.replace( + rom.file_name_no_tags or rom.file_name_no_ext, + data.get("name", rom.name), + ) + new_file_name = sanitize_filename(new_file_name) + fs_rom_handler.rename_file( + old_name=rom.file_name, + new_name=new_file_name, + file_path=rom.file_path, + ) + elif rom.file_name != new_file_name: + new_file_name = sanitize_filename(new_file_name) fs_rom_handler.rename_file( old_name=rom.file_name, - new_name=fs_safe_file_name, + new_name=new_file_name, file_path=rom.file_path, ) except RomAlreadyExistsException as exc: @@ -358,12 +405,12 @@ async def update_rom( cleaned_data.update( { - "file_name": fs_safe_file_name, + "file_name": new_file_name, "file_name_no_tags": fs_rom_handler.get_file_name_with_no_tags( - fs_safe_file_name + new_file_name ), "file_name_no_ext": fs_rom_handler.get_file_name_with_no_extension( - fs_safe_file_name + new_file_name ), } ) diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index a8c3c062c..2af19497c 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -7,7 +7,7 @@ import socketio # type: ignore from config import SCAN_TIMEOUT from endpoints.responses.platform import PlatformSchema -from endpoints.responses.rom import RomSchema +from endpoints.responses.rom import SimpleRomSchema from exceptions.fs_exceptions import ( FirmwareNotFoundException, FolderStructureNotMatchException, @@ -21,6 +21,7 @@ fs_resource_handler, fs_rom_handler, ) +from handler.filesystem.roms_handler import FSRom from handler.metadata.igdb_handler import IGDB_API_ENABLED from handler.metadata.moby_handler import MOBY_API_ENABLED from handler.redis_handler import high_prio_queue, redis_client, redis_url @@ -81,6 +82,7 @@ def _should_scan_rom(scan_type: ScanType, rom: Rom, roms_ids: list): return ( (scan_type in {ScanType.NEW_PLATFORMS, ScanType.QUICK} and not rom) or (scan_type == ScanType.COMPLETE) + or (scan_type == ScanType.HASHES) or ( rom and ( @@ -224,7 +226,7 @@ async def _identify_platform( # Scanning firmware try: - fs_firmware = fs_firmware_handler.get_firmware(platform) + fs_firmware = fs_firmware_handler.get_firmware(platform.fs_slug) except FirmwareNotFoundException: fs_firmware = [] @@ -241,7 +243,7 @@ async def _identify_platform( # Scanning roms try: - fs_roms = fs_rom_handler.get_roms(platform) + fs_roms = fs_rom_handler.get_roms(platform.fs_slug) except RomsNotFoundException as e: log.error(e) return scan_stats @@ -302,7 +304,7 @@ async def _identify_firmware( async def _identify_rom( platform: Platform, - fs_rom: dict, + fs_rom: FSRom, scan_type: ScanType, roms_ids: list[str], metadata_sources: list[str], @@ -317,11 +319,17 @@ async def _identify_rom( rom = db_rom_handler.get_rom_by_filename(platform.id, fs_rom["file_name"]) if not _should_scan_rom(scan_type=scan_type, rom=rom, roms_ids=roms_ids): + # Just to update the filesystem data + rom.file_name = fs_rom["file_name"] + rom.multi = fs_rom["multi"] + rom.files = fs_rom["files"] + db_rom_handler.add_rom(rom) + return scan_stats scanned_rom = await scan_rom( platform=platform, - rom_attrs=fs_rom, + fs_rom=fs_rom, scan_type=scan_type, rom=rom, metadata_sources=metadata_sources, @@ -333,6 +341,10 @@ async def _identify_rom( _added_rom = db_rom_handler.add_rom(scanned_rom) + # Return early if we're only scanning for hashes + if scan_type == ScanType.HASHES: + return scan_stats + path_cover_s, path_cover_l = await fs_resource_handler.get_cover( overwrite=True, entity=_added_rom, @@ -361,7 +373,7 @@ async def _identify_rom( { "platform_name": platform.name, "platform_slug": platform.slug, - **RomSchema.model_validate(_added_rom).model_dump( + **SimpleRomSchema.model_validate(_added_rom).model_dump( exclude={"created_at", "updated_at", "rom_user"} ), }, @@ -386,6 +398,14 @@ async def scan_handler(_sid: str, options: dict): roms_ids = options.get("roms_ids", []) metadata_sources = options.get("apis", []) + # Uncomment this to run scan in the current process + # await scan_platforms( + # platform_ids=platform_ids, + # scan_type=scan_type, + # roms_ids=roms_ids, + # metadata_sources=metadata_sources, + # ) + return high_prio_queue.enqueue( scan_platforms, platform_ids, diff --git a/backend/handler/database/base_handler.py b/backend/handler/database/base_handler.py index 6d1d44e31..6d3fda8ba 100644 --- a/backend/handler/database/base_handler.py +++ b/backend/handler/database/base_handler.py @@ -2,8 +2,8 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +sync_engine = create_engine(ConfigManager.get_db_engine(), pool_pre_ping=True) +sync_session = sessionmaker(bind=sync_engine, expire_on_commit=False) -class DBBaseHandler: - def __init__(self) -> None: - self.engine = create_engine(ConfigManager.get_db_engine(), pool_pre_ping=True) - self.session = sessionmaker(bind=self.engine, expire_on_commit=False) + +class DBBaseHandler: ... diff --git a/backend/handler/database/platforms_handler.py b/backend/handler/database/platforms_handler.py index f2c0aee5f..de3976fa1 100644 --- a/backend/handler/database/platforms_handler.py +++ b/backend/handler/database/platforms_handler.py @@ -1,49 +1,36 @@ -import functools - from decorators.database import begin_session from models.platform import Platform from models.rom import Rom from sqlalchemy import Select, delete, or_, select -from sqlalchemy.orm import Query, Session, selectinload +from sqlalchemy.orm import Session from .base_handler import DBBaseHandler -def with_roms(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - session = kwargs.get("session") - if session is None: - raise ValueError("session is required") - - kwargs["query"] = select(Platform).options( - selectinload(Platform.roms).load_only(Rom.id) - ) - return func(*args, **kwargs) - - return wrapper - - class DBPlatformsHandler(DBBaseHandler): @begin_session - @with_roms def add_platform( - self, platform: Platform, query: Query = None, session: Session = None + self, + platform: Platform, + session: Session, ) -> Platform: platform = session.merge(platform) session.flush() - return session.scalar(query.filter_by(id=platform.id).limit(1)) + new_platform = session.scalar( + select(Platform).filter_by(id=platform.id).limit(1) + ) + if not new_platform: + raise ValueError("Could not find newlyewly created platform") + + return new_platform @begin_session - @with_roms - def get_platform( - self, id: int, *, query: Query = None, session: Session = None - ) -> Platform | None: - return session.scalar(query.filter_by(id=id).limit(1)) + def get_platform(self, id: int, *, session: Session) -> Platform | None: + return session.scalar(select(Platform).filter_by(id=id).limit(1)) @begin_session - def get_platforms(self, *, session: Session = None) -> Select[tuple[Platform]]: + def get_platforms(self, *, session: Session) -> Select[tuple[Platform]]: return ( session.scalars(select(Platform).order_by(Platform.name.asc())) # type: ignore[attr-defined] .unique() @@ -51,28 +38,28 @@ def get_platforms(self, *, session: Session = None) -> Select[tuple[Platform]]: ) @begin_session - @with_roms def get_platform_by_fs_slug( - self, fs_slug: str, query: Query = None, session: Session = None + self, fs_slug: str, session: Session ) -> Platform | None: - return session.scalar(query.filter_by(fs_slug=fs_slug).limit(1)) + return session.scalar(select(Platform).filter_by(fs_slug=fs_slug).limit(1)) @begin_session - def delete_platform(self, id: int, session: Session = None) -> int: + def delete_platform(self, id: int, session: Session) -> None: # Remove all roms from that platforms first session.execute( delete(Rom) .where(Rom.platform_id == id) .execution_options(synchronize_session="evaluate") ) - return session.execute( + + session.execute( delete(Platform) .where(Platform.id == id) .execution_options(synchronize_session="evaluate") ) @begin_session - def purge_platforms(self, fs_platforms: list[str], session: Session = None) -> int: + def purge_platforms(self, fs_platforms: list[str], session: Session) -> int: return session.execute( delete(Platform) .where(or_(Platform.fs_slug.not_in(fs_platforms), Platform.slug.is_(None))) # type: ignore[attr-defined] diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index f9acc12a0..d15d92e3c 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -23,6 +23,7 @@ def wrapper(*args, **kwargs): selectinload(Rom.states), selectinload(Rom.screenshots), selectinload(Rom.rom_users), + selectinload(Rom.sibling_roms), ) return func(*args, **kwargs) @@ -38,7 +39,9 @@ def wrapper(*args, **kwargs): f"{func} is missing required kwarg 'session' with type 'Session'" ) - kwargs["query"] = select(Rom).options(selectinload(Rom.rom_users)) + kwargs["query"] = select(Rom).options( + selectinload(Rom.rom_users), selectinload(Rom.sibling_roms) + ) return func(*args, **kwargs) return wrapper @@ -153,32 +156,6 @@ def get_rom_by_filename_no_ext( query.filter_by(file_name_no_ext=file_name_no_ext).limit(1) ) - @begin_session - @with_simple - def get_sibling_roms( - self, rom: Rom, query: Query = None, session: Session = None - ) -> list[Rom]: - return session.scalars( - query.where( - and_( - Rom.platform_id == rom.platform_id, - Rom.id != rom.id, - or_( - and_( - Rom.igdb_id == rom.igdb_id, - Rom.igdb_id.isnot(None), - Rom.igdb_id != "", - ), - and_( - Rom.moby_id == rom.moby_id, - Rom.moby_id.isnot(None), - Rom.moby_id != "", - ), - ), - ) - ) - ).all() - @begin_session def get_rom_collections( self, rom: Rom, session: Session = None diff --git a/backend/handler/filesystem/firmware_handler.py b/backend/handler/filesystem/firmware_handler.py index 24ccc38a6..4aa00c13f 100644 --- a/backend/handler/filesystem/firmware_handler.py +++ b/backend/handler/filesystem/firmware_handler.py @@ -11,8 +11,8 @@ ) from fastapi import UploadFile from logger.logger import log -from models.platform import Platform from utils.filesystem import iter_files +from utils.hashing import crc32_to_hex from .base_handler import FSHandler @@ -27,7 +27,7 @@ def remove_file(self, file_name: str, file_path: str): except IsADirectoryError: shutil.rmtree(f"{LIBRARY_BASE_PATH}/{file_path}/{file_name}") - def get_firmware(self, platform: Platform): + def get_firmware(self, platform_fs_slug: str): """Gets all filesystem firmware for a platform Args: @@ -35,13 +35,13 @@ def get_firmware(self, platform: Platform): Returns: list with all the filesystem firmware for a platform found in the LIBRARY_BASE_PATH """ - firmware_path = self.get_firmware_fs_structure(platform.fs_slug) + firmware_path = self.get_firmware_fs_structure(platform_fs_slug) firmware_file_path = f"{LIBRARY_BASE_PATH}/{firmware_path}" try: fs_firmware_files = [f for _, f in iter_files(firmware_file_path)] except IndexError as exc: - raise FirmwareNotFoundException(platform.fs_slug) from exc + raise FirmwareNotFoundException(platform_fs_slug) from exc return [f for f in self._exclude_files(fs_firmware_files, "single")] @@ -51,13 +51,20 @@ def get_firmware_file_size(self, firmware_path: str, file_name: str): def calculate_file_hashes(self, firmware_path: str, file_name: str): with open(f"{LIBRARY_BASE_PATH}/{firmware_path}/{file_name}", "rb") as f: - data = f.read() + crc_c = 0 + md5_h = hashlib.md5(usedforsecurity=False) + sha1_h = hashlib.sha1(usedforsecurity=False) + + # Read in chunks to avoid memory issues + while chunk := f.read(8192): + md5_h.update(chunk) + sha1_h.update(chunk) + crc_c = binascii.crc32(chunk, crc_c) + return { - "crc_hash": (binascii.crc32(data) & 0xFFFFFFFF) - .to_bytes(4, byteorder="big") - .hex(), - "md5_hash": hashlib.md5(data, usedforsecurity=False).hexdigest(), - "sha1_hash": hashlib.sha1(data, usedforsecurity=False).hexdigest(), + "crc_hash": crc32_to_hex(crc_c), + "md5_hash": md5_h.hexdigest(), + "sha1_hash": sha1_h.hexdigest(), } def file_exists(self, path: str, file_name: str): diff --git a/backend/handler/filesystem/roms_handler.py b/backend/handler/filesystem/roms_handler.py index 5aa786b0e..01f870f45 100644 --- a/backend/handler/filesystem/roms_handler.py +++ b/backend/handler/filesystem/roms_handler.py @@ -1,13 +1,23 @@ +import binascii +import bz2 +import hashlib import os import re import shutil +import tarfile +import zipfile +from collections.abc import Iterator from pathlib import Path +from typing import Any, Final, TypedDict +import magic +import py7zr from config import LIBRARY_BASE_PATH from config.config_manager import config_manager as cm from exceptions.fs_exceptions import RomAlreadyExistsException, RomsNotFoundException -from models.platform import Platform +from models.rom import RomFile from utils.filesystem import iter_directories, iter_files +from utils.hashing import crc32_to_hex from .base_handler import ( LANGUAGES_BY_SHORTCODE, @@ -18,12 +28,106 @@ FSHandler, ) +# list of known compressed file MIME types +COMPRESSED_MIME_TYPES: Final = [ + "application/zip", + "application/x-tar", + "application/x-gzip", + "application/x-7z-compressed", + "application/x-bzip2", +] + +# list of known file extensions that are compressed +COMPRESSED_FILE_EXTENSIONS = [ + ".zip", + ".tar", + ".gz", + ".7z", + ".bz2", +] + +FILE_READ_CHUNK_SIZE = 1024 * 8 + + +class FSRom(TypedDict): + multi: bool + file_name: str + files: list[RomFile] + + +def is_compressed_file(file_path: str) -> bool: + mime = magic.Magic(mime=True) + file_type = mime.from_file(file_path) + + return file_type in COMPRESSED_MIME_TYPES or file_path.endswith( + tuple(COMPRESSED_FILE_EXTENSIONS) + ) + + +def read_basic_file(file_path: Path) -> Iterator[bytes]: + with open(file_path, "rb") as f: + while chunk := f.read(FILE_READ_CHUNK_SIZE): + yield chunk + + +def read_zip_file(file_path: Path) -> Iterator[bytes]: + try: + with zipfile.ZipFile(file_path, "r") as z: + for file in z.namelist(): + with z.open(file, "r") as f: + while chunk := f.read(FILE_READ_CHUNK_SIZE): + yield chunk + except zipfile.BadZipFile: + for chunk in read_basic_file(file_path): + yield chunk + + +def read_tar_file(file_path: Path, mode: str = "r") -> Iterator[bytes]: + try: + with tarfile.open(file_path, mode) as f: + for member in f.getmembers(): + # Ignore metadata files created by macOS + if member.name.startswith("._"): + continue + + with f.extractfile(member) as ef: # type: ignore + while chunk := ef.read(FILE_READ_CHUNK_SIZE): + yield chunk + except tarfile.ReadError: + for chunk in read_basic_file(file_path): + yield chunk + + +def read_gz_file(file_path: Path) -> Iterator[bytes]: + return read_tar_file(file_path, "r:gz") + + +def read_7z_file(file_path: Path) -> Iterator[bytes]: + try: + with py7zr.SevenZipFile(file_path, "r") as f: + for _name, bio in f.readall().items(): + while chunk := bio.read(FILE_READ_CHUNK_SIZE): + yield chunk + except py7zr.Bad7zFile: + for chunk in read_basic_file(file_path): + yield chunk + + +def read_bz2_file(file_path: Path) -> Iterator[bytes]: + try: + with bz2.BZ2File(file_path, "rb") as f: + while chunk := f.read(FILE_READ_CHUNK_SIZE): + yield chunk + except EOFError: + for chunk in read_basic_file(file_path): + yield chunk + class FSRomsHandler(FSHandler): def __init__(self) -> None: pass - def remove_file(self, file_name: str, file_path: str): + def remove_file(self, file_name: str, file_path: str) -> None: try: os.remove(f"{LIBRARY_BASE_PATH}/{file_path}/{file_name}") except IsADirectoryError: @@ -74,7 +178,7 @@ def parse_tags(self, file_name: str) -> tuple: other_tags.append(tag) return regs, rev, langs, other_tags - def _exclude_multi_roms(self, roms) -> list[str]: + def _exclude_multi_roms(self, roms: list[str]) -> list[str]: excluded_names = cm.get_config().EXCLUDED_MULTI_FILES filtered_files: list = [] @@ -84,16 +188,96 @@ def _exclude_multi_roms(self, roms) -> list[str]: return [f for f in roms if f not in filtered_files] - def get_rom_files(self, rom: str, roms_path: str) -> list[str]: - rom_files: list[str] = [] + def _build_rom_file(self, path: Path) -> RomFile: + return RomFile( + filename=path.name, + size=os.stat(path).st_size, + last_modified=os.path.getmtime(path), + ) + + def get_rom_files(self, rom: str, roms_path: str) -> list[RomFile]: + rom_files: list[RomFile] = [] - for path, _, files in os.walk(f"{roms_path}/{rom}"): - for f in self._exclude_files(files, "multi_parts"): - rom_files.append(f"{Path(path, f)}".replace(f"{roms_path}/{rom}/", "")) + # Check if rom is a multi-part rom + if os.path.isdir(f"{roms_path}/{rom}"): + multi_files = os.listdir(f"{roms_path}/{rom}") + for file in self._exclude_files(multi_files, "multi_parts"): + path = Path(roms_path, rom, file) + rom_files.append(self._build_rom_file(path)) + else: + path = Path(roms_path, rom) + rom_files.append(self._build_rom_file(path)) return rom_files - def get_roms(self, platform: Platform): + def _calculate_rom_hashes( + self, file_path: Path, crc_c: int, md5_h: Any, sha1_h: Any + ) -> tuple[int, Any, Any]: + mime = magic.Magic(mime=True) + file_type = mime.from_file(file_path) + extension = Path(file_path).suffix.lower() + + def update_hashes(chunk: bytes): + md5_h.update(chunk) + sha1_h.update(chunk) + nonlocal crc_c + crc_c = binascii.crc32(chunk, crc_c) + + if extension == ".zip" or file_type == "application/zip": + for chunk in read_zip_file(file_path): + update_hashes(chunk) + + elif extension == ".tar" or file_type == "application/x-tar": + for chunk in read_tar_file(file_path): + update_hashes(chunk) + + elif extension == ".gz" or file_type == "application/x-gzip": + for chunk in read_gz_file(file_path): + update_hashes(chunk) + + elif extension == ".7z" or file_type == "application/x-7z-compressed": + for chunk in read_7z_file(file_path): + update_hashes(chunk) + + elif extension == ".bz2" or file_type == "application/x-bzip2": + for chunk in read_bz2_file(file_path): + update_hashes(chunk) + + else: + for chunk in read_basic_file(file_path): + update_hashes(chunk) + + return crc_c, md5_h, sha1_h + + def get_rom_hashes(self, rom: str, roms_path: str) -> dict[str, str]: + roms_file_path = f"{LIBRARY_BASE_PATH}/{roms_path}" + + crc_c = 0 + md5_h = hashlib.md5(usedforsecurity=False) + sha1_h = hashlib.sha1(usedforsecurity=False) + + # Check if rom is a multi-part rom + if os.path.isdir(f"{roms_file_path}/{rom}"): + multi_files = os.listdir(f"{roms_file_path}/{rom}") + for file in self._exclude_files(multi_files, "multi_parts"): + path = Path(roms_file_path, rom, file) + # Pass the raw hashes to the next iteration + crc_c, md5_h, sha1_h = self._calculate_rom_hashes( + path, crc_c, md5_h, sha1_h + ) + else: + path = Path(roms_file_path, rom) + crc_c, md5_h, sha1_h = self._calculate_rom_hashes( + path, crc_c, md5_h, sha1_h + ) + + return { + "crc_hash": crc32_to_hex(crc_c), + "md5_hash": md5_h.hexdigest(), + "sha1_hash": sha1_h.hexdigest(), + } + + def get_roms(self, platform_fs_slug: str) -> list[FSRom]: """Gets all filesystem roms for a platform Args: @@ -101,18 +285,18 @@ def get_roms(self, platform: Platform): Returns: list with all the filesystem roms for a platform found in the LIBRARY_BASE_PATH """ - roms_path = self.get_roms_fs_structure(platform.fs_slug) + roms_path = self.get_roms_fs_structure(platform_fs_slug) roms_file_path = f"{LIBRARY_BASE_PATH}/{roms_path}" try: fs_single_roms = [f for _, f in iter_files(roms_file_path)] except IndexError as exc: - raise RomsNotFoundException(platform.fs_slug) from exc + raise RomsNotFoundException(platform_fs_slug) from exc try: fs_multi_roms = [d for _, d in iter_directories(roms_file_path)] except IndexError as exc: - raise RomsNotFoundException(platform.fs_slug) from exc + raise RomsNotFoundException(platform_fs_slug) from exc fs_roms: list[dict] = [ {"multi": False, "file_name": rom} @@ -123,34 +307,15 @@ def get_roms(self, platform: Platform): ] return [ - dict( - rom, + FSRom( + multi=rom["multi"], + file_name=rom["file_name"], files=self.get_rom_files(rom["file_name"], roms_file_path), ) for rom in fs_roms ] - def get_rom_file_size( - self, - roms_path: str, - file_name: str, - multi: bool, - multi_files: list[str] | None = None, - ): - if multi_files is None: - multi_files = [] - - files = ( - [f"{LIBRARY_BASE_PATH}/{roms_path}/{file_name}"] - if not multi - else [ - f"{LIBRARY_BASE_PATH}/{roms_path}/{file_name}/{file}" - for file in multi_files - ] - ) - return sum([os.stat(file).st_size for file in files]) - - def file_exists(self, path: str, file_name: str): + def file_exists(self, path: str, file_name: str) -> bool: """Check if file exists in filesystem Args: @@ -161,7 +326,7 @@ def file_exists(self, path: str, file_name: str): """ return bool(os.path.exists(f"{LIBRARY_BASE_PATH}/{path}/{file_name}")) - def rename_file(self, old_name: str, new_name: str, file_path: str): + def rename_file(self, old_name: str, new_name: str, file_path: str) -> None: if new_name != old_name: if self.file_exists(path=file_path, file_name=new_name): raise RomAlreadyExistsException(new_name) @@ -171,6 +336,6 @@ def rename_file(self, old_name: str, new_name: str, file_path: str): f"{LIBRARY_BASE_PATH}/{file_path}/{new_name}", ) - def build_upload_file_path(self, fs_slug: str): + def build_upload_file_path(self, fs_slug: str) -> str: file_path = self.get_roms_fs_structure(fs_slug) return f"{LIBRARY_BASE_PATH}/{file_path}" diff --git a/backend/handler/filesystem/tests/test_fs.py b/backend/handler/filesystem/tests/test_fs.py index c825dfbd4..360539046 100644 --- a/backend/handler/filesystem/tests/test_fs.py +++ b/backend/handler/filesystem/tests/test_fs.py @@ -29,7 +29,7 @@ def test_get_roms_fs_structure(): def test_get_roms(): platform = Platform(name="Nintendo 64", slug="n64", fs_slug="n64") - roms = fs_rom_handler.get_roms(platform=platform) + roms = fs_rom_handler.get_roms(platform_fs_slug=platform.fs_slug) assert len(roms) == 2 assert roms[0]["file_name"] == "Paper Mario (USA).z64" @@ -39,28 +39,6 @@ def test_get_roms(): assert roms[1]["multi"] -def test_rom_size(): - rom_size = fs_rom_handler.get_rom_file_size( - roms_path=fs_rom_handler.get_roms_fs_structure(fs_slug="n64"), - file_name="Paper Mario (USA).z64", - multi=False, - ) - - assert rom_size == 1024 - - rom_size = fs_rom_handler.get_rom_file_size( - roms_path=fs_rom_handler.get_roms_fs_structure(fs_slug="n64"), - file_name="Super Mario 64 (J) (Rev A)", - multi=True, - multi_files=[ - "Super Mario 64 (J) (Rev A) [Part 1].z64", - "Super Mario 64 (J) (Rev A) [Part 2].z64", - ], - ) - - assert rom_size == 2048 - - def test_exclude_files(): from config.config_manager import ConfigManager diff --git a/backend/handler/metadata/base_hander.py b/backend/handler/metadata/base_hander.py index f163b8fdf..9bfce872a 100644 --- a/backend/handler/metadata/base_hander.py +++ b/backend/handler/metadata/base_hander.py @@ -2,6 +2,7 @@ import os import re import unicodedata +from itertools import batched from typing import Final from handler.redis_handler import async_cache, sync_cache @@ -11,7 +12,6 @@ SWITCH_TITLEDB_INDEX_KEY, update_switch_titledb_task, ) -from utils.iterators import batched def conditionally_set_cache( diff --git a/backend/handler/metadata/igdb_handler.py b/backend/handler/metadata/igdb_handler.py index 5d2f7eff4..87181485f 100644 --- a/backend/handler/metadata/igdb_handler.py +++ b/backend/handler/metadata/igdb_handler.py @@ -1,7 +1,7 @@ import functools import re import time -from typing import Final, NotRequired +from typing import Final, NotRequired, TypedDict import httpx import pydash @@ -9,7 +9,6 @@ from fastapi import HTTPException, status from handler.redis_handler import sync_cache from logger.logger import log -from typing_extensions import TypedDict from unidecode import unidecode as uc from utils.context import ctx_httpx_client diff --git a/backend/handler/metadata/moby_handler.py b/backend/handler/metadata/moby_handler.py index a623638e5..28774e987 100644 --- a/backend/handler/metadata/moby_handler.py +++ b/backend/handler/metadata/moby_handler.py @@ -1,7 +1,7 @@ import asyncio import http import re -from typing import Final, NotRequired +from typing import Final, NotRequired, TypedDict from urllib.parse import quote import httpx @@ -10,7 +10,6 @@ from config import MOBYGAMES_API_KEY from fastapi import HTTPException, status from logger.logger import log -from typing_extensions import TypedDict from unidecode import unidecode as uc from utils.context import ctx_httpx_client @@ -621,6 +620,7 @@ class SlugToMobyId(TypedDict): "sega-cd": {"id": 20, "name": "SEGA CD"}, "segacd": {"id": 20, "name": "SEGA CD"}, # IGDB "sega-master-system": {"id": 26, "name": "SEGA Master System"}, + "sms": {"id": 26, "name": "SEGA Master System"}, # IGDB "sega-pico": {"id": 103, "name": "SEGA Pico"}, "sega-saturn": {"id": 23, "name": "SEGA Saturn"}, "saturn": {"id": 23, "name": "SEGA Saturn"}, # IGDB diff --git a/backend/handler/scan_handler.py b/backend/handler/scan_handler.py index b574d0d91..13bc0937e 100644 --- a/backend/handler/scan_handler.py +++ b/backend/handler/scan_handler.py @@ -1,3 +1,4 @@ +import asyncio from enum import Enum from typing import Any @@ -5,6 +6,7 @@ from config.config_manager import config_manager as cm from handler.database import db_platform_handler from handler.filesystem import fs_asset_handler, fs_firmware_handler, fs_rom_handler +from handler.filesystem.roms_handler import FSRom from handler.metadata import meta_igdb_handler, meta_moby_handler from handler.metadata.igdb_handler import IGDBPlatform, IGDBRom from handler.metadata.moby_handler import MobyGamesPlatform, MobyGamesRom @@ -22,6 +24,7 @@ class ScanType(Enum): UNIDENTIFIED = "unidentified" PARTIAL = "partial" COMPLETE = "complete" + HASHES = "hashes" async def _get_main_platform_igdb_id(platform: Platform): @@ -156,7 +159,7 @@ def scan_firmware( async def scan_rom( platform: Platform, - rom_attrs: dict, + fs_rom: FSRom, scan_type: ScanType, rom: Rom | None = None, metadata_sources: list[str] | None = None, @@ -166,22 +169,21 @@ async def scan_rom( roms_path = fs_rom_handler.get_roms_fs_structure(platform.fs_slug) - log.info(f"\t · {rom_attrs['file_name']}") + log.info(f"\t · {fs_rom['file_name']}") - if rom_attrs.get("multi", False): - for file in rom_attrs["files"]: - log.info(f"\t\t · {file}") + if fs_rom.get("multi", False): + for file in fs_rom["files"]: + log.info(f"\t\t · {file['filename']}") # Set default properties - rom_attrs.update( - { - "id": rom.id if rom else None, - "platform_id": platform.id, - "name": rom_attrs["file_name"], - "url_cover": "", - "url_screenshots": [], - } - ) + rom_attrs = { + **fs_rom, + "id": rom.id if rom else None, + "platform_id": platform.id, + "name": fs_rom["file_name"], + "url_cover": "", + "url_screenshots": [], + } # Update properties from existing rom if not a complete rescan if rom and scan_type != ScanType.COMPLETE: @@ -204,12 +206,7 @@ async def scan_rom( ) # Update properties that don't require metadata - file_size = fs_rom_handler.get_rom_file_size( - multi=rom_attrs["multi"], - file_name=rom_attrs["file_name"], - multi_files=rom_attrs["files"], - roms_path=roms_path, - ) + file_size = sum([file["size"] for file in rom_attrs["files"]]) regs, rev, langs, other_tags = fs_rom_handler.parse_tags(rom_attrs["file_name"]) rom_attrs.update( { @@ -233,42 +230,59 @@ async def scan_rom( } ) - igdb_handler_rom: IGDBRom = IGDBRom(igdb_id=None) - moby_handler_rom: MobyGamesRom = MobyGamesRom(moby_id=None) - - if ( - "igdb" in metadata_sources - and platform.igdb_id - and ( - not rom - or scan_type == ScanType.COMPLETE - or (scan_type == ScanType.PARTIAL and not rom.igdb_id) - or (scan_type == ScanType.UNIDENTIFIED and not rom.igdb_id) - ) - ): - main_platform_igdb_id = await _get_main_platform_igdb_id(platform) - igdb_handler_rom = await meta_igdb_handler.get_rom( - rom_attrs["file_name"], main_platform_igdb_id - ) + # Calculating hashes is expensive, so we only do it if necessary + if not rom or scan_type == ScanType.COMPLETE or scan_type == ScanType.HASHES: + rom_hashes = fs_rom_handler.get_rom_hashes(rom_attrs["file_name"], roms_path) + rom_attrs.update(**rom_hashes) - if ( - "moby" in metadata_sources - and platform.moby_id - and ( - not rom - or scan_type == ScanType.COMPLETE - or (scan_type == ScanType.PARTIAL and not rom.moby_id) - or (scan_type == ScanType.UNIDENTIFIED and not rom.moby_id) - ) - ): - moby_handler_rom = await meta_moby_handler.get_rom( - rom_attrs["file_name"], platform_moby_id=platform.moby_id - ) + # If no metadata scan is required + if scan_type == ScanType.HASHES: + return Rom(**rom_attrs) + + async def fetch_igdb_rom(): + if ( + "igdb" in metadata_sources + and platform.igdb_id + and ( + not rom + or scan_type == ScanType.COMPLETE + or (scan_type == ScanType.PARTIAL and not rom.igdb_id) + or (scan_type == ScanType.UNIDENTIFIED and not rom.igdb_id) + ) + ): + main_platform_igdb_id = await _get_main_platform_igdb_id(platform) + return await meta_igdb_handler.get_rom( + rom_attrs["file_name"], main_platform_igdb_id + ) + + return IGDBRom(igdb_id=None) + + async def fetch_moby_rom(): + if ( + "moby" in metadata_sources + and platform.moby_id + and ( + not rom + or scan_type == ScanType.COMPLETE + or (scan_type == ScanType.PARTIAL and not rom.moby_id) + or (scan_type == ScanType.UNIDENTIFIED and not rom.moby_id) + ) + ): + return await meta_moby_handler.get_rom( + rom_attrs["file_name"], platform_moby_id=platform.moby_id + ) + + return MobyGamesRom(moby_id=None) + + # Run both metadata fetches concurrently + igdb_handler_rom, moby_handler_rom = await asyncio.gather( + fetch_igdb_rom(), fetch_moby_rom() + ) # Reversed to prioritize IGDB rom_attrs.update({**moby_handler_rom, **igdb_handler_rom}) - # Return early if not found in IGDB or MobyGames + # If not found in IGDB or MobyGames if not igdb_handler_rom.get("igdb_id") and not moby_handler_rom.get("moby_id"): log.warning( emoji.emojize(f"\t {rom_attrs['file_name']} not found :cross_mark:") diff --git a/backend/handler/tests/cassettes/test_fastapi/test_scan_rom.yaml b/backend/handler/tests/cassettes/test_fastapi/test_scan_rom.yaml index 9924c89fc..4ca969dd0 100644 --- a/backend/handler/tests/cassettes/test_fastapi/test_scan_rom.yaml +++ b/backend/handler/tests/cassettes/test_fastapi/test_scan_rom.yaml @@ -17,7 +17,7 @@ interactions: Content-Length: - "796" User-Agent: - - python-requests/2.32.3 + - python-requests/2.32.0 method: POST uri: https://api.igdb.com/v4/games response: @@ -33,7 +33,7 @@ interactions: CF-Cache-Status: - DYNAMIC CF-RAY: - - 8954458a8d1ac1c8-BUD + - 8a4509ef2f06ab0c-YYZ Connection: - keep-alive Content-Length: @@ -41,12 +41,12 @@ interactions: Content-Type: - application/json Date: - - Mon, 17 Jun 2024 16:03:47 GMT + - Tue, 16 Jul 2024 21:20:53 GMT Server: - cloudflare Set-Cookie: - - __cf_bm=Pkqts9nrlZzzqm8MAWz11FD9VAhWJG81SmXWzhFZVXQ-1718640227-1.0.1.1-IlZEHZANhqRkqtx6l9gZD7ncaSkhbRdt7pvVairBZqrcfrq7bZZIIFOLHNRRYBZn.9wEBmD.UmLrxmhaSg2y7A; - path=/; expires=Mon, 17-Jun-24 16:33:47 GMT; domain=.igdb.com; HttpOnly; Secure; + - __cf_bm=K63xoXgTxGJm.n8D27NJG_ly4hN5z2J6yYDTzgLbD80-1721164853-1.0.1.1-p2nx3KwSJSjeNAWPDw0cRQAUg6YHgO_Y3Z0jYU6sB2Yr5fkj8Db6FRqWdlVSS5EN_6KieLIwqUxDUg4J45h50g; + path=/; expires=Tue, 16-Jul-24 21:50:53 GMT; domain=.igdb.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload @@ -55,17 +55,17 @@ interactions: alt-svc: - h3=":443"; ma=86400 via: - - 1.1 ebc0709f2918acef5e26208dffcb618c.cloudfront.net (CloudFront) + - 1.1 1acedc07a77a02e11785c2290515f2e4.cloudfront.net (CloudFront) x-amz-apigw-id: - - ZhOvkGSYPHcEk1A= + - bBiYcG8dvHcEJyg= x-amz-cf-id: - - 0lKqIn-0Z3y8symJxCZXC5axLtvm5dfu9qs8-s50-DR6KG1wAXCVhA== + - 5BP2CkODtFEThqTb1WSzRtjpsi2tscDJRx4NmPXzCpmEQod5maIjnQ== x-amz-cf-pop: - - FRA56-P8 + - ORD51-C2 x-amzn-errortype: - AccessDeniedException x-amzn-requestid: - - 406ba6c1-1be5-4eee-8f8d-03579da25c54 + - b6b00df1-1438-4aa1-bda3-0cd0f7c23296 x-cache: - Error from cloudfront status: @@ -83,13 +83,13 @@ interactions: Content-Length: - "0" User-Agent: - - python-requests/2.32.3 + - python-requests/2.32.0 method: POST uri: https://id.twitch.tv/oauth2/token?client_id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&client_secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&grant_type=client_credentials response: body: string: - '{"access_token":"yg0ucvq4spk9v1eict05z4bpkctaso","expires_in":5268473,"token_type":"bearer"} + '{"access_token":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","expires_in":5624809,"token_type":"bearer"} ' headers: @@ -102,11 +102,11 @@ interactions: Content-Type: - application/json Date: - - Mon, 17 Jun 2024 16:03:48 GMT + - Tue, 16 Jul 2024 21:20:54 GMT Server: - nginx X-Ctxlog-Logid: - - 1-66705e64-4daa4ef9086314ea51864a45 + - 1-6696e436-783213912ac0e5067f1e9592 status: code: 200 message: OK @@ -120,7 +120,7 @@ interactions: Accept-Encoding: - gzip, deflate Authorization: - - Bearer yg0ucvq4spk9v1eict05z4bpkctaso + - Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Client-ID: - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Connection: @@ -128,7 +128,7 @@ interactions: Content-Length: - "796" User-Agent: - - python-requests/2.32.3 + - python-requests/2.32.0 method: POST uri: https://api.igdb.com/v4/games response: @@ -206,7 +206,7 @@ interactions: the Mushroom Kingdom\\u0027s diverse locales and biomes, meeting its inhabitants, fighthing unruly enemies and recruiting an array of companions in order to once again save Princess Peach from the clutches of the evil Koopa King Bowser.\",\n - \ \"total_rating\": 88.63712520126188,\n \"collections\": [\n {\n + \ \"total_rating\": 88.63753217601834,\n \"collections\": [\n {\n \ \"id\": 240,\n \"name\": \"Super Mario\"\n },\n {\n \ \"id\": 593,\n \"name\": \"Paper Mario\"\n }\n ]\n \ }\n]" @@ -214,20 +214,20 @@ interactions: CF-Cache-Status: - DYNAMIC CF-RAY: - - 8954459468d768bb-BUD + - 8a4509f43a89abb8-YYZ Connection: - keep-alive Content-Length: - - "1529" + - "1528" Content-Type: - application/json Date: - - Mon, 17 Jun 2024 16:03:48 GMT + - Tue, 16 Jul 2024 21:20:54 GMT Server: - cloudflare Set-Cookie: - - __cf_bm=MZGQ45rHyOlT6dLcQvaxjOTTbOgnNg4SGP9mJGrJCJ0-1718640228-1.0.1.1-5d1QCqvlQBVVBcfqZHDeacIHc5d8UKwR1QBiaLjeF7wu5qdTPIFI9uGU.9LSOBjbiqUwW73rQd2EbPuqduXO3Q; - path=/; expires=Mon, 17-Jun-24 16:33:48 GMT; domain=.igdb.com; HttpOnly; Secure; + - __cf_bm=PsncMQhPTWpR1oHjMpk15qRUP7WbtGRS.NGfZOY65l8-1721164854-1.0.1.1-cYy4SO3EJA2a4jqwmm1x1Zl3Z9o7Y8cIfRAv5mv0GnC8rjX2I1KEmbMp3aurQ3iwuGOS_LwNuMReKOD74TwXbA; + path=/; expires=Tue, 16-Jul-24 21:50:54 GMT; domain=.igdb.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload @@ -236,19 +236,19 @@ interactions: alt-svc: - h3=":443"; ma=86400 via: - - 1.1 1e0f88a39289286be3e03ff93487da80.cloudfront.net (CloudFront) + - 1.1 412b0215b557780a6efcc1651037dc90.cloudfront.net (CloudFront) x-amz-apigw-id: - - ZhOvxGLJPHcEiyw= + - bBiYjGocvHcES9Q= x-amz-cf-id: - - BRUeRhAc8MkId4W5Ytgwib4jTq6EIo4VgQKkZoAw-vECfwX38KZw4A== + - qNRcxGGRlOiahu_AQFHB6TMBbOASu2LA0t9PmPndvxSI4nkv96V3dA== x-amz-cf-pop: - - FRA56-P8 + - YTO50-P1 x-amzn-remapped-content-length: - - "1529" + - "1528" x-amzn-remapped-date: - - Mon, 17 Jun 2024 16:03:48 GMT + - Tue, 16 Jul 2024 21:20:54 GMT x-amzn-requestid: - - f83c4ba8-e83b-4b27-b769-3de078d84ba2 + - 2659af3f-8bbb-4d7b-9e1c-dc972fc6367f x-cache: - Miss from cloudfront x-count: @@ -266,7 +266,7 @@ interactions: Accept-Encoding: - gzip, deflate Authorization: - - Bearer yg0ucvq4spk9v1eict05z4bpkctaso + - Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Client-ID: - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Connection: @@ -274,7 +274,7 @@ interactions: Content-Length: - "121" User-Agent: - - python-requests/2.32.3 + - python-requests/2.32.0 method: POST uri: https://api.igdb.com/v4/search response: @@ -306,18 +306,18 @@ interactions: CF-Cache-Status: - DYNAMIC CF-RAY: - - 89544596b8d6684c-BUD + - 8a4509f668ce7119-YYZ Connection: - keep-alive Content-Type: - application/json Date: - - Mon, 17 Jun 2024 16:03:49 GMT + - Tue, 16 Jul 2024 21:20:55 GMT Server: - cloudflare Set-Cookie: - - __cf_bm=S3S9rrHhBOvzvmVCHIeVraPhJyoqg7eFaasmWKy4WrE-1718640229-1.0.1.1-MynYCqaygKWR4apMN6El6uR.QP9ouU93PdAsZ4IXfYQYAFER1Z9ek7QyBfLq60KBam5Te3U.RvZcUdnUrDIw0g; - path=/; expires=Mon, 17-Jun-24 16:33:49 GMT; domain=.igdb.com; HttpOnly; Secure; + - __cf_bm=KkAmqxhQPavV7fCKkDnuvzC9DyvenDp7tA7GNRkkVAc-1721164855-1.0.1.1-Ae5ASfcHkdEuszHbLuVn2rOsIOFO4lhkF0wJz_zkUMyHZGyjfMfbxc23IZI6vc_liGEP2Xgy2dI6I4xyduE1pA; + path=/; expires=Tue, 16-Jul-24 21:50:55 GMT; domain=.igdb.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload @@ -328,19 +328,19 @@ interactions: alt-svc: - h3=":443"; ma=86400 via: - - 1.1 193d38535c6cb246e365763e9c32e672.cloudfront.net (CloudFront) + - 1.1 19d1514f5f81da4dca6349d0f75a352c.cloudfront.net (CloudFront) x-amz-apigw-id: - - ZhOv1HtuPHcETtw= + - bBiYnGaWvHcEMVw= x-amz-cf-id: - - 5tJYJzvDTLMBuOc9kX5sQbOwUU8M31kbfrCwEwn6Kmrf2Y6e7LFZEg== + - qgoGtG02JXbCB4cXLi0sKJRw1f7uX6caTDqPIzUFUAiiaaTYr0DPmw== x-amz-cf-pop: - - FRA56-P8 + - YUL62-C2 x-amzn-remapped-content-length: - "1421" x-amzn-remapped-date: - - Mon, 17 Jun 2024 16:03:49 GMT + - Tue, 16 Jul 2024 21:20:54 GMT x-amzn-requestid: - - 55032156-444c-4347-af24-51c20426ee8b + - b12a507c-48c8-4e57-8028-af00f0ec2ecf x-cache: - Miss from cloudfront x-count: @@ -358,7 +358,7 @@ interactions: Accept-Encoding: - gzip, deflate Authorization: - - Bearer yg0ucvq4spk9v1eict05z4bpkctaso + - Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Client-ID: - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Connection: @@ -366,7 +366,7 @@ interactions: Content-Length: - "739" User-Agent: - - python-requests/2.32.3 + - python-requests/2.32.0 method: POST uri: https://api.igdb.com/v4/games response: @@ -444,7 +444,7 @@ interactions: the Mushroom Kingdom\\u0027s diverse locales and biomes, meeting its inhabitants, fighthing unruly enemies and recruiting an array of companions in order to once again save Princess Peach from the clutches of the evil Koopa King Bowser.\",\n - \ \"total_rating\": 88.63712520126188,\n \"collections\": [\n {\n + \ \"total_rating\": 88.63753217601834,\n \"collections\": [\n {\n \ \"id\": 240,\n \"name\": \"Super Mario\"\n },\n {\n \ \"id\": 593,\n \"name\": \"Paper Mario\"\n }\n ]\n \ }\n]" @@ -452,20 +452,20 @@ interactions: CF-Cache-Status: - DYNAMIC CF-RAY: - - 8954459c292fc1bc-BUD + - 8a4509fc9e8da246-YYZ Connection: - keep-alive Content-Length: - - "1529" + - "1528" Content-Type: - application/json Date: - - Mon, 17 Jun 2024 16:03:50 GMT + - Tue, 16 Jul 2024 21:20:56 GMT Server: - cloudflare Set-Cookie: - - __cf_bm=ou6Xt5NppxZER6H5U_Hf7JhkNeFmqvfi_r26YZDDiDA-1718640230-1.0.1.1-8odH4zUHWZ2rEQs6Es07QiilEn4uihIVSrpuO7nEvnsZyiCrpTXBUJeocFATmAs2nlDkU1rzUo3ywc0a4Vza1w; - path=/; expires=Mon, 17-Jun-24 16:33:50 GMT; domain=.igdb.com; HttpOnly; Secure; + - __cf_bm=5bPHYIegBMi1xJ02b0hjaxBDqtvwnry9PO980Xd7lNc-1721164856-1.0.1.1-cu9HyVrxbU2z.sX3r4j0oAGX.uCZMrSGh9G3DRsu7t2.CCaDCEZcRTqdbASeR2V9LLJseMkdn7TF7FBTjNY2DA; + path=/; expires=Tue, 16-Jul-24 21:50:56 GMT; domain=.igdb.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload @@ -474,19 +474,19 @@ interactions: alt-svc: - h3=":443"; ma=86400 via: - - 1.1 b7c8b552077b93dc0acaa0b82d11fa62.cloudfront.net (CloudFront) + - 1.1 490c6f54e6cd81b80f07ff6be833267e.cloudfront.net (CloudFront) x-amz-apigw-id: - - ZhOv9EwOPHcEDOQ= + - bBiYxEaCPHcEErA= x-amz-cf-id: - - 6Sn4tTPxhiFmPanVHta__qD8AW6cJVXrSXtZvgRED-1CCTbimEhZzw== + - syxc_2spiC05CJNtd7tJxBxPfHKt0FzGAI0rbGnWlRK5_zFEK10LWA== x-amz-cf-pop: - - FRA56-P8 + - YTO50-P1 x-amzn-remapped-content-length: - - "1529" + - "1528" x-amzn-remapped-date: - - Mon, 17 Jun 2024 16:03:49 GMT + - Tue, 16 Jul 2024 21:20:55 GMT x-amzn-requestid: - - c60d2696-7381-4b4d-ac0f-18b1c9292fb5 + - 5674e595-eafc-4003-91be-0267d28f76c9 x-cache: - Miss from cloudfront x-count: diff --git a/backend/handler/tests/test_fastapi.py b/backend/handler/tests/test_fastapi.py index e757ddee5..6b17b5d6c 100644 --- a/backend/handler/tests/test_fastapi.py +++ b/backend/handler/tests/test_fastapi.py @@ -1,7 +1,7 @@ import pytest from handler.scan_handler import ScanType, scan_platform, scan_rom from models.platform import Platform -from models.rom import Rom +from models.rom import Rom, RomFile from utils.context import initialize_context @@ -30,13 +30,17 @@ async def test_scan_rom(): platform = Platform(fs_slug="n64", igdb_id=4) async with initialize_context(): + files = [ + RomFile( + filename="Paper Mario (USA).z64", + size=1024, + last_modified=1620000000, + ) + ] + rom = await scan_rom( platform, - { - "file_name": "Paper Mario (USA).z64", - "multi": False, - "files": ["Paper Mario (USA).z64"], - }, + {"file_name": "Paper Mario (USA).z64", "multi": False, "files": files}, ScanType.QUICK, ) @@ -45,6 +49,6 @@ async def test_scan_rom(): assert rom.name == "Paper Mario" assert rom.igdb_id == 3340 assert rom.file_size_bytes == 1024 - assert rom.files == ["Paper Mario (USA).z64"] + assert rom.files == files assert rom.tags == [] assert not rom.multi diff --git a/backend/main.py b/backend/main.py index 8f3eb38b1..c810f870d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -32,7 +32,6 @@ ) from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi.middleware.gzip import GZipMiddleware from handler.auth.base_handler import ALGORITHM from handler.auth.hybrid_auth import HybridAuthBackend from handler.auth.middleware import CustomCSRFMiddleware, SessionMiddleware @@ -68,9 +67,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: exempt_urls=[re.compile(r"^/token.*"), re.compile(r"^/ws")], ) -# Enable GZip compression for responses -app.add_middleware(GZipMiddleware, minimum_size=1024) - # Handles both basic and oauth authentication app.add_middleware( AuthenticationMiddleware, diff --git a/backend/models/firmware.py b/backend/models/firmware.py index cb79190f2..06b2c73f5 100644 --- a/backend/models/firmware.py +++ b/backend/models/firmware.py @@ -60,11 +60,10 @@ def is_verified(self) -> bool: ) if cache_entry: cache_json = json.loads(cache_entry) - return ( - self.file_size_bytes == int(cache_json.get("size", 0)) - and self.md5_hash == cache_json.get("md5") - and self.sha1_hash == cache_json.get("sha1") - and self.crc_hash == cache_json.get("crc") + return self.file_size_bytes == int(cache_json.get("size", 0)) and ( + self.md5_hash == cache_json.get("md5") + or self.sha1_hash == cache_json.get("sha1") + or self.crc_hash == cache_json.get("crc") ) return False diff --git a/backend/models/fixtures/known_bios_files.json b/backend/models/fixtures/known_bios_files.json index 8b9b3f163..34e844225 100644 --- a/backend/models/fixtures/known_bios_files.json +++ b/backend/models/fixtures/known_bios_files.json @@ -299,7 +299,7 @@ "md5": "66223be1497460f1e60885eeb35e03cc", "sha1": "db6b504744281369794e26ba71a6e385cf6227fa" }, - "odyssey-2-slash-videopac-g7000o2rom.bin": { + "odyssey-2-slash-videopac-g7000:o2rom.bin": { "size": "1024", "crc": "8016a315", "md5": "562d5ebf9e030a40d6fabfc2f33139fd", @@ -875,12 +875,24 @@ "md5": "e10c53c2f8b90bab96ead2d368858623", "sha1": "8951d1bb219ab2ff8583033d2119c899cc81f18c" }, + "dc:boot.bin": { + "size": "2097152", + "crc": "89f2b1a1", + "md5": "e10c53c2f8b90bab96ead2d368858623", + "sha1": "8951d1bb219ab2ff8583033d2119c899cc81f18c" + }, "dc:dc_flash.bin": { "size": "131072", "crc": "c611b498", "md5": "0a93f7940c455905bea6e392dfde92a4", "sha1": "94d44d7f9529ec1642ba3771ed3c5f756d5bc872" }, + "dc:flash.bin": { + "size": "131072", + "crc": "c611b498", + "md5": "0a93f7940c455905bea6e392dfde92a4", + "sha1": "94d44d7f9529ec1642ba3771ed3c5f756d5bc872" + }, "dc:naomi_boot.bin": { "size": "2097152", "crc": "d2a1c6bf", @@ -1189,21 +1201,21 @@ }, "zxs:plus3-0.rom": { "size": "16384", - "crc": "7f4a5482", - "md5": "bc123f625e245c225f92ef05933ed134", - "sha1": "649fbd233490bf58b35350b0123d36caaaa011eb" + "crc": "a10230c0", + "md5": "3abdc20e72890a750dd3c745d286dfba", + "sha1": "a837f66977040f7b51ed053a2483c10f3d070ab7" }, "zxs:plus3-1.rom": { "size": "16384", - "crc": "5aeb4675", - "md5": "617364264c587d20c9fc4746c29679f2", - "sha1": "f12198108cbb14de4f03c6695bc16d08c85ee214" + "crc": "09b9c3ca", + "md5": "8361a1d9c8bcef89c0c39293776564ad", + "sha1": "6a4364f25513e4079f048f2de131a896d30edc64" }, "zxs:plus3-2.rom": { "size": "16384", - "crc": "504755cb", - "md5": "c363e95dcd0a90e6e7f847e6e47e0179", - "sha1": "773633dce5ba323a9e00d9d0f9e4d8c295df7c87" + "crc": "a60285a0", + "md5": "f36c5c2d1f2a682caadeaa6f947db0da", + "sha1": "0a747cc0b827a94b4fd74cfd818ca792437a38f7" }, "zxs:plus3-3.rom": { "size": "16384", @@ -1547,6 +1559,12 @@ "md5": "b9d9a0286c33dc6b7237bb13cd46fdee", "sha1": "8d5de56a79954f29e9006929ba3fed9b6a418c1d" }, + "ps:scph9002(7502).bin": { + "size": "524288", + "crc": "318178bf", + "md5": "b9d9a0286c33dc6b7237bb13cd46fdee", + "sha1": "8d5de56a79954f29e9006929ba3fed9b6a418c1d" + }, "psp:ppge_atlas.zim": { "size": "666530", "crc": "7b57fa78", @@ -1564,5 +1582,545 @@ "crc": "a93f1c4b", "md5": "a17e0e0150155400d8cced329563d9c8", "sha1": "718c1a00d38e0810a1ad0ffde79f73447f846f01" + }, + "acpc:cpc464.rom": { + "size": "32768", + "crc": "40852f25", + "md5": "a993f85b88ac4350cf4d41554e87fe4f", + "sha1": "56d39c463da60968d93e58b4ba0e675829412a20" + }, + "acpc:cpc664.rom": { + "size": "32768", + "crc": "9ab5a036", + "md5": "5a384a2310f472c7857888371c00ed66", + "sha1": "073a7665527b5bd8a148747a3947dbd3328682c8" + }, + "acpc:cpc6128.rom": { + "size": "32768", + "crc": "9e827fe1", + "md5": "b96280dc6c95a48857b4b8eb931533ae", + "sha1": "5977adbad3f7c1e0e082cd02fe76a700d9860c30" + }, + "acpc:cpc_amsdos.rom": { + "size": "16384", + "crc": "1fe22ecd", + "md5": "25629dfe870d097469c217b95fdc1c95", + "sha1": "39102c8e9cb55fcc0b9b62098780ed4a3cb6a4bb" + }, + "enterprise:hun.rom": { + "size": "16384", + "crc": "596ab6d6", + "md5": "22167938f142c222f40992839aa21a06", + "sha1": "325a5e28c2a0d896711f8829e7ff14fed5dd4103" + }, + "enterprise:brd.rom": { + "size": "16384", + "crc": "6999d6a3", + "md5": "6af0402906944fd134004b85097c8524", + "sha1": "f34f0c330b44dbf2548329bea954d5991dec30ca" + }, + "enterprise:exos20.rom": { + "size": "32768", + "crc": "d421795f", + "md5": "5ad3baaad3b5156d6b60b34229a676fb", + "sha1": "6033a0535136c40c47137e4d1cd9273c06d5fdff" + }, + "enterprise:exos21.rom": { + "size": "32768", + "crc": "982a3b44", + "md5": "f36f24cbb87745fbd2714e4df881db09", + "sha1": "55315b20fecb4441a07ee4bc5dc7153f396e0a2e" + }, + "enterprise:zt19uk.rom": { + "size": "32768", + "crc": "d6deedf1", + "md5": "228540b6be83ae2acd7569c8ff0f91d0", + "sha1": "b7af62f0bc95fdca4b31d236f8327dafc80f83b7" + }, + "enterprise:basic20.rom": { + "size": "16384", + "crc": "1228de34", + "md5": "8e18edce4a7acb2c33cc0ab18f988482", + "sha1": "61d0987b906146e21b94f265d5b51b4938c986a9" + }, + "enterprise:basic21.rom": { + "size": "16384", + "crc": "55f96251", + "md5": "e972fe42b398c9ff1d93ff014786aec6", + "sha1": "03bbb386cf530e804363acdfc1d13e64cf28af2e" + }, + "enterprise:exdos13.rom": { + "size": "32768", + "crc": "e0135929", + "md5": "ddff70c014d1958dc75378b6c9aab6f8", + "sha1": "cb43ab3676b93c279f1ed8ffcb0d4dcd4b34e631" + }, + "enterprise:epd19hft.rom": { + "size": "32768", + "crc": "bd503eeb", + "md5": "12cfc9c7e48c8a16c2e09edbd926d467", + "sha1": "8f28fe73d13e94dd1da02519908ecc6eebe104f6" + }, + "enterprise:zt18hfnt.rom": { + "size": "32768", + "crc": "76c9dbf6", + "md5": "3082dc488d32f30a612761b99074199b", + "sha1": "283be1ce417a759a3368bb4bbe72f692fd43ca6d" + }, + "enterprise:epfileio.rom": { + "size": "16384", + "crc": "60c79925", + "md5": "a68ebcbc73a4d2178d755b7755bf18fe", + "sha1": "2f9077bcd89b1ec42dbdcd55d335bdbaf361eff3" + }, + "enterprise:exos24uk.rom": { + "size": "65536", + "crc": "c099a5e3", + "md5": "55af78f877a21ca45eb2df68a74fcc60", + "sha1": "cf12e971623a54bf8c4f891ca3a36d969f205c49" + }, + "ps2:ps2-0100jd-20000117.bin": { + "size": "4194304", + "crc": "5a04500c", + "md5": "32f2e4d5ff5ee11072a6bc45530f5765", + "sha1": "5b33170323ed6344e2363fed8115dc3918bb96a4" + }, + "ps2:ps2-0100j-20000117.bin": { + "size": "4194304", + "crc": "b7ef81a9", + "md5": "acf4730ceb38ac9d8c7d8e21f2614600", + "sha1": "aea061e6e263fdcc1c4fdbd68553ef78dae74263" + }, + "ps2:ps2-0101jd-20000217.bin": { + "size": "4194304", + "crc": "4f8b4205", + "md5": "acf9968c8f596d2b15f42272082513d1", + "sha1": "16f4a284d0e760ee13a2aff2f7dda928255e3080" + }, + "ps2:ps2-0101j-20000217.bin": { + "size": "4194304", + "crc": "211dfb6a", + "md5": "b1459d7446c69e3e97e6ace3ae23dd1c", + "sha1": "916e02431bcd73140504da3355c9598143b77e11" + }, + "ps2:ps2-0101xd-20000224.bin": { + "size": "4194304", + "crc": "2fef9faf", + "md5": "d3f1853a16c2ec18f3cd1ae655213308", + "sha1": "4440b246bfde7bb31002c584a76c6ef384908e84" + }, + "ps2:ps2-0110ad-20000727.bin": { + "size": "4194304", + "crc": "795578c1", + "md5": "63e6fd9b3c72e0d7b920e80cf76645cd", + "sha1": "339c646cf0699268552df5b05f18f0a03a9f55ff" + }, + "ps2:ps2-0110a-20000727.bin": { + "size": "4194304", + "crc": "9678ad6a", + "md5": "a20c97c02210f16678ca3010127caf36", + "sha1": "20f6ce6693cf97e9494f8f0227f2b7988ffaf961" + }, + "ps2:ps2-0120a-20000902.bin": { + "size": "4194304", + "crc": "1ae71e5d", + "md5": "8db2fbbac7413bf3e7154c1e0715e565", + "sha1": "dbc2318a1029347b5af3a0c74b0bdf88d19efee6" + }, + "ps2:ps2-0120ed-20000902.bin": { + "size": "4194304", + "crc": "25495aa7", + "md5": "91c87cb2f2eb6ce529a2360f80ce2457", + "sha1": "3bb1eecd618ab5c973c7bc53671a4475a02e1d5b" + }, + "ps2:ps2-0120ed-20000902-20030110.bin": { + "size": "4194304", + "crc": "e2f78425", + "md5": "3016b3dd42148a67e2c048595ca4d7ce", + "sha1": "1b73dec999fcc2b92fa958110ff6bfe4d0af276e" + }, + "ps2:ps2-0120e-20000902.bin": { + "size": "4194304", + "crc": "7b08c33b", + "md5": "b7fa11e87d51752a98b38e3e691cbf17", + "sha1": "274c05fec654913a3f698d4b0d592085866a2cbd" + }, + "ps2:ps2-0120j-20001027-185015.bin": { + "size": "4194304", + "crc": "9b096622", + "md5": "f63bc530bd7ad7c026fcd6f7bd0d9525", + "sha1": "e481079eca752225555f0c26d14c9d0f94d9a8e9" + }, + "ps2:ps2-0120j-20001027-191435.bin": { + "size": "4194304", + "crc": "c1ccf3f6", + "md5": "cee06bd68c333fc5768244eae77e4495", + "sha1": "a9f5d8ed56cfff18add1b599010493461fa02448" + }, + "ps2:ps2-0150ad-20001228-20030520.bin": { + "size": "4194304", + "crc": "0380c2ce", + "md5": "0bf988e9c7aaa4c051805b0fa6eb3387", + "sha1": "7284b9d16df9935afc384318e024c87ef0574fe5" + }, + "ps2:ps2-0150a-20001228.bin": { + "size": "4194304", + "crc": "bce74746", + "md5": "8accc3c49ac45f5ae2c5db0adc854633", + "sha1": "5af5b5077d84a9c037ebe12bfab8a38b31d8a543" + }, + "ps2:ps2-0150ed-20001228-20030520.bin": { + "size": "4194304", + "crc": "60bc0031", + "md5": "6f9a6feb749f0533aaae2cc45090b0ed", + "sha1": "d004326c9d8060812b4433c3f07646b04854d6c4" + }, + "ps2:ps2-0150e-20001228.bin": { + "size": "4194304", + "crc": "1559fd43", + "md5": "838544f12de9b0abc90811279ee223c8", + "sha1": "e22ef231faf3661edd92f2ee449a71297c82a092" + }, + "ps2:ps2-0150jd-20010118.bin": { + "size": "4194304", + "crc": "0b3ec2bc", + "md5": "bb6bbc850458fff08af30e969ffd0175", + "sha1": "334e029fc7fd50222a399c50384ff42732652259" + }, + "ps2:ps2-0150j-20010118.bin": { + "size": "4194304", + "crc": "4fc3b495", + "md5": "815ac991d8bc3b364696bead3457de7d", + "sha1": "d6f365a0f07cd04ed28108e6ec5076e2f81e5f72" + }, + "ps2:ps2-0160a-20010427.bin": { + "size": "4194304", + "crc": "4008ac18", + "md5": "b107b5710042abe887c0f6175f6e94bb", + "sha1": "7331a40b4b4feb1b3f0f77b013b6d38483577baa" + }, + "ps2:ps2-0160j-20010427.bin": { + "size": "4194304", + "crc": "c268ef47", + "md5": "ab55cceea548303c22c72570cfd4dd71", + "sha1": "e525a0c900e37acf0ae5a655d82a0abcb07c6f1f" + }, + "ps2:ps2-0160a-20010704.bin": { + "size": "4194304", + "crc": "c506c693", + "md5": "18bcaadb9ff74ed3add26cdf709fff2e", + "sha1": "ce92e8e8c88665f2f645a9522e337823d47a914a" + }, + "ps2:ps2-0160e-20010704.bin": { + "size": "4194304", + "crc": "f1ac735f", + "md5": "491209dd815ceee9de02dbbc408c06d6", + "sha1": "3cbd048e437c785b5a05a0feced00117a8a42545" + }, + "ps2:ps2-0160a-20011004.bin": { + "size": "4194304", + "crc": "a01ec625", + "md5": "7200a03d51cacc4c14fcdfdbc4898431", + "sha1": "d257bce6ecaf3bafb704c75a1b4741b910bd2d49" + }, + "ps2:ps2-0160e-20011004.bin": { + "size": "4194304", + "crc": "82aa5055", + "md5": "8359638e857c8bc18c3c18ac17d9cc3c", + "sha1": "ee34c3a87c53c75ca2a37d77b0042ca24d07831f" + }, + "ps2:ps2-0160h-20010730.bin": { + "size": "4194304", + "crc": "75f83c67", + "md5": "352d2ff9b3f68be7e6fa7e6dd8389346", + "sha1": "ba15dcf7aac13864c08222037e9321d7468c87d1" + }, + "ps2:ps2-0160a-20020207.bin": { + "size": "4194304", + "crc": "a19e0bf5", + "md5": "d5ce2c7d119f563ce04bc04dbc3a323e", + "sha1": "f9a5d629a036b99128f7cb530c6e3ca016e9c8b7" + }, + "ps2:ps2-0160e-20020319.bin": { + "size": "4194304", + "crc": "2fe21e4d", + "md5": "0d2228e6fd4fb639c9c39d077a9ec10c", + "sha1": "bff2902bd0ce9729a060581132541e9fd1a9fab6" + }, + "ps2:ps2-0160j-20020426.bin": { + "size": "4194304", + "crc": "c9363baf", + "md5": "72da56fccb8fcd77bba16d1b6f479914", + "sha1": "003628c137dae577ff3b04b93ca1787b0c944702" + }, + "ps2:ps2-0160e-20020426.bin": { + "size": "4194304", + "crc": "dad0baec", + "md5": "5b1f47fbeb277c6be2fccdd6344ff2fd", + "sha1": "d106b757ae2544dfe63f7e1924e59d5ad44c0c29" + }, + "ps2:ps2-0160h-20020426.bin": { + "size": "4194304", + "crc": "3355623e", + "md5": "315a4003535dfda689752cb25f24785c", + "sha1": "e3a74125c426bcacabca00b513fab928665c8846" + }, + "ps2:ps2-0170j-20030206.bin": { + "size": "4194304", + "crc": "9457f64e", + "md5": "312ad4816c232a9606e56f946bc0678a", + "sha1": "d812ac65c357d392396ca9edee812dc41bed8bde" + }, + "ps2:ps2-0170ed-20030227.bin": { + "size": "4194304", + "crc": "970a9c56", + "md5": "666018ffec65c5c7e04796081295c6c7", + "sha1": "e220bb282378c1f48ea1b585b3675e51a6dca572" + }, + "ps2:ps2-0170e-20030227.bin": { + "size": "4194304", + "crc": "51b5fb8b", + "md5": "6e69920fa6eef8522a1d688a11e41bc6", + "sha1": "ad15bd7eabd5bd81ba011516a5be44947d6641aa" + }, + "ps2:ps2-0170ad-20030325.bin": { + "size": "4194304", + "crc": "0e1ece79", + "md5": "eb960de68f0c0f7f9fa083e9f79d0360", + "sha1": "c5bc6e893b4c43d528142e56c96073024de64157" + }, + "ps2:ps2-0170a-20030325.bin": { + "size": "4194304", + "crc": "9a99e3f4", + "md5": "8aa12ce243210128c5074552d3b86251", + "sha1": "d269d1ed513227f3ef7133c76cf1b3a64f97b15d" + }, + "ps2:ps2-0180cd-20030224.bin": { + "size": "4194304", + "crc": "8c1a04cf", + "md5": "240d4c5ddd4b54069bdc4a3cd2faf99d", + "sha1": "2de87767008fc4a303af64a46251156e965d9065" + }, + "ps2:ps2-0180j-20031028.bin": { + "size": "4194304", + "crc": "585fd27c", + "md5": "1c6cd089e6c83da618fbf2a081eb4888", + "sha1": "aa4a35c14ee342cf7a03b1dde294ca10e64889e1" + }, + "ps2:ps2-0190j-20030623.bin": { + "size": "4194304", + "crc": "7c10a967", + "md5": "463d87789c555a4a7604e97d7db545d1", + "sha1": "6a6ecfe6c10e42eff1ca056349def799b5629067" + }, + "ps2:ps2-0190a-20030623.bin": { + "size": "4194304", + "crc": "b3e87709", + "md5": "35461cecaa51712b300b2d6798825048", + "sha1": "c74d92a2952a2912b6698cbcf7742adac8f784d3" + }, + "ps2:ps2-0190e-20030623.bin": { + "size": "4194304", + "crc": "1752a52e", + "md5": "bd6415094e1ce9e05daabe85de807666", + "sha1": "18b9ba833c469c4683676cc20da5124080d980bb" + }, + "ps2:ps2-0190h-20030623.bin": { + "size": "4194304", + "crc": "41391dd3", + "md5": "2e70ad008d4ec8549aada8002fdf42fb", + "sha1": "caa18ed04854a91e68f2d61a782560edd6373bbf" + }, + "ps2:ps2-0190r-20030623.bin": { + "size": "4194304", + "crc": "25f6212a", + "md5": "b53d51edc7fc086685e31b811dc32aad", + "sha1": "34a81db03ab617fbfdd7f9b861692dd2ecd57b82" + }, + "ps2:ps2-0190c-20030623.bin": { + "size": "4194304", + "crc": "d2347ee7", + "md5": "1b6e631b536247756287b916f9396872", + "sha1": "92d9eb4b11cef97bb69a275b2851b72f7b0023d6" + }, + "ps2:ps2-0190j-20030822.bin": { + "size": "4194304", + "crc": "79d60546", + "md5": "00da1b177096cfd2532c8fa22b43e667", + "sha1": "0ea98a25a32145dda514de2f0d4bfbbd806bd00c" + }, + "ps2:ps2-0190e-20030822.bin": { + "size": "4194304", + "crc": "3afd1d1e", + "md5": "afde410bd026c16be605a1ae4bd651fd", + "sha1": "6e1f0eb4aec51a6288b3d802d3bcdb477cf52104" + }, + "ps2:ps2-0190a-20040329.bin": { + "size": "4194304", + "crc": "9ba4c32f", + "md5": "81f4336c1de607dd0865011c0447052e", + "sha1": "b68c05f5cd86bf03cb38a643a723b7a97b759531" + }, + "ps2:ps2-0200ed-20040614.bin": { + "size": "4194304", + "crc": "881c9aa9", + "md5": "63ead1d74893bf7f36880af81f68a82d", + "sha1": "902f4680b258abd40c0922f6b0d581cbd8f8a73e" + }, + "ps2:ps2-0200h-20040614.bin": { + "size": "4194304", + "crc": "b57201bf", + "md5": "3e3e030c0f600442fa05b94f87a1e238", + "sha1": "7f8e812cab7c7393c85eac6c42661e1fd0a642df" + }, + "ps2:ps2-0210j-20040917.bin": { + "size": "4194304", + "crc": "55710d11", + "md5": "1ad977bb539fc9448a08ab276a836bbc", + "sha1": "bbb1af3085e77599691ec430d147810157da934f" + }, + "ps2:ps2-0220j-20050620.bin": { + "size": "4194304", + "crc": "d27fc41d", + "md5": "eb4f40fcf4911ede39c1bbfe91e7a89a", + "sha1": "7ffa75d142cb8eeea6c777dbcf263143655275d5" + }, + "ps2:ps2-0220ad-20050620.bin": { + "size": "4194304", + "crc": "181f1bda", + "md5": "9959ad7a8685cad66206e7752ca23f8b", + "sha1": "7c7efdfcec7705f4e84bb47f45322104e39eed09" + }, + "ps2:ps2-0220a-20050620.bin": { + "size": "4194304", + "crc": "d305a97a", + "md5": "929a14baca1776b00869f983aa6e14d2", + "sha1": "48d0445dffd1e879c7ae752c5166ec3101921555" + }, + "ps2:ps2-0220e-20050620.bin": { + "size": "4194304", + "crc": "e2862e39", + "md5": "573f7d4a430c32b3cc0fd0c41e104bbd", + "sha1": "929a85e974faf4b40d0a7785023b758402c43bd9" + }, + "ps2:ps2-0220h-20050620.bin": { + "size": "4194304", + "crc": "e27c4a6c", + "md5": "df63a604e8bff5b0599bd1a6c2721bd0", + "sha1": "0a071d1b46607a7694770407606d8599f62a372b" + }, + "ps2:ps2-0220j-20060210.bin": { + "size": "4194304", + "crc": "1303918e", + "md5": "5b1ba4bb914406fae75ab8e38901684d", + "sha1": "0edf1fbb772a8e6a79ae00e977450e3ade25c4f3" + }, + "ps2:ps2-0220a-20060210.bin": { + "size": "4194304", + "crc": "1279fce9", + "md5": "cb801b7920a7d536ba07b6534d2433ca", + "sha1": "92e488d5b2705e4cca83d4d1efbc421012faf83e" + }, + "ps2:ps2-0220e-20060210.bin": { + "size": "4194304", + "crc": "23fa7baa", + "md5": "af60e6d1a939019d55e5b330d24b1c25", + "sha1": "28ad756d0cfd1e7b2e2de3de5d9e14207ee89761" + }, + "ps2:ps2-0220h-20060210.bin": { + "size": "4194304", + "crc": "23001fff", + "md5": "549a66d0c698635ca9fa3ab012da7129", + "sha1": "fce2a24e5e0400cc6d98c08f426405d19173813e" + }, + "ps2:ps2-0220j-20060905.bin": { + "size": "4194304", + "crc": "1d6d879b", + "md5": "5de9d0d730ff1e7ad122806335332524", + "sha1": "3baf847c1c217aa71ac6d298389c88edb3db32e2" + }, + "ps2:ps2-0220ad-20060905.bin": { + "size": "4194304", + "crc": "431d9b7f", + "md5": "21fe4cad111f7dc0f9af29477057f88d", + "sha1": "4191b5842f31a9985b5428bc9d2b733ce3abb583" + }, + "ps2:ps2-0220a-20060905.bin": { + "size": "4194304", + "crc": "1c17eafc", + "md5": "40c11c063b3b9409aa5e4058e984e30c", + "sha1": "8361d615cc895962e0f0838489337574dbdc9173" + }, + "ps2:ps2-0220e-20060905.bin": { + "size": "4194304", + "crc": "2d946dbf", + "md5": "80bbb237a6af9c611df43b16b930b683", + "sha1": "da5aacead2fb55807d6d4e70b1f10f4fdcfd3281" + }, + "ps2:ps2-0220h-20060905.bin": { + "size": "4194304", + "crc": "2d6e09ea", + "md5": "c37bce95d32b2be480f87dd32704e664", + "sha1": "a5a2ee0dd9a86ca35b94e97ca92476a584f755bf" + }, + "ps2:ps2-0230j-20080220.bin": { + "size": "4194304", + "crc": "2912faa5", + "md5": "80ac46fa7e77b8ab4366e86948e54f83", + "sha1": "fbd54bfc020af34008b317dcb80b812dd29b3759" + }, + "ps2:ps2-0230a-20080220.bin": { + "size": "4194304", + "crc": "286897c2", + "md5": "21038400dc633070a78ad53090c53017", + "sha1": "f9229fe159d0353b9f0632f3fdc66819c9030458" + }, + "ps2:ps2-0230e-20080220.bin": { + "size": "4194304", + "crc": "19eb1081", + "md5": "dc69f0643a3030aaa4797501b483d6c4", + "sha1": "9915b5ba56798f4027ac1bd8d10abe0c1c9c326a" + }, + "ps2:ps2-0230h-20080220.bin": { + "size": "4194304", + "crc": "191174d4", + "md5": "30d56e79d89fbddf10938fa67fe3f34e", + "sha1": "a277b456849697abec11285c6b35bc734598c220" + }, + "ps2:ps2-0250e-20100415.bin": { + "size": "4194304", + "crc": "7e75fc28", + "md5": "93ea3bcee4252627919175ff1b16a1d9", + "sha1": "b9cb5775af29cd4d1ec5521e8231f8b6636e2e44" + }, + "ps2:ps2-0250j-20100415.bin": { + "size": "4194304", + "crc": "4e8c160c", + "md5": "d3e81e95db25f5a86a7b7474550a2155", + "sha1": "4b5ef16b67e3b523d28ed2406106cb80470a06d0" + }, + "tvc:tvcfileio.rom": { + "size": "8192", + "crc": "950e32fd", + "md5": "a2cf86ba8e7fc58b242137fe59036832", + "sha1": "98889c3a56b11dedf077f866ed2e12d51b604113" + }, + "tvc:tvc22_ext.rom": { + "size": "8192", + "crc": "05e1c3a8", + "md5": "5ce95a26ceed5bec73995d83568da9cf", + "sha1": "abf119cf947ea32defd08b29a8a25d75f6bd4987" + }, + "tvc:tvc22_sys.rom": { + "size": "16384", + "crc": "79fa818c", + "md5": "8c54285f541930cde766069942bad0f2", + "sha1": "f2572ee83d09fc08f4de4a62f101c8bb301a9505" + }, + "tvc:tvc_dos12d.rom": { + "size": "16384", + "crc": "1466aed4", + "md5": "88dc7876d584f90e4106f91444ab23b7", + "sha1": "072c6160d4e7d406f5d8f5b1b66066c797d35561" } } diff --git a/backend/models/rom.py b/backend/models/rom.py index 06c8a3548..0f196a251 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -1,11 +1,19 @@ from __future__ import annotations from functools import cached_property -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypedDict from config import FRONTEND_RESOURCES_PATH from models.base import BaseModel -from sqlalchemy import JSON, BigInteger, ForeignKey, String, Text, UniqueConstraint +from sqlalchemy import ( + JSON, + BigInteger, + ForeignKey, + Integer, + String, + Text, + UniqueConstraint, +) from sqlalchemy.dialects.mysql.json import JSON as MySQLJSON from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -16,6 +24,12 @@ from models.user import User +class RomFile(TypedDict): + filename: str + size: int + last_modified: float | None + + class Rom(BaseModel): __tablename__ = "roms" @@ -59,13 +73,21 @@ class Rom(BaseModel): ) multi: Mapped[bool] = mapped_column(default=False) - files: Mapped[list[str] | None] = mapped_column(JSON, default=[]) + files: Mapped[list[RomFile] | None] = mapped_column(JSON, default=[]) + crc_hash: Mapped[str | None] = mapped_column(String(100)) + md5_hash: Mapped[str | None] = mapped_column(String(100)) + sha1_hash: Mapped[str | None] = mapped_column(String(100)) platform_id: Mapped[int] = mapped_column( ForeignKey("platforms.id", ondelete="CASCADE") ) platform: Mapped[Platform] = relationship(lazy="immediate") + sibling_roms: Mapped[list[Rom]] = relationship( + secondary="sibling_roms", + primaryjoin="Rom.id == SiblingRom.rom_id", + secondaryjoin="Rom.id == SiblingRom.sibling_rom_id", + ) saves: Mapped[list[Save]] = relationship(back_populates="rom") states: Mapped[list[State]] = relationship(back_populates="rom") @@ -98,12 +120,6 @@ def merged_screenshots(self) -> list[str]: f"{FRONTEND_RESOURCES_PATH}/{s}" for s in self.path_screenshots ] - # This is an expensive operation so don't call it on a list of roms - def get_sibling_roms(self) -> list[Rom]: - from handler.database import db_rom_handler - - return db_rom_handler.get_sibling_roms(self) - def get_collections(self) -> list[Collection]: from handler.database import db_rom_handler @@ -176,3 +192,14 @@ class RomUser(BaseModel): @property def user__username(self) -> str: return self.user.username + + +class SiblingRom(BaseModel): + __tablename__ = "sibling_roms" + + rom_id: Mapped[int] = mapped_column(Integer, primary_key=True) + sibling_rom_id: Mapped[int] = mapped_column(Integer, primary_key=True) + + __table_args__ = ( + UniqueConstraint("rom_id", "sibling_rom_id", name="unique_sibling_roms"), + ) diff --git a/backend/tasks/update_switch_titledb.py b/backend/tasks/update_switch_titledb.py index 1529e8c9e..89d46d880 100644 --- a/backend/tasks/update_switch_titledb.py +++ b/backend/tasks/update_switch_titledb.py @@ -1,4 +1,5 @@ import json +from itertools import batched from typing import Final from config import ( @@ -9,7 +10,6 @@ from logger.logger import log from tasks.tasks import RemoteFilePullTask from utils.context import initialize_context -from utils.iterators import batched SWITCH_TITLEDB_INDEX_KEY: Final = "romm:switch_titledb" SWITCH_PRODUCT_ID_KEY: Final = "romm:switch_product_id" diff --git a/backend/utils/filesystem.py b/backend/utils/filesystem.py index 3db3fabde..1cc0bfd54 100644 --- a/backend/utils/filesystem.py +++ b/backend/utils/filesystem.py @@ -1,4 +1,5 @@ import os +import re from collections.abc import Iterator from pathlib import Path @@ -27,3 +28,36 @@ def iter_directories(path: str, recursive: bool = False) -> Iterator[tuple[Path, yield Path(root), directory if not recursive: break + + +INVALID_CHARS_HYPHENS = re.compile(r"[\\/:|]") +INVALUD_CHARS_EMPTY = re.compile(r'[*?"<>]') + + +def sanitize_filename(filename): + """ + Replace invalid characters in the filename to make it valid across common filesystems + + Args: + - filename (str): The filename to sanitize. + + Returns: + - str: The sanitized filename. + """ + # Replace some invalid characters with hyphen + sanitized_filename = INVALID_CHARS_HYPHENS.sub("-", filename) + + # Remove other invalid characters + sanitized_filename = INVALUD_CHARS_EMPTY.sub("", sanitized_filename) + + # Ensure null bytes are not included (ZFS allows any characters except null bytes) + sanitized_filename = sanitized_filename.replace("\0", "") + + # Remove leading/trailing whitespace + sanitized_filename = sanitized_filename.strip() + + # Ensure the filename is not empty + if not sanitized_filename: + raise ValueError("Filename cannot be empty after sanitization") + + return sanitized_filename diff --git a/backend/utils/hashing.py b/backend/utils/hashing.py new file mode 100644 index 000000000..a47340474 --- /dev/null +++ b/backend/utils/hashing.py @@ -0,0 +1,2 @@ +def crc32_to_hex(value: int) -> str: + return (value & 0xFFFFFFFF).to_bytes(4, byteorder="big").hex() diff --git a/backend/utils/iterators.py b/backend/utils/iterators.py deleted file mode 100644 index 324503f09..000000000 --- a/backend/utils/iterators.py +++ /dev/null @@ -1,17 +0,0 @@ -import sys - -if sys.version_info >= (3, 12): - from itertools import batched # noqa: F401 -else: - from collections.abc import Iterable, Iterator - from itertools import islice - from typing import TypeVar - - T = TypeVar("T") - - def batched(iterable: Iterable[T], n: int) -> Iterator[tuple[T, ...]]: - if n < 1: - raise ValueError("n must be at least one") - iterator = iter(iterable) - while batch := tuple(islice(iterator, n)): - yield batch diff --git a/backend/utils/nginx.py b/backend/utils/nginx.py new file mode 100644 index 000000000..56d6d4758 --- /dev/null +++ b/backend/utils/nginx.py @@ -0,0 +1,49 @@ +import dataclasses +from collections.abc import Collection +from typing import Any + +from fastapi.responses import Response + + +@dataclasses.dataclass(frozen=True) +class ZipContentLine: + """Dataclass for lines returned in the response body, for usage with the `mod_zip` module. + + Reference: + https://github.com/evanmiller/mod_zip?tab=readme-ov-file#usage + """ + + crc32: str | None + size_bytes: int + encoded_location: str + filename: str + + def __str__(self) -> str: + crc32 = self.crc32 or "-" + return f"{crc32} {self.size_bytes} {self.encoded_location} {self.filename}" + + +class ZipResponse(Response): + """Response class for returning a ZIP archive with multiple files, using the `mod_zip` module.""" + + def __init__( + self, + *, + content_lines: Collection[ZipContentLine], + filename: str, + **kwargs: Any, + ): + if kwargs.get("content"): + raise ValueError( + "Argument 'content' must not be provided, as it is generated from 'content_lines'" + ) + + kwargs["content"] = "\n".join(str(line) for line in content_lines) + kwargs.setdefault("headers", {}).update( + { + "Content-Disposition": f'attachment; filename="{filename}"', + "X-Archive-Files": "zip", + } + ) + + super().__init__(**kwargs) diff --git a/backend/utils/router.py b/backend/utils/router.py index 9abb9bf45..e9bfd1023 100644 --- a/backend/utils/router.py +++ b/backend/utils/router.py @@ -1,4 +1,5 @@ -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from fastapi import APIRouter as FastAPIRouter from fastapi.types import DecoratedCallable @@ -29,7 +30,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/docker/Dockerfile b/docker/Dockerfile index 1f2c3a966..2ee544689 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,7 @@ -ARG ALPINE_VERSION=3.19 -ARG NGINX_VERSION=1.27.0 -ARG NODE_VERSION=lts -ARG PYTHON_VERSION=3.11 +ARG ALPINE_VERSION=3.20 +ARG NGINX_VERSION=1.27.1 +ARG NODE_VERSION=20.16 +ARG PYTHON_VERSION=3.12 # Build frontend FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS front-build-stage @@ -33,13 +33,46 @@ WORKDIR /src COPY ./pyproject.toml ./poetry.lock /src/ RUN poetry install --no-ansi --no-cache --only main +# Build nginx modules +FROM alpine:${ALPINE_VERSION} AS nginx-build + +RUN apk add --no-cache \ + gcc \ + git \ + libc-dev \ + make \ + pcre-dev \ + zlib-dev + +ARG NGINX_VERSION +# The specified commit SHA is the latest commit on the `master` branch at the time of writing. +# It includes a fix to correctly calculate CRC-32 checksums when using upstream subrequests. +# TODO: Move to a tagged release of `mod_zip`, once a version newer than 1.3.0 is released. +ARG NGINX_MOD_ZIP_SHA=8e65b82c82c7890f67a6107271c127e9881b6313 + +# Clone both nginx and `ngx_http_zip_module` repositories, needed to compile the module from source. +# This is needed to be able to dinamically load it as a module in the final image. `nginx` Docker +# images do not have a simple way to include third-party modules. +RUN git clone https://github.com/evanmiller/mod_zip.git && \ + cd ./mod_zip && \ + git checkout "${NGINX_MOD_ZIP_SHA}" && \ + cd ../ && \ + git clone --branch "release-${NGINX_VERSION}" --depth 1 https://github.com/nginx/nginx.git && \ + cd ./nginx && \ + ./auto/configure --with-compat --add-dynamic-module=../mod_zip/ && \ + make -f ./objs/Makefile modules && \ + chmod 644 ./objs/ngx_http_zip_module.so + # Setup frontend and backend -FROM nginx:${NGINX_VERSION}-alpine${ALPINE_VERSION}-slim AS production-stage +FROM nginx:${NGINX_VERSION}-alpine${ALPINE_VERSION} AS production-stage ARG WEBSERVER_FOLDER=/var/www/html +COPY --from=nginx-build ./nginx/objs/ngx_http_zip_module.so /usr/lib/nginx/modules/ + COPY --from=front-build-stage /front/dist ${WEBSERVER_FOLDER} COPY ./frontend/assets/default ${WEBSERVER_FOLDER}/assets/default COPY ./frontend/assets/emulatorjs ${WEBSERVER_FOLDER}/assets/emulatorjs +COPY ./frontend/assets/ruffle ${WEBSERVER_FOLDER}/assets/ruffle COPY ./frontend/assets/scrappers ${WEBSERVER_FOLDER}/assets/scrappers COPY ./frontend/assets/platforms ${WEBSERVER_FOLDER}/assets/platforms COPY ./frontend/assets/webrcade/feed ${WEBSERVER_FOLDER}/assets/webrcade/feed @@ -53,12 +86,15 @@ RUN apk add --no-cache \ mariadb-connector-c \ python3 \ tzdata \ - redis + redis \ + libmagic \ + p7zip COPY ./backend /backend # Setup init script and config files COPY ./docker/init_scripts/* / +COPY ./docker/nginx/js/ /etc/nginx/js/ COPY ./docker/nginx/default.conf /etc/nginx/nginx.conf # User permissions diff --git a/docker/init_scripts/init b/docker/init_scripts/init index b071b9aec..fb44bc8dd 100755 --- a/docker/init_scripts/init +++ b/docker/init_scripts/init @@ -52,6 +52,7 @@ start_bin_gunicorn() { --bind=0.0.0.0:5000 \ --bind=unix:/tmp/gunicorn.sock \ --pid=/tmp/gunicorn.pid \ + --forwarded-allow-ips="*" \ --workers "${GUNICORN_WORKERS:=2}" \ main:app & } diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 5565cad7b..59420be46 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -1,3 +1,6 @@ +load_module modules/ngx_http_js_module.so; +load_module modules/ngx_http_zip_module.so; + worker_processes auto; pid /tmp/nginx.pid; @@ -14,9 +17,14 @@ http { scgi_temp_path /tmp/scgi; sendfile on; + client_body_buffer_size 128k; client_max_body_size 0; + client_header_buffer_size 1k; + large_client_header_buffers 4 16k; + send_timeout 60s; + keepalive_timeout 65s; tcp_nopush on; - # types_hash_max_size 2048; + tcp_nodelay on; include /etc/nginx/mime.types; default_type application/octet-stream; @@ -24,6 +32,8 @@ http { ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE ssl_prefer_server_ciphers on; + js_import /etc/nginx/js/decode.js; + map $time_iso8601 $date { ~([^+]+)T $1; } @@ -41,6 +51,13 @@ http { error_log /dev/stderr; gzip on; + gzip_proxied any; + gzip_vary on; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_min_length 1024; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; # include /etc/nginx/conf.d/*.conf; # include /etc/nginx/sites-enabled/*; @@ -87,5 +104,41 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } + + # Internally redirect download requests + location /library { + internal; + alias /romm/library; + } + + # This location, and the related server at port 8081, are used to serve files when + # using the `mod_zip` module. This is because the `mod_zip` module does not support + # calculating CRC-32 values when using subrequests pointing directly to internal + # locations that access the filesystem. + # TODO: If that gets fixed, this workaround can be removed, and the `/library` location + # can be used directly (also removing the server at port 8081). + # Related issue: https://github.com/evanmiller/mod_zip/issues/90 + location /library-zip { + internal; + rewrite ^/library-zip/(.*)$ /library/$1 break; + proxy_pass http://localhost:8081; + # Proxy buffering must be disabled, for the module to correctly calculate CRC-32 values. + proxy_buffering off; + } + + # Internal decoding endpoint, used to decode base64 encoded data + location /decode { + internal; + js_content decode.decodeBase64; + } + } + + server { + listen 8081; + server_name localhost; + + location /library { + alias /romm/library; + } } } diff --git a/docker/nginx/js/decode.js b/docker/nginx/js/decode.js new file mode 100644 index 000000000..b371fb179 --- /dev/null +++ b/docker/nginx/js/decode.js @@ -0,0 +1,19 @@ +// Decode a Base64 encoded string received as a query parameter named 'value', +// and return the decoded value in the response body. +function decodeBase64(r) { + var encodedValue = r.args.value; + + if (!encodedValue) { + r.return(400, "Missing 'value' query parameter"); + return; + } + + try { + var decodedValue = atob(encodedValue); + r.return(200, decodedValue); + } catch (e) { + r.return(400, "Invalid Base64 encoding"); + } +} + +export default { decodeBase64 }; diff --git a/env.template b/env.template index 63035ca63..76dd91517 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 @@ -39,3 +38,7 @@ ENABLE_SCHEDULED_RESCAN=true SCHEDULED_RESCAN_CRON=0 3 * * * ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB=true SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON=0 4 * * * + +# In-browser emulation +DISABLE_EMULATOR_JS=false +DISABLE_RUFFLE_RS=false diff --git a/frontend/assets/platforms/pico.ico b/frontend/assets/platforms/pico.ico new file mode 100644 index 000000000..e886acaab Binary files /dev/null and b/frontend/assets/platforms/pico.ico differ diff --git a/frontend/assets/platforms/sega-master-system.ico b/frontend/assets/platforms/sega-master-system.ico new file mode 100644 index 000000000..a64f3177c Binary files /dev/null and b/frontend/assets/platforms/sega-master-system.ico differ diff --git a/frontend/assets/ruffle/powered_by_ruffle.png b/frontend/assets/ruffle/powered_by_ruffle.png new file mode 100644 index 000000000..9c0694fe1 Binary files /dev/null and b/frontend/assets/ruffle/powered_by_ruffle.png differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 46fb8fb30..4146fc6c8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,10 +11,11 @@ "license": "GPL-3.0-only", "dependencies": { "@mdi/font": "7.0.96", + "@ruffle-rs/ruffle": "^0.1.0-nightly.2024.7.29", "axios": "^1.7.4", "core-js": "^3.37.1", "cronstrue": "^2.50.0", - "emulatorjs": "github:emulatorjs/emulatorjs#v4.0.12", + "emulatorjs": "github:emulatorjs/emulatorjs#v4.1.1", "file-saver": "^2.0.5", "js-cookie": "^3.0.5", "jszip": "^3.10.1", @@ -2582,6 +2583,11 @@ } } }, + "node_modules/@ruffle-rs/ruffle": { + "version": "0.1.0-nightly.2024.7.29", + "resolved": "https://registry.npmjs.org/@ruffle-rs/ruffle/-/ruffle-0.1.0-nightly.2024.7.29.tgz", + "integrity": "sha512-hChElD2KhZgwP8jkGik4Dl92c1HWAAgu81w+6VBZ0I6MFXJOCEiECjvoj1jrEgAs/pVdordLrAOBqw+v2UDhpA==" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "license": "MIT" @@ -3219,7 +3225,8 @@ }, "node_modules/async": { "version": "2.6.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dependencies": { "lodash": "^4.17.14" } @@ -3251,9 +3258,9 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -3311,7 +3318,8 @@ }, "node_modules/basic-auth": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", "dependencies": { "safe-buffer": "5.1.2" }, @@ -3618,7 +3626,8 @@ }, "node_modules/corser": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", "engines": { "node": ">= 0.4.0" } @@ -3837,9 +3846,8 @@ }, "node_modules/emulatorjs": { "name": "@emulatorjs/emulatorjs", - "version": "4.0.11", - "resolved": "git+ssh://git@github.com/emulatorjs/emulatorjs.git#62c0424e68c3382932237b36b0b03b56e7171c1e", - "license": "GPL-3.0", + "version": "4.0.12", + "resolved": "git+ssh://git@github.com/emulatorjs/emulatorjs.git#a7b59f19a3e9415c2a42121461da3df03b25d3b5", "dependencies": { "http-server": "^14.1.1" } @@ -4250,7 +4258,8 @@ }, "node_modules/eventemitter3": { "version": "4.0.7", - "license": "MIT" + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -4746,7 +4755,8 @@ }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "dependencies": { "whatwg-encoding": "^2.0.0" }, @@ -4756,7 +4766,8 @@ }, "node_modules/http-proxy": { "version": "1.18.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -4768,7 +4779,8 @@ }, "node_modules/http-server": { "version": "14.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", "dependencies": { "basic-auth": "^2.0.1", "chalk": "^4.1.2", @@ -4793,7 +4805,8 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -5502,7 +5515,8 @@ }, "node_modules/mime": { "version": "1.6.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "bin": { "mime": "cli.js" }, @@ -5551,7 +5565,8 @@ }, "node_modules/mkdirp": { "version": "0.5.6", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dependencies": { "minimist": "^1.2.6" }, @@ -5675,7 +5690,8 @@ }, "node_modules/opener": { "version": "1.5.2", - "license": "(WTFPL OR MIT)", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "bin": { "opener": "bin/opener-bin.js" } @@ -5914,7 +5930,8 @@ }, "node_modules/portfinder": { "version": "1.0.32", - "license": "MIT", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", "dependencies": { "async": "^2.6.4", "debug": "^3.2.7", @@ -5926,7 +5943,8 @@ }, "node_modules/portfinder/node_modules/debug": { "version": "3.2.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dependencies": { "ms": "^2.1.1" } @@ -6035,8 +6053,9 @@ } }, "node_modules/qs": { - "version": "6.12.1", - "license": "BSD-3-Clause", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { "side-channel": "^1.0.6" }, @@ -6188,7 +6207,8 @@ }, "node_modules/requires-port": { "version": "1.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, "node_modules/resolve": { "version": "1.22.8", @@ -6338,7 +6358,8 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { "version": "1.77.1", @@ -6358,7 +6379,8 @@ }, "node_modules/secure-compare": { "version": "3.0.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==" }, "node_modules/semver": { "version": "7.6.2", @@ -6969,6 +6991,8 @@ }, "node_modules/union": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", "dependencies": { "qs": "^6.4.0" }, @@ -7043,7 +7067,8 @@ }, "node_modules/url-join": { "version": "4.0.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, "node_modules/util-deprecate": { "version": "1.0.2", @@ -7331,7 +7356,8 @@ }, "node_modules/whatwg-encoding": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", "dependencies": { "iconv-lite": "0.6.3" }, diff --git a/frontend/package.json b/frontend/package.json index f216e0ff6..c0f3e7c68 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,10 +29,11 @@ }, "dependencies": { "@mdi/font": "7.0.96", + "@ruffle-rs/ruffle": "^0.1.0-nightly.2024.7.29", "axios": "^1.7.4", "core-js": "^3.37.1", "cronstrue": "^2.50.0", - "emulatorjs": "github:emulatorjs/emulatorjs#v4.0.12", + "emulatorjs": "github:emulatorjs/emulatorjs#v4.1.1", "file-saver": "^2.0.5", "js-cookie": "^3.0.5", "jszip": "^3.10.1", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 5e38ff7a3..001e2c1ff 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,5 +1,6 @@ @@ -83,6 +83,7 @@ watch( variant="flat" rounded="0" size="small" + class="ml-2" @click="toggleMainSibling" > - Size + Info - {{ - formatBytes(rom.file_size_bytes) - }} + + Size: {{ formatBytes(rom.file_size_bytes) }} + + + SHA-1: {{ rom.sha1_hash }} + + + MD5: {{ rom.md5_hash }} + + + CRC: {{ rom.crc_hash }} + @@ -170,7 +195,7 @@ watch( (); const { smAndDown } = useDisplay(); const releaseDate = new Date( - Number(props.rom.first_release_date) * 1000 + Number(props.rom.first_release_date) * 1000, ).toLocaleDateString("en-US", { day: "2-digit", month: "short", @@ -44,6 +44,7 @@ const hasReleaseDate = Number(props.rom.first_release_date) > 0; diff --git a/frontend/src/components/Details/VersionSwitcher.vue b/frontend/src/components/Details/VersionSwitcher.vue index b25bf68e1..6c8e550ba 100644 --- a/frontend/src/components/Details/VersionSwitcher.vue +++ b/frontend/src/components/Details/VersionSwitcher.vue @@ -11,12 +11,11 @@ const router = useRouter(); const version = ref(props.rom.id); // Functions -function formatItem(rom: DetailedRom) { +function formatTitle(rom: DetailedRom) { const langs = rom.languages.map((l) => languageToEmoji(l)).join(" "); const regions = rom.regions.map((r) => regionToEmoji(r)).join(" "); const tags = rom.tags.map((t) => `(${t})`).join(" "); - if (langs || regions || tags) return `${langs} ${regions} ${tags}`; - return rom.file_name; + return `${langs} ${regions} ${tags}`.trim(); } function updateVersion() { @@ -39,7 +38,7 @@ function updateVersion() { hide-details :items=" [rom, ...rom.sibling_roms].map((i) => ({ - title: formatItem(i), + title: formatTitle(i), value: i.id, })) " diff --git a/frontend/src/components/Gallery/AppBar/Collection/Base.vue b/frontend/src/components/Gallery/AppBar/Collection/Base.vue index 584696d13..b867a1c7c 100644 --- a/frontend/src/components/Gallery/AppBar/Collection/Base.vue +++ b/frontend/src/components/Gallery/AppBar/Collection/Base.vue @@ -1,4 +1,5 @@ - + + diff --git a/frontend/src/components/Gallery/AppBar/Platform/Base.vue b/frontend/src/components/Gallery/AppBar/Platform/Base.vue index abb62d621..a5f57fde7 100644 --- a/frontend/src/components/Gallery/AppBar/Platform/Base.vue +++ b/frontend/src/components/Gallery/AppBar/Platform/Base.vue @@ -1,4 +1,6 @@ - - mdi-chevron-up - + + + mdi-chevron-up + - - - - {{ selectedRoms.length }} - - + + + + {{ selectedRoms.length }} + + - - mdi-delete - - - - - - - - - + + mdi-delete + + + + + + + + + + + diff --git a/frontend/src/components/Management/Dialog/CreatePlatformBinding.vue b/frontend/src/components/Management/Dialog/CreatePlatformBinding.vue index 94cccc420..63f959dd1 100644 --- a/frontend/src/components/Management/Dialog/CreatePlatformBinding.vue +++ b/frontend/src/components/Management/Dialog/CreatePlatformBinding.vue @@ -42,10 +42,10 @@ emitter?.on( }); fsSlugToCreate.value = fsSlug; selectedPlatform.value = supportedPlatforms.value?.find( - (platform) => platform.slug == slug + (platform) => platform.slug == slug, ); show.value = true; - } + }, ); // Functions @@ -60,7 +60,7 @@ function addBindPlatform() { if (selectedPlatform.value) { configStore.addPlatformBinding( fsSlugToCreate.value, - selectedPlatform.value.slug + selectedPlatform.value.slug, ); } }) @@ -136,6 +136,7 @@ function closeDialog() { :key="item.raw.slug" :size="35" :slug="item.raw.slug" + :name="item.raw.name" /> @@ -147,6 +148,7 @@ function closeDialog() { :size="35" :key="item.raw.slug" :slug="item.raw.slug" + :name="item.raw.name" /> diff --git a/frontend/src/components/Management/Dialog/CreatePlatformVersion.vue b/frontend/src/components/Management/Dialog/CreatePlatformVersion.vue index e447fd20b..c375864d2 100644 --- a/frontend/src/components/Management/Dialog/CreatePlatformVersion.vue +++ b/frontend/src/components/Management/Dialog/CreatePlatformVersion.vue @@ -42,10 +42,10 @@ emitter?.on( }); fsSlugToCreate.value = fsSlug; selectedPlatform.value = supportedPlatforms.value?.find( - (platform) => platform.slug == slug + (platform) => platform.slug == slug, ); show.value = true; - } + }, ); // Functions @@ -60,7 +60,7 @@ function addVersionPlatform() { if (selectedPlatform.value) { configStore.addPlatformBinding( fsSlugToCreate.value, - selectedPlatform.value.slug + selectedPlatform.value.slug, ); } }) @@ -136,6 +136,7 @@ function closeDialog() { :key="item.raw.slug" :size="35" :slug="item.raw.slug" + :name="item.raw.name" /> @@ -147,6 +148,7 @@ function closeDialog() { :size="35" :key="item.raw.slug" :slug="item.raw.slug" + :name="item.raw.name" /> diff --git a/frontend/src/components/common/Game/Card/ActionBar.vue b/frontend/src/components/common/Game/Card/ActionBar.vue index 618f54621..eaffbdbff 100644 --- a/frontend/src/components/common/Game/Card/ActionBar.vue +++ b/frontend/src/components/common/Game/Card/ActionBar.vue @@ -1,20 +1,34 @@ - + - + + .action-bar-btn-small { - max-width: 22px; max-height: 30px; + width: unset; } diff --git a/frontend/src/components/common/Game/Card/Base.vue b/frontend/src/components/common/Game/Card/Base.vue index 1cc2f7d1a..3f892a104 100644 --- a/frontend/src/components/common/Game/Card/Base.vue +++ b/frontend/src/components/common/Game/Card/Base.vue @@ -9,7 +9,6 @@ import storeDownload from "@/stores/download"; import storeGalleryView from "@/stores/galleryView"; import storeRoms from "@/stores/roms"; import { type SimpleRom } from "@/stores/roms.js"; -import { storeToRefs } from "pinia"; import { onMounted, ref } from "vue"; import { useTheme } from "vuetify"; @@ -41,7 +40,7 @@ const props = withDefaults( withBorder: false, withBorderRommAccent: false, src: "", - } + }, ); const romsStore = storeRoms(); const emit = defineEmits(["click", "touchstart", "touchend"]); @@ -100,29 +99,29 @@ onMounted(() => { src ? src : romsStore.isSimpleRom(rom) - ? !rom.igdb_id && !rom.moby_id && !rom.has_cover - ? `/assets/default/cover/big_${theme.global.name.value}_unmatched.png` - : (rom.igdb_id || rom.moby_id) && !rom.has_cover - ? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png` - : `/assets/romm/resources/${rom.path_cover_l}?ts=${rom.updated_at}` - : !rom.igdb_url_cover && !rom.moby_url_cover - ? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png` - : rom.igdb_url_cover - ? rom.igdb_url_cover - : rom.moby_url_cover + ? !rom.igdb_id && !rom.moby_id && !rom.has_cover + ? `/assets/default/cover/big_${theme.global.name.value}_unmatched.png` + : (rom.igdb_id || rom.moby_id) && !rom.has_cover + ? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png` + : `/assets/romm/resources/${rom.path_cover_l}?ts=${rom.updated_at}` + : !rom.igdb_url_cover && !rom.moby_url_cover + ? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png` + : rom.igdb_url_cover + ? rom.igdb_url_cover + : rom.moby_url_cover " :lazy-src=" romsStore.isSimpleRom(rom) ? !rom.igdb_id && !rom.moby_id && !rom.has_cover ? `/assets/default/cover/big_${theme.global.name.value}_unmatched.png` : (rom.igdb_id || rom.moby_id) && !rom.has_cover - ? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png` - : `/assets/romm/resources/${rom.path_cover_s}?ts=${rom.updated_at}` + ? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png` + : `/assets/romm/resources/${rom.path_cover_s}?ts=${rom.updated_at}` : !rom.igdb_url_cover && !rom.moby_url_cover - ? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png` - : rom.igdb_url_cover - ? rom.igdb_url_cover - : rom.moby_url_cover + ? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png` + : rom.igdb_url_cover + ? rom.igdb_url_cover + : rom.moby_url_cover " :aspect-ratio="2 / 3" > @@ -162,6 +161,7 @@ onMounted(() => { :size="25" :key="rom.platform_slug" :slug="rom.platform_slug" + :name="rom.platform_name" class="label-platform" /> diff --git a/frontend/src/components/common/Game/Dialog/EditRom.vue b/frontend/src/components/common/Game/Dialog/EditRom.vue index e23239f56..0606c2073 100644 --- a/frontend/src/components/common/Game/Dialog/EditRom.vue +++ b/frontend/src/components/common/Game/Dialog/EditRom.vue @@ -6,13 +6,13 @@ import storeHeartbeat from "@/stores/heartbeat"; import storeRoms, { type SimpleRom } from "@/stores/roms"; import type { Events } from "@/types/emitter"; import type { Emitter } from "mitt"; -import { inject, ref } from "vue"; +import { inject, ref, computed } from "vue"; import { useRoute } from "vue-router"; import { useDisplay, useTheme } from "vuetify"; // Props const theme = useTheme(); -const { lgAndUp } = useDisplay(); +const { lgAndUp, smAndUp } = useDisplay(); const heartbeat = storeHeartbeat(); const route = useRoute(); const show = ref(false); @@ -20,10 +20,6 @@ const rom = ref(); const romsStore = storeRoms(); const imagePreviewUrl = ref(""); const removeCover = ref(false); -const fileNameInputRules = { - required: (value: string) => !!value || "Required", - newFileName: (value: string) => !value.includes("/") || "Invalid characters", -}; const emitter = inject>("emitter"); emitter?.on("showEditRomDialog", (romToEdit: UpdateRom | undefined) => { show.value = true; @@ -59,33 +55,27 @@ async function removeArtwork() { removeCover.value = true; } -async function updateRom() { - if (!rom.value) return; - - if (rom.value.file_name.includes("/")) { - emitter?.emit("snackbarShow", { - msg: "Couldn't edit rom: invalid file name characters", - icon: "mdi-close-circle", - color: "red", - }); - return; - } else if (!rom.value.file_name) { - emitter?.emit("snackbarShow", { - msg: "Couldn't edit rom: file name required", - icon: "mdi-close-circle", - color: "red", - }); - return; - } +const noMetadataMatch = computed(() => { + return !rom.value?.igdb_id && !rom.value?.moby_id && !rom.value?.sgdb_id; +}); +async function handleRomUpdate( + options: { + rom: UpdateRom; + renameAsSource?: boolean; + removeCover?: boolean; + unmatch?: boolean; + }, + successMessage: string, +) { show.value = false; emitter?.emit("showLoadingDialog", { loading: true, scrim: true }); await romApi - .updateRom({ rom: rom.value, removeCover: removeCover.value }) + .updateRom(options) .then(({ data }) => { emitter?.emit("snackbarShow", { - msg: "Rom updated successfully!", + msg: successMessage, icon: "mdi-check-bold", color: "green", }); @@ -108,6 +98,30 @@ async function updateRom() { }); } +async function unmatchRom() { + if (!rom.value) return; + await handleRomUpdate( + { rom: rom.value, unmatch: true }, + "Rom unmatched successfully", + ); +} + +async function updateRom() { + if (!rom.value?.file_name) { + emitter?.emit("snackbarShow", { + msg: "Cannot save: file name is required", + icon: "mdi-close-circle", + color: "red", + }); + return; + } + + await handleRomUpdate( + { rom: rom.value, removeCover: removeCover.value }, + "Rom updated successfully!", + ); +} + function closeDialog() { show.value = false; imagePreviewUrl.value = ""; @@ -126,7 +140,7 @@ function closeDialog() { - + - + + > + + + mdi-folder-file-outline + + + /romm/library/{{ rom.file_path }}/{{ rom.file_name }} + + + - + + + + + Unmatch Rom + + + + Cancel + + Save + + + @@ -177,13 +221,15 @@ function closeDialog() { @@ -221,16 +267,6 @@ function closeDialog() { - - - - Cancel - - Apply - - - - + + diff --git a/frontend/src/components/common/Game/Dialog/SearchRom.vue b/frontend/src/components/common/Game/Dialog/SearchRom.vue index 16277ca38..63c7414c9 100644 --- a/frontend/src/components/common/Game/Dialog/SearchRom.vue +++ b/frontend/src/components/common/Game/Dialog/SearchRom.vue @@ -43,7 +43,7 @@ async function filterRoms() { } else { filteredRoms.value = searchedRoms.value.filter( (rom: { platform_name: string }) => - rom.platform_name == selectedPlatform.value?.platform_name + rom.platform_name == selectedPlatform.value?.platform_name, ) as SimpleRom[]; } } @@ -73,7 +73,7 @@ async function searchRoms() { platform_name: rom.platform_name, platform_slug: rom.platform_slug, }, - ]) + ]), ).values(), ]; filterRoms(); @@ -148,6 +148,7 @@ onBeforeUnmount(() => { :size="35" :key="(item as SelectItem).raw.platform_slug" :slug="(item as SelectItem).raw.platform_slug" + :name="(item as SelectItem).raw.platform_name" /> @@ -162,6 +163,7 @@ onBeforeUnmount(() => { :size="35" :key="(item as SelectItem).raw.platform_slug" :slug="(item as SelectItem).raw.platform_slug" + :name="(item as SelectItem).raw.platform_name" /> diff --git a/frontend/src/components/common/Game/Dialog/UploadRom.vue b/frontend/src/components/common/Game/Dialog/UploadRom.vue index 886139fd8..5e210eaf7 100644 --- a/frontend/src/components/common/Game/Dialog/UploadRom.vue +++ b/frontend/src/components/common/Game/Dialog/UploadRom.vue @@ -6,6 +6,7 @@ import romApi from "@/services/api/rom"; import socket from "@/services/socket"; import storeHeartbeat from "@/stores/heartbeat"; import { type Platform } from "@/stores/platforms"; +import storeUpload from "@/stores/upload"; import storeScanning from "@/stores/scanning"; import type { Events } from "@/types/emitter"; import { formatBytes } from "@/utils"; @@ -16,11 +17,12 @@ import { useDisplay } from "vuetify"; // Props const { xs, mdAndUp, smAndUp } = useDisplay(); const show = ref(false); -const romsToUpload = ref([]); +const filesToUpload = ref([]); const scanningStore = storeScanning(); const selectedPlatform = ref(null); const supportedPlatforms = ref(); const heartbeat = storeHeartbeat(); +const uploadStore = storeUpload(); const HEADERS = [ { title: "Name", @@ -50,9 +52,7 @@ emitter?.on("showUploadRomDialog", (platformWhereUpload) => { }) .catch(({ response, message }) => { emitter?.emit("snackbarShow", { - msg: `Unable to upload roms: ${ - response?.data?.detail || response?.statusText || message - }`, + msg: `Unable to upload roms: ${response?.data?.detail || response?.statusText || message}`, icon: "mdi-close-circle", color: "red", timeout: 4000, @@ -64,7 +64,6 @@ emitter?.on("showUploadRomDialog", (platformWhereUpload) => { async function uploadRoms() { if (!selectedPlatform.value) return; show.value = false; - scanningStore.set(true); if (selectedPlatform.value.id == -1) { await platformApi @@ -93,36 +92,40 @@ async function uploadRoms() { } const platformId = selectedPlatform.value.id; - emitter?.emit("snackbarShow", { - msg: `Uploading ${romsToUpload.value.length} roms to ${selectedPlatform.value.name}...`, - icon: "mdi-loading mdi-spin", - color: "romm-accent-1", - }); await romApi .uploadRoms({ - romsToUpload: romsToUpload.value, + filesToUpload: filesToUpload.value, platformId: platformId, }) - .then(({ data }) => { - const { uploaded_roms, skipped_roms } = data; + .then((responses: PromiseSettledResult[]) => { + const successfulUploads = responses.filter( + (d) => d.status == "fulfilled", + ); + const failedUploads = responses.filter((d) => d.status == "rejected"); + + if (failedUploads.length == 0) { + uploadStore.clearAll(); + } - if (uploaded_roms.length == 0) { + if (successfulUploads.length == 0) { return emitter?.emit("snackbarShow", { msg: `All files skipped, nothing to upload.`, icon: "mdi-close-circle", color: "orange", - timeout: 2000, + timeout: 5000, }); } emitter?.emit("snackbarShow", { - msg: `${uploaded_roms.length} files uploaded successfully (and ${skipped_roms.length} skipped). Starting scan...`, + msg: `${successfulUploads.length} files uploaded successfully (and ${failedUploads.length} skipped/failed). Starting scan...`, icon: "mdi-check-bold", color: "green", - timeout: 2000, + timeout: 3000, }); + scanningStore.set(true); + if (!socket.connected) socket.connect(); setTimeout(() => { socket.emit("scan", { @@ -134,15 +137,13 @@ async function uploadRoms() { }) .catch(({ response, message }) => { emitter?.emit("snackbarShow", { - msg: `Unable to upload roms: ${ - response?.data?.detail || response?.statusText || message - }`, + msg: `Unable to upload roms: ${response?.data?.detail || response?.statusText || message}`, icon: "mdi-close-circle", color: "red", timeout: 4000, }); }); - romsToUpload.value = []; + filesToUpload.value = []; selectedPlatform.value = null; } @@ -152,17 +153,19 @@ function triggerFileInput() { } function removeRomFromList(romName: string) { - romsToUpload.value = romsToUpload.value.filter((rom) => rom.name !== romName); + filesToUpload.value = filesToUpload.value.filter( + (rom) => rom.name !== romName, + ); } function closeDialog() { show.value = false; - romsToUpload.value = []; + filesToUpload.value = []; selectedPlatform.value = null; } function updateDataTablePages() { - pageCount.value = Math.ceil(romsToUpload.value.length / itemsPerPage.value); + pageCount.value = Math.ceil(filesToUpload.value.length / itemsPerPage.value); } watch(itemsPerPage, async () => { updateDataTablePages(); @@ -201,6 +204,7 @@ watch(itemsPerPage, async () => { :key="item.raw.slug" :size="35" :slug="item.raw.slug" + :name="item.raw.name" /> @@ -212,6 +216,7 @@ watch(itemsPerPage, async () => { :size="35" :key="item.raw.slug" :slug="item.raw.slug" + :name="item.raw.name" /> @@ -232,7 +237,7 @@ watch(itemsPerPage, async () => { { { Cancel { localStorage.setItem("romsPerPage", itemsPerPage.value.toString()); updateDataTablePages(); @@ -136,7 +147,11 @@ onMounted(() => { > @@ -175,11 +190,23 @@ onMounted(() => { mdi-download + mdi-play + + { const response = await fetch( - "https://api.github.com/repos/rommapp/romm/releases/latest" + "https://api.github.com/repos/rommapp/romm/releases/latest", ); const json = await response.json(); GITHUB_VERSION.value = json.tag_name; @@ -29,17 +29,7 @@ onMounted(async () => { - + - + New version available v{{ GITHUB_VERSION }} - + Dismiss { - + + diff --git a/frontend/src/components/common/Notification.vue b/frontend/src/components/common/Notification.vue index 65ddd4868..3d732ebf0 100644 --- a/frontend/src/components/common/Notification.vue +++ b/frontend/src/components/common/Notification.vue @@ -17,7 +17,6 @@ emitter?.on("snackbarShow", (snackbar: SnackbarStatus) => { show.value = true; snackbarStatus.value = snackbar; snackbarStatus.value.id = notificationStore.notifications.length + 1; - // notificationStore.add(snackbarStatus.value); }); function closeDialog() { diff --git a/frontend/src/components/common/Platform/Card.vue b/frontend/src/components/common/Platform/Card.vue index 42a6b74be..4cad9adf1 100644 --- a/frontend/src/components/common/Platform/Card.vue +++ b/frontend/src/components/common/Platform/Card.vue @@ -27,6 +27,7 @@ defineProps<{ platform: Platform }>(); diff --git a/frontend/src/components/common/Platform/Dialog/DeletePlatform.vue b/frontend/src/components/common/Platform/Dialog/DeletePlatform.vue index 953400de4..c34d661f1 100644 --- a/frontend/src/components/common/Platform/Dialog/DeletePlatform.vue +++ b/frontend/src/components/common/Platform/Dialog/DeletePlatform.vue @@ -66,7 +66,7 @@ function closeDialog() { Removing platform - + {{ platform.name }} - [{{ platform.fs_slug @@ -79,13 +79,8 @@ function closeDialog() { - - Cancel - - + Cancel + Confirm diff --git a/frontend/src/components/common/Platform/Icon.vue b/frontend/src/components/common/Platform/Icon.vue index 664f6e128..7d36a37ea 100644 --- a/frontend/src/components/common/Platform/Icon.vue +++ b/frontend/src/components/common/Platform/Icon.vue @@ -3,22 +3,32 @@ import storeConfig from "@/stores/config"; import { storeToRefs } from "pinia"; const props = withDefaults( - defineProps<{ slug: string; size?: number; rounded?: number }>(), - { size: 40, rounded: 0 } + defineProps<{ + slug: string; + name?: string; + size?: number; + rounded?: number; + }>(), + { size: 40, rounded: 0 }, ); const configStore = storeConfig(); const { config } = storeToRefs(configStore); - + + :src="`/assets/platforms/${config.PLATFORMS_VERSIONS?.[ + props.slug + ]?.toLowerCase()}.ico`" + > + + + + + + + + diff --git a/frontend/src/components/common/Platform/ListItem.vue b/frontend/src/components/common/Platform/ListItem.vue index 384159ce2..bdced0c83 100644 --- a/frontend/src/components/common/Platform/ListItem.vue +++ b/frontend/src/components/common/Platform/ListItem.vue @@ -15,8 +15,12 @@ withDefaults(defineProps<{ platform: Platform; rail?: boolean }>(), { :value="platform.slug" > - + (), { size: 40 }); - + diff --git a/frontend/src/components/common/UploadInProgress.vue b/frontend/src/components/common/UploadInProgress.vue new file mode 100644 index 000000000..f096ab871 --- /dev/null +++ b/frontend/src/components/common/UploadInProgress.vue @@ -0,0 +1,106 @@ + + + + + + + + + {{ file.filename }} + + + + {{ file.failureReason }} + + + + + {{ file.filename }} + + + + + + {{ formatBytes(file.rate) }}/s + + {{ formatBytes(file.loaded) }} / + {{ formatBytes(file.total) }} + + + + + + + + {{ formatBytes(file.total) }} + + + + + + + + + Clear finished + + + + + + diff --git a/frontend/src/layouts/Dashboard/Recent.vue b/frontend/src/layouts/Dashboard/Recent.vue index 1acc737f1..d285c56c7 100644 --- a/frontend/src/layouts/Dashboard/Recent.vue +++ b/frontend/src/layouts/Dashboard/Recent.vue @@ -8,12 +8,20 @@ import { useRouter } from "vue-router"; // Props const romsStore = storeRoms(); -const { recentRoms } = storeToRefs(romsStore) +const { recentRoms } = storeToRefs(romsStore); const router = useRouter(); // Functions function onGameClick(emitData: { rom: SimpleRom; event: MouseEvent }) { - router.push({ name: "rom", params: { rom: emitData.rom.id } }); + if (emitData.event.metaKey || emitData.event.ctrlKey) { + const link = router.resolve({ + name: "rom", + params: { rom: emitData.rom.id }, + }); + window.open(link.href, "_blank"); + } else { + router.push({ name: "rom", params: { rom: emitData.rom.id } }); + } } diff --git a/frontend/src/layouts/NotificationStack.vue b/frontend/src/layouts/NotificationStack.vue deleted file mode 100644 index 3a17d5a94..000000000 --- a/frontend/src/layouts/NotificationStack.vue +++ /dev/null @@ -1,23 +0,0 @@ - - - - - diff --git a/frontend/src/plugins/router.ts b/frontend/src/plugins/router.ts index 8c172d948..52a8eaed4 100644 --- a/frontend/src/plugins/router.ts +++ b/frontend/src/plugins/router.ts @@ -39,9 +39,14 @@ const routes = [ component: () => import("@/views/GameDetails.vue"), }, { - path: "/rom/:rom/play", - name: "play", - component: () => import("@/views/Play/Base.vue"), + path: "/rom/:rom/ejs", + name: "emulatorjs", + component: () => import("@/views/EmulatorJS/Base.vue"), + }, + { + path: "/rom/:rom/ruffle", + name: "ruffle", + component: () => import("@/views/RuffleRS/Base.vue"), }, { path: "/scan", diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts index ea13dfc2a..5d2210b81 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -1,32 +1,51 @@ -import type { - AddRomsResponse, - MessageResponse, - SearchRomSchema, -} from "@/__generated__"; +import type { MessageResponse, SearchRomSchema } from "@/__generated__"; import api from "@/services/api/index"; import socket from "@/services/socket"; -import storeDownload from "@/stores/download"; +import storeUpload from "@/stores/upload"; import type { DetailedRom, SimpleRom } from "@/stores/roms"; import { getDownloadLink } from "@/utils"; +import type { AxiosProgressEvent } from "axios"; export const romApi = api; async function uploadRoms({ platformId, - romsToUpload, + filesToUpload, }: { platformId: number; - romsToUpload: File[]; -}): Promise<{ data: AddRomsResponse }> { - const formData = new FormData(); - romsToUpload.forEach((rom) => formData.append("roms", rom)); - - return api.post("/roms", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - params: { platform_id: platformId }, + filesToUpload: File[]; +}): Promise[]> { + if (!socket.connected) socket.connect(); + const uploadStore = storeUpload(); + + const promises = filesToUpload.map((file) => { + const formData = new FormData(); + formData.append(file.name, file); + + uploadStore.start(file.name); + + return new Promise((resolve, reject) => { + api + .post("/roms", formData, { + headers: { + "Content-Type": "multipart/form-data; boundary=boundary", + "X-Upload-Platform": platformId.toString(), + "X-Upload-Filename": file.name, + }, + params: {}, + onUploadProgress: (progressEvent: AxiosProgressEvent) => { + uploadStore.update(file.name, progressEvent); + }, + }) + .then(resolve) + .catch((error) => { + uploadStore.fail(file.name, error.response?.data?.detail); + reject(error); + }); + }); }); + + return Promise.allSettled(promises); } async function getRoms({ @@ -67,14 +86,6 @@ async function getRom({ return api.get(`/roms/${romId}`); } -function clearRomFromDownloads({ id }: { id: number }) { - const downloadStore = storeDownload(); - downloadStore.remove(id); - - // Disconnect socket when no more downloads are in progress - if (downloadStore.value.length === 0) socket.disconnect(); -} - async function searchRom({ romId, searchTerm, @@ -93,9 +104,6 @@ async function searchRom({ }); } -// Listen for multi-file download completion events -socket.on("download:complete", clearRomFromDownloads); - // Used only for multi-file downloads async function downloadRom({ rom, @@ -110,17 +118,6 @@ async function downloadRom({ document.body.appendChild(a); a.click(); document.body.removeChild(a); - - // Only connect socket if multi-file download - if (rom.multi && files.length > 1) { - if (!socket.connected) socket.connect(); - storeDownload().add(rom.id); - - // Clear download state after 60 seconds in case error/timeout - setTimeout(() => { - clearRomFromDownloads(rom); - }, 60 * 1000); - } } export type UpdateRom = SimpleRom & { @@ -131,10 +128,12 @@ async function updateRom({ rom, renameAsSource = false, removeCover = false, + unmatch = false, }: { rom: UpdateRom; renameAsSource?: boolean; removeCover?: boolean; + unmatch?: boolean; }): Promise<{ data: DetailedRom }> { const formData = new FormData(); if (rom.igdb_id) formData.append("igdb_id", rom.igdb_id.toString()); @@ -146,7 +145,11 @@ async function updateRom({ if (rom.artwork) formData.append("artwork", rom.artwork); return api.put(`/roms/${rom.id}`, formData, { - params: { rename_as_source: renameAsSource, remove_cover: removeCover }, + params: { + rename_as_source: renameAsSource, + remove_cover: removeCover, + unmatch_metadata: unmatch, + }, }); } diff --git a/frontend/src/stores/download.ts b/frontend/src/stores/download.ts index 036e6ace8..601ffca4b 100644 --- a/frontend/src/stores/download.ts +++ b/frontend/src/stores/download.ts @@ -3,7 +3,7 @@ import { defineStore } from "pinia"; export default defineStore("download", { state: () => ({ value: [] as number[], - filesToDownloadMultiFileRom: [] as string[], + filesToDownload: [] as string[], }), actions: { @@ -15,7 +15,7 @@ export default defineStore("download", { }, clear() { this.value = [] as number[]; - this.filesToDownloadMultiFileRom = [] as string[]; + this.filesToDownload = [] as string[]; }, }, }); diff --git a/frontend/src/stores/heartbeat.ts b/frontend/src/stores/heartbeat.ts index 3d1d0f76c..53db5d992 100644 --- a/frontend/src/stores/heartbeat.ts +++ b/frontend/src/stores/heartbeat.ts @@ -2,6 +2,8 @@ import type { HeartbeatResponse } from "@/__generated__"; import { defineStore } from "pinia"; import { computed } from "vue"; +export type Heartbeat = HeartbeatResponse; + export default defineStore("heartbeat", { state: () => { return { value: {} as HeartbeatResponse }; diff --git a/frontend/src/stores/roms.ts b/frontend/src/stores/roms.ts index b9456821b..ba20a097e 100644 --- a/frontend/src/stores/roms.ts +++ b/frontend/src/stores/roms.ts @@ -1,5 +1,5 @@ import type { SearchRomSchema } from "@/__generated__"; -import type { DetailedRomSchema, RomSchema } from "@/__generated__/"; +import type { DetailedRomSchema, SimpleRomSchema } from "@/__generated__/"; import { type Platform } from "@/stores/platforms"; import { type Collection } from "@/stores/collections"; import type { ExtractPiniaStoreType } from "@/types"; @@ -13,8 +13,7 @@ type GalleryFilterStore = ExtractPiniaStoreType; const collectionStore = storeCollection(); -export type SimpleRom = RomSchema; - +export type SimpleRom = SimpleRomSchema; export type DetailedRom = DetailedRomSchema; export default defineStore("roms", { @@ -24,8 +23,8 @@ export default defineStore("roms", { currentRom: null as DetailedRom | null, allRoms: [] as SimpleRom[], _grouped: [] as SimpleRom[], - _filteredIDs: [] as number[], - _selectedIDs: [] as number[], + _filteredIDs: new Set(), + _selectedIDs: new Set(), recentRoms: [] as SimpleRom[], lastSelectedIndex: -1, selecting: false, @@ -35,9 +34,9 @@ export default defineStore("roms", { getters: { filteredRoms: (state) => - state._grouped.filter((rom) => state._filteredIDs.includes(rom.id)), + state._grouped.filter((rom) => state._filteredIDs.has(rom.id)), selectedRoms: (state) => - state._grouped.filter((rom) => state._selectedIDs.includes(rom.id)), + state._grouped.filter((rom) => state._selectedIDs.has(rom.id)), }, actions: { @@ -116,22 +115,18 @@ export default defineStore("roms", { return rom.id === value.id; }); }); - this._filteredIDs = this._filteredIDs.filter((value) => { - return !roms.find((rom) => { - return rom.id === value; - }); - }); + roms.forEach((rom) => this._filteredIDs.delete(rom.id)); }, reset() { this.allRoms = []; this._grouped = []; - this._filteredIDs = []; - this._selectedIDs = []; + this._filteredIDs = new Set(); + this._selectedIDs = new Set(); this.lastSelectedIndex = -1; }, // Filter roms by gallery filter store state setFiltered(roms: SimpleRom[], galleryFilter: GalleryFilterStore) { - this._filteredIDs = roms.map((rom) => rom.id); + this._filteredIDs = new Set(roms.map((rom) => rom.id)); if (galleryFilter.filterSearch) { this._filterSearch(galleryFilter.filterSearch); } @@ -158,68 +153,108 @@ export default defineStore("roms", { } }, _filterSearch(searchFilter: string) { - this._filteredIDs = this.filteredRoms - .filter( - (rom) => - rom.name?.toLowerCase().includes(searchFilter.toLowerCase()) || - rom.file_name?.toLowerCase().includes(searchFilter.toLowerCase()), - ) - .map((roms) => roms.id); + const bySearch = new Set( + this.filteredRoms + .filter( + (rom) => + rom.name?.toLowerCase().includes(searchFilter.toLowerCase()) || + rom.file_name?.toLowerCase().includes(searchFilter.toLowerCase()), + ) + .map((roms) => roms.id), + ); + + // @ts-expect-error intersection is recently defined on Set + this._filteredIDs = bySearch.intersection(this._filteredIDs); }, _filterUnmatched() { - this._filteredIDs = this.filteredRoms - .filter((rom) => !rom.igdb_id && !rom.moby_id) - .map((roms) => roms.id); + const byUnmatched = new Set( + this.filteredRoms + .filter((rom) => !rom.igdb_id && !rom.moby_id) + .map((roms) => roms.id), + ); + + // @ts-expect-error intersection is recently defined on Set + this._filteredIDs = byUnmatched.intersection(this._filteredIDs); }, _filterFavourites() { - this._filteredIDs = this.filteredRoms - .filter((rom) => collectionStore.favCollection?.roms?.includes(rom.id)) - .map((roms) => roms.id); + const byFavourites = new Set( + this.filteredRoms + .filter((rom) => + collectionStore.favCollection?.roms?.includes(rom.id), + ) + .map((roms) => roms.id), + ); + + // @ts-expect-error intersection is recently defined on Set + this._filteredIDs = byFavourites.intersection(this._filteredIDs); }, _filterDuplicates() { - this._filteredIDs = this.filteredRoms - .filter((rom) => rom.sibling_roms?.length) - .map((rom) => rom.id); + const byDuplicates = new Set( + this.filteredRoms + .filter((rom) => rom.sibling_roms?.length) + .map((rom) => rom.id), + ); + + // @ts-expect-error intersection is recently defined on Set + this._filteredIDs = byDuplicates.intersection(this._filteredIDs); }, _filterGenre(genreToFilter: string) { - this._filteredIDs = this.filteredRoms - .filter((rom) => rom.genres.some((genre) => genre === genreToFilter)) - .map((rom) => rom.id); + const byGenre = new Set( + this.filteredRoms + .filter((rom) => rom.genres.some((genre) => genre === genreToFilter)) + .map((rom) => rom.id), + ); + + // @ts-expect-error intersection is recently defined on Set + this._filteredIDs = byGenre.intersection(this._filteredIDs); }, _filterFranchise(franchiseToFilter: string) { - this._filteredIDs = this.filteredRoms - .filter((rom) => - rom.franchises.some((franchise) => franchise === franchiseToFilter), - ) - .map((rom) => rom.id); + const byFranchise = new Set( + this.filteredRoms + .filter((rom) => + rom.franchises.some((franchise) => franchise === franchiseToFilter), + ) + .map((rom) => rom.id), + ); + + // @ts-expect-error intersection is recently defined on Set + this._filteredIDs = byFranchise.intersection(this._filteredIDs); }, _filterCollection(collectionToFilter: string) { - this._filteredIDs = this.filteredRoms - .filter((rom) => - rom.collections.some( - (collection) => collection === collectionToFilter, - ), - ) - .map((rom) => rom.id); + const byCollection = new Set( + this.filteredRoms + .filter((rom) => + rom.collections.some( + (collection) => collection === collectionToFilter, + ), + ) + .map((rom) => rom.id), + ); + + // @ts-expect-error intersection is recently defined on Set + this._filteredIDs = byCollection.intersection(this._filteredIDs); }, _filterCompany(companyToFilter: string) { - this._filteredIDs = this.filteredRoms - .filter((rom) => - rom.companies.some((company) => company === companyToFilter), - ) - .map((rom) => rom.id); + const byCompany = new Set( + this.filteredRoms + .filter((rom) => + rom.companies.some((company) => company === companyToFilter), + ) + .map((rom) => rom.id), + ); + + // @ts-expect-error intersection is recently defined on Set + this._filteredIDs = byCompany.intersection(this._filteredIDs); }, // Selected roms setSelection(roms: SimpleRom[]) { - this._selectedIDs = roms.map((rom) => rom.id); + this._selectedIDs = new Set(roms.map((rom) => rom.id)); }, addToSelection(rom: SimpleRom) { - this._selectedIDs.push(rom.id); + this._selectedIDs.add(rom.id); }, removeFromSelection(rom: SimpleRom) { - this._selectedIDs = this._selectedIDs.filter((id) => { - return id !== rom.id; - }); + this._selectedIDs.delete(rom.id); }, updateLastSelected(index: number) { this.lastSelectedIndex = index; @@ -228,7 +263,7 @@ export default defineStore("roms", { this.selecting = !this.selecting; }, resetSelection() { - this._selectedIDs = []; + this._selectedIDs = new Set(); this.lastSelectedIndex = -1; }, isSimpleRom(rom: SimpleRom | SearchRomSchema): rom is SimpleRom { diff --git a/frontend/src/stores/upload.ts b/frontend/src/stores/upload.ts new file mode 100644 index 000000000..b6265a91d --- /dev/null +++ b/frontend/src/stores/upload.ts @@ -0,0 +1,60 @@ +import type { AxiosProgressEvent } from "axios"; +import { defineStore } from "pinia"; + +class UploadingFile { + filename: string; + progress: number; + total: number; + loaded: number; + rate: number; + finished: boolean; + failed: boolean; + failureReason: string; + + constructor(filename: string) { + this.filename = filename; + this.progress = 0; + this.total = 0; + this.loaded = 0; + this.rate = 0; + this.finished = false; + this.failed = false; + this.failureReason = ""; + } +} + +export default defineStore("upload", { + state: () => ({ + files: [] as UploadingFile[], + }), + actions: { + start(filename: string) { + this.files = [...this.files, new UploadingFile(filename)]; + }, + update(filename: string, progressEvent: AxiosProgressEvent) { + const file = this.files.find((f) => f.filename === filename); + if (!file) return; + + file.progress = progressEvent.progress + ? progressEvent.progress * 100 + : file.progress; + file.total = progressEvent.total || file.total; + file.loaded = progressEvent.loaded; + file.rate = progressEvent.rate || file.rate; + file.finished = progressEvent.loaded === progressEvent.total; + }, + fail(filename: string, reason: string) { + const file = this.files.find((f) => f.filename === filename); + if (!file) return; + + file.failed = true; + file.failureReason = reason; + }, + clearFinished() { + this.files = this.files.filter((f) => !f.finished && !f.failed); + }, + clearAll() { + this.files = []; + }, + }, +}); diff --git a/frontend/src/styles/common.css b/frontend/src/styles/common.css index 32749cf2f..e754663f8 100644 --- a/frontend/src/styles/common.css +++ b/frontend/src/styles/common.css @@ -60,6 +60,12 @@ body { border: 1px solid rgba(var(--v-theme-romm-accent-1)) !important; transform: scale(1.05); } +#main-app-bar { + top: 0 !important; +} +#main-app-bar ~ #gallery-app-bar { + top: 45px !important; +} /* Calculation to fix 100dvh: main-app-bar -> 45px diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index 926ebffb1..7360da90b 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -1,5 +1,6 @@ import cronstrue from "cronstrue"; import type { SimpleRom } from "@/stores/roms"; +import type { Heartbeat } from "@/stores/heartbeat"; export const views: Record< number, @@ -265,6 +266,7 @@ export function languageToEmoji(language: string) { const _EJS_CORES_MAP = { "3do": ["opera"], amiga: ["puae"], + "amiga-cd32": ["puae"], arcade: [ "mame2003", "mame2003_plus", @@ -340,10 +342,26 @@ const _EJS_CORES_MAP = { export type EJSPlatformSlug = keyof typeof _EJS_CORES_MAP; -export function getSupportedCores(platformSlug: string) { +export function getSupportedEJSCores(platformSlug: string) { return _EJS_CORES_MAP[platformSlug.toLowerCase() as EJSPlatformSlug] || []; } -export function isEmulationSupported(platformSlug: string) { - return platformSlug.toLowerCase() in _EJS_CORES_MAP; +export function isEJSEmulationSupported( + platformSlug: string, + heartbeat: Heartbeat, +) { + return ( + platformSlug.toLowerCase() in _EJS_CORES_MAP && + !heartbeat.EMULATION.DISABLE_EMULATOR_JS + ); +} + +export function isRuffleEmulationSupported( + platformSlug: string, + heartbeat: Heartbeat, +) { + return ( + ["flash", "browser"].includes(platformSlug.toLowerCase()) && + !heartbeat.EMULATION.DISABLE_RUFFLE_RS + ); } diff --git a/frontend/src/views/Play/Base.vue b/frontend/src/views/EmulatorJS/Base.vue similarity index 97% rename from frontend/src/views/Play/Base.vue rename to frontend/src/views/EmulatorJS/Base.vue index 98fc9f0b4..2e57ef995 100644 --- a/frontend/src/views/Play/Base.vue +++ b/frontend/src/views/EmulatorJS/Base.vue @@ -1,14 +1,15 @@ + + + + + + + + + + + + + + + + + {{ rom.name }} + {{ + rom.file_name + }} + + + + + + + + + {{ + fullScreenOnPlay + ? "mdi-checkbox-outline" + : "mdi-checkbox-blank-outline" + }}Full screen + + + Play + + + + Reset session + + Back to game details + + Back to gallery + + + + + + + + + + diff --git a/frontend/src/views/Scan.vue b/frontend/src/views/Scan.vue index eb195e0c2..934a98420 100644 --- a/frontend/src/views/Scan.vue +++ b/frontend/src/views/Scan.vue @@ -126,6 +126,7 @@ async function stopScan() { :key="item.raw.slug" :size="35" :slug="item.raw.slug" + :name="item.raw.name" /> @@ -140,6 +141,7 @@ async function stopScan() { @@ -309,7 +311,11 @@ async function stopScan() { - + {{ platform.name }} diff --git a/frontend/src/views/Setup.vue b/frontend/src/views/Setup.vue index c067d981d..efb0e616e 100644 --- a/frontend/src/views/Setup.vue +++ b/frontend/src/views/Setup.vue @@ -43,7 +43,7 @@ const step = ref(1); const filledAdminUser = computed( () => defaultAdminUser.value.username != "" && - defaultAdminUser.value.password != "" + defaultAdminUser.value.password != "", ); const isFirstStep = computed(() => step.value == 1); const isLastStep = computed(() => step.value == 2); @@ -85,6 +85,7 @@ async function finishWizard() { @@ -92,6 +93,7 @@ async function finishWizard() { @@ -154,6 +156,7 @@ async function finishWizard() { - {{ - isFirstStep ? "" : "previous" - }} + {{ isFirstStep ? "" : "previous" }} - {{ - !isLastStep ? "Next" : "Finish" - }} + {{ !isLastStep ? "Next" : "Finish" }} @@ -220,7 +229,9 @@ async function finishWizard() { max-width: 300px; } #version { - text-shadow: 1px 1px 1px #000000, 0 0 1px #000000; + text-shadow: + 1px 1px 1px #000000, + 0 0 1px #000000; bottom: 0.3rem; right: 0.5rem; } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index e565a9fb7..7a5e66d96 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -11,6 +11,6 @@ "paths": { "@/*": ["./src/*"] }, - "types": ["./src/plugins/pinia.d.ts"] + "types": ["./src/plugins/pinia.d.ts", "./src/types/main.d.ts"] } } diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 629f5060e..749cf0398 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -51,6 +51,10 @@ export default defineConfig(({ mode }) => { src: "node_modules/emulatorjs/data/*", dest: "assets/emulatorjs/", }, + { + src: "node_modules/@ruffle-rs/ruffle/*", + dest: "assets/ruffle/", + }, ], }), ], diff --git a/poetry.lock b/poetry.lock index 1282bc17a..c86e352cb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -79,17 +79,6 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] -[[package]] -name = "async-timeout" -version = "4.0.3" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.7" -files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] - [[package]] name = "bcrypt" version = "4.1.3" @@ -141,6 +130,137 @@ files = [ {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, ] +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "brotlicffi" +version = "1.1.0.0" +description = "Python CFFI bindings to the Brotli library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, + {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, +] + +[package.dependencies] +cffi = ">=1.0.0" + [[package]] name = "certifi" version = "2024.7.4" @@ -606,6 +726,73 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[[package]] +name = "inflate64" +version = "1.0.0" +description = "deflate64 compression/decompression library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "inflate64-1.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a90c0bdf4a7ecddd8a64cc977181810036e35807f56b0bcacee9abb0fcfd18dc"}, + {file = "inflate64-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57fe7c14aebf1c5a74fc3b70d355be1280a011521a76aa3895486e62454f4242"}, + {file = "inflate64-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d90730165f471d61a1a694a5e354f3ffa938227e8dcecb62d5d728e8069cee94"}, + {file = "inflate64-1.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:543f400201f5c101141af3c79c82059e1aa6ef4f1584a7f1fa035fb2e465097f"}, + {file = "inflate64-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ceca14f7ec19fb44b047f56c50efb7521b389d222bba2b0a10286a0caeb03fa"}, + {file = "inflate64-1.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b559937a42f0c175b4d2dfc7eb53b97bdc87efa9add15ed5549c6abc1e89d02f"}, + {file = "inflate64-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5ff8bd2a562343fcbc4eea26fdc368904a3b5f6bb8262344274d3d74a1de15bb"}, + {file = "inflate64-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:0fe481f31695d35a433c3044ac8fd5d9f5069aaad03a0c04b570eb258ce655aa"}, + {file = "inflate64-1.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a45f6979ad5874d4d4898c2fc770b136e61b96b850118fdaec5a5af1b9123a"}, + {file = "inflate64-1.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:022ca1cc928e7365a05f7371ff06af143c6c667144965e2cf9a9236a2ae1c291"}, + {file = "inflate64-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46792ecf3565d64fd2c519b0a780c03a57e195613c9954ef94e739a057b3fd06"}, + {file = "inflate64-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a70ea2e456c15f7aa7c74b8ab8f20b4f8940ec657604c9f0a9de3342f280fff"}, + {file = "inflate64-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e243ea9bd36a035059f2365bd6d156ff59717fbafb0255cb0c75bf151bf6904"}, + {file = "inflate64-1.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4dc392dec1cd11cacda3d2637214ca45e38202e8a4f31d4a4e566d6e90625fc4"}, + {file = "inflate64-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8b402a50eda7ee75f342fc346d33a41bca58edc222a4b17f9be0db1daed459fa"}, + {file = "inflate64-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:f5924499dc8800928c0ee4580fa8eb4ffa880b2cce4431537d0390e503a9c9ee"}, + {file = "inflate64-1.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0c644bf7208e20825ca3bbb5fb1f7f495cfcb49eb01a5f67338796d44a42f2bf"}, + {file = "inflate64-1.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9964a4eaf26a9d36f82a1d9b12c28e35800dd3d99eb340453ed12ac90c2976a8"}, + {file = "inflate64-1.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2cccded63865640d03253897be7232b2bbac295fe43914c61f86a57aa23bb61d"}, + {file = "inflate64-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d491f104fb3701926ebd82b8c9250dfba0ddcab584504e26f1e4adb26730378d"}, + {file = "inflate64-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ebad4a6cd2a2c1d81be0b09d4006479f3b258803c49a9224ef8ca0b649072fa"}, + {file = "inflate64-1.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6823b2c0cff3a8159140f3b17ec64fb8ec0e663b45a6593618ecdde8aeecb5b2"}, + {file = "inflate64-1.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:228d504239d27958e71fc77e3119a6ac4528127df38468a0c95a5bd3927204b8"}, + {file = "inflate64-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae2572e06bcfe15e3bbf77d4e4a6d6c55e2a70d6abceaaf60c5c3653ddb96dfd"}, + {file = "inflate64-1.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c10ca61212a753bbce6d341e7cfa779c161b839281f1f9fdc15cf1f324ce7c5b"}, + {file = "inflate64-1.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a982dc93920f9450da4d4f25c5e5c1288ef053b1d618cedc91adb67e035e35f5"}, + {file = "inflate64-1.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ca0310b2c55bc40394c5371db2a22f705fd594226cc09432e1eb04d3aed83930"}, + {file = "inflate64-1.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e95044ae55a161144445527a2efad550851fecc699066423d24b2634a6a83710"}, + {file = "inflate64-1.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34de6902c39d9225459583d5034182d371fc694bc3cfd6c0fc89aa62e9809faf"}, + {file = "inflate64-1.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ebafbd813213dc470719cd0a2bcb53aab89d9059f4e75386048b4c4dcdb2fd99"}, + {file = "inflate64-1.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:75448c7b414dadaeeb11dab9f75e022aa1e0ee19b00f570e9f58e933603d71ac"}, + {file = "inflate64-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:2be4e01c1b04761874cb44b35b6103ca5846bc36c18fc3ff5e8cbcd8bfc15e9f"}, + {file = "inflate64-1.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bf2981b95c1f26242bb084d9a07f3feb0cfe3d6d0a8d90f42389803bc1252c4a"}, + {file = "inflate64-1.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9373ccf0661cc72ac84a0ad622634144da5ce7d57c9572ed0723d67a149feed2"}, + {file = "inflate64-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4650c6f65011ec57cf5cd96b92d5b7c6f59e502930c86eb8227c93cf02dc270"}, + {file = "inflate64-1.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a475e8822f1a74c873e60b8f270773757ade024097ca39e43402d47c049c67d4"}, + {file = "inflate64-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4367480733ac8daf368f6fc704b7c9db85521ee745eb5bd443f4b97d2051acc"}, + {file = "inflate64-1.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c5775c91f94f5eced9160fb0af12a09f3e030194f91a6a46e706a79350bd056"}, + {file = "inflate64-1.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d76d205b844d78ce04768060084ef20e64dcc63a3e9166674f857acaf4d140ed"}, + {file = "inflate64-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f0dc6af0e8e97324981178dc442956cbff1247a56d1e201af8d865244653f8"}, + {file = "inflate64-1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f79542478e49e471e8b23556700e6f688a40dc93e9a746f77a546c13251b59b1"}, + {file = "inflate64-1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a270be6b10cde01258c0097a663a307c62d12c78eb8f62f8e29f205335942c9"}, + {file = "inflate64-1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1616a87ff04f583e9558cc247ec0b72a30d540ee0c17cc77823be175c0ec92f0"}, + {file = "inflate64-1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:137ca6b315f0157a786c3a755a09395ca69aed8bcf42ad3437cb349f5ebc86d2"}, + {file = "inflate64-1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8140942d1614bdeb5a9ddd7559348c5c77f884a42424aef7ccf149ccfb93aa08"}, + {file = "inflate64-1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fe3f9051338bb7d07b5e7d88420d666b5109f33ae39aa55ecd1a053c0f22b1b"}, + {file = "inflate64-1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36342338e957c790fc630d4afcdcc3926beb2ecaea0b302336079e8fa37e57a0"}, + {file = "inflate64-1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:9b65cc701ef33ab20dbfd1d64088ffd89a8c265b356d2c21ba0ec565661645ef"}, + {file = "inflate64-1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dd6d3e7d47df43210a995fd1f5989602b64de3f2a17cf4cbff553518b3577fd4"}, + {file = "inflate64-1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f033b2879696b855200cde5ca4e293132c7499df790acb2c0dacb336d5e83b1"}, + {file = "inflate64-1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f816d1c8a0593375c289e285c96deaee9c2d8742cb0edbd26ee05588a9ae657"}, + {file = "inflate64-1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1facd35319b6a391ee4c3d709c7c650bcada8cd7141d86cd8c2257287f45e6e6"}, + {file = "inflate64-1.0.0.tar.gz", hash = "sha256:3278827b803cf006a1df251f3e13374c7d26db779e5a33329cc11789b804bc2d"}, +] + +[package.extras] +check = ["check-manifest", "flake8", "flake8-black", "flake8-deprecated", "isort (>=5.0.3)", "mypy (>=0.940)", "mypy-extensions (>=0.4.1)", "pygments", "readme-renderer", "twine"] +docs = ["docutils", "sphinx (>=5.0)"] +test = ["pyannotate", "pytest"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -686,7 +873,6 @@ prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5.13.0" -typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} [package.extras] all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] @@ -1015,6 +1201,22 @@ files = [ {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] +[[package]] +name = "multivolumefile" +version = "0.2.3" +description = "multi volume file wrapper library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "multivolumefile-0.2.3-py3-none-any.whl", hash = "sha256:237f4353b60af1703087cf7725755a1f6fcaeeea48421e1896940cd1c920d678"}, + {file = "multivolumefile-0.2.3.tar.gz", hash = "sha256:a0648d0aafbc96e59198d5c17e9acad7eb531abea51035d08ce8060dcad709d6"}, +] + +[package.extras] +check = ["check-manifest", "flake8", "flake8-black", "isort (>=5.0.3)", "pygments", "readme-renderer", "twine"] +test = ["coverage[toml] (>=5.2)", "coveralls (>=2.1.1)", "hypothesis", "pyannotate", "pytest", "pytest-cov"] +type = ["mypy", "mypy-extensions"] + [[package]] name = "mypy" version = "1.10.1" @@ -1339,6 +1541,90 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "py7zr" +version = "0.21.1" +description = "Pure python 7-zip library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "py7zr-0.21.1-py3-none-any.whl", hash = "sha256:57e5be6fafaa417fe93fa9c81f7f01bb579d3cfe1631f535a3e641200ac87dc2"}, + {file = "py7zr-0.21.1.tar.gz", hash = "sha256:dede8ed8b7b32b3586ac476da3a482b69dd433229420bf0f62c495404b72c799"}, +] + +[package.dependencies] +brotli = {version = ">=1.1.0", markers = "platform_python_implementation == \"CPython\""} +brotlicffi = {version = ">=1.1.0.0", markers = "platform_python_implementation == \"PyPy\""} +inflate64 = ">=1.0.0,<1.1.0" +multivolumefile = ">=0.2.3" +psutil = {version = "*", markers = "sys_platform != \"cygwin\""} +pybcj = ">=1.0.0,<1.1.0" +pycryptodomex = ">=3.16.0" +pyppmd = ">=1.1.0,<1.2.0" +pyzstd = ">=0.15.9" +texttable = "*" + +[package.extras] +check = ["black (>=23.1.0)", "check-manifest", "flake8 (<8)", "flake8-black (>=0.3.6)", "flake8-deprecated", "flake8-isort", "isort (>=5.0.3)", "lxml", "mypy (>=0.940)", "mypy-extensions (>=0.4.1)", "pygments", "readme-renderer", "twine", "types-psutil"] +debug = ["pytest", "pytest-leaks", "pytest-profiling"] +docs = ["docutils", "sphinx (>=5.0)", "sphinx-a4doc", "sphinx-py3doc-enhanced-theme"] +test = ["coverage[toml] (>=5.2)", "coveralls (>=2.1.1)", "py-cpuinfo", "pyannotate", "pytest", "pytest-benchmark", "pytest-cov", "pytest-remotedata", "pytest-timeout"] +test-compat = ["libarchive-c"] + +[[package]] +name = "pybcj" +version = "1.0.2" +description = "bcj filter library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pybcj-1.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7bff28d97e47047d69a4ac6bf59adda738cf1d00adde8819117fdb65d966bdbc"}, + {file = "pybcj-1.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:198e0b4768b4025eb3309273d7e81dc53834b9a50092be6e0d9b3983cfd35c35"}, + {file = "pybcj-1.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa26415b4a118ea790de9d38f244312f2510a9bb5c65e560184d241a6f391a2d"}, + {file = "pybcj-1.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fabb2be57e4ca28ea36c13146cdf97d73abd27c51741923fc6ba1e8cd33e255c"}, + {file = "pybcj-1.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d6d613bae6f27678d5e44e89d61018779726aa6aa950c516d33a04b8af8c59"}, + {file = "pybcj-1.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ffae79ef8a1ea81ea2748ad7b7ad9b882aa88ddf65ce90f9e944df639eccc61"}, + {file = "pybcj-1.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bdb4d8ff5cba3e0bd1adee7d20dbb2b4d80cb31ac04d6ea1cd06cfc02d2ecd0d"}, + {file = "pybcj-1.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a29be917fbc99eca204b08407e0971e0205bfdad4b74ec915930675f352b669d"}, + {file = "pybcj-1.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2562ebe5a0abec4da0229f8abb5e90ee97b178f19762eb925c1159be36828b3"}, + {file = "pybcj-1.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:af19bc61ded933001cd68f004ae2042bf1a78eb498a3c685ebd655fa1be90dbe"}, + {file = "pybcj-1.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3f4a447800850aba7724a2274ea0a4800724520c1caf38f7d0dabf2f89a5e15"}, + {file = "pybcj-1.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1c8af7a4761d2b1b531864d84113948daa0c4245775c63bd9874cb955f4662"}, + {file = "pybcj-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8007371f6f2b462f5aa05d5c2135d0a1bcf5b7bdd9bd15d86c730f588d10b7d3"}, + {file = "pybcj-1.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1079ca63ff8da5c936b76863690e0bd2489e8d4e0a3a340e032095dae805dd91"}, + {file = "pybcj-1.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e9a785eb26884429d9b9f6326e68c3638828c83bf6d42d2463c97ad5385caff2"}, + {file = "pybcj-1.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:9ea46e2d45469d13b7f25b08efcdb140220bab1ac5a850db0954591715b8caaa"}, + {file = "pybcj-1.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:21b5f2460629167340403d359289a173e0729ce8e84e3ce99462009d5d5e01a4"}, + {file = "pybcj-1.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2940fb85730b9869254559c491cd83cf777e56c76a8a60df60e4be4f2a4248d7"}, + {file = "pybcj-1.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f40f3243139d675f43793a4e35c410c370f7b91ccae74e70c8b2f4877869f90e"}, + {file = "pybcj-1.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c2b3e60b65c7ac73e44335934e1e122da8d56db87840984601b3c5dc0ae4c19"}, + {file = "pybcj-1.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746550dc7b5af4d04bb5fa4d065f18d39c925bcb5dee30db75747cd9a58bb6e8"}, + {file = "pybcj-1.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8ce9b62b6aaa5b08773be8a919ecc4e865396c969f982b685eeca6e80c82abb7"}, + {file = "pybcj-1.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:493eab2b1f6f546730a6de0c5ceb75ce16f3767154e8ae30e2b70d41b928b7d2"}, + {file = "pybcj-1.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ef55b96b7f2ed823e0b924de902065ec42ade856366c287dbb073fabd6b90ec1"}, + {file = "pybcj-1.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ed5b3dd9c209fe7b90990dee4ef21870dca39db1cd326553c314ee1b321c1cc"}, + {file = "pybcj-1.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22a94885723f8362d4cb468e68910eef92d3e2b1293de82b8eacb4198ef6655f"}, + {file = "pybcj-1.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b8f9368036c9e658d8e3b3534086d298a5349c864542b34657cbe57c260daa49"}, + {file = "pybcj-1.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87108181c7a6ac4d3fc1e4551cab5db5eea7f9fdca611175243234cd94bcc59b"}, + {file = "pybcj-1.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db57f26b8c0162cfddb52b869efb1741b8c5e67fc536994f743074985f714c55"}, + {file = "pybcj-1.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bdf5bcac4f1da36ad43567ea6f6ef404347658dbbe417c87cdb1699f327d6337"}, + {file = "pybcj-1.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c3171bb95c9b45cbcad25589e1ae4f4ca4ea99dc1724c4e0671eb6b9055514e"}, + {file = "pybcj-1.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:f9a2585e0da9cf343ea27421995b881736a1eb604a7c1d4ca74126af94c3d4a8"}, + {file = "pybcj-1.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fdb7cd8271471a5979d84915c1ee57eea7e0a69c893225fc418db66883b0e2a7"}, + {file = "pybcj-1.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e96ae14062bdcddc3197300e6ee4efa6fbc6749be917db934eac66d0daaecb68"}, + {file = "pybcj-1.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a54ebdc8423ba99d75372708a882fcfc3b14d9d52cf195295ad53e5a47dab37f"}, + {file = "pybcj-1.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3602be737c6e9553c45ae89e6b0e556f64f34dabf27d5260317d1824d31b79d3"}, + {file = "pybcj-1.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63dd2ca52a48841f561bfec0fa3f208d375b0a8dcd3d7b236459e683ae29221d"}, + {file = "pybcj-1.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8204a714029784b1a08a3d790430d80b423b68615c5b1e67aabca5bd5419b77d"}, + {file = "pybcj-1.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fde2376b180ae2620c102fbc3ef06638d306feae83964aaa5051ecbdda54845a"}, + {file = "pybcj-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:3b8d7810fb587adbffba025330cf212d9bbed8f29559656d05cb6609673f306a"}, + {file = "pybcj-1.0.2.tar.gz", hash = "sha256:c7f5bef7f47723c53420e377bc64d2553843bee8bcac5f0ad076ab1524780018"}, +] + +[package.extras] +check = ["check-manifest", "flake8 (<5)", "flake8-black", "flake8-colors", "flake8-isort", "flake8-pyi", "flake8-typing-imports", "mypy (>=0.812)", "mypy-extensions (>=0.4.3)", "pygments", "readme-renderer"] +test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-cov"] + [[package]] name = "pycparser" version = "2.22" @@ -1351,44 +1637,44 @@ files = [ ] [[package]] -name = "pycryptodome" +name = "pycryptodomex" version = "3.20.0" description = "Cryptographic library for Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ - {file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, - {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc"}, + {file = "pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458"}, + {file = "pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427"}, + {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, ] [[package]] @@ -1545,6 +1831,92 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyppmd" +version = "1.1.0" +description = "PPMd compression/decompression library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyppmd-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5cd428715413fe55abf79dc9fc54924ba7e518053e1fc0cbdf80d0d99cf1442"}, + {file = "pyppmd-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e96cc43f44b7658be2ea764e7fa99c94cb89164dbb7cdf209178effc2168319"}, + {file = "pyppmd-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd20142869094bceef5ab0b160f4fff790ad1f612313a1e3393a51fc3ba5d57e"}, + {file = "pyppmd-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4f9b51e45c11e805e74ea6f6355e98a6423b5bbd92f45aceee24761bdc3d3b8"}, + {file = "pyppmd-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:459f85e928fb968d0e34fb6191fd8c4e710012d7d884fa2b317b2e11faac7c59"}, + {file = "pyppmd-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f73cf2aaf60477eef17f5497d14b6099d8be9748390ad2b83d1c88214d050c05"}, + {file = "pyppmd-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2ea3ae0e92c0b5345cd3a4e145e01bbd79c2d95355481ea5d833b5c0cb202a2d"}, + {file = "pyppmd-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:775172c740133c0162a01c1a5443d0e312246881cdd6834421b644d89a634b91"}, + {file = "pyppmd-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:14421030f1d46f69829698bdd960698a3b3df0925e3c470e82cfcdd4446b7bc1"}, + {file = "pyppmd-1.1.0-cp310-cp310-win32.whl", hash = "sha256:b691264f9962532aca3bba5be848b6370e596d0a2ca722c86df388be08d0568a"}, + {file = "pyppmd-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:216b0d969a3f06e35fbfef979706d987d105fcb1e37b0b1324f01ee143719c4a"}, + {file = "pyppmd-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1f8c51044ee4df1b004b10bf6b3c92f95ea86cfe1111210d303dca44a56e4282"}, + {file = "pyppmd-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac25b3a13d1ac9b8f0bde46952e10848adc79d932f2b548a6491ef8825ae0045"}, + {file = "pyppmd-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8d3003eebe6aabe22ba744a38a146ed58a25633420d5da882b049342b7c8036"}, + {file = "pyppmd-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c520656bc12100aa6388df27dd7ac738577f38bf43f4a4bea78e1861e579ea5"}, + {file = "pyppmd-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c2a3e807028159a705951f5cb5d005f94caed11d0984e59cc50506de543e22d"}, + {file = "pyppmd-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8a2447e69444703e2b273247bfcd4b540ec601780eff07da16344c62d2993d"}, + {file = "pyppmd-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b9e0c8053e69cad6a92a0889b3324f567afc75475b4f54727de553ac4fc85780"}, + {file = "pyppmd-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5938d256e8d2a2853dc3af8bb58ae6b4a775c46fc891dbe1826a0b3ceb624031"}, + {file = "pyppmd-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1ce5822d8bea920856232ccfb3c26b56b28b6846ea1b0eb3d5cb9592a026649e"}, + {file = "pyppmd-1.1.0-cp311-cp311-win32.whl", hash = "sha256:2a9e894750f2a52b03e3bc0d7cf004d96c3475a59b1af7e797d808d7d29c9ffe"}, + {file = "pyppmd-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:969555c72e72fe2b4dd944127521a8f2211caddb5df452bbc2506b5adfac539e"}, + {file = "pyppmd-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d6ef8fd818884e914bc209f7961c9400a4da50d178bba25efcef89f09ec9169"}, + {file = "pyppmd-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95f28e2ecf3a9656bd7e766aaa1162b6872b575627f18715f8b046e8617c124a"}, + {file = "pyppmd-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f3557ea65ee417abcdf5f49d35df00bb9f6f252639cae57aeefcd0dd596133"}, + {file = "pyppmd-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e84b25d088d7727d50218f57f92127cdb839acd6ec3de670b6680a4cf0b2d2a"}, + {file = "pyppmd-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99ed42891986dac8c2ecf52bddfb777900233d867aa18849dbba6f3335600466"}, + {file = "pyppmd-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6fe69b82634488ada75ba07efb90cd5866fa3d64a2c12932b6e8ae207a14e5f"}, + {file = "pyppmd-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:60981ffde1fe6ade750b690b35318c41a1160a8505597fda2c39a74409671217"}, + {file = "pyppmd-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46e8240315476f57aac23d71e6de003e122b65feba7c68f4cc46a089a82a7cd4"}, + {file = "pyppmd-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0308e2e76ecb4c878a18c2d7a7c61dbca89b4ef138f65d5f5ead139154dcdea"}, + {file = "pyppmd-1.1.0-cp312-cp312-win32.whl", hash = "sha256:b4fa4c27dc1314d019d921f2aa19e17f99250557e7569eeb70e180558f46af74"}, + {file = "pyppmd-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c269d21e15f4175df27cf00296476097af76941f948734c642d7fb6e85b9b3b9"}, + {file = "pyppmd-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a04ef5fd59818b035855723af85ce008c8191d31216706ffcbeedc505efca269"}, + {file = "pyppmd-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e3ebcf5f95142268afa5cc46457d9dab2d29a3ccfd020a1129dd9d6bd021be1"}, + {file = "pyppmd-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4ad046a9525d1f52e93bc642a4cec0bf344a3ba1a15923e424e7a50f8ca003d8"}, + {file = "pyppmd-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169e5023c86ed1f7587961900f58aa78ad8a3d59de1e488a2228b5ba3de52402"}, + {file = "pyppmd-1.1.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baf798e76edd9da975cc536f943756a1b1755eb8ed87371f86f76d7c16e8d034"}, + {file = "pyppmd-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d63be8c068879194c1e7548d0c57f54a4d305ba204cd0c7499b678f0aee893ef"}, + {file = "pyppmd-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5fc178a3c21af78858acbac9782fca6a927267694c452e0882c55fec6e78319"}, + {file = "pyppmd-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:28a1ab1ef0a31adce9b4c837b7b9acb01ce8f1f702ff3ff884f03d21c2f6b9bb"}, + {file = "pyppmd-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5fef43bfe98ada0a608adf03b2d205e071259027ab50523954c42eef7adcef67"}, + {file = "pyppmd-1.1.0-cp38-cp38-win32.whl", hash = "sha256:6b980902797eab821299a1c9f42fa78eff2826a6b0b0f6bde8a621f9765ffd55"}, + {file = "pyppmd-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:80cde69013f357483abe0c3ff30c55dc5e6b4f72b068f91792ce282c51dc0bff"}, + {file = "pyppmd-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aeea1bf585c6b8771fa43a6abd704da92f8a46a6d0020953af15d7f3c82e48c"}, + {file = "pyppmd-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7759bdb137694d4ab0cfa5ff2c75c212d90714c7da93544694f68001a0c38e12"}, + {file = "pyppmd-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db64a4fe956a2e700a737a1d019f526e6ccece217c163b28b354a43464cc495b"}, + {file = "pyppmd-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f788ae8f5a9e79cd777b7969d3401b2a2b87f47abe306c2a03baca30595e9bd"}, + {file = "pyppmd-1.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:324a178935c140210fca2043c688b77e79281da8172d2379a06e094f41735851"}, + {file = "pyppmd-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:363030bbcb7902fb9eeb59ffc262581ca5dd7790ba950328242fd2491c54d99b"}, + {file = "pyppmd-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:31b882584f86440b0ff7906385c9f9d9853e5799197abaafdae2245f87d03f01"}, + {file = "pyppmd-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b991b4501492ec3380b605fe30bee0b61480d305e98519d81c2a658b2de01593"}, + {file = "pyppmd-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6108044d943b826f97a9e79201242f61392d6c1fadba463b2069c4e6bc961e1"}, + {file = "pyppmd-1.1.0-cp39-cp39-win32.whl", hash = "sha256:c45ce2968b7762d2cacf622b0a8f260295c6444e0883fd21a21017e3eaef16ed"}, + {file = "pyppmd-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5289f32ab4ec5f96a95da51309abd1769f928b0bff62047b3bc25c878c16ccb"}, + {file = "pyppmd-1.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ad5da9f7592158e6b6b51d7cd15e536d8b23afbb4d22cba4e5744c7e0a3548b1"}, + {file = "pyppmd-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc6543e7d12ef0a1466d291d655e3d6bca59c7336dbb53b62ccdd407822fb52b"}, + {file = "pyppmd-1.1.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5e4008a45910e3c8c227f6f240de67eb14454c015dc3d8060fc41e230f395d3"}, + {file = "pyppmd-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9301fa39d1fb0ed09a10b4c5d7f0074113e96a1ead16ba7310bedf95f7ef660c"}, + {file = "pyppmd-1.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:59521a3c6028da0cb5780ba16880047b00163432a6b975da2f6123adfc1b0be8"}, + {file = "pyppmd-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d7ec02f1778dd68547e497625d66d7858ce10ea199146eb1d80ee23ba42954be"}, + {file = "pyppmd-1.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f062ca743f9b99fe88d417b4d351af9b4ff1a7cbd3d765c058bb97de976d57f1"}, + {file = "pyppmd-1.1.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088e326b180a0469ac936849f5e1e5320118c22c9d9e673e9c8551153b839c84"}, + {file = "pyppmd-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:897fa9ab5ff588a1000b8682835c5acf219329aa2bbfec478100e57d1204eeab"}, + {file = "pyppmd-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3af4338cc48cd59ee213af61d936419774a0f8600b9aa2013cd1917b108424f0"}, + {file = "pyppmd-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cce8cd2d4ceebe2dbf41db6dfebe4c2e621314b3af8a2df2cba5eb5fa277f122"}, + {file = "pyppmd-1.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62e57927dbcb91fb6290a41cd83743b91b9d85858efb16a0dd34fac208ee1c6b"}, + {file = "pyppmd-1.1.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:435317949a6f35e54cdf08e0af6916ace427351e7664ac1593980114668f0aaa"}, + {file = "pyppmd-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f66b0d0e32b8fb8707f1d2552f13edfc2917e8ed0bdf4d62e2ce190d2c70834"}, + {file = "pyppmd-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:650a663a591e06fb8096c213f4070b158981c8c3bf9c166ce7e4c360873f2750"}, + {file = "pyppmd-1.1.0.tar.gz", hash = "sha256:1d38ce2e4b7eb84b53bc8a52380b94f66ba6c39328b8800b30c2b5bf31693973"}, +] + +[package.extras] +check = ["check-manifest", "flake8 (<5)", "flake8-black", "flake8-isort", "isort (>=5.0.3)", "mypy (>=0.812)", "mypy-extensions (>=0.4.3)", "pygments", "readme-renderer"] +docs = ["sphinx (>=2.3)", "sphinx-rtd-theme"] +fuzzer = ["atheris", "hypothesis"] +test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-benchmark", "pytest-cov", "pytest-timeout"] + [[package]] name = "pytest" version = "8.3.2" @@ -1683,6 +2055,17 @@ asyncio-client = ["aiohttp (>=3.4)"] client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"] docs = ["sphinx"] +[[package]] +name = "python-magic" +version = "0.4.27" +description = "File type identification using libmagic" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b"}, + {file = "python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"}, +] + [[package]] name = "python-multipart" version = "0.0.9" @@ -1900,6 +2283,104 @@ files = [ [package.dependencies] cffi = {version = "*", markers = "implementation_name == \"pypy\""} +[[package]] +name = "pyzstd" +version = "0.16.0" +description = "Python bindings to Zstandard (zstd) compression library." +optional = false +python-versions = ">=3.5" +files = [ + {file = "pyzstd-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78f5e65eb15d93f687715be9241c8b55d838fba9b7045d83530f8831544f1413"}, + {file = "pyzstd-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:35962bc10480aebd5b32fa344430bddd19ef384286501c1c8092b6a9a1ee6a99"}, + {file = "pyzstd-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48037009be790fca505a62705a7997eef0cd384c3ef6c10a769734660245ee73"}, + {file = "pyzstd-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a57f2a0531ad2cd33bb78d8555e85a250877e555a68c0add6308ceeca8d84f1"}, + {file = "pyzstd-0.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa219d5d6124f1623b39f296a1fcc4cac1d8c82f137516bd362a38c16adcd92b"}, + {file = "pyzstd-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f560d24557bbc54eb1aa01ee6e587d4d199b785593462567ddf752de3c1c4974"}, + {file = "pyzstd-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d14862ce066da0494e0f9466afc3b8fcd6c03f7250323cf8ef62c67158c77e57"}, + {file = "pyzstd-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5d0db66651ed5a866a1452e7a450e41a5ec743abbeea1f1bc85ef7c64f5f6b8f"}, + {file = "pyzstd-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f47aada7fdc6bcad8ec4ee4ff00a8d2d9a0e05b5516df3f304afbf527b026221"}, + {file = "pyzstd-0.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5c43e2222bbbe660dea8fe335f5c633b3c9ed10628a4b53a160ddd54d15cffc2"}, + {file = "pyzstd-0.16.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d897ec18822e348f8ef9a17e421716ed224a3726fde806aae04469fec8f0ac9d"}, + {file = "pyzstd-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d5c98986d774e9321fb1d4fe0362658560e14c1d7afbe2d298b89a24c2f7b4f"}, + {file = "pyzstd-0.16.0-cp310-cp310-win32.whl", hash = "sha256:84135917c99476c6abeee420ffd005a856d8fde0e5f585b0c484d5923392035b"}, + {file = "pyzstd-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:06b9dfd615fb5635c05153431e520954a0e81683c5a6e3ed1134f60cc45b80f1"}, + {file = "pyzstd-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c9c1ede5c4e35b059e8734dfa8d23a59b8fcfe3e0ece4f7d226bc5e1816512c9"}, + {file = "pyzstd-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75f4363157777cbcbbd14ff823388fddfca597d44c77c27473c4c4000d7a5c99"}, + {file = "pyzstd-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ff680078aec3b9515f149010981c7feeef6c2706987ac7bdc7cc1ea05f8f7d"}, + {file = "pyzstd-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbeaa0af865427405a1c0e8c65841a23de66af8ca5d796522f7b105386cd8522"}, + {file = "pyzstd-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f27e083a63b9463fd2640065af1b924f05831839f23d936a97c4f510a54f6b"}, + {file = "pyzstd-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dd4592c2fca923041c57aa2bfe428de14cc45f3a00ab825b353160994bc15e7"}, + {file = "pyzstd-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9f22fb00bfcca4b2e0b36afd4f3a3194c1bc93b2a76e51932ccfd3b6aa62501"}, + {file = "pyzstd-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:586538aa2a992a55c10d88c58166e6023968a9825719bce5a09397b73eea658f"}, + {file = "pyzstd-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8e51d69446d96f5767e0f1b0676341d5d576c151dfe3dd14aff7a163db1b4d7c"}, + {file = "pyzstd-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8c675edd26cd2531163e51dcb3c7c73145e2fa3b77a1ff59ce9ed963ff56017"}, + {file = "pyzstd-0.16.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a765c5fc05fe1c843863cc3723e39e8207c28d9a7152ee6d621fa3908ef4880"}, + {file = "pyzstd-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79f4c9f1d7906eb890dafae4820f69bd24658297e9ebcdd74867330e8e7bf9b0"}, + {file = "pyzstd-0.16.0-cp311-cp311-win32.whl", hash = "sha256:6aa796663db6d1d01ebdcd80022de840005ae173e01a7b03b3934811b7ae39bc"}, + {file = "pyzstd-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:7a82cd4e772e5d1400502d68da7ecd71a6f1ff37243017f284bee3d2106a2496"}, + {file = "pyzstd-0.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e0f5a1865a00798a74d50fcc9956a3d7fa7413cbc1c6d6d04833d89f36e35226"}, + {file = "pyzstd-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:00954290d6d46ab13535becbbc1327c56f0a9c5d7b7cf967e6587c1395cade42"}, + {file = "pyzstd-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:796a29cbb6414b6cb84d8e7448262ba286847b946de9a149dec97469a4789552"}, + {file = "pyzstd-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c68761529a43358151ac507aeb9c6b7c1a990235ce7b7d41f8ea62c62d4679e"}, + {file = "pyzstd-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8436ce4fa7e7ddaa8d29717fd73e0699883ef6e78ef4d785c244779a7ad1942b"}, + {file = "pyzstd-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:349d643aeb8d7d9e0a407cef29d6210afbe646cc19b4e237456e585591eda223"}, + {file = "pyzstd-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4cf0fed2d5c9de3da211dceff3ed9a09b8f998f7df57da847145863a786454b"}, + {file = "pyzstd-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:691cadd48f225097a2588e7db492ac88c669c061208749bc0200ee39e4425e32"}, + {file = "pyzstd-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:33efaf2cc4efd2b100699d953cd70b5a54c3ca912297211fda01875f4636f655"}, + {file = "pyzstd-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b3cc09eecd318310cfd6e7f245248cf16ca014ea5903580d72231d93330952de"}, + {file = "pyzstd-0.16.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:89187af1ca5a9b65c477817e0fe7e411f4edd99e5575aaaef6a9e5ff62028854"}, + {file = "pyzstd-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7d5888e206190d36fbffed6d7e9cacd79e64fd34e9a07359e16862973d90b33"}, + {file = "pyzstd-0.16.0-cp312-cp312-win32.whl", hash = "sha256:3c5f28a145677431347772b43a9604b67691b16e233ec7a92fc77fc5fb670608"}, + {file = "pyzstd-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a2d5a8b74db3df772bb4f230319241e73629b04cb777b22f9dcd2084d92977a"}, + {file = "pyzstd-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:94fe8c5f1f11397b5db8b1850168e5bed13b3f3e1bc36e4292819d85be51a63c"}, + {file = "pyzstd-0.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d1e6ae36c717abd32b55a275d7fbf9041b6de3a103639739ec3e8c8283773fb3"}, + {file = "pyzstd-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33bc6f6048f7f7fc506e6ad03fb822a78c2b8209e73b2eddc69d3d6767d0385c"}, + {file = "pyzstd-0.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c4cdb0e407bec2f3ece10275449822575f6634727ee1a18e87c5e5a7b565bb1"}, + {file = "pyzstd-0.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e4cf6d11427d43734e8cb246ecfb7af169983ef796b415379602ea0605f5116"}, + {file = "pyzstd-0.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c0bbdb3ae1c300941c1f89219a8d09d142ddb7bfc78e61da80c8bdc03c05be8"}, + {file = "pyzstd-0.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c34c06a6496b4aacdab03133671dd5638417bda09a1f186ba1a39c1dbd1add24"}, + {file = "pyzstd-0.16.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:29ca6db3fb72d17bcec091b9ba485c715f63ca00bfcd993f92cb20037ae98b25"}, + {file = "pyzstd-0.16.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:26e42ccb76a53c1b943021eeb0eb4d78f46093c16e4e658a7204c838d5b36df0"}, + {file = "pyzstd-0.16.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:76697baa4d9fd621bd5b99719d3b55fadeb665af9a49523debfc9ae5fbefef13"}, + {file = "pyzstd-0.16.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:708c442f8f6540ffad24a894bdea3c019250e02dcdbd0fbd27fc977b1a88b4f2"}, + {file = "pyzstd-0.16.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:994a21a75d7b2602a78c2f88f809427ce1051e43af7aad6cda524ccdc859354e"}, + {file = "pyzstd-0.16.0-cp38-cp38-win32.whl", hash = "sha256:80962ff81a3389b5579d1206bea1bb48da38991407442d2a9287f6da1ccb2c80"}, + {file = "pyzstd-0.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:363c11a4d60fa0e2e7437f7494291c24eaf2752c8d8e3adf8f92cb0168073464"}, + {file = "pyzstd-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:094cec5425097ae1f9a40bb02de917d2274bfa872665fe2e5b4101ee94d8b31d"}, + {file = "pyzstd-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ca9f1f6bd487c9b990e509c17e0a701f554db9e77bd5121c27f1db4594ac4c0a"}, + {file = "pyzstd-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff99a11dd76aec5a5234c1158d6b8dacb61b208f3f30a2bf7ae3b23243190581"}, + {file = "pyzstd-0.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2820b607be0346b3e24b097d759393bd4bcccc0620e8e825591061a2c3a0add5"}, + {file = "pyzstd-0.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef883837c16c076f11da37323f589779806073eeacaef3912f2da0359cb8c2cf"}, + {file = "pyzstd-0.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c3181a462cdb55df5ddeffe3cf5223cda36c81feceeb231688af08d30f11022"}, + {file = "pyzstd-0.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80741b9f18149264acb639287347cfc6eecff109b5c6d95dbf7222756b107b57"}, + {file = "pyzstd-0.16.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fb70083bf00426194a85d69939c52b1759462873bf6e4d62f481e2bc3e642ea1"}, + {file = "pyzstd-0.16.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:44f818ea8c191285365a0add6fc03f88225f1fdcff570dc78e9f548444042441"}, + {file = "pyzstd-0.16.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:983ea93ed937d329c88ef15d5e3b09e32372590c1a80586b2013f17aed436cb8"}, + {file = "pyzstd-0.16.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0eadba403ec861fa4c600ad43dbd8ac17b7c22a796d3bd9d92918f4e8a15a6e8"}, + {file = "pyzstd-0.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a4e12b6702481ace7071357c1b81b9faf6f660da55ff9ccd6383fed474348cc6"}, + {file = "pyzstd-0.16.0-cp39-cp39-win32.whl", hash = "sha256:bc5e630db572362aef4d8a78f82a40e2b9756de7622feb07031bd400a696ad78"}, + {file = "pyzstd-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:8ef9fa7fe28dd6b7d09b8be89aea4e8f2d18b23a89294f51aa48dbc6c306a039"}, + {file = "pyzstd-0.16.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1b8db95f23d928ba87297afe6d4fff21bbb1af343147ff50c174674312afc29d"}, + {file = "pyzstd-0.16.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3f661848fa1984f3b17da676c88ccd08d8c3fab5501a1d1c8ac5abece48566f2"}, + {file = "pyzstd-0.16.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acfe529ff44d379ee889f03c2d353f94b1f16c83a92852061f9672982a3ef32d"}, + {file = "pyzstd-0.16.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:493edd702bc16dae1f4d76461688714c488af1b33f5b3a77c1a86d5c81240f9e"}, + {file = "pyzstd-0.16.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10143cad228ebeb9eda7793995b2d0b3fef0685258d9b794f6320824302c47d7"}, + {file = "pyzstd-0.16.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:784f7f87ae2e25459ef78282fbe9f0d2fec9ced84e4acb5d28621a0db274a13b"}, + {file = "pyzstd-0.16.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:35ba0ee9d6d502da2bc01d78d22f51a1812ff8d55fb444447f7782f5ce8c1e35"}, + {file = "pyzstd-0.16.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:e8eae552db2aa587c986f460915786bf9058a88d831d562cadba01f3069736a9"}, + {file = "pyzstd-0.16.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e31e0d2023b693ca530d95df7cff8d736f66b755018398bc518160f91e80bd0a"}, + {file = "pyzstd-0.16.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0fa1ef68839d99b0c0d66fe060303f7f2916f021289a7e04a818ef9461bbbe1"}, + {file = "pyzstd-0.16.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a55aac43a685b7d2b9e7c4f9f3768ad6e0d5f9ad7698b8bf9124fbeb814d43"}, + {file = "pyzstd-0.16.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:20259fa302f1050bd02d78d93db78870bed385c6d3d299990fe806095426869f"}, + {file = "pyzstd-0.16.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bd27ab78269148c65d988a6b26471d621d4cc6eed6b92462b7f8850162e5c4f2"}, + {file = "pyzstd-0.16.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5d8a3263b7e23a3593eb4fcc5cc77e053c7d15c874db16ce6ee8b4d94f8d825"}, + {file = "pyzstd-0.16.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75f5e862e1646f1688e97f4aa69988d6589a1e036f081e98a3f202fa4647e69b"}, + {file = "pyzstd-0.16.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19deddb2975af861320fd7b68196fbb2a4a8500897354919baf693702786e349"}, + {file = "pyzstd-0.16.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48b4368b832233205a74e9f1dfe2647d9bc49ea8357b09963fd5f15062bdd0a"}, + {file = "pyzstd-0.16.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:74521d819ceea90794aded974cc3024c65c094050e6c4a6f4b7478af3461e3ad"}, + {file = "pyzstd-0.16.0.tar.gz", hash = "sha256:fd43a0ae38ae15223fb1057729001829c3336e90f4acf04cf12ebdec33346658"}, +] + [[package]] name = "redis" version = "5.0.7" @@ -1911,9 +2392,6 @@ files = [ {file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"}, ] -[package.dependencies] -async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} - [package.extras] hiredis = ["hiredis (>=1.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] @@ -1978,6 +2456,31 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "smart-open" +version = "7.0.4" +description = "Utils for streaming large files (S3, HDFS, GCS, Azure Blob Storage, gzip, bz2...)" +optional = false +python-versions = "<4.0,>=3.7" +files = [ + {file = "smart_open-7.0.4-py3-none-any.whl", hash = "sha256:4e98489932b3372595cddc075e6033194775165702887216b65eba760dfd8d47"}, + {file = "smart_open-7.0.4.tar.gz", hash = "sha256:62b65852bdd1d1d516839fcb1f6bc50cd0f16e05b4ec44b52f43d38bcb838524"}, +] + +[package.dependencies] +wrapt = "*" + +[package.extras] +all = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "paramiko", "requests", "zstandard"] +azure = ["azure-common", "azure-core", "azure-storage-blob"] +gcs = ["google-cloud-storage (>=2.6.0)"] +http = ["requests"] +s3 = ["boto3"] +ssh = ["paramiko"] +test = ["azure-common", "azure-core", "azure-storage-blob", "boto3", "google-cloud-storage (>=2.6.0)", "moto[server]", "paramiko", "pytest", "pytest-rerunfailures", "requests", "responses", "zstandard"] +webhdfs = ["requests"] +zst = ["zstandard"] + [[package]] name = "sniffio" version = "1.3.1" @@ -2156,22 +2659,56 @@ itsdangerous = ">=2.0.1,<3.0.0" starlette = ">=0.14.2" [[package]] -name = "stream-zip" -version = "0.0.81" -description = "Python function to construct a ZIP archive with stream processing - without having to store the entire ZIP in memory or disk" +name = "streaming-form-data" +version = "1.16.0" +description = "Streaming parser for multipart/form-data" optional = false -python-versions = ">=3.6.7" +python-versions = ">=3.8" files = [ - {file = "stream_zip-0.0.81-py3-none-any.whl", hash = "sha256:29ffcad046e16219da16b57e5129bd4e2bc9a9acce222556de6ffe0a27a748f9"}, - {file = "stream_zip-0.0.81.tar.gz", hash = "sha256:ad5aa0dc8c21c014073ca27eb4b28fe8b5e2108ccab8b099462362c1ec973e0f"}, + {file = "streaming-form-data-1.16.0.tar.gz", hash = "sha256:cd95cde7a1e362c0f2b6e8bf2bcaf7339df1d4727b06de29968d010fcbbb9f5c"}, + {file = "streaming_form_data-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:68657763f5b9147d1cc54729ccc972af33097a405ee6b72607b7949c4eeecec8"}, + {file = "streaming_form_data-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b14be1adb391a2021a25b238ec48cefd20b34f85952b990996d6ffd0abfe0d8"}, + {file = "streaming_form_data-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb3b54af8d170d58dfd36d6418352c105c43a1a0b85e36772f23f0134bd8a741"}, + {file = "streaming_form_data-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a96f8a8013f6a209262592eedebf773de3316c22286ed3f1ef5ff39a19f1db9e"}, + {file = "streaming_form_data-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:929b7c69460b5516e65551b76333de1831cab8cc44fb0c5fc200847f3bd28538"}, + {file = "streaming_form_data-1.16.0-cp310-cp310-win32.whl", hash = "sha256:945c642f3d7fbee36cc4682124487ab2c3ee8f6b669c5bff07257ce89625b20c"}, + {file = "streaming_form_data-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5b598d1a36ea619aa26a14dff9a5b83baa6368d1d79f56e8132b72d2070f413"}, + {file = "streaming_form_data-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:36e337fe1639fcb67161464e4f18389c288d0d57fa8a65b1f4bbd3f21af58d94"}, + {file = "streaming_form_data-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0aa082e2944d5305c41e0acb60c3d2261126c181a3330ff787aeac13d7cf1ce"}, + {file = "streaming_form_data-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f2f293e0591ff26efe31197929c4f46614a48e2cdbe46e051fef3258499886"}, + {file = "streaming_form_data-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df9b6e16ccfd92137c59a0c59d0ccdfd641e6fd10564404ef3387d84e1aebc5e"}, + {file = "streaming_form_data-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4d401b337659404babecbad6ddc157f593568f861ecb0be71cbe7b6e5a38dca8"}, + {file = "streaming_form_data-1.16.0-cp311-cp311-win32.whl", hash = "sha256:90690e80f206fa42736599ec9cd88bd2ba85d4645dd048c301472502110be911"}, + {file = "streaming_form_data-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:6edb1e379fad699522492a21d3e91ab1ee1b1df34e0a147498251673b7cbbc00"}, + {file = "streaming_form_data-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ef7b63d02f4243251a5a6e7dc62bb5dda0d9342e141c724fb196410b04191508"}, + {file = "streaming_form_data-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b61d097ad8a673272222acfa504cee9a745881d5a091dcb4e7ca39ed8e0edcb9"}, + {file = "streaming_form_data-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c6d920e03146d652ceea0b46682e8ca4d7ff9017cb09ec24a1687c809c1582c"}, + {file = "streaming_form_data-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1bcdfccf7b3885f74ec78c86c5a74f2f4b766341c209b344459a1f42b9581f80"}, + {file = "streaming_form_data-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:79debb41691c669e1b7b254f41120c24ab27889dbc543861660df4472471e4ce"}, + {file = "streaming_form_data-1.16.0-cp38-cp38-win32.whl", hash = "sha256:16399b9dc231e856cfd2138356ab7017f43025a1c994371deb7fbd31bd73b411"}, + {file = "streaming_form_data-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:d561e216501356b1142232d3a5f3a41ea435c50120e01996c853b2779ef3626b"}, + {file = "streaming_form_data-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9d2540acfb7f8db7ab94d893f42d5b2fcf0145a25cd795e53bc841c001ded4f9"}, + {file = "streaming_form_data-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cbcfa5bf257c4e6539df01e96d52d9e0f49085a58c5b8efdc98c9e88a3a39595"}, + {file = "streaming_form_data-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:decf1af5fad98a938acd93fba413312630a9ca9a03972bc55ff903f6ce4247d4"}, + {file = "streaming_form_data-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:391fb3ff03113ffa71a0067ab88ca622d0a1a085913c4e21962afae61e366871"}, + {file = "streaming_form_data-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e530661137aa3c38bbd2a98ec4bced7aa15ed8d8a61be6d98abe071e5a8dfc7b"}, + {file = "streaming_form_data-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ba7430f542e9d8359b5f9b1445fd2d20b81930e031100e4e76cb7f064208811d"}, + {file = "streaming_form_data-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:f90d142755e8d60764e4ef883af9cc5fc5d103606fa27adcbbf986d17bff102a"}, ] [package.dependencies] -pycryptodome = ">=3.10.1" +smart-open = ">=6.0" -[package.extras] -ci = ["coverage (==6.2)", "mypy (==0.971)", "pycryptodome (==3.10.1)", "pytest (==7.0.1)", "pytest-cov (==3.0.0)", "pyzipper (==0.3.6)", "stream-unzip (==0.0.86)", "types-contextvars (==2.4.7.3)"] -dev = ["coverage (>=6.2)", "mypy (>=0.971)", "pytest (>=7.0.1)", "pytest-cov (>=3.0.0)", "pyzipper (>=0.3.6)", "stream-unzip (>=0.0.86)", "types-contextvars (>=2.4.7.3)"] +[[package]] +name = "texttable" +version = "1.7.0" +description = "module to create simple ASCII tables" +optional = false +python-versions = "*" +files = [ + {file = "texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917"}, + {file = "texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638"}, +] [[package]] name = "tornado" @@ -2694,5 +3231,5 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" -python-versions = "^3.11" -content-hash = "f8d8c12320cb8e5b47b182c507b4c988185910e60f7fb162f9508685072f2214" +python-versions = "^3.12" +content-hash = "cfe0cc6ccf4d75141fa2c4c5ebce9011b17040ff09171595a579d317710b9789" diff --git a/pyproject.toml b/pyproject.toml index 1f8324ab8..191acddf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ repository = "https://github.com/rommapp/romm" authors = ["Zurdi ", "Arcane "] [tool.poetry.dependencies] -python = "^3.11" +python = "^3.12" anyio = "^4.4" fastapi = "0.110.0" uvicorn = "0.29.0" @@ -30,7 +30,6 @@ types-pyyaml = "^6.0.12.20240311" types-redis = "^4.6.0.20240311 " passlib = { extras = ["bcrypt"], version = "^1.7.4" } itsdangerous = "^2.1.2" -stream-zip = "^0.0.81" rq-scheduler = "^0.13.1" starlette-csrf = "^3.0.0" httpx = "^0.27.0" @@ -41,6 +40,9 @@ yarl = "^1.9.4" joserfc = "^0.9.0" pillow = "^10.3.0" certifi = "2024.07.04" +python-magic = "^0.4.27" +py7zr = "^0.21.1" +streaming-form-data = "^1.16.0" [tool.poetry.group.test.dependencies] fakeredis = "^2.21.3"