mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 14:56:01 +00:00
Merge pull request #1416 from rommapp/feature/screenscraper-integration
feat: Screenscraper integration
This commit is contained in:
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@@ -35,7 +35,7 @@
|
||||
{
|
||||
"label": "Setup testing environment",
|
||||
"type": "shell",
|
||||
"command": "export $(cat .env | grep DB_ROOT_PASSWD | xargs) && docker exec -i mariadb mariadb -u root -p$DB_ROOT_PASSWD < backend/romm_test/setup.sql",
|
||||
"command": "export $(cat .env | grep DB_ROOT_PASSWD | xargs) && docker exec -i romm-db-dev mariadb -u root -p$DB_ROOT_PASSWD < backend/romm_test/setup.sql",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Change empty string in users.email to NULL.
|
||||
|
||||
Revision ID: 951473b0c581
|
||||
Revision ID: 0030_user_email_null
|
||||
Revises: 0029_platforms_custom_name
|
||||
Create Date: 2025-01-14 01:30:39.696257
|
||||
|
||||
|
||||
46
backend/alembic/versions/0035_screenscraper.py
Normal file
46
backend/alembic/versions/0035_screenscraper.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 0035_screenscraper
|
||||
Revises: 0034_virtual_collections_db_view
|
||||
Create Date: 2025-01-02 18:58:55.557123
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from utils.database import CustomJSON
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0035_screenscraper"
|
||||
down_revision = "0034_virtual_collections_db_view"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("platforms", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("ss_id", sa.Integer(), nullable=True))
|
||||
|
||||
with op.batch_alter_table("roms", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("ss_id", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column("ss_metadata", CustomJSON(), nullable=True))
|
||||
batch_op.add_column(sa.Column("url_manual", sa.Text(), nullable=True)),
|
||||
batch_op.add_column(sa.Column("path_manual", sa.Text(), nullable=True)),
|
||||
batch_op.create_index("idx_roms_ss_id", ["ss_id"], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("roms", schema=None) as batch_op:
|
||||
batch_op.drop_index("idx_roms_ss_id")
|
||||
batch_op.drop_column("ss_id")
|
||||
batch_op.drop_column("ss_metadata")
|
||||
batch_op.drop_column("url_manual")
|
||||
batch_op.drop_column("path_manual")
|
||||
batch_op.drop_index("idx_roms_ss_id")
|
||||
|
||||
with op.batch_alter_table("platforms", schema=None) as batch_op:
|
||||
batch_op.drop_column("ss_id")
|
||||
# ### end Alembic commands ###
|
||||
34
backend/alembic/versions/0036_screenscraper_platforms_id.py
Normal file
34
backend/alembic/versions/0036_screenscraper_platforms_id.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 0036_screenscraper_platforms_id
|
||||
Revises: 0035_screenscraper
|
||||
Create Date: 2025-01-02 18:58:55.557123
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from handler.metadata.ss_handler import SLUG_TO_SS_ID
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0036_screenscraper_platforms_id"
|
||||
down_revision = "0035_screenscraper"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
connection = op.get_bind()
|
||||
for slug, ss_id in SLUG_TO_SS_ID.items():
|
||||
connection.execute(
|
||||
sa.text("UPDATE platforms SET ss_id = :ss_id WHERE slug = :slug"),
|
||||
{"ss_id": ss_id["id"], "slug": slug},
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
@@ -57,6 +57,10 @@ IGDB_CLIENT_SECRET: Final = os.environ.get(
|
||||
"IGDB_CLIENT_SECRET", os.environ.get("CLIENT_SECRET", "")
|
||||
)
|
||||
|
||||
# SCREENSCRAPER
|
||||
SCREENSCRAPER_USER: Final = os.environ.get("SCREENSCRAPER_USER", "")
|
||||
SCREENSCRAPER_PASSWORD: Final = os.environ.get("SCREENSCRAPER_PASSWORD", "")
|
||||
|
||||
# STEAMGRIDDB
|
||||
STEAMGRIDDB_API_KEY: Final = os.environ.get("STEAMGRIDDB_API_KEY", "")
|
||||
|
||||
|
||||
@@ -78,8 +78,8 @@ async def add_collection(
|
||||
)
|
||||
else:
|
||||
path_cover_s, path_cover_l = await fs_resource_handler.get_cover(
|
||||
overwrite=True,
|
||||
entity=_added_collection,
|
||||
overwrite=True,
|
||||
url_cover=_added_collection.url_cover,
|
||||
)
|
||||
|
||||
@@ -247,8 +247,8 @@ async def update_collection(
|
||||
{"url_cover": data.get("url_cover", collection.url_cover)}
|
||||
)
|
||||
path_cover_s, path_cover_l = await fs_resource_handler.get_cover(
|
||||
overwrite=True,
|
||||
entity=collection,
|
||||
overwrite=True,
|
||||
url_cover=data.get("url_cover", ""), # type: ignore
|
||||
)
|
||||
cleaned_data.update(
|
||||
|
||||
@@ -18,6 +18,7 @@ from handler.filesystem import fs_platform_handler
|
||||
from handler.metadata.igdb_handler import IGDB_API_ENABLED
|
||||
from handler.metadata.moby_handler import MOBY_API_ENABLED
|
||||
from handler.metadata.sgdb_handler import STEAMGRIDDB_API_ENABLED
|
||||
from handler.metadata.ss_handler import SS_API_ENABLED
|
||||
from utils import get_version
|
||||
from utils.router import APIRouter
|
||||
|
||||
@@ -40,9 +41,12 @@ def heartbeat() -> HeartbeatResponse:
|
||||
"SHOW_SETUP_WIZARD": len(db_user_handler.get_admin_users()) == 0,
|
||||
},
|
||||
"METADATA_SOURCES": {
|
||||
"ANY_SOURCE_ENABLED": IGDB_API_ENABLED or MOBY_API_ENABLED,
|
||||
"ANY_SOURCE_ENABLED": IGDB_API_ENABLED
|
||||
or MOBY_API_ENABLED
|
||||
or SS_API_ENABLED,
|
||||
"IGDB_API_ENABLED": IGDB_API_ENABLED,
|
||||
"MOBY_API_ENABLED": MOBY_API_ENABLED,
|
||||
"SS_API_ENABLED": SS_API_ENABLED,
|
||||
"STEAMGRIDDB_ENABLED": STEAMGRIDDB_API_ENABLED,
|
||||
},
|
||||
"FILESYSTEM": {
|
||||
|
||||
@@ -25,6 +25,7 @@ class MetadataSourcesDict(TypedDict):
|
||||
ANY_SOURCE_ENABLED: bool
|
||||
IGDB_API_ENABLED: bool
|
||||
MOBY_API_ENABLED: bool
|
||||
SS_API_ENABLED: bool
|
||||
STEAMGRIDDB_ENABLED: bool
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class PlatformSchema(BaseModel):
|
||||
igdb_id: int | None = None
|
||||
sgdb_id: int | None = None
|
||||
moby_id: int | None = None
|
||||
ss_id: int | None = None
|
||||
category: str | None = None
|
||||
generation: int | None = None
|
||||
family_name: str | None = None
|
||||
|
||||
@@ -9,6 +9,7 @@ from endpoints.responses.collection import CollectionSchema
|
||||
from fastapi import Request
|
||||
from handler.metadata.igdb_handler import IGDBMetadata
|
||||
from handler.metadata.moby_handler import MobyMetadata
|
||||
from handler.metadata.ss_handler import SSMetadata
|
||||
from models.rom import Rom, RomFileCategory, RomUserStatus
|
||||
from pydantic import computed_field
|
||||
|
||||
@@ -26,6 +27,11 @@ RomMobyMetadata = TypedDict( # type: ignore[misc]
|
||||
dict((k, NotRequired[v]) for k, v in get_type_hints(MobyMetadata).items()),
|
||||
total=False,
|
||||
)
|
||||
RomSSMetadata = TypedDict( # type: ignore[misc]
|
||||
"RomSSMetadata",
|
||||
dict((k, NotRequired[v]) for k, v in get_type_hints(SSMetadata).items()),
|
||||
total=False,
|
||||
)
|
||||
|
||||
|
||||
def rom_user_schema_factory() -> RomUserSchema:
|
||||
@@ -120,6 +126,7 @@ class RomSchema(BaseModel):
|
||||
igdb_id: int | None
|
||||
sgdb_id: int | None
|
||||
moby_id: int | None
|
||||
ss_id: int | None
|
||||
|
||||
platform_id: int
|
||||
platform_slug: str
|
||||
@@ -152,10 +159,16 @@ class RomSchema(BaseModel):
|
||||
age_ratings: list[str]
|
||||
igdb_metadata: RomIGDBMetadata | None
|
||||
moby_metadata: RomMobyMetadata | None
|
||||
ss_metadata: RomSSMetadata | None
|
||||
|
||||
path_cover_small: str | None
|
||||
path_cover_large: str | None
|
||||
url_cover: str | None
|
||||
|
||||
has_manual: bool
|
||||
path_manual: str | None
|
||||
url_manual: str | None
|
||||
|
||||
is_unidentified: bool
|
||||
|
||||
revision: str | None
|
||||
|
||||
@@ -4,11 +4,13 @@ from .base import BaseModel
|
||||
class SearchRomSchema(BaseModel):
|
||||
igdb_id: int | None = None
|
||||
moby_id: int | None = None
|
||||
ss_id: int | None = None
|
||||
slug: str
|
||||
name: str
|
||||
summary: str
|
||||
igdb_url_cover: str = ""
|
||||
moby_url_cover: str = ""
|
||||
ss_url_cover: str = ""
|
||||
platform_id: int
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import binascii
|
||||
import os
|
||||
from base64 import b64encode
|
||||
from datetime import datetime, timezone
|
||||
from io import BytesIO
|
||||
@@ -31,7 +32,8 @@ from handler.auth.constants import Scope
|
||||
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 handler.metadata import meta_igdb_handler, meta_moby_handler, meta_ss_handler
|
||||
from logger.formatter import highlight as hl
|
||||
from logger.logger import log
|
||||
from models.rom import Rom, RomFile, RomUser
|
||||
from PIL import Image
|
||||
@@ -492,6 +494,7 @@ async def update_rom(
|
||||
"igdb_id": None,
|
||||
"sgdb_id": None,
|
||||
"moby_id": None,
|
||||
"ss_id": None,
|
||||
"name": rom.fs_name,
|
||||
"summary": "",
|
||||
"url_screenshots": [],
|
||||
@@ -499,9 +502,11 @@ async def update_rom(
|
||||
"path_cover_s": "",
|
||||
"path_cover_l": "",
|
||||
"url_cover": "",
|
||||
"url_manual": "",
|
||||
"slug": "",
|
||||
"igdb_metadata": {},
|
||||
"moby_metadata": {},
|
||||
"ss_metadata": {},
|
||||
"revision": "",
|
||||
},
|
||||
)
|
||||
@@ -515,6 +520,7 @@ async def update_rom(
|
||||
cleaned_data: dict[str, Any] = {
|
||||
"igdb_id": data.get("igdb_id", rom.igdb_id),
|
||||
"moby_id": data.get("moby_id", rom.moby_id),
|
||||
"ss_id": data.get("ss_id", rom.ss_id),
|
||||
}
|
||||
|
||||
moby_id = cleaned_data["moby_id"]
|
||||
@@ -527,9 +533,23 @@ async def update_rom(
|
||||
)
|
||||
cleaned_data.update({"path_screenshots": path_screenshots})
|
||||
|
||||
igdb_id = cleaned_data["igdb_id"]
|
||||
if igdb_id and int(igdb_id) != rom.igdb_id:
|
||||
igdb_rom = await meta_igdb_handler.get_rom_by_id(int(igdb_id))
|
||||
if (
|
||||
cleaned_data.get("ss_id", "")
|
||||
and int(cleaned_data.get("ss_id", "")) != rom.ss_id
|
||||
):
|
||||
ss_rom = await meta_ss_handler.get_rom_by_id(cleaned_data["ss_id"])
|
||||
cleaned_data.update(ss_rom)
|
||||
path_screenshots = await fs_resource_handler.get_rom_screenshots(
|
||||
rom=rom,
|
||||
url_screenshots=cleaned_data.get("url_screenshots", []),
|
||||
)
|
||||
cleaned_data.update({"path_screenshots": path_screenshots})
|
||||
|
||||
if (
|
||||
cleaned_data.get("igdb_id", "")
|
||||
and int(cleaned_data.get("igdb_id", "")) != rom.igdb_id
|
||||
):
|
||||
igdb_rom = await meta_igdb_handler.get_rom_by_id(cleaned_data["igdb_id"])
|
||||
cleaned_data.update(igdb_rom)
|
||||
path_screenshots = await fs_resource_handler.get_rom_screenshots(
|
||||
rom=rom,
|
||||
@@ -613,14 +633,29 @@ async def update_rom(
|
||||
):
|
||||
cleaned_data.update({"url_cover": data.get("url_cover", rom.url_cover)})
|
||||
path_cover_s, path_cover_l = await fs_resource_handler.get_cover(
|
||||
overwrite=True,
|
||||
entity=rom,
|
||||
overwrite=True,
|
||||
url_cover=str(data.get("url_cover") or ""),
|
||||
)
|
||||
cleaned_data.update(
|
||||
{"path_cover_s": path_cover_s, "path_cover_l": path_cover_l}
|
||||
)
|
||||
|
||||
if data.get("url_manual", "") != rom.url_manual or not (
|
||||
await fs_resource_handler.manual_exists(rom)
|
||||
):
|
||||
cleaned_data.update({"url_manual": data.get("url_manual", rom.url_manual)})
|
||||
url_manual = await fs_resource_handler.get_manual(
|
||||
rom=rom,
|
||||
overwrite=True,
|
||||
url_manual=str(data.get("url_manual") or ""),
|
||||
)
|
||||
cleaned_data.update({"url_manual": url_manual})
|
||||
|
||||
log.debug(
|
||||
f"Updating {hl(cleaned_data.get('name', ''))} [{id}] with data {cleaned_data}"
|
||||
)
|
||||
|
||||
db_rom_handler.update_rom(id, cleaned_data)
|
||||
rom = db_rom_handler.get_rom(id)
|
||||
if not rom:
|
||||
@@ -629,6 +664,60 @@ async def update_rom(
|
||||
return DetailedRomSchema.from_orm_with_request(rom, request)
|
||||
|
||||
|
||||
@protected_route(router.post, "/{id}/manuals", [Scope.ROMS_WRITE])
|
||||
async def add_rom_manuals(request: Request, id: int):
|
||||
"""Upload manuals for a rom
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
|
||||
Raises:
|
||||
HTTPException: No files were uploaded
|
||||
"""
|
||||
rom = db_rom_handler.get_rom(id)
|
||||
if not rom:
|
||||
raise RomNotFoundInDatabaseException(id)
|
||||
|
||||
filename = request.headers.get("x-upload-filename")
|
||||
|
||||
manuals_path = f"{RESOURCES_BASE_PATH}/{rom.fs_resources_path}/manual"
|
||||
file_location = Path(f"{manuals_path}/{rom.id}.pdf")
|
||||
log.info(f"Uploading {file_location}")
|
||||
|
||||
if not os.path.exists(manuals_path):
|
||||
await Path(manuals_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
parser = StreamingFormDataParser(headers=request.headers)
|
||||
parser.register("x-upload-platform", NullTarget())
|
||||
parser.register(filename, FileTarget(str(file_location)))
|
||||
|
||||
async def cleanup_partial_file():
|
||||
if await file_location.exists():
|
||||
await file_location.unlink()
|
||||
|
||||
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
|
||||
|
||||
path_manual = await fs_resource_handler.get_manual(
|
||||
rom=rom, overwrite=False, url_manual=None
|
||||
)
|
||||
|
||||
db_rom_handler.update_rom(id, {"path_manual": path_manual})
|
||||
|
||||
return Response(status_code=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@protected_route(router.post, "/delete", [Scope.ROMS_WRITE])
|
||||
async def delete_roms(
|
||||
request: Request,
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import asyncio
|
||||
|
||||
import emoji
|
||||
from decorators.auth import protected_route
|
||||
from endpoints.responses.search import SearchCoverSchema, SearchRomSchema
|
||||
from fastapi import HTTPException, Request, status
|
||||
from handler.auth.constants import Scope
|
||||
from handler.database import db_rom_handler
|
||||
from handler.metadata import meta_igdb_handler, meta_moby_handler, meta_sgdb_handler
|
||||
from handler.metadata import (
|
||||
meta_igdb_handler,
|
||||
meta_moby_handler,
|
||||
meta_sgdb_handler,
|
||||
meta_ss_handler,
|
||||
)
|
||||
from handler.metadata.igdb_handler import IGDB_API_ENABLED, IGDBRom
|
||||
from handler.metadata.moby_handler import MOBY_API_ENABLED, MobyGamesRom
|
||||
from handler.metadata.sgdb_handler import STEAMGRIDDB_API_ENABLED
|
||||
from handler.metadata.ss_handler import SS_API_ENABLED, SSRom
|
||||
from handler.scan_handler import _get_main_platform_igdb_id
|
||||
from logger.logger import log
|
||||
from utils.router import APIRouter
|
||||
@@ -39,7 +47,7 @@ async def search_rom(
|
||||
list[SearchRomSchema]: List of matched roms
|
||||
"""
|
||||
|
||||
if not IGDB_API_ENABLED and not MOBY_API_ENABLED:
|
||||
if not IGDB_API_ENABLED and not SS_API_ENABLED and not MOBY_API_ENABLED:
|
||||
log.error("Search error: No metadata providers enabled")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
@@ -64,11 +72,15 @@ async def search_rom(
|
||||
|
||||
igdb_matched_roms: list[IGDBRom] = []
|
||||
moby_matched_roms: list[MobyGamesRom] = []
|
||||
ss_matched_roms: list[SSRom] = []
|
||||
|
||||
if search_by.lower() == "id":
|
||||
try:
|
||||
igdb_rom = await meta_igdb_handler.get_matched_rom_by_id(int(search_term))
|
||||
moby_rom = await meta_moby_handler.get_matched_rom_by_id(int(search_term))
|
||||
igdb_rom, moby_rom, ss_rom = await asyncio.gather(
|
||||
meta_igdb_handler.get_matched_rom_by_id(int(search_term)),
|
||||
meta_moby_handler.get_matched_rom_by_id(int(search_term)),
|
||||
meta_ss_handler.get_matched_rom_by_id(int(search_term)),
|
||||
)
|
||||
except ValueError as exc:
|
||||
log.error(f"Search error: invalid ID '{search_term}'")
|
||||
raise HTTPException(
|
||||
@@ -78,24 +90,39 @@ async def search_rom(
|
||||
else:
|
||||
igdb_matched_roms = [igdb_rom] if igdb_rom else []
|
||||
moby_matched_roms = [moby_rom] if moby_rom else []
|
||||
ss_matched_roms = [ss_rom] if ss_rom else []
|
||||
elif search_by.lower() == "name":
|
||||
main_platform_igdb_id = await _get_main_platform_igdb_id(rom.platform)
|
||||
igdb_matched_roms = await meta_igdb_handler.get_matched_roms_by_name(
|
||||
search_term, main_platform_igdb_id or rom.platform.igdb_id
|
||||
)
|
||||
moby_matched_roms = await meta_moby_handler.get_matched_roms_by_name(
|
||||
search_term, rom.platform.moby_id
|
||||
igdb_matched_roms, moby_matched_roms, ss_matched_roms = await asyncio.gather(
|
||||
meta_igdb_handler.get_matched_roms_by_name(
|
||||
search_term, (await _get_main_platform_igdb_id(rom.platform))
|
||||
),
|
||||
meta_moby_handler.get_matched_roms_by_name(
|
||||
search_term, rom.platform.moby_id
|
||||
),
|
||||
meta_ss_handler.get_matched_roms_by_name(search_term, rom.platform.ss_id),
|
||||
)
|
||||
|
||||
merged_dict = {
|
||||
item["name"]: {**item, "igdb_url_cover": item.pop("url_cover", "")} # type: ignore
|
||||
for item in igdb_matched_roms
|
||||
}
|
||||
for item in moby_matched_roms:
|
||||
merged_dict[item["name"]] = { # type: ignore
|
||||
**item,
|
||||
"moby_url_cover": item.pop("url_cover", ""),
|
||||
**merged_dict.get(item.get("name", ""), {}),
|
||||
merged_dict: dict[str, dict] = {}
|
||||
|
||||
for igdb_rom in igdb_matched_roms:
|
||||
merged_dict[igdb_rom["name"]] = {
|
||||
**igdb_rom,
|
||||
"igdb_url_cover": igdb_rom.pop("url_cover", ""),
|
||||
**merged_dict.get(igdb_rom.get("name", ""), {}),
|
||||
}
|
||||
|
||||
for moby_rom in moby_matched_roms:
|
||||
merged_dict[moby_rom["name"]] = { # type: ignore
|
||||
**moby_rom,
|
||||
"moby_url_cover": moby_rom.pop("url_cover", ""),
|
||||
**merged_dict.get(moby_rom.get("name", ""), {}),
|
||||
}
|
||||
|
||||
for ss_rom in ss_matched_roms:
|
||||
merged_dict[ss_rom["name"]] = {
|
||||
**ss_rom,
|
||||
"ss_url_cover": ss_rom.pop("url_cover", ""),
|
||||
**merged_dict.get(ss_rom.get("name", ""), {}),
|
||||
}
|
||||
|
||||
matched_roms = [
|
||||
@@ -106,6 +133,7 @@ async def search_rom(
|
||||
"summary": "",
|
||||
"igdb_url_cover": "",
|
||||
"moby_url_cover": "",
|
||||
"ss_url_cover": "",
|
||||
"platform_id": rom.platform_id,
|
||||
},
|
||||
**item,
|
||||
|
||||
@@ -25,7 +25,13 @@ from handler.filesystem import (
|
||||
)
|
||||
from handler.filesystem.roms_handler import FSRom
|
||||
from handler.redis_handler import high_prio_queue, redis_client
|
||||
from handler.scan_handler import ScanType, scan_firmware, scan_platform, scan_rom
|
||||
from handler.scan_handler import (
|
||||
MetadataSource,
|
||||
ScanType,
|
||||
scan_firmware,
|
||||
scan_platform,
|
||||
scan_rom,
|
||||
)
|
||||
from handler.socket_handler import socket_handler
|
||||
from logger.formatter import LIGHTYELLOW, RED
|
||||
from logger.formatter import highlight as hl
|
||||
@@ -153,7 +159,7 @@ async def scan_platforms(
|
||||
roms_ids = []
|
||||
|
||||
if not metadata_sources:
|
||||
metadata_sources = ["igdb", "moby"]
|
||||
metadata_sources = [MetadataSource.IGDB, MetadataSource.MOBY, MetadataSource.SS]
|
||||
|
||||
sm = _get_socket_manager()
|
||||
|
||||
@@ -485,11 +491,17 @@ async def _identify_rom(
|
||||
return scan_stats
|
||||
|
||||
path_cover_s, path_cover_l = await fs_resource_handler.get_cover(
|
||||
overwrite=True,
|
||||
entity=_added_rom,
|
||||
overwrite=True,
|
||||
url_cover=_added_rom.url_cover,
|
||||
)
|
||||
|
||||
path_manual = await fs_resource_handler.get_manual(
|
||||
rom=_added_rom,
|
||||
overwrite=True,
|
||||
url_manual=_added_rom.url_manual,
|
||||
)
|
||||
|
||||
path_screenshots = await fs_resource_handler.get_rom_screenshots(
|
||||
rom=_added_rom,
|
||||
url_screenshots=_added_rom.url_screenshots,
|
||||
@@ -498,6 +510,7 @@ async def _identify_rom(
|
||||
_added_rom.path_cover_s = path_cover_s
|
||||
_added_rom.path_cover_l = path_cover_l
|
||||
_added_rom.path_screenshots = path_screenshots
|
||||
_added_rom.path_manual = path_manual
|
||||
# Update the scanned rom with the cover and screenshots paths and update database
|
||||
db_rom_handler.update_rom(
|
||||
_added_rom.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from handler.filesystem.roms_handler import FSRomsHandler
|
||||
from handler.metadata.igdb_handler import IGDBBaseHandler, IGDBRom
|
||||
from handler.metadata.igdb_handler import IGDBHandler, IGDBRom
|
||||
from main import app
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def test_get_all_roms(client, access_token, rom, platform):
|
||||
|
||||
|
||||
@patch.object(FSRomsHandler, "rename_fs_rom")
|
||||
@patch.object(IGDBBaseHandler, "get_rom_by_id", return_value=IGDBRom(igdb_id=None))
|
||||
@patch.object(IGDBHandler, "get_rom_by_id", return_value=IGDBRom(igdb_id=None))
|
||||
def test_update_rom(rename_fs_rom_mock, get_rom_by_id_mock, client, access_token, rom):
|
||||
response = client.put(
|
||||
f"/api/roms/{rom.id}",
|
||||
|
||||
@@ -200,3 +200,66 @@ class FSResourcesHandler(FSHandler):
|
||||
path_screenshots.append(self._get_screenshot_path(rom, str(idx)))
|
||||
|
||||
return path_screenshots
|
||||
|
||||
@staticmethod
|
||||
async def manual_exists(rom: Rom) -> bool:
|
||||
"""Check if rom manual exists in filesystem
|
||||
|
||||
Args:
|
||||
rom: Rom object
|
||||
Returns
|
||||
True if manual exists in filesystem else False
|
||||
"""
|
||||
async for _ in Path(
|
||||
f"{RESOURCES_BASE_PATH}/{rom.fs_resources_path}/manual"
|
||||
).glob(f"{rom.id}.pdf"):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def _store_manual(rom: Rom, url_manual: str):
|
||||
manual_path = Path(f"{RESOURCES_BASE_PATH}/{rom.fs_resources_path}/manual")
|
||||
manual_file = manual_path / Path(f"{rom.id}.pdf")
|
||||
|
||||
httpx_client = ctx_httpx_client.get()
|
||||
try:
|
||||
async with httpx_client.stream("GET", url_manual, timeout=120) as response:
|
||||
if response.status_code == 200:
|
||||
await manual_path.mkdir(parents=True, exist_ok=True)
|
||||
async with await manual_file.open("wb") as f:
|
||||
async for chunk in response.aiter_raw():
|
||||
await f.write(chunk)
|
||||
except httpx.NetworkError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Unable to fetch cover at {url_manual}: {str(exc)}",
|
||||
) from exc
|
||||
except httpx.ProtocolError:
|
||||
log.warning(f"Failure writing cover {url_manual} to file (ProtocolError)")
|
||||
|
||||
@staticmethod
|
||||
async def _get_manual_path(rom: Rom) -> str:
|
||||
"""Returns rom manual filesystem path adapted to frontend folder structure
|
||||
|
||||
Args:
|
||||
rom: Rom object
|
||||
"""
|
||||
async for matched_file in Path(
|
||||
f"{RESOURCES_BASE_PATH}/{rom.fs_resources_path}/manual"
|
||||
).glob(f"{rom.id}.pdf"):
|
||||
return str(matched_file.relative_to(RESOURCES_BASE_PATH))
|
||||
return ""
|
||||
|
||||
async def get_manual(
|
||||
self, rom: Rom | None, overwrite: bool, url_manual: str | None
|
||||
) -> str:
|
||||
if not rom:
|
||||
return ""
|
||||
|
||||
manual_exists = await self.manual_exists(rom)
|
||||
if url_manual and (overwrite or not manual_exists):
|
||||
await self._store_manual(rom, url_manual)
|
||||
manual_exists = await self.manual_exists(rom)
|
||||
path_manual = (await self._get_manual_path(rom)) if manual_exists else ""
|
||||
|
||||
return path_manual
|
||||
|
||||
@@ -7,7 +7,7 @@ from models.platform import Platform
|
||||
|
||||
async def test_get_rom_cover():
|
||||
path_cover_s, path_cover_l = await fs_resource_handler.get_cover(
|
||||
overwrite=False, entity=None, url_cover=""
|
||||
entity=None, overwrite=False, url_cover=""
|
||||
)
|
||||
|
||||
assert "" in path_cover_s
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from .igdb_handler import IGDBBaseHandler
|
||||
from .igdb_handler import IGDBHandler
|
||||
from .moby_handler import MobyGamesHandler
|
||||
from .sgdb_handler import SGDBBaseHandler
|
||||
from .ss_handler import SSHandler
|
||||
|
||||
meta_igdb_handler = IGDBBaseHandler()
|
||||
meta_igdb_handler = IGDBHandler()
|
||||
meta_moby_handler = MobyGamesHandler()
|
||||
meta_ss_handler = SSHandler()
|
||||
meta_sgdb_handler = SGDBBaseHandler()
|
||||
|
||||
@@ -202,13 +202,17 @@ class MetadataHandler:
|
||||
- "client_id"
|
||||
- "client_secret"
|
||||
- "api_key"
|
||||
- "ssid"
|
||||
- "sspassword"
|
||||
- "devid"
|
||||
- "devpassword"
|
||||
"""
|
||||
return {
|
||||
key: (
|
||||
f"Bearer {values[key].split(' ')[1][:3]}***{values[key].split(' ')[1][-3:]}"
|
||||
f"Bearer {values[key].split(' ')[1][:2]}***{values[key].split(' ')[1][-2:]}"
|
||||
if key == "Authorization" and values[key].startswith("Bearer ")
|
||||
else (
|
||||
f"{values[key][:3]}***{values[key][-3:]}"
|
||||
f"{values[key][:2]}***{values[key][-2:]}"
|
||||
if key
|
||||
in {
|
||||
"Client-ID",
|
||||
@@ -216,6 +220,10 @@ class MetadataHandler:
|
||||
"client_id",
|
||||
"client_secret",
|
||||
"api_key",
|
||||
"ssid",
|
||||
"sspassword",
|
||||
"devid",
|
||||
"devpassword",
|
||||
}
|
||||
# Leave other keys unchanged
|
||||
else values[key]
|
||||
|
||||
@@ -207,7 +207,7 @@ def extract_metadata_from_igdb_rom(rom: dict) -> IGDBMetadata:
|
||||
)
|
||||
|
||||
|
||||
class IGDBBaseHandler(MetadataHandler):
|
||||
class IGDBHandler(MetadataHandler):
|
||||
def __init__(self) -> None:
|
||||
self.BASE_URL = "https://api.igdb.com/v4"
|
||||
self.platform_endpoint = f"{self.BASE_URL}/platforms"
|
||||
|
||||
@@ -79,8 +79,8 @@ def extract_metadata_from_moby_rom(rom: dict) -> MobyMetadata:
|
||||
class MobyGamesHandler(MetadataHandler):
|
||||
def __init__(self) -> None:
|
||||
self.BASE_URL = "https://api.mobygames.com/v1"
|
||||
self.platform_url = f"{self.BASE_URL}/platforms"
|
||||
self.games_url = f"{self.BASE_URL}/games"
|
||||
self.platform_endpoint = f"{self.BASE_URL}/platforms"
|
||||
self.games_endpoint = f"{self.BASE_URL}/games"
|
||||
|
||||
async def _request(self, url: str, timeout: int = 120) -> dict:
|
||||
httpx_client = ctx_httpx_client.get()
|
||||
@@ -148,7 +148,7 @@ class MobyGamesHandler(MetadataHandler):
|
||||
return None
|
||||
|
||||
search_term = uc(search_term)
|
||||
url = yarl.URL(self.games_url).with_query(
|
||||
url = yarl.URL(self.games_endpoint).with_query(
|
||||
platform=[platform_moby_id],
|
||||
title=quote(search_term, safe="/ "),
|
||||
)
|
||||
@@ -283,7 +283,7 @@ class MobyGamesHandler(MetadataHandler):
|
||||
if not MOBY_API_ENABLED:
|
||||
return MobyGamesRom(moby_id=None)
|
||||
|
||||
url = yarl.URL(self.games_url).with_query(id=moby_id)
|
||||
url = yarl.URL(self.games_endpoint).with_query(id=moby_id)
|
||||
roms = (await self._request(str(url))).get("games", [])
|
||||
res = pydash.get(roms, "[0]", None)
|
||||
|
||||
@@ -319,7 +319,7 @@ class MobyGamesHandler(MetadataHandler):
|
||||
return []
|
||||
|
||||
search_term = uc(search_term)
|
||||
url = yarl.URL(self.games_url).with_query(
|
||||
url = yarl.URL(self.games_endpoint).with_query(
|
||||
platform=[platform_moby_id], title=quote(search_term, safe="/ ")
|
||||
)
|
||||
matched_roms = (await self._request(str(url))).get("games", [])
|
||||
|
||||
781
backend/handler/metadata/ss_handler.py
Normal file
781
backend/handler/metadata/ss_handler.py
Normal file
@@ -0,0 +1,781 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import http
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Final, NotRequired, TypedDict
|
||||
from urllib.parse import quote
|
||||
|
||||
import httpx
|
||||
import pydash
|
||||
import yarl
|
||||
from config import SCREENSCRAPER_PASSWORD, SCREENSCRAPER_USER
|
||||
from fastapi import HTTPException, status
|
||||
from logger.logger import log
|
||||
from unidecode import unidecode as uc
|
||||
from utils.context import ctx_httpx_client
|
||||
|
||||
from .base_hander import (
|
||||
PS2_OPL_REGEX,
|
||||
SONY_SERIAL_REGEX,
|
||||
SWITCH_PRODUCT_ID_REGEX,
|
||||
SWITCH_TITLEDB_REGEX,
|
||||
MetadataHandler,
|
||||
)
|
||||
|
||||
# Used to display the Screenscraper API status in the frontend
|
||||
SS_API_ENABLED: Final = bool(SCREENSCRAPER_USER) and bool(SCREENSCRAPER_PASSWORD)
|
||||
SS_DEV_ID: Final = base64.b64decode("enVyZGkxNQ==").decode()
|
||||
SS_DEV_PASSWORD: Final = base64.b64decode("eFRKd29PRmpPUUc=").decode()
|
||||
|
||||
PS1_SS_ID: Final = 57
|
||||
PS2_SS_ID: Final = 58
|
||||
PSP_SS_ID: Final = 61
|
||||
SWITCH_SS_ID: Final = 225
|
||||
ARCADE_SS_IDS: Final = [
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
47,
|
||||
49,
|
||||
52,
|
||||
53,
|
||||
54,
|
||||
55,
|
||||
56,
|
||||
68,
|
||||
69,
|
||||
75,
|
||||
112,
|
||||
142,
|
||||
147,
|
||||
148,
|
||||
149,
|
||||
150,
|
||||
151,
|
||||
152,
|
||||
153,
|
||||
154,
|
||||
155,
|
||||
156,
|
||||
157,
|
||||
158,
|
||||
159,
|
||||
160,
|
||||
161,
|
||||
162,
|
||||
163,
|
||||
164,
|
||||
165,
|
||||
166,
|
||||
167,
|
||||
168,
|
||||
169,
|
||||
170,
|
||||
173,
|
||||
174,
|
||||
175,
|
||||
176,
|
||||
177,
|
||||
178,
|
||||
179,
|
||||
180,
|
||||
181,
|
||||
182,
|
||||
183,
|
||||
184,
|
||||
185,
|
||||
186,
|
||||
187,
|
||||
188,
|
||||
189,
|
||||
190,
|
||||
191,
|
||||
192,
|
||||
193,
|
||||
194,
|
||||
195,
|
||||
196,
|
||||
209,
|
||||
227,
|
||||
130,
|
||||
158,
|
||||
269,
|
||||
]
|
||||
|
||||
|
||||
class SSPlatform(TypedDict):
|
||||
slug: str
|
||||
ss_id: int | None
|
||||
name: NotRequired[str]
|
||||
|
||||
|
||||
class SSAgeRating(TypedDict):
|
||||
rating: str
|
||||
category: str
|
||||
rating_cover_url: str
|
||||
|
||||
|
||||
class SSMetadata(TypedDict):
|
||||
ss_score: str
|
||||
first_release_date: int | None
|
||||
alternative_names: list[str]
|
||||
companies: list[str]
|
||||
franchises: list[str]
|
||||
game_modes: list[str]
|
||||
genres: list[str]
|
||||
|
||||
|
||||
class SSRom(TypedDict):
|
||||
ss_id: int | None
|
||||
slug: NotRequired[str]
|
||||
name: NotRequired[str]
|
||||
summary: NotRequired[str]
|
||||
url_cover: NotRequired[str]
|
||||
url_manual: NotRequired[str]
|
||||
url_screenshots: NotRequired[list[str]]
|
||||
ss_metadata: NotRequired[SSMetadata]
|
||||
|
||||
|
||||
def extract_metadata_from_ss_rom(rom: dict) -> SSMetadata:
|
||||
def _normalize_score(score: str) -> str:
|
||||
"""Normalize the score to be between 0 and 10 because for some reason Screenscraper likes to rate over 20."""
|
||||
try:
|
||||
return str(int(score) / 2)
|
||||
except (ValueError, TypeError):
|
||||
return ""
|
||||
|
||||
def _get_lowest_date(dates: list[str]) -> int | None:
|
||||
lowest_date = pydash.chain(dates).map("text").sort().head().value()
|
||||
if lowest_date:
|
||||
try:
|
||||
lowest_date = int(
|
||||
datetime.strptime(lowest_date, "%Y-%m-%d").timestamp()
|
||||
)
|
||||
except ValueError:
|
||||
try:
|
||||
lowest_date = int(datetime.strptime(lowest_date, "%Y").timestamp())
|
||||
except ValueError:
|
||||
lowest_date = None
|
||||
else:
|
||||
lowest_date = None
|
||||
return lowest_date
|
||||
|
||||
return SSMetadata(
|
||||
{
|
||||
"ss_score": _normalize_score(pydash.get(rom, "note.text", None)),
|
||||
"alternative_names": pydash.map_(rom.get("noms", []), "text"),
|
||||
"companies": [
|
||||
pydash.get(rom, "editeur.text", None),
|
||||
pydash.get(rom, "developpeur.text", None),
|
||||
],
|
||||
"genres": pydash.chain(rom.get("genres", []))
|
||||
.map("noms")
|
||||
.flatten()
|
||||
.filter({"langue": "en"})
|
||||
.map("text")
|
||||
.value(),
|
||||
"first_release_date": _get_lowest_date(rom.get("dates", [])),
|
||||
"franchises": pydash.chain(rom.get("familles", []))
|
||||
.map("noms")
|
||||
.flatten()
|
||||
.filter({"langue": "en"})
|
||||
.map("text")
|
||||
.value()
|
||||
or pydash.chain(rom.get("familles", []))
|
||||
.map("noms")
|
||||
.flatten()
|
||||
.filter({"langue": "fr"})
|
||||
.map("text")
|
||||
.value(),
|
||||
"game_modes": pydash.chain(rom.get("modes", []))
|
||||
.map("noms")
|
||||
.flatten()
|
||||
.filter({"langue": "en"})
|
||||
.map("text")
|
||||
.value()
|
||||
or pydash.chain(rom.get("modes", []))
|
||||
.map("noms")
|
||||
.flatten()
|
||||
.filter({"langue": "fr"})
|
||||
.map("text")
|
||||
.value(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SSHandler(MetadataHandler):
|
||||
def __init__(self) -> None:
|
||||
self.BASE_URL = "https://api.screenscraper.fr/api2"
|
||||
self.search_endpoint = f"{self.BASE_URL}/jeuRecherche.php"
|
||||
self.platform_endpoint = f"{self.BASE_URL}/systemesListe.php"
|
||||
self.games_endpoint = f"{self.BASE_URL}/jeuInfos.php"
|
||||
self.LOGIN_ERROR_CHECK: Final = "Erreur de login"
|
||||
self.NO_GAME_ERROR: Final = "Erreur : Jeu non trouvée !"
|
||||
|
||||
async def _request(self, url: str, timeout: int = 120) -> dict:
|
||||
httpx_client = ctx_httpx_client.get()
|
||||
authorized_url = yarl.URL(url).update_query(
|
||||
ssid=SCREENSCRAPER_USER,
|
||||
sspassword=SCREENSCRAPER_PASSWORD,
|
||||
devid=SS_DEV_ID,
|
||||
devpassword=SS_DEV_PASSWORD,
|
||||
softname="romm",
|
||||
output="json",
|
||||
)
|
||||
masked_url = authorized_url.with_query(
|
||||
self._mask_sensitive_values(dict(authorized_url.query))
|
||||
)
|
||||
|
||||
log.debug(
|
||||
"API request: URL=%s, Timeout=%s",
|
||||
masked_url,
|
||||
timeout,
|
||||
)
|
||||
|
||||
try:
|
||||
res = await httpx_client.get(str(authorized_url), timeout=timeout)
|
||||
res.raise_for_status()
|
||||
if self.LOGIN_ERROR_CHECK in res.text:
|
||||
log.error("Invalid screenscraper credentials")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid screenscraper credentials",
|
||||
)
|
||||
elif self.NO_GAME_ERROR in res.text:
|
||||
return {}
|
||||
return res.json()
|
||||
except httpx.NetworkError as exc:
|
||||
log.critical(
|
||||
"Connection error: can't connect to Screenscrapper", exc_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Can't connect to Screenscrapper, check your internet connection",
|
||||
) from exc
|
||||
except httpx.HTTPStatusError as err:
|
||||
if err.response.status_code == http.HTTPStatus.UNAUTHORIZED:
|
||||
# Sometimes Screenscrapper returns 401 even with a valid API key
|
||||
log.error(err)
|
||||
return {}
|
||||
elif err.response.status_code == http.HTTPStatus.TOO_MANY_REQUESTS:
|
||||
# Retry after 2 seconds if rate limit hit
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
# Log the error and return an empty dict if the request fails with a different code
|
||||
log.error(err)
|
||||
return {}
|
||||
except httpx.TimeoutException:
|
||||
log.debug(
|
||||
"Request to URL=%s timed out. Retrying with URL=%s", masked_url, url
|
||||
)
|
||||
# Retry the request once if it times out
|
||||
try:
|
||||
log.debug(
|
||||
"API request: URL=%s, Timeout=%s",
|
||||
url,
|
||||
timeout,
|
||||
)
|
||||
res = await httpx_client.get(url, timeout=timeout)
|
||||
res.raise_for_status()
|
||||
if self.LOGIN_ERROR_CHECK in res.text:
|
||||
log.error("Invalid screenscraper credentials")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid screenscraper credentials",
|
||||
)
|
||||
elif self.NO_GAME_ERROR in res.text:
|
||||
return {}
|
||||
except (httpx.HTTPStatusError, httpx.TimeoutException) as err:
|
||||
# Log the error and return an empty dict if the request fails with a different code
|
||||
log.error(err)
|
||||
return {}
|
||||
|
||||
return res.json()
|
||||
|
||||
async def _search_rom(self, search_term: str, platform_ss_id: int) -> dict | None:
|
||||
if not platform_ss_id:
|
||||
return None
|
||||
|
||||
search_term = uc(search_term)
|
||||
url = yarl.URL(self.search_endpoint).with_query(
|
||||
systemeid=[platform_ss_id],
|
||||
recherche=quote(search_term, safe="/ "),
|
||||
)
|
||||
found_roms = (await self._request(str(url))).get("response", {}).get("jeux", [])
|
||||
# If no roms are return, "jeux" is list with an empty dict that can lead to issues. It needs to be checked.
|
||||
roms = [] if len(found_roms) == 1 and not found_roms[0] else found_roms
|
||||
return pydash.get(roms, "[0]", None)
|
||||
|
||||
def get_platform(self, slug: str) -> SSPlatform:
|
||||
platform = SLUG_TO_SS_ID.get(slug, None)
|
||||
|
||||
if not platform:
|
||||
return SSPlatform(ss_id=None, slug=slug)
|
||||
|
||||
return SSPlatform(
|
||||
ss_id=platform["id"],
|
||||
slug=slug,
|
||||
name=platform["name"],
|
||||
)
|
||||
|
||||
async def get_rom(self, file_name: str, platform_ss_id: int) -> SSRom:
|
||||
from handler.filesystem import fs_rom_handler
|
||||
|
||||
if not SS_API_ENABLED:
|
||||
return SSRom(ss_id=None)
|
||||
|
||||
if not platform_ss_id:
|
||||
return SSRom(ss_id=None)
|
||||
|
||||
search_term = fs_rom_handler.get_file_name_with_no_tags(file_name)
|
||||
fallback_rom = SSRom(ss_id=None)
|
||||
|
||||
# Support for PS2 OPL filename format
|
||||
match = PS2_OPL_REGEX.match(file_name)
|
||||
if platform_ss_id == PS2_SS_ID and match:
|
||||
search_term = await self._ps2_opl_format(match, search_term)
|
||||
fallback_rom = SSRom(ss_id=None, name=search_term)
|
||||
|
||||
# Support for sony serial filename format (PS, PS3, PS3)
|
||||
match = SONY_SERIAL_REGEX.search(file_name, re.IGNORECASE)
|
||||
if platform_ss_id == PS1_SS_ID and match:
|
||||
search_term = await self._ps1_serial_format(match, search_term)
|
||||
fallback_rom = SSRom(ss_id=None, name=search_term)
|
||||
|
||||
if platform_ss_id == PS2_SS_ID and match:
|
||||
search_term = await self._ps2_serial_format(match, search_term)
|
||||
fallback_rom = SSRom(ss_id=None, name=search_term)
|
||||
|
||||
if platform_ss_id == PSP_SS_ID and match:
|
||||
search_term = await self._psp_serial_format(match, search_term)
|
||||
fallback_rom = SSRom(ss_id=None, name=search_term)
|
||||
|
||||
# Support for switch titleID filename format
|
||||
match = SWITCH_TITLEDB_REGEX.search(file_name)
|
||||
if platform_ss_id == SWITCH_SS_ID and match:
|
||||
search_term, index_entry = await self._switch_titledb_format(
|
||||
match, search_term
|
||||
)
|
||||
if index_entry:
|
||||
fallback_rom = SSRom(
|
||||
ss_id=None,
|
||||
name=index_entry["name"],
|
||||
summary=index_entry.get("description", ""),
|
||||
url_cover=index_entry.get("iconUrl", ""),
|
||||
url_manual=index_entry.get("iconUrl", ""),
|
||||
url_screenshots=index_entry.get("screenshots", None) or [],
|
||||
)
|
||||
|
||||
# Support for switch productID filename format
|
||||
match = SWITCH_PRODUCT_ID_REGEX.search(file_name)
|
||||
if platform_ss_id == SWITCH_SS_ID and match:
|
||||
search_term, index_entry = await self._switch_productid_format(
|
||||
match, search_term
|
||||
)
|
||||
if index_entry:
|
||||
fallback_rom = SSRom(
|
||||
ss_id=None,
|
||||
name=index_entry["name"],
|
||||
summary=index_entry.get("description", ""),
|
||||
url_cover=index_entry.get("iconUrl", ""),
|
||||
url_manual=index_entry.get("iconUrl", ""),
|
||||
url_screenshots=index_entry.get("screenshots", None) or [],
|
||||
)
|
||||
|
||||
# Support for MAME arcade filename format
|
||||
if platform_ss_id in ARCADE_SS_IDS:
|
||||
search_term = await self._mame_format(search_term)
|
||||
fallback_rom = SSRom(ss_id=None, name=search_term)
|
||||
|
||||
search_term = self.normalize_search_term(search_term)
|
||||
res = await self._search_rom(search_term, platform_ss_id)
|
||||
|
||||
# Some MAME games have two titles split by a slash
|
||||
if not res and "/" in search_term:
|
||||
for term in search_term.split("/"):
|
||||
res = await self._search_rom(term.strip(), platform_ss_id)
|
||||
if res:
|
||||
break
|
||||
|
||||
if not res or not res.get("id", None):
|
||||
return fallback_rom
|
||||
|
||||
ss_id: int = int(res.get("id", None))
|
||||
|
||||
rom = {
|
||||
"ss_id": ss_id,
|
||||
"name": pydash.chain(res.get("noms", []))
|
||||
.filter({"region": "ss"})
|
||||
.map("text")
|
||||
.head()
|
||||
.value(),
|
||||
"slug": pydash.chain(res.get("noms", []))
|
||||
.filter({"region": "ss"})
|
||||
.map("text")
|
||||
.head()
|
||||
.value(),
|
||||
"summary": pydash.chain(res.get("synopsis", []))
|
||||
.filter({"langue": "en"})
|
||||
.map("text")
|
||||
.head()
|
||||
.value(),
|
||||
"url_cover": pydash.chain(res.get("medias", []))
|
||||
.filter({"region": "us", "type": "box-2D", "parent": "jeu"})
|
||||
.map("url")
|
||||
.head()
|
||||
.value()
|
||||
or "",
|
||||
"url_manual": pydash.chain(res.get("medias", []))
|
||||
.filter(
|
||||
{"region": "us", "type": "manuel", "parent": "jeu", "format": "pdf"}
|
||||
)
|
||||
.map("url")
|
||||
.head()
|
||||
.value()
|
||||
or pydash.chain(res.get("medias", []))
|
||||
.filter(
|
||||
{"region": "eu", "type": "manuel", "parent": "jeu", "format": "pdf"}
|
||||
)
|
||||
.map("url")
|
||||
.head()
|
||||
.value()
|
||||
or "",
|
||||
"url_screenshots": [],
|
||||
"ss_metadata": extract_metadata_from_ss_rom(res),
|
||||
}
|
||||
|
||||
return SSRom({k: v for k, v in rom.items() if v}) # type: ignore[misc]
|
||||
|
||||
async def get_rom_by_id(self, ss_id: int) -> SSRom:
|
||||
if not SS_API_ENABLED:
|
||||
return SSRom(ss_id=None)
|
||||
|
||||
url = yarl.URL(self.games_endpoint).with_query(gameid=ss_id)
|
||||
res = (await self._request(str(url))).get("response", {}).get("jeu", [])
|
||||
|
||||
if not res:
|
||||
return SSRom(ss_id=None)
|
||||
|
||||
rom = {
|
||||
"ss_id": res.get("id"),
|
||||
"name": pydash.chain(res.get("noms", []))
|
||||
.filter({"region": "ss"})
|
||||
.map("text")
|
||||
.head()
|
||||
.value(),
|
||||
"slug": pydash.chain(res.get("noms", []))
|
||||
.filter({"region": "ss"})
|
||||
.map("text")
|
||||
.head()
|
||||
.value(),
|
||||
"summary": pydash.chain(res.get("synopsis", []))
|
||||
.filter({"langue": "en"})
|
||||
.map("text")
|
||||
.head()
|
||||
.value(),
|
||||
"url_cover": pydash.chain(res.get("medias", []))
|
||||
.filter({"region": "us", "type": "box-2D", "parent": "jeu"})
|
||||
.map("url")
|
||||
.head()
|
||||
.value()
|
||||
or "",
|
||||
"url_manual": pydash.chain(res.get("medias", []))
|
||||
.filter(
|
||||
{"region": "us", "type": "manuel", "parent": "jeu", "format": "pdf"}
|
||||
)
|
||||
.map("url")
|
||||
.head()
|
||||
.value()
|
||||
or pydash.chain(res.get("medias", []))
|
||||
.filter(
|
||||
{"region": "eu", "type": "manuel", "parent": "jeu", "format": "pdf"}
|
||||
)
|
||||
.map("url")
|
||||
.head()
|
||||
.value()
|
||||
or "",
|
||||
"url_screenshots": [],
|
||||
"ss_metadata": extract_metadata_from_ss_rom(res),
|
||||
}
|
||||
|
||||
return SSRom({k: v for k, v in rom.items() if v}) # type: ignore[misc]
|
||||
|
||||
async def get_matched_rom_by_id(self, ss_id: int) -> SSRom | None:
|
||||
if not SS_API_ENABLED:
|
||||
return None
|
||||
|
||||
rom = await self.get_rom_by_id(ss_id)
|
||||
return rom if rom.get("ss_id", "") else None
|
||||
|
||||
async def get_matched_roms_by_name(
|
||||
self, search_term: str, platform_ss_id: int
|
||||
) -> list[SSRom]:
|
||||
if not SS_API_ENABLED:
|
||||
return []
|
||||
|
||||
if not platform_ss_id:
|
||||
return []
|
||||
|
||||
search_term = uc(search_term)
|
||||
url = yarl.URL(self.search_endpoint).with_query(
|
||||
systemeid=[platform_ss_id],
|
||||
recherche=quote(search_term, safe="/ "),
|
||||
)
|
||||
roms = (await self._request(str(url))).get("response", {}).get("jeux", [])
|
||||
# If no roms are return, "jeux" is list with an empty dict that can lead to issues. It needs to be checked.
|
||||
matched_roms = [] if len(roms) == 1 and not roms[0] else roms
|
||||
|
||||
return [
|
||||
SSRom( # type: ignore[misc]
|
||||
{
|
||||
k: v
|
||||
for k, v in {
|
||||
"ss_id": rom.get("id"),
|
||||
"name": pydash.chain(rom.get("noms", []))
|
||||
.filter({"region": "ss"})
|
||||
.map("text")
|
||||
.head()
|
||||
.value(),
|
||||
"slug": pydash.chain(rom.get("noms", []))
|
||||
.filter({"region": "ss"})
|
||||
.map("text")
|
||||
.head()
|
||||
.value(),
|
||||
"summary": pydash.chain(rom.get("synopsis", []))
|
||||
.filter({"langue": "en"})
|
||||
.map("text")
|
||||
.head()
|
||||
.value(),
|
||||
"url_cover": pydash.chain(rom.get("medias", []))
|
||||
.filter({"region": "us", "type": "box-2D", "parent": "jeu"})
|
||||
.map("url")
|
||||
.head()
|
||||
.value()
|
||||
or "",
|
||||
"url_manual": pydash.chain(rom.get("medias", []))
|
||||
.filter(
|
||||
{
|
||||
"region": "us",
|
||||
"type": "manuel",
|
||||
"parent": "jeu",
|
||||
"format": "pdf",
|
||||
}
|
||||
)
|
||||
.map("url")
|
||||
.head()
|
||||
.value()
|
||||
or pydash.chain(rom.get("medias", []))
|
||||
.filter(
|
||||
{
|
||||
"region": "eu",
|
||||
"type": "manuel",
|
||||
"parent": "jeu",
|
||||
"format": "pdf",
|
||||
}
|
||||
)
|
||||
.map("url")
|
||||
.head()
|
||||
.value()
|
||||
or "",
|
||||
"url_screenshots": [],
|
||||
"ss_metadata": extract_metadata_from_ss_rom(rom),
|
||||
}.items()
|
||||
if v
|
||||
and pydash.chain(rom.get("noms", []))
|
||||
.filter({"region": "ss"})
|
||||
.map("text")
|
||||
.head()
|
||||
.value()
|
||||
and rom.get("id", None)
|
||||
}
|
||||
)
|
||||
for rom in matched_roms
|
||||
]
|
||||
|
||||
|
||||
class SlugToSSId(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
|
||||
SLUG_TO_SS_ID: dict[str, SlugToSSId] = {
|
||||
"3do": {"id": 29, "name": "3DO"},
|
||||
"amiga": {"id": 64, "name": "Amiga"},
|
||||
"amiga-cd32": {"id": 134, "name": "Amiga CD"},
|
||||
"cpc": {"id": 60, "name": "CPC"},
|
||||
"acpc": {"id": 60, "name": "CPC"}, # IGDB
|
||||
"android": {"id": 63, "name": "Android"},
|
||||
"apple2": {"id": 86, "name": "Apple II"},
|
||||
"appleii": {"id": 86, "name": "Apple II"}, # IGDB
|
||||
"apple2gs": {"id": 217, "name": "Apple IIGS"},
|
||||
"apple-iigs": {"id": 51, "name": "Apple IIGS"}, # IGDB
|
||||
"arcadia-2001": {"id": 94, "name": "Arcadia 2001"},
|
||||
"arduboy": {"id": 263, "name": "Arduboy"},
|
||||
"atari-2600": {"id": 26, "name": "Atari 2600"},
|
||||
"atari2600": {"id": 26, "name": "Atari 2600"}, # IGDB
|
||||
"atari-5200": {"id": 40, "name": "Atari 5200"},
|
||||
"atari5200": {"id": 40, "name": "Atari 5200"}, # IGDB
|
||||
"atari-7800": {"id": 41, "name": "Atari 7800"},
|
||||
"atari7800": {"id": 41, "name": "Atari 7800"}, # IGDB
|
||||
"atari-8-bit": {"id": 43, "name": "Atari 8bit"},
|
||||
"atari8bit": {"id": 43, "name": "Atari 8bit"}, # IGDB
|
||||
"atari-st": {"id": 42, "name": "Atari ST"},
|
||||
"atom": {"id": 36, "name": "Atom"},
|
||||
"bbc-micro": {"id": 37, "name": "BBC Micro"},
|
||||
"bbcmicro": {"id": 37, "name": "BBC Micro"}, # IGDB
|
||||
"bally-astrocade": {"id": 44, "name": "Astrocade"},
|
||||
"astrocade": {"id": 44, "name": "Astrocade"}, # IGDB
|
||||
"cd-i": {"id": 133, "name": "CD-i"},
|
||||
"philips-cd-i": {"id": 133, "name": "CD-i"}, # IGDB
|
||||
"cdtv": {"id": 129, "name": "Amiga CDTV"},
|
||||
"commodore-cdtv": {"id": 129, "name": "Amiga CDTV"}, # IGDB
|
||||
"camputers-lynx": {"id": 88, "name": "Camputers Lynx"},
|
||||
"casio-loopy": {"id": 98, "name": "Loopy"},
|
||||
"casio-pv-1000": {"id": 74, "name": "PV-1000"},
|
||||
"channel-f": {"id": 80, "name": "Channel F"},
|
||||
"fairchild-channel-f": {"id": 80, "name": "Channel F"}, # IGDB
|
||||
"colecoadam": {"id": 89, "name": "Adam"},
|
||||
"colecovision": {"id": 48, "name": "Colecovision"},
|
||||
"colour-genie": {"id": 92, "name": "EG2000 Colour Genie"},
|
||||
"c128": {"id": 66, "name": "Commodore 64"},
|
||||
"commodore-16-plus4": {"id": 99, "name": "Plus/4"},
|
||||
"c-plus-4": {"id": 99, "name": "Plus/4"}, # IGDB
|
||||
"c16": {"id": 99, "name": "Plus/4"}, # IGDB
|
||||
"c64": {"id": 66, "name": "Commodore 64"},
|
||||
"pet": {"id": 240, "name": "PET"},
|
||||
"cpet": {"id": 240, "name": "PET"}, # IGDB
|
||||
"creativision": {"id": 241, "name": "CreatiVision"},
|
||||
"dos": {"id": 135, "name": "PC Dos"},
|
||||
"dragon-3264": {"id": 91, "name": "Dragon 32/64"},
|
||||
"dragon-32-slash-64": {"id": 91, "name": "Dragon 32/64"}, # IGDB
|
||||
"dreamcast": {"id": 23, "name": "Dreamcast"},
|
||||
"dc": {"id": 23, "name": "Dreamcast"}, # IGDB
|
||||
"electron": {"id": 85, "name": "Electron"},
|
||||
"acorn-electron": {"id": 85, "name": "Electron"}, # IGDB
|
||||
"epoch-game-pocket-computer": {"id": 95, "name": "Game Pocket Computer"},
|
||||
"epoch-super-cassette-vision": {"id": 67, "name": "Super Cassette Vision"},
|
||||
"exelvision": {"id": 96, "name": "EXL 100"},
|
||||
"exidy-sorcerer": {"id": 165, "name": "Exidy"},
|
||||
"fmtowns": {"id": 253, "name": "FM Towns"},
|
||||
"fm-towns": {"id": 253, "name": "FM Towns"}, # IGDB
|
||||
"fm-7": {"id": 97, "name": "FM-7"},
|
||||
"g-and-w": {"id": 52, "name": "Game & Watch"}, # IGDB (Game & Watch)
|
||||
"gp32": {"id": 101, "name": "GP32"},
|
||||
"gameboy": {"id": 9, "name": "Game Boy"},
|
||||
"gb": {"id": 9, "name": "Game Boy"}, # IGDB
|
||||
"gameboy-advance": {"id": 12, "name": "Game Boy Advance"},
|
||||
"gba": {"id": 12, "name": "Game Boy Advance"}, # IGDB
|
||||
"gameboy-color": {"id": 10, "name": "Game Boy Color"},
|
||||
"gbc": {"id": 10, "name": "Game Boy Color"}, # IGDB
|
||||
"game-gear": {"id": 21, "name": "Game Gear"},
|
||||
"gamegear": {"id": 21, "name": "Game Gear"}, # IGDB
|
||||
"game-com": {"id": 121, "name": "Game.com"},
|
||||
"game-dot-com": {"id": 121, "name": "Game.com"}, # IGDB
|
||||
"gamecube": {"id": 13, "name": "GameCube"},
|
||||
"ngc": {"id": 13, "name": "GameCube"}, # IGDB
|
||||
"genesis": {"id": 1, "name": "Megadrive"},
|
||||
"genesis-slash-megadrive": {"id": 1, "name": "Megadrive"},
|
||||
"intellivision": {"id": 115, "name": "Intellivision"},
|
||||
"jaguar": {"id": 27, "name": "Jaguar"},
|
||||
"jupiter-ace": {"id": 126, "name": "Jupiter Ace"},
|
||||
"linux": {"id": 145, "name": "Linux"},
|
||||
"lynx": {"id": 28, "name": "Lynx"},
|
||||
"msx": {"id": 113, "name": "MSX"},
|
||||
"macintosh": {"id": 146, "name": "Mac OS"},
|
||||
"mac": {"id": 146, "name": "Mac OS"}, # IGDB
|
||||
"ngage": {"id": 30, "name": "N-Gage"},
|
||||
"nes": {"id": 3, "name": "NES"},
|
||||
"famicom": {"id": 3, "name": "NES"},
|
||||
"neo-geo": {"id": 142, "name": "Neo-Geo"},
|
||||
"neogeoaes": {"id": 142, "name": "Neo-Geo"}, # IGDB
|
||||
"neogeomvs": {"id": 68, "name": "Neo-Geo MVS"}, # IGDB
|
||||
"neo-geo-cd": {"id": 70, "name": "Neo-Geo CD"},
|
||||
"neo-geo-pocket": {"id": 25, "name": "Neo-Geo Pocket"},
|
||||
"neo-geo-pocket-color": {"id": 82, "name": "Neo-Geo Pocket Color"},
|
||||
"3ds": {"id": 17, "name": "Nintendo 3DS"},
|
||||
"n64": {"id": 14, "name": "Nintendo 64"},
|
||||
"nintendo-ds": {"id": 15, "name": "Nintendo DS"},
|
||||
"nds": {"id": 15, "name": "Nintendo DS"}, # IGDB
|
||||
"nintendo-dsi": {"id": 15, "name": "Nintendo DS"},
|
||||
"switch": {"id": 225, "name": "Switch"},
|
||||
"odyssey-2": {"id": 104, "name": "Videopac G7000"},
|
||||
"odyssey-2-slash-videopac-g7000": {"id": 104, "name": "Videopac G7000"},
|
||||
"oric": {"id": 131, "name": "Oric 1 / Atmos"},
|
||||
"pc88": {"id": 221, "name": "NEC PC-8801"},
|
||||
"pc-8800-series": {"id": 221, "name": "NEC PC-8801"}, # IGDB
|
||||
"pc98": {"id": 208, "name": "NEC PC-9801"},
|
||||
"pc-9800-series": {"id": 208, "name": "NEC PC-9801"}, # IGDB
|
||||
"pc-fx": {"id": 72, "name": "PC-FX"},
|
||||
"pico": {"id": 234, "name": "Pico-8"},
|
||||
"ps-vita": {"id": 62, "name": "PS Vita"},
|
||||
"psvita": {"id": 62, "name": "PS Vita"}, # IGDB
|
||||
"psp": {"id": 61, "name": "PSP"},
|
||||
"palmos": {"id": 219, "name": "Palm OS"},
|
||||
"palm-os": {"id": 219, "name": "Palm OS"}, # IGDB
|
||||
"philips-vg-5000": {"id": 261, "name": "Philips VG 5000"},
|
||||
"playstation": {"id": 57, "name": "Playstation"},
|
||||
"ps": {"id": 57, "name": "Playstation"}, # IGDB
|
||||
"ps2": {"id": 58, "name": "Playstation 2"},
|
||||
"ps3": {"id": 59, "name": "Playstation 3"},
|
||||
"playstation-4": {"id": 60, "name": "Playstation 4"},
|
||||
"ps4--1": {"id": 60, "name": "Playstation 4"}, # IGDB
|
||||
"playstation-5": {"id": 284, "name": "Playstation 5"},
|
||||
"ps5": {"id": 284, "name": "Playstation 5"}, # IGDB
|
||||
"pokemon-mini": {"id": 211, "name": "Pokémon mini"},
|
||||
"sam-coupe": {"id": 213, "name": "MGT SAM Coupé"},
|
||||
"sega-32x": {"id": 19, "name": "Megadrive 32X"},
|
||||
"sega32": {"id": 19, "name": "Megadrive 32X"}, # IGDB
|
||||
"sega-cd": {"id": 20, "name": "Mega-CD"},
|
||||
"segacd": {"id": 20, "name": "Mega-CD"}, # IGDB
|
||||
"sega-master-system": {"id": 2, "name": "Master System"},
|
||||
"sms": {"id": 2, "name": "Master System"}, # IGDB
|
||||
"sega-pico": {"id": 250, "name": "Sega Pico"},
|
||||
"sega-saturn": {"id": 22, "name": "Saturn"},
|
||||
"saturn": {"id": 22, "name": "Saturn"}, # IGDB
|
||||
"sg-1000": {"id": 109, "name": "SG-1000"},
|
||||
"snes": {"id": 4, "name": "Super Nintendo"},
|
||||
"sharp-x1": {"id": 220, "name": "Sharp X1"},
|
||||
"x1": {"id": 220, "name": "Sharp X1"}, # IGDB
|
||||
"sharp-x68000": {"id": 79, "name": "Sharp X68000"},
|
||||
"spectravideo": {"id": 218, "name": "Spectravideo"},
|
||||
"super-acan": {"id": 100, "name": "Super A'can"},
|
||||
"supergrafx": {"id": 105, "name": "PC Engine SuperGrafx"},
|
||||
"supervision": {"id": 207, "name": "Watara Supervision"},
|
||||
"ti-99": {"id": 205, "name": "TI-99/4A"}, # IGDB
|
||||
"trs-80-coco": {"id": 144, "name": "TRS-80 Color Computer"},
|
||||
"trs-80-color-computer": {"id": 144, "name": "TRS-80 Color Computer"}, # IGDB
|
||||
"taito-x-55": {"id": 112, "name": "Type X"},
|
||||
"thomson-mo": {"id": 141, "name": "Thomson MO/TO"},
|
||||
"thomson-mo5": {"id": 141, "name": "Thomson MO/TO"},
|
||||
"thomson-to": {"id": 141, "name": "Thomson MO/TO"},
|
||||
"turbografx-cd": {"id": 114, "name": "PC Engine CD-Rom"},
|
||||
"turbografx-16-slash-pc-engine-cd": {"id": 114, "name": "PC Engine CD-Rom"},
|
||||
"turbo-grafx": {"id": 31, "name": "PC Engine"},
|
||||
"turbografx16--1": {"id": 31, "name": "PC Engine"}, # IGDB
|
||||
"vsmile": {"id": 120, "name": "V.Smile"},
|
||||
"vic-20": {"id": 73, "name": "Vic-20"},
|
||||
"vectrex": {"id": 102, "name": "Vectrex"},
|
||||
"videopac-g7400": {"id": 104, "name": "Videopac G7000"},
|
||||
"virtual-boy": {"id": 11, "name": "Virtual Boy"},
|
||||
"virtualboy": {"id": 11, "name": "Virtual Boy"},
|
||||
"wii": {"id": 18, "name": "Wii"},
|
||||
"wii-u": {"id": 18, "name": "Wii U"},
|
||||
"wiiu": {"id": 18, "name": "Wii U"},
|
||||
"windows": {"id": 3, "name": "Windows"},
|
||||
"win": {"id": 138, "name": "PC Windows"}, # IGDB
|
||||
"win3x": {"id": 136, "name": "PC Win3.xx"},
|
||||
"wonderswan": {"id": 45, "name": "WonderSwan"},
|
||||
"wonderswan-color": {"id": 46, "name": "WonderSwan Color"},
|
||||
"xbox": {"id": 32, "name": "Xbox"},
|
||||
"xbox360": {"id": 33, "name": "Xbox 360"},
|
||||
"xbox-one": {"id": 34, "name": "Xbox One"},
|
||||
"xboxone": {"id": 34, "name": "Xbox One"},
|
||||
"z-machine": {"id": 215, "name": "Z-Machine"},
|
||||
"zx-spectrum": {"id": 76, "name": "ZX Spectrum"},
|
||||
"zx81": {"id": 77, "name": "ZX81"},
|
||||
"sinclair-zx81": {"id": 77, "name": "ZX81"}, # IGDB
|
||||
}
|
||||
|
||||
# Reverse lookup
|
||||
SS_ID_TO_SLUG = {v["id"]: k for k, v in SLUG_TO_SS_ID.items()}
|
||||
@@ -7,9 +7,10 @@ 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 import meta_igdb_handler, meta_moby_handler, meta_ss_handler
|
||||
from handler.metadata.igdb_handler import IGDBPlatform, IGDBRom
|
||||
from handler.metadata.moby_handler import MobyGamesPlatform, MobyGamesRom
|
||||
from handler.metadata.ss_handler import SSPlatform, SSRom
|
||||
from logger.formatter import BLUE
|
||||
from logger.formatter import highlight as hl
|
||||
from logger.logger import log
|
||||
@@ -29,6 +30,12 @@ class ScanType(Enum):
|
||||
HASHES = "hashes"
|
||||
|
||||
|
||||
class MetadataSource:
|
||||
IGDB = "igdb"
|
||||
MOBY = "moby"
|
||||
SS = "ss"
|
||||
|
||||
|
||||
async def _get_main_platform_igdb_id(platform: Platform):
|
||||
cnfg = cm.get_config()
|
||||
|
||||
@@ -64,7 +71,7 @@ async def scan_platform(
|
||||
log.info(f"· {hl(fs_slug)}")
|
||||
|
||||
if metadata_sources is None:
|
||||
metadata_sources = ["igdb", "moby"]
|
||||
metadata_sources = [MetadataSource.IGDB, MetadataSource.MOBY, MetadataSource.SS]
|
||||
|
||||
platform_attrs: dict[str, Any] = {}
|
||||
platform_attrs["fs_slug"] = fs_slug
|
||||
@@ -99,19 +106,30 @@ async def scan_platform(
|
||||
|
||||
igdb_platform = (
|
||||
(await meta_igdb_handler.get_platform(platform_attrs["slug"]))
|
||||
if "igdb" in metadata_sources
|
||||
if MetadataSource.IGDB in metadata_sources
|
||||
else IGDBPlatform(igdb_id=None, slug=platform_attrs["slug"])
|
||||
)
|
||||
moby_platform = (
|
||||
meta_moby_handler.get_platform(platform_attrs["slug"])
|
||||
if "moby" in metadata_sources
|
||||
if MetadataSource.MOBY in metadata_sources
|
||||
else MobyGamesPlatform(moby_id=None, slug=platform_attrs["slug"])
|
||||
)
|
||||
ss_platform = (
|
||||
meta_ss_handler.get_platform(platform_attrs["slug"])
|
||||
if MetadataSource.SS in metadata_sources
|
||||
else SSPlatform(ss_id=None, slug=platform_attrs["slug"])
|
||||
)
|
||||
|
||||
platform_attrs["name"] = platform_attrs["slug"].replace("-", " ").title()
|
||||
platform_attrs.update({**moby_platform, **igdb_platform}) # Reverse order
|
||||
platform_attrs.update(
|
||||
{**moby_platform, **ss_platform, **igdb_platform}
|
||||
) # Reverse order
|
||||
|
||||
if platform_attrs["igdb_id"] or platform_attrs["moby_id"]:
|
||||
if (
|
||||
platform_attrs["igdb_id"]
|
||||
or platform_attrs["moby_id"]
|
||||
or platform_attrs["ss_id"]
|
||||
):
|
||||
log.info(
|
||||
emoji.emojize(
|
||||
f" Identified as {hl(platform_attrs['name'], color=BLUE)} :video_game:"
|
||||
@@ -180,7 +198,7 @@ async def scan_rom(
|
||||
metadata_sources: list[str] | None = None,
|
||||
) -> Rom:
|
||||
if not metadata_sources:
|
||||
metadata_sources = ["igdb", "moby"]
|
||||
metadata_sources = [MetadataSource.IGDB, MetadataSource.MOBY, MetadataSource.SS]
|
||||
|
||||
roms_path = fs_rom_handler.get_roms_fs_structure(platform.fs_slug)
|
||||
|
||||
@@ -198,6 +216,7 @@ async def scan_rom(
|
||||
"platform_id": platform.id,
|
||||
"name": fs_rom["fs_name"],
|
||||
"url_cover": "",
|
||||
"url_manual": "",
|
||||
"url_screenshots": [],
|
||||
}
|
||||
|
||||
@@ -207,6 +226,7 @@ async def scan_rom(
|
||||
{
|
||||
"igdb_id": rom.igdb_id,
|
||||
"moby_id": rom.moby_id,
|
||||
"ss_id": rom.ss_id,
|
||||
"sgdb_id": rom.sgdb_id,
|
||||
"name": rom.name,
|
||||
"slug": rom.slug,
|
||||
@@ -214,6 +234,7 @@ async def scan_rom(
|
||||
"igdb_metadata": rom.igdb_metadata,
|
||||
"moby_metadata": rom.moby_metadata,
|
||||
"url_cover": rom.url_cover,
|
||||
"url_manual": rom.url_manual,
|
||||
"path_cover_s": rom.path_cover_s,
|
||||
"path_cover_l": rom.path_cover_l,
|
||||
"path_screenshots": rom.path_screenshots,
|
||||
@@ -253,7 +274,7 @@ async def scan_rom(
|
||||
|
||||
async def fetch_igdb_rom():
|
||||
if (
|
||||
"igdb" in metadata_sources
|
||||
MetadataSource.IGDB in metadata_sources
|
||||
and platform.igdb_id
|
||||
and (
|
||||
not rom
|
||||
@@ -271,7 +292,7 @@ async def scan_rom(
|
||||
|
||||
async def fetch_moby_rom():
|
||||
if (
|
||||
"moby" in metadata_sources
|
||||
MetadataSource.MOBY in metadata_sources
|
||||
and platform.moby_id
|
||||
and (
|
||||
not rom
|
||||
@@ -286,16 +307,37 @@ async def scan_rom(
|
||||
|
||||
return MobyGamesRom(moby_id=None)
|
||||
|
||||
async def fetch_ss_rom():
|
||||
if (
|
||||
MetadataSource.SS in metadata_sources
|
||||
and platform.ss_id
|
||||
and (
|
||||
not rom
|
||||
or scan_type == ScanType.COMPLETE
|
||||
or (scan_type == ScanType.PARTIAL and not rom.ss_id)
|
||||
or (scan_type == ScanType.UNIDENTIFIED and not rom.ss_id)
|
||||
)
|
||||
):
|
||||
return await meta_ss_handler.get_rom(
|
||||
rom_attrs["fs_name"], platform_ss_id=platform.ss_id
|
||||
)
|
||||
|
||||
return SSRom(ss_id=None)
|
||||
|
||||
# Run both metadata fetches concurrently
|
||||
igdb_handler_rom, moby_handler_rom = await asyncio.gather(
|
||||
fetch_igdb_rom(), fetch_moby_rom()
|
||||
igdb_handler_rom, moby_handler_rom, ss_handler_rom = await asyncio.gather(
|
||||
fetch_igdb_rom(), fetch_moby_rom(), fetch_ss_rom()
|
||||
)
|
||||
|
||||
# Reversed to prioritize IGDB
|
||||
rom_attrs.update({**moby_handler_rom, **igdb_handler_rom})
|
||||
rom_attrs.update({**moby_handler_rom, **ss_handler_rom, **igdb_handler_rom})
|
||||
|
||||
# If not found in IGDB or MobyGames
|
||||
if not igdb_handler_rom.get("igdb_id") and not moby_handler_rom.get("moby_id"):
|
||||
# If not found in IGDB, MobyGames and Screenscraper
|
||||
if (
|
||||
not igdb_handler_rom.get("igdb_id")
|
||||
and not moby_handler_rom.get("moby_id")
|
||||
and not ss_handler_rom.get("ss_id")
|
||||
):
|
||||
log.warning(
|
||||
emoji.emojize(
|
||||
f"\t Rom {rom_attrs['fs_name']} not identified :cross_mark:"
|
||||
|
||||
@@ -21,6 +21,7 @@ class Platform(BaseModel):
|
||||
igdb_id: Mapped[int | None]
|
||||
sgdb_id: Mapped[int | None]
|
||||
moby_id: Mapped[int | None]
|
||||
ss_id: Mapped[int | None]
|
||||
slug: Mapped[str] = mapped_column(String(length=100))
|
||||
fs_slug: Mapped[str] = mapped_column(String(length=100))
|
||||
name: Mapped[str] = mapped_column(String(length=400))
|
||||
|
||||
@@ -71,10 +71,12 @@ class Rom(BaseModel):
|
||||
igdb_id: Mapped[int | None]
|
||||
sgdb_id: Mapped[int | None]
|
||||
moby_id: Mapped[int | None]
|
||||
ss_id: Mapped[int | None]
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_roms_igdb_id", "igdb_id"),
|
||||
Index("idx_roms_moby_id", "moby_id"),
|
||||
Index("idx_roms_ss_id", "ss_id"),
|
||||
)
|
||||
|
||||
fs_name: Mapped[str] = mapped_column(String(length=450))
|
||||
@@ -92,6 +94,9 @@ class Rom(BaseModel):
|
||||
moby_metadata: Mapped[dict[str, Any] | None] = mapped_column(
|
||||
CustomJSON(), default=dict
|
||||
)
|
||||
ss_metadata: Mapped[dict[str, Any] | None] = mapped_column(
|
||||
CustomJSON(), default=dict
|
||||
)
|
||||
|
||||
path_cover_s: Mapped[str | None] = mapped_column(Text, default="")
|
||||
path_cover_l: Mapped[str | None] = mapped_column(Text, default="")
|
||||
@@ -99,6 +104,11 @@ class Rom(BaseModel):
|
||||
Text, default="", doc="URL to cover image stored in IGDB"
|
||||
)
|
||||
|
||||
path_manual: Mapped[str | None] = mapped_column(Text, default="")
|
||||
url_manual: Mapped[str | None] = mapped_column(
|
||||
Text, default="", doc="URL to manual stored in ScreenScraper"
|
||||
)
|
||||
|
||||
revision: Mapped[str | None] = mapped_column(String(length=100))
|
||||
regions: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[])
|
||||
languages: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[])
|
||||
@@ -160,6 +170,10 @@ class Rom(BaseModel):
|
||||
def full_path(self) -> str:
|
||||
return f"{self.fs_path}/{self.fs_name}"
|
||||
|
||||
@cached_property
|
||||
def has_manual(self) -> bool:
|
||||
return bool(self.path_manual)
|
||||
|
||||
@cached_property
|
||||
def merged_screenshots(self) -> list[str]:
|
||||
screenshots = [s.download_path for s in self.screenshots]
|
||||
@@ -213,6 +227,7 @@ class Rom(BaseModel):
|
||||
return (
|
||||
(self.igdb_metadata or {}).get("alternative_names", None)
|
||||
or (self.moby_metadata or {}).get("alternate_titles", None)
|
||||
or (self.ss_metadata or {}).get("alternative_names", None)
|
||||
or []
|
||||
)
|
||||
|
||||
@@ -220,6 +235,8 @@ class Rom(BaseModel):
|
||||
def first_release_date(self) -> int | None:
|
||||
if self.igdb_metadata:
|
||||
return safe_int(self.igdb_metadata.get("first_release_date") or 0) * 1000
|
||||
elif self.ss_metadata:
|
||||
return safe_int(self.ss_metadata.get("first_release_date") or 0) * 1000
|
||||
|
||||
return None
|
||||
|
||||
@@ -235,8 +252,13 @@ class Rom(BaseModel):
|
||||
if self.moby_metadata
|
||||
else 0.0
|
||||
)
|
||||
ss_rating = (
|
||||
safe_float(self.ss_metadata.get("ss_score") or 0)
|
||||
if self.ss_metadata
|
||||
else 0.0
|
||||
)
|
||||
|
||||
ratings = [r for r in [igdb_rating, moby_rating * 10] if r != 0.0]
|
||||
ratings = [r for r in [igdb_rating, moby_rating * 10, ss_rating] if r != 0.0]
|
||||
|
||||
return sum(ratings) / len([r for r in ratings if r]) if any(ratings) else None
|
||||
|
||||
@@ -245,6 +267,7 @@ class Rom(BaseModel):
|
||||
return (
|
||||
(self.igdb_metadata or {}).get("genres", None)
|
||||
or (self.moby_metadata or {}).get("genres", None)
|
||||
or (self.ss_metadata or {}).get("genres", None)
|
||||
or []
|
||||
)
|
||||
|
||||
@@ -252,6 +275,8 @@ class Rom(BaseModel):
|
||||
def franchises(self) -> list[str]:
|
||||
if self.igdb_metadata:
|
||||
return self.igdb_metadata.get("franchises", [])
|
||||
elif self.ss_metadata:
|
||||
return self.ss_metadata.get("franchises", [])
|
||||
return []
|
||||
|
||||
@property
|
||||
@@ -264,12 +289,16 @@ class Rom(BaseModel):
|
||||
def companies(self) -> list[str]:
|
||||
if self.igdb_metadata:
|
||||
return self.igdb_metadata.get("companies", [])
|
||||
elif self.ss_metadata:
|
||||
return self.ss_metadata.get("companies", [])
|
||||
return []
|
||||
|
||||
@property
|
||||
def game_modes(self) -> list[str]:
|
||||
if self.igdb_metadata:
|
||||
return self.igdb_metadata.get("game_modes", [])
|
||||
elif self.ss_metadata:
|
||||
return self.ss_metadata.get("game_modes", [])
|
||||
return []
|
||||
|
||||
@property
|
||||
|
||||
@@ -3,7 +3,8 @@ DEV_MODE=true
|
||||
KIOSK_MODE=false
|
||||
|
||||
# Gunicorn (optional)
|
||||
GUNICORN_WORKERS=4 # (2 × CPU cores) + 1
|
||||
# Workers -> (2 × CPU cores) + 1
|
||||
GUNICORN_WORKERS=4
|
||||
|
||||
# IGDB credentials
|
||||
IGDB_CLIENT_ID=
|
||||
@@ -12,6 +13,10 @@ IGDB_CLIENT_SECRET=
|
||||
# Mobygames
|
||||
MOBYGAMES_API_KEY=
|
||||
|
||||
# Screenscraper
|
||||
SCREENSCRAPER_USER=
|
||||
SCREENSCRAPER_PASSWORD=
|
||||
|
||||
# SteamGridDB
|
||||
STEAMGRIDDB_API_KEY=
|
||||
|
||||
|
||||
BIN
frontend/assets/auth_background_static.png
Normal file
BIN
frontend/assets/auth_background_static.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/assets/logos/romm_logo_xbox_one_circle_grayscale.png
Normal file
BIN
frontend/assets/logos/romm_logo_xbox_one_circle_grayscale.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="1024" height="1024" viewBox="0 0 270.933 270.933"><defs><clipPath id="a" clipPathUnits="userSpaceOnUse"><ellipse cx="511.875" cy="512.551" rx="255.946" ry="255.984" style="fill:none"/></clipPath><clipPath id="b" clipPathUnits="userSpaceOnUse"><ellipse cx="135.457" cy="135.471" rx="135.436" ry="135.456" style="fill:none"/></clipPath><clipPath id="c" clipPathUnits="userSpaceOnUse"><ellipse cx="511.875" cy="512.551" rx="255.946" ry="255.984" style="fill:none"/></clipPath></defs><path d="M255.928 256.564H767.82V768.54H255.928Z" class="background-light" clip-path="url(#a)" style="fill:#f0f0f0;fill-opacity:1;stroke-width:0" transform="translate(-135.407 -135.75)scale(.52916)"/><path d="m714.366 256.564-458.44 458.44v53.536H767.82V256.564Z" class="background-dark" clip-path="url(#a)" style="fill:#999;fill-opacity:1;stroke-width:0" transform="translate(-135.407 -135.75)scale(.52916)"/><g clip-path="url(#b)" style="display:inline"><path d="M601.91 768.54c-2.735-.066-43.37-47.717-83.736-79.261-2.784-2.176-6.65-5.2-8.593-6.721-3.886-3.045-10.784-7.06-11.479-7.461-1.19-.687-2.393-1.272-6.665-3.15-4.524-1.99-13.567-4.542-18.772-5.298-8.498-1.236-33.84-1.639-96.251-1.582l-120.486.11c-.016-82.184-.01-236.036-.013-305.108l88.831.232c39.029.101 40.847-.189 47.144-3.008 13.443-6.019 68.906-76.53 83.114-82.973 7.254-3.29 16.603-4.702 23.918-3.202 1.581.324 6.626 1.157 11.212 1.852 10.895 1.65 18.218 2.754 35.434 7.376 28.676 7.697 56.638 19.386 58.66 20.31 11.937 5.453 25.411 12.472 29.43 16.271 1.59 1.504 3.282 3.542 4.083 4.852 3.524 5.765 11.034 19.845 11.756 22.333 9.175 22.007 17.356 44.381 25.733 66.7 3.979 11.415 8.336 22.57 12.276 33.945 3.358 9.785 10.614 30.128 11.045 30.968 5.604 15.717 10.419 31.655 15.204 47.643 2.989 9.93 6.674 22.862 10.253 36.626 5.39 18.118 10.909 36.205 14.226 54.889.718 3.96 3.264 19.802 3.445 21.053.437 3.026 3.277 25.867 3.851 31.073 2.626 23.81 2.15 68.545-17.678 101.54-.1.167-2.65.031-2.707.032-9.042.065-31.907-.03-39.664-.043-15.94-.029-83.501.005-83.508.005z" class="logo-secondary" style="fill:#333;fill-opacity:1;stroke-width:0;stroke-dasharray:none" transform="translate(-135.407 -135.75)scale(.52916)"/><path d="M115.894 9.256c-7 3.173-37.415 41.18-43.992 44.125-3.331 1.492-4.294 1.645-24.947 1.591l-46.94-.189c0 36.55-.002 117.963.006 161.45l26.456-.023L204.26 38.36c-.75-1.387-1.84-3.095-2.388-3.99-.424-.694-1.18-1.633-2.021-2.428-2.126-2.01-9.256-5.725-15.573-8.61-10.144-4.633-20.204-7.68-31.041-10.748-9.11-2.446-17.249-3.626-18.752-3.903-2.413-.444-5.031-.84-5.932-.98-.641-.098-6.525-1.225-12.658 1.555z" class="logo-primary" style="fill:#666;fill-opacity:1;stroke-width:0"/></g><g clip-path="url(#c)" style="display:inline" transform="translate(-135.407 -135.75)scale(.52916)"><circle cx="530.532" cy="381.817" r="31.529" class="dot-light" style="fill:#f0f0f0;fill-opacity:1;stroke-width:0;stroke-linejoin:bevel;stroke-dasharray:none"/><circle cx="471.565" cy="440.348" r="31.529" class="dot-light" style="fill:#f0f0f0;fill-opacity:1;stroke-width:0;stroke-linejoin:bevel;stroke-dasharray:none"/><circle cx="590.601" cy="439.252" r="31.529" class="dot-dark" style="fill:#999;fill-opacity:1;stroke-width:0;stroke-linejoin:bevel;stroke-dasharray:none"/><circle cx="530.441" cy="499.948" r="31.529" class="dot-dark" style="fill:#999;fill-opacity:1;stroke-width:0;stroke-linejoin:bevel;stroke-dasharray:none"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 50 24"><path fill="#305b79" d="M0 0h41v18H0z"/><path fill="#4787b4" d="M3 2h41v18H3z"/><path fill="#5fb4f0" d="M6 4h41v18H6z"/><path fill="#3a6e92" d="M9 6h41v18H9z"/></svg>
|
||||
|
Before Width: | Height: | Size: 238 B |
BIN
frontend/assets/scrappers/ss.png
Normal file
BIN
frontend/assets/scrappers/ss.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
9
frontend/package-lock.json
generated
9
frontend/package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"vue": "^3.4.27",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue-router": "^4.3.2",
|
||||
"vue3-pdf-app": "^1.0.3",
|
||||
"vuetify": "^3.7.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -8303,6 +8304,14 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue3-pdf-app": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vue3-pdf-app/-/vue3-pdf-app-1.0.3.tgz",
|
||||
"integrity": "sha512-qegWTIF4wYKiocZ3KreB70wRXhqSdXWbdERDyyKzT7d5PbjKbS9tD6vaKkCqh3PzTM84NyKPYrQ3iuwJb60YPQ==",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "3.7.11",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.11.tgz",
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"vue": "^3.4.27",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue-router": "^4.3.2",
|
||||
"vue3-pdf-app": "^1.0.3",
|
||||
"vuetify": "^3.7.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
1
frontend/src/__generated__/index.ts
generated
1
frontend/src/__generated__/index.ts
generated
@@ -36,6 +36,7 @@ export type { RomFileSchema } from './models/RomFileSchema';
|
||||
export type { RomIGDBMetadata } from './models/RomIGDBMetadata';
|
||||
export type { RomMobyMetadata } from './models/RomMobyMetadata';
|
||||
export type { RomSchema } from './models/RomSchema';
|
||||
export type { RomSSMetadata } from './models/RomSSMetadata';
|
||||
export type { RomUserSchema } from './models/RomUserSchema';
|
||||
export type { RomUserStatus } from './models/RomUserStatus';
|
||||
export type { SaveSchema } from './models/SaveSchema';
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { RomFileSchema } from './RomFileSchema';
|
||||
import type { RomIGDBMetadata } from './RomIGDBMetadata';
|
||||
import type { RomMobyMetadata } from './RomMobyMetadata';
|
||||
import type { RomSchema } from './RomSchema';
|
||||
import type { RomSSMetadata } from './RomSSMetadata';
|
||||
import type { RomUserSchema } from './RomUserSchema';
|
||||
import type { SaveSchema } from './SaveSchema';
|
||||
import type { ScreenshotSchema } from './ScreenshotSchema';
|
||||
@@ -17,6 +18,7 @@ export type DetailedRomSchema = {
|
||||
igdb_id: (number | null);
|
||||
sgdb_id: (number | null);
|
||||
moby_id: (number | null);
|
||||
ss_id: (number | null);
|
||||
platform_id: number;
|
||||
platform_slug: string;
|
||||
platform_fs_slug: string;
|
||||
@@ -44,9 +46,13 @@ export type DetailedRomSchema = {
|
||||
age_ratings: Array<string>;
|
||||
igdb_metadata: (RomIGDBMetadata | null);
|
||||
moby_metadata: (RomMobyMetadata | null);
|
||||
ss_metadata: (RomSSMetadata | null);
|
||||
path_cover_small: (string | null);
|
||||
path_cover_large: (string | null);
|
||||
url_cover: (string | null);
|
||||
has_manual: boolean;
|
||||
path_manual: (string | null);
|
||||
url_manual: (string | null);
|
||||
is_unidentified: boolean;
|
||||
revision: (string | null);
|
||||
regions: Array<string>;
|
||||
|
||||
@@ -6,6 +6,7 @@ export type MetadataSourcesDict = {
|
||||
ANY_SOURCE_ENABLED: boolean;
|
||||
IGDB_API_ENABLED: boolean;
|
||||
MOBY_API_ENABLED: boolean;
|
||||
SS_API_ENABLED: boolean;
|
||||
STEAMGRIDDB_ENABLED: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export type PlatformSchema = {
|
||||
igdb_id?: (number | null);
|
||||
sgdb_id?: (number | null);
|
||||
moby_id?: (number | null);
|
||||
ss_id?: (number | null);
|
||||
category?: (string | null);
|
||||
generation?: (number | null);
|
||||
family_name?: (string | null);
|
||||
|
||||
14
frontend/src/__generated__/models/RomSSMetadata.ts
generated
Normal file
14
frontend/src/__generated__/models/RomSSMetadata.ts
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type RomSSMetadata = {
|
||||
ss_score?: string;
|
||||
first_release_date?: (number | null);
|
||||
alternative_names?: Array<string>;
|
||||
companies?: Array<string>;
|
||||
franchises?: Array<string>;
|
||||
game_modes?: Array<string>;
|
||||
genres?: Array<string>;
|
||||
};
|
||||
|
||||
6
frontend/src/__generated__/models/RomSchema.ts
generated
6
frontend/src/__generated__/models/RomSchema.ts
generated
@@ -5,11 +5,13 @@
|
||||
import type { RomFileSchema } from './RomFileSchema';
|
||||
import type { RomIGDBMetadata } from './RomIGDBMetadata';
|
||||
import type { RomMobyMetadata } from './RomMobyMetadata';
|
||||
import type { RomSSMetadata } from './RomSSMetadata';
|
||||
export type RomSchema = {
|
||||
id: number;
|
||||
igdb_id: (number | null);
|
||||
sgdb_id: (number | null);
|
||||
moby_id: (number | null);
|
||||
ss_id: (number | null);
|
||||
platform_id: number;
|
||||
platform_slug: string;
|
||||
platform_fs_slug: string;
|
||||
@@ -37,9 +39,13 @@ export type RomSchema = {
|
||||
age_ratings: Array<string>;
|
||||
igdb_metadata: (RomIGDBMetadata | null);
|
||||
moby_metadata: (RomMobyMetadata | null);
|
||||
ss_metadata: (RomSSMetadata | null);
|
||||
path_cover_small: (string | null);
|
||||
path_cover_large: (string | null);
|
||||
url_cover: (string | null);
|
||||
has_manual: boolean;
|
||||
path_manual: (string | null);
|
||||
url_manual: (string | null);
|
||||
is_unidentified: boolean;
|
||||
revision: (string | null);
|
||||
regions: Array<string>;
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
export type SearchRomSchema = {
|
||||
igdb_id?: (number | null);
|
||||
moby_id?: (number | null);
|
||||
ss_id?: (number | null);
|
||||
slug: string;
|
||||
name: string;
|
||||
summary: string;
|
||||
igdb_url_cover?: string;
|
||||
moby_url_cover?: string;
|
||||
ss_url_cover?: string;
|
||||
platform_id: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ import type { RomFileSchema } from './RomFileSchema';
|
||||
import type { RomIGDBMetadata } from './RomIGDBMetadata';
|
||||
import type { RomMobyMetadata } from './RomMobyMetadata';
|
||||
import type { RomSchema } from './RomSchema';
|
||||
import type { RomSSMetadata } from './RomSSMetadata';
|
||||
import type { RomUserSchema } from './RomUserSchema';
|
||||
export type SimpleRomSchema = {
|
||||
id: number;
|
||||
igdb_id: (number | null);
|
||||
sgdb_id: (number | null);
|
||||
moby_id: (number | null);
|
||||
ss_id: (number | null);
|
||||
platform_id: number;
|
||||
platform_slug: string;
|
||||
platform_fs_slug: string;
|
||||
@@ -39,9 +41,13 @@ export type SimpleRomSchema = {
|
||||
age_ratings: Array<string>;
|
||||
igdb_metadata: (RomIGDBMetadata | null);
|
||||
moby_metadata: (RomMobyMetadata | null);
|
||||
ss_metadata: (RomSSMetadata | null);
|
||||
path_cover_small: (string | null);
|
||||
path_cover_large: (string | null);
|
||||
url_cover: (string | null);
|
||||
has_manual: boolean;
|
||||
path_manual: (string | null);
|
||||
url_manual: (string | null);
|
||||
is_unidentified: boolean;
|
||||
revision: (string | null);
|
||||
regions: Array<string>;
|
||||
|
||||
151
frontend/src/components/Details/PDFViewer.vue
Normal file
151
frontend/src/components/Details/PDFViewer.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<script setup lang="ts">
|
||||
import { useTheme, useDisplay } from "vuetify";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import VuePdfApp from "vue3-pdf-app";
|
||||
|
||||
// Props
|
||||
const props = defineProps<{ rom: DetailedRom }>();
|
||||
const { xs } = useDisplay();
|
||||
const theme = useTheme();
|
||||
const pdfViewerConfig = {
|
||||
sidebarToggle: "sidebarToggleId",
|
||||
pageNumber: "pageNumberId",
|
||||
numPages: "numPagesId",
|
||||
zoomIn: "zoomInId",
|
||||
zoomOut: "zoomOutId",
|
||||
firstPage: "firstPageId",
|
||||
previousPage: "previousPageId",
|
||||
nextPage: "nextPageId",
|
||||
lastPage: "lastPageId",
|
||||
download: "downloadId",
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-toolbar class="bg-toplayer px-2" density="compact" :elevation="0">
|
||||
<button
|
||||
class="pdfv-toolbar-btn"
|
||||
:id="pdfViewerConfig.sidebarToggle"
|
||||
type="button"
|
||||
>
|
||||
<v-icon>mdi-menu</v-icon>
|
||||
</button>
|
||||
<v-spacer />
|
||||
|
||||
<input
|
||||
class="px-1"
|
||||
style="width: 40px"
|
||||
:id="pdfViewerConfig.pageNumber"
|
||||
type="number"
|
||||
/>
|
||||
<span class="ml-2" :id="pdfViewerConfig.numPages"></span>
|
||||
<button
|
||||
class="pdfv-toolbar-btn"
|
||||
:class="{ 'ml-8': !xs, 'ml-4': xs }"
|
||||
:id="pdfViewerConfig.firstPage"
|
||||
type="button"
|
||||
>
|
||||
<v-icon>mdi-page-first</v-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-show="!xs"
|
||||
class="pdfv-toolbar-btn"
|
||||
:id="pdfViewerConfig.previousPage"
|
||||
type="button"
|
||||
>
|
||||
<v-icon>mdi-chevron-left</v-icon>
|
||||
</button>
|
||||
<button
|
||||
v-show="!xs"
|
||||
class="pdfv-toolbar-btn"
|
||||
:id="pdfViewerConfig.nextPage"
|
||||
type="button"
|
||||
>
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="pdfv-toolbar-btn"
|
||||
:id="pdfViewerConfig.lastPage"
|
||||
type="button"
|
||||
>
|
||||
<v-icon>mdi-page-last</v-icon>
|
||||
</button>
|
||||
<button
|
||||
class="pdfv-toolbar-btn"
|
||||
:class="{ 'ml-8': !xs, 'ml-4': xs }"
|
||||
:id="pdfViewerConfig.zoomIn"
|
||||
type="button"
|
||||
>
|
||||
<v-icon>mdi-magnify-plus-outline</v-icon>
|
||||
</button>
|
||||
<button
|
||||
class="pdfv-toolbar-btn"
|
||||
:id="pdfViewerConfig.zoomOut"
|
||||
type="button"
|
||||
>
|
||||
<v-icon>mdi-magnify-minus-outline</v-icon>
|
||||
</button>
|
||||
<v-spacer />
|
||||
<button
|
||||
class="pdfv-toolbar-btn"
|
||||
:id="pdfViewerConfig.download"
|
||||
type="button"
|
||||
>
|
||||
<v-icon>mdi-download</v-icon>
|
||||
</button>
|
||||
</v-toolbar>
|
||||
<vue-pdf-app
|
||||
:id-config="pdfViewerConfig"
|
||||
:config="{ toolbar: false }"
|
||||
:theme="theme.name.value == 'dark' ? 'dark' : 'light'"
|
||||
style="height: 100dvh"
|
||||
:pdf="`/assets/romm/resources/${rom.path_manual}`"
|
||||
>
|
||||
</vue-pdf-app>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* the vue-pdf-app needs to be styled because it only accepts basic html elements */
|
||||
.pdf-app.light,
|
||||
.pdf-app.dark {
|
||||
--pdf-app-background-color: rgba(var(--v-theme-surface)) !important;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
text-align: right;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
border: 1px solid rgba(var(--v-theme-secondary));
|
||||
border-radius: 5px;
|
||||
-webkit-transition: 0.5s;
|
||||
transition: 0.5s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.pdfv-toolbar-btn {
|
||||
transition:
|
||||
color 0.15s ease-in-out,
|
||||
transform 0.1s ease-in-out background-color 0.15s linear;
|
||||
border-radius: 5px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.pdfv-toolbar-btn:hover {
|
||||
color: rgba(var(--v-theme-primary));
|
||||
background-color: rgba(var(--v-theme-surface));
|
||||
}
|
||||
|
||||
.pdfv-toolbar-btn:active {
|
||||
transform: translateY(1px) translateX(1px);
|
||||
}
|
||||
</style>
|
||||
@@ -139,7 +139,7 @@ const hasReleaseDate = Number(props.rom.first_release_date) > 0;
|
||||
</v-row>
|
||||
|
||||
<v-row
|
||||
v-if="rom.igdb_id || rom.moby_id"
|
||||
v-if="rom.igdb_id || rom.moby_id || rom.ss_id"
|
||||
class="text-white text-shadow mt-2"
|
||||
:class="{ 'text-center': smAndDown }"
|
||||
no-gutters
|
||||
@@ -151,12 +151,14 @@ const hasReleaseDate = Number(props.rom.first_release_date) > 0;
|
||||
:href="`https://www.igdb.com/games/${rom.slug}`"
|
||||
target="_blank"
|
||||
>
|
||||
<v-chip size="x-small" @click.stop>
|
||||
<span>IGDB</span>
|
||||
<v-chip class="pl-0 mt-1" size="small" @click.stop>
|
||||
<v-avatar class="mr-2" size="30" rounded="0">
|
||||
<v-img src="/assets/scrappers/igdb.png" />
|
||||
</v-avatar>
|
||||
<span>{{ rom.igdb_id }}</span>
|
||||
<v-divider class="mx-2 border-opacity-25" vertical />
|
||||
<span>ID: {{ rom.igdb_id }}</span>
|
||||
<v-divider class="mx-2 border-opacity-25" vertical />
|
||||
<span>Rating: {{ rom.igdb_metadata?.total_rating }}</span>
|
||||
<span>{{ rom.igdb_metadata?.total_rating }}</span>
|
||||
<v-icon class="ml-1">mdi-star</v-icon>
|
||||
</v-chip>
|
||||
</a>
|
||||
<a
|
||||
@@ -166,12 +168,31 @@ const hasReleaseDate = Number(props.rom.first_release_date) > 0;
|
||||
target="_blank"
|
||||
:class="{ 'ml-1': rom.igdb_id }"
|
||||
>
|
||||
<v-chip size="x-small" @click.stop>
|
||||
<span>Mobygames</span>
|
||||
<v-chip class="pl-0 mt-1" size="small" @click.stop>
|
||||
<v-avatar class="mr-2" size="30" rounded="0">
|
||||
<v-img src="/assets/scrappers/moby.png" />
|
||||
</v-avatar>
|
||||
<span>{{ rom.moby_id }}</span>
|
||||
<v-divider class="mx-2 border-opacity-25" vertical />
|
||||
<span>ID: {{ rom.moby_id }}</span>
|
||||
<span>{{ rom.moby_metadata?.moby_score }}</span>
|
||||
<v-icon class="ml-1">mdi-star</v-icon>
|
||||
</v-chip>
|
||||
</a>
|
||||
<a
|
||||
v-if="rom.ss_id"
|
||||
style="text-decoration: none; color: inherit"
|
||||
:href="`https://www.screenscraper.fr/gameinfos.php?gameid=${rom.ss_id}`"
|
||||
target="_blank"
|
||||
:class="{ 'ml-1': rom.igdb_id || rom.moby_id }"
|
||||
>
|
||||
<v-chip class="pl-0 mt-1" size="small" @click.stop>
|
||||
<v-avatar class="mr-2" size="30" rounded="0">
|
||||
<v-img src="/assets/scrappers/ss.png" />
|
||||
</v-avatar>
|
||||
<span>{{ rom.ss_id }}</span>
|
||||
<v-divider class="mx-2 border-opacity-25" vertical />
|
||||
<span>Rating: {{ rom.moby_metadata?.moby_score }}</span>
|
||||
<span>{{ rom.ss_metadata?.ss_score }}</span>
|
||||
<v-icon class="ml-1">mdi-star</v-icon>
|
||||
</v-chip>
|
||||
</a>
|
||||
</v-col>
|
||||
|
||||
@@ -195,7 +195,6 @@ async function updateCollection() {
|
||||
:with-link="false"
|
||||
:collection="currentCollection"
|
||||
:src="imagePreviewUrl"
|
||||
title-on-hover
|
||||
>
|
||||
<template v-if="isEditable" #append-inner>
|
||||
<v-btn-group divided density="compact">
|
||||
@@ -373,10 +372,10 @@ async function updateCollection() {
|
||||
z-index: 1;
|
||||
}
|
||||
.drawer-desktop {
|
||||
top: 54px !important;
|
||||
top: 56px !important;
|
||||
}
|
||||
.drawer-mobile {
|
||||
top: 114px !important;
|
||||
top: 110px !important;
|
||||
width: calc(100% - 16px) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -277,31 +277,60 @@ watch(
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<a
|
||||
v-if="currentPlatform.igdb_id"
|
||||
style="text-decoration: none; color: inherit"
|
||||
:href="currentPlatform.url ? currentPlatform.url : ''"
|
||||
target="_blank"
|
||||
>
|
||||
<v-chip size="x-small" @click.stop>
|
||||
<span>IGDB</span>
|
||||
<v-divider class="mx-2 border-opacity-25" vertical />
|
||||
<span>ID: {{ currentPlatform.igdb_id }}</span>
|
||||
</v-chip>
|
||||
</a>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
class="ml-1"
|
||||
@click.stop
|
||||
:class="{ 'ml-1': currentPlatform.igdb_id }"
|
||||
v-if="currentPlatform.moby_id"
|
||||
>
|
||||
<span>Mobygames</span>
|
||||
<v-divider class="mx-2 border-opacity-25" vertical />
|
||||
<span>ID: {{ currentPlatform.moby_id }}</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-row
|
||||
v-if="
|
||||
currentPlatform.igdb_id ||
|
||||
currentPlatform.moby_id ||
|
||||
currentPlatform.ss_id
|
||||
"
|
||||
class="text-white text-shadow mt-2 text-center"
|
||||
no-gutters
|
||||
>
|
||||
<v-col cols="12">
|
||||
<a
|
||||
v-if="currentPlatform.igdb_id"
|
||||
style="text-decoration: none; color: inherit"
|
||||
:href="`https://www.igdb.com/platforms/${currentPlatform.slug}`"
|
||||
target="_blank"
|
||||
>
|
||||
<v-chip class="pl-0 mt-1" size="small" @click.stop>
|
||||
<v-avatar class="mr-2" size="30" rounded="0">
|
||||
<v-img src="/assets/scrappers/igdb.png" />
|
||||
</v-avatar>
|
||||
<span>{{ currentPlatform.igdb_id }}</span>
|
||||
</v-chip>
|
||||
</a>
|
||||
<a
|
||||
v-if="currentPlatform.moby_id"
|
||||
style="text-decoration: none; color: inherit"
|
||||
target="_blank"
|
||||
:class="{ 'ml-1': currentPlatform.igdb_id }"
|
||||
>
|
||||
<v-chip class="pl-0 mt-1" size="small" @click.stop>
|
||||
<v-avatar class="mr-2" size="30" rounded="0">
|
||||
<v-img src="/assets/scrappers/moby.png" />
|
||||
</v-avatar>
|
||||
<span>{{ currentPlatform.moby_id }}</span>
|
||||
</v-chip>
|
||||
</a>
|
||||
<a
|
||||
v-if="currentPlatform.ss_id"
|
||||
style="text-decoration: none; color: inherit"
|
||||
:href="`https://www.screenscraper.fr/systemeinfos.php?plateforme=${currentPlatform.ss_id}`"
|
||||
target="_blank"
|
||||
:class="{
|
||||
'ml-1': currentPlatform.igdb_id || currentPlatform.moby_id,
|
||||
}"
|
||||
>
|
||||
<v-chip class="pl-0 mt-1" size="small" @click.stop>
|
||||
<v-avatar class="mr-2" size="30" rounded="0">
|
||||
<v-img src="/assets/scrappers/ss.png" />
|
||||
</v-avatar>
|
||||
<span>{{ currentPlatform.ss_id }}</span>
|
||||
</v-chip>
|
||||
</a>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card class="mt-4 bg-toplayer fill-width" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<template
|
||||
|
||||
@@ -76,7 +76,7 @@ const computedAspectRatio = computed(() => {
|
||||
return parseFloat(ratio.toString());
|
||||
});
|
||||
const fallbackCoverImage = computed(() =>
|
||||
props.rom.igdb_id || props.rom.moby_id
|
||||
props.rom.igdb_id || props.rom.moby_id || props.rom.ss_id
|
||||
? getMissingCoverImage(props.rom.name || props.rom.slug || "")
|
||||
: getUnmatchedCoverImage(props.rom.name || props.rom.slug || ""),
|
||||
);
|
||||
@@ -128,6 +128,7 @@ const fallbackCoverImage = computed(() =>
|
||||
? rom.path_cover_large || fallbackCoverImage
|
||||
: rom.igdb_url_cover ||
|
||||
rom.moby_url_cover ||
|
||||
rom.ss_url_cover ||
|
||||
fallbackCoverImage)
|
||||
"
|
||||
:lazy-src="
|
||||
@@ -136,6 +137,7 @@ const fallbackCoverImage = computed(() =>
|
||||
? rom.path_cover_small || fallbackCoverImage
|
||||
: rom.igdb_url_cover ||
|
||||
rom.moby_url_cover ||
|
||||
rom.ss_url_cover ||
|
||||
fallbackCoverImage)
|
||||
"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
@@ -146,10 +148,13 @@ const fallbackCoverImage = computed(() =>
|
||||
<div
|
||||
v-if="
|
||||
isHovering ||
|
||||
(romsStore.isSimpleRom(rom) && rom.is_unidentified) ||
|
||||
(romsStore.isSimpleRom(rom) &&
|
||||
rom.is_unidentified &&
|
||||
!rom.path_cover_large) ||
|
||||
(!romsStore.isSimpleRom(rom) &&
|
||||
!rom.igdb_url_cover &&
|
||||
!rom.moby_url_cover)
|
||||
!rom.moby_url_cover &&
|
||||
!rom.ss_url_cover)
|
||||
"
|
||||
class="translucent-dark text-caption text-white"
|
||||
>
|
||||
|
||||
@@ -14,7 +14,7 @@ defineProps<{ rom: SearchRomSchema }>();
|
||||
open-delay="500"
|
||||
><template #activator="{ props }">
|
||||
<v-avatar v-bind="props" v-if="rom.igdb_id" size="30" rounded="1">
|
||||
<v-img src="/assets/scrappers/igdb.png" /> </v-avatar></template
|
||||
<v-img src="/assets/scrappers/igdb.png" /></v-avatar></template
|
||||
></v-tooltip>
|
||||
<v-tooltip
|
||||
location="top"
|
||||
@@ -30,7 +30,23 @@ defineProps<{ rom: SearchRomSchema }>();
|
||||
size="30"
|
||||
rounded="1"
|
||||
>
|
||||
<v-img src="/assets/scrappers/moby.png" /> </v-avatar></template
|
||||
<v-img src="/assets/scrappers/moby.png" /></v-avatar></template
|
||||
></v-tooltip>
|
||||
<v-tooltip
|
||||
location="top"
|
||||
class="tooltip"
|
||||
transition="fade-transition"
|
||||
text="Screenscraper matched"
|
||||
open-delay="500"
|
||||
><template #activator="{ props }">
|
||||
<v-avatar
|
||||
v-bind="props"
|
||||
v-if="rom.ss_id"
|
||||
class="ml-1"
|
||||
size="30"
|
||||
rounded="1"
|
||||
>
|
||||
<v-img src="/assets/scrappers/ss.png" /></v-avatar></template
|
||||
></v-tooltip>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@ import storeGalleryView from "@/stores/galleryView";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import storePlatforms from "@/stores/platforms";
|
||||
import storeRoms, { type SimpleRom } from "@/stores/roms";
|
||||
import storeUpload from "@/stores/upload";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { computed, inject, ref } from "vue";
|
||||
@@ -24,8 +25,10 @@ const rom = ref<UpdateRom>();
|
||||
const romsStore = storeRoms();
|
||||
const imagePreviewUrl = ref<string | undefined>("");
|
||||
const removeCover = ref(false);
|
||||
const manualFiles = ref<File[]>([]);
|
||||
const platfotmsStore = storePlatforms();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const uploadStore = storeUpload();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
emitter?.on("showEditRomDialog", (romToEdit: UpdateRom | undefined) => {
|
||||
show.value = true;
|
||||
@@ -78,7 +81,7 @@ async function removeArtwork() {
|
||||
}
|
||||
|
||||
const noMetadataMatch = computed(() => {
|
||||
return !rom.value?.igdb_id && !rom.value?.moby_id && !rom.value?.sgdb_id;
|
||||
return !rom.value?.igdb_id && !rom.value?.moby_id && !rom.value?.ss_id;
|
||||
});
|
||||
|
||||
async function handleRomUpdate(
|
||||
@@ -120,6 +123,51 @@ async function handleRomUpdate(
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadManuals() {
|
||||
if (!rom.value) return;
|
||||
|
||||
await romApi
|
||||
.uploadManuals({
|
||||
romId: rom.value.id,
|
||||
filesToUpload: manualFiles.value,
|
||||
})
|
||||
.then((responses: PromiseSettledResult<unknown>[]) => {
|
||||
const successfulUploads = responses.filter(
|
||||
(d) => d.status == "fulfilled",
|
||||
);
|
||||
const failedUploads = responses.filter((d) => d.status == "rejected");
|
||||
|
||||
if (failedUploads.length == 0) {
|
||||
uploadStore.reset();
|
||||
}
|
||||
|
||||
if (successfulUploads.length == 0) {
|
||||
return emitter?.emit("snackbarShow", {
|
||||
msg: `All manuals skipped, nothing to upload.`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "orange",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `${successfulUploads.length} manuals uploaded successfully (and ${failedUploads.length} skipped/failed).`,
|
||||
icon: "mdi-check-bold",
|
||||
color: "green",
|
||||
timeout: 3000,
|
||||
});
|
||||
})
|
||||
.catch(({ response, message }) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Unable to upload manuals: ${response?.data?.detail || response?.statusText || message}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
timeout: 4000,
|
||||
});
|
||||
});
|
||||
manualFiles.value = [];
|
||||
}
|
||||
|
||||
async function unmatchRom() {
|
||||
if (!rom.value) return;
|
||||
await handleRomUpdate(
|
||||
@@ -213,6 +261,54 @@ function closeDialog() {
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="px-2 mt-2" no-gutters>
|
||||
<v-col>
|
||||
<v-chip
|
||||
:variant="rom.has_manual ? 'flat' : 'tonal'"
|
||||
label
|
||||
size="large"
|
||||
class="pr-0 bg-toplayer"
|
||||
>
|
||||
<span
|
||||
:class="{
|
||||
'text-romm-red': !rom.has_manual,
|
||||
'text-romm-green': rom.has_manual,
|
||||
}"
|
||||
>{{ t("rom.manual")
|
||||
}}<v-icon class="ml-1">{{
|
||||
rom.has_manual ? "mdi-check" : "mdi-close"
|
||||
}}</v-icon></span
|
||||
>
|
||||
<v-btn
|
||||
@click="triggerFileInput"
|
||||
class="bg-toplayer ml-3"
|
||||
icon="mdi-upload"
|
||||
rounded="0"
|
||||
size="small"
|
||||
>
|
||||
<v-icon size="large">mdi-upload</v-icon>
|
||||
<v-file-input
|
||||
id="file-input"
|
||||
v-model="manualFiles"
|
||||
accept="application/pdf"
|
||||
hide-details
|
||||
multiple
|
||||
required
|
||||
class="file-input"
|
||||
@change="uploadManuals"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-chip>
|
||||
<div v-if="rom.has_manual" class="mt-1">
|
||||
<v-label class="text-caption text-wrap">
|
||||
<v-icon size="small" class="mr-2 text-primary">
|
||||
mdi-folder-file-outline
|
||||
</v-icon>
|
||||
<span> /romm/resources/{{ rom.path_manual }} </span>
|
||||
</v-label>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" xl="3">
|
||||
<v-row
|
||||
|
||||
@@ -6,6 +6,7 @@ import romApi from "@/services/api/rom";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import storeRoms, { type SimpleRom } from "@/stores/roms";
|
||||
import storePlatforms from "@/stores/platforms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { computed, inject, onBeforeUnmount, ref } from "vue";
|
||||
@@ -16,7 +17,7 @@ import { getMissingCoverImage } from "@/utils/covers";
|
||||
|
||||
type MatchedSource = {
|
||||
url_cover: string | undefined;
|
||||
name: "IGDB" | "Mobygames";
|
||||
name: "IGDB" | "Mobygames" | "Screenscraper";
|
||||
logo_path: string;
|
||||
};
|
||||
|
||||
@@ -27,6 +28,7 @@ const show = ref(false);
|
||||
const rom = ref<SimpleRom | null>(null);
|
||||
const romsStore = storeRoms();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const platfotmsStore = storePlatforms();
|
||||
const searching = ref(false);
|
||||
const route = useRoute();
|
||||
const searchTerm = ref("");
|
||||
@@ -42,9 +44,17 @@ const sources = ref<MatchedSource[]>([]);
|
||||
const heartbeat = storeHeartbeat();
|
||||
const isIGDBFiltered = ref(true);
|
||||
const isMobyFiltered = ref(true);
|
||||
const isSSFiltered = ref(true);
|
||||
const computedAspectRatio = computed(() => {
|
||||
const ratio =
|
||||
platfotmsStore.getAspectRatio(rom.value?.platform_id ?? -1) ||
|
||||
galleryViewStore.defaultAspectRatioCover;
|
||||
return parseFloat(ratio.toString());
|
||||
});
|
||||
emitter?.on("showMatchRomDialog", (romToSearch) => {
|
||||
rom.value = romToSearch;
|
||||
show.value = true;
|
||||
matchedRoms.value = [];
|
||||
|
||||
// Use name as search term, only when it's matched
|
||||
// Otherwise use the filename without tags and extensions
|
||||
@@ -70,11 +80,17 @@ function toggleSourceFilter(source: MatchedSource["name"]) {
|
||||
heartbeat.value.METADATA_SOURCES.MOBY_API_ENABLED
|
||||
) {
|
||||
isMobyFiltered.value = !isMobyFiltered.value;
|
||||
} else if (
|
||||
source == "Screenscraper" &&
|
||||
heartbeat.value.METADATA_SOURCES.SS_API_ENABLED
|
||||
) {
|
||||
isSSFiltered.value = !isSSFiltered.value;
|
||||
}
|
||||
filteredMatchedRoms.value = matchedRoms.value.filter((rom) => {
|
||||
if (
|
||||
(rom.igdb_id && isIGDBFiltered.value) ||
|
||||
(rom.moby_id && isMobyFiltered.value)
|
||||
(rom.moby_id && isMobyFiltered.value) ||
|
||||
(rom.ss_id && isSSFiltered.value)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -103,7 +119,8 @@ async function searchRom() {
|
||||
filteredMatchedRoms.value = matchedRoms.value.filter((rom) => {
|
||||
if (
|
||||
(rom.igdb_id && isIGDBFiltered.value) ||
|
||||
(rom.moby_id && isMobyFiltered.value)
|
||||
(rom.moby_id && isMobyFiltered.value) ||
|
||||
(rom.ss_id && isSSFiltered.value)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -131,16 +148,31 @@ function showSources(matchedRom: SearchRomSchema) {
|
||||
}
|
||||
showSelectSource.value = true;
|
||||
selectedMatchRom.value = matchedRom;
|
||||
sources.value.push({
|
||||
url_cover: matchedRom.igdb_url_cover,
|
||||
name: "IGDB",
|
||||
logo_path: "/assets/scrappers/igdb.png",
|
||||
});
|
||||
sources.value.push({
|
||||
url_cover: matchedRom.moby_url_cover,
|
||||
name: "Mobygames",
|
||||
logo_path: "/assets/scrappers/moby.png",
|
||||
});
|
||||
sources.value = [];
|
||||
if (matchedRom.igdb_url_cover || matchedRom.igdb_id) {
|
||||
sources.value.push({
|
||||
url_cover: matchedRom.igdb_url_cover,
|
||||
name: "IGDB",
|
||||
logo_path: "/assets/scrappers/igdb.png",
|
||||
});
|
||||
}
|
||||
if (matchedRom.moby_url_cover || matchedRom.moby_id) {
|
||||
sources.value.push({
|
||||
url_cover: matchedRom.moby_url_cover,
|
||||
name: "Mobygames",
|
||||
logo_path: "/assets/scrappers/moby.png",
|
||||
});
|
||||
}
|
||||
if (matchedRom.ss_url_cover || matchedRom.ss_id) {
|
||||
sources.value.push({
|
||||
url_cover: matchedRom.ss_url_cover,
|
||||
name: "Screenscraper",
|
||||
logo_path: "/assets/scrappers/ss.png",
|
||||
});
|
||||
}
|
||||
if (sources.value.length == 1) {
|
||||
selectedCover.value = sources.value[0];
|
||||
}
|
||||
}
|
||||
|
||||
function selectCover(source: MatchedSource) {
|
||||
@@ -213,7 +245,6 @@ function closeDialog() {
|
||||
selectedCover.value = undefined;
|
||||
selectedMatchRom.value = undefined;
|
||||
renameAsSource.value = false;
|
||||
matchedRoms.value = [];
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -291,6 +322,34 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<v-img src="/assets/scrappers/moby.png" /></v-avatar></template
|
||||
></v-tooltip>
|
||||
<v-tooltip
|
||||
location="top"
|
||||
class="tooltip"
|
||||
transition="fade-transition"
|
||||
:text="
|
||||
heartbeat.value.METADATA_SOURCES.SS_API_ENABLED
|
||||
? 'Filter Screenscraper matches'
|
||||
: 'Screenscraper source is not enabled'
|
||||
"
|
||||
open-delay="500"
|
||||
><template #activator="{ props }">
|
||||
<v-avatar
|
||||
@click="toggleSourceFilter('Screenscraper')"
|
||||
v-bind="props"
|
||||
class="ml-3 cursor-pointer opacity-40"
|
||||
:class="{
|
||||
'opacity-100':
|
||||
isSSFiltered && heartbeat.value.METADATA_SOURCES.SS_API_ENABLED,
|
||||
'cursor-not-allowed':
|
||||
!heartbeat.value.METADATA_SOURCES.SS_API_ENABLED,
|
||||
}"
|
||||
size="30"
|
||||
rounded="1"
|
||||
>
|
||||
<v-img src="/assets/scrappers/ss.png" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template #toolbar>
|
||||
<v-row class="align-center" no-gutters>
|
||||
@@ -324,6 +383,7 @@ onBeforeUnmount(() => {
|
||||
@click="searchRom()"
|
||||
class="bg-toplayer"
|
||||
variant="text"
|
||||
rounded="0"
|
||||
icon="mdi-search-web"
|
||||
block
|
||||
:disabled="searching"
|
||||
@@ -371,7 +431,7 @@ onBeforeUnmount(() => {
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-col v-if="sources.length > 1" cols="12">
|
||||
<v-row no-gutters class="mt-4 justify-center text-center">
|
||||
<v-col>
|
||||
<span class="text-body-1">{{
|
||||
@@ -397,7 +457,7 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<v-img
|
||||
:src="source.url_cover || missingCoverImage"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCover"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
cover
|
||||
lazy
|
||||
>
|
||||
|
||||
@@ -76,6 +76,7 @@ function clear() {
|
||||
<template
|
||||
v-if="showVirtualCollections && filteredVirtualCollections.length > 0"
|
||||
>
|
||||
<v-divider class="my-4 mx-4" />
|
||||
<v-list-subheader class="uppercase">{{
|
||||
t("common.virtual-collections").toUpperCase()
|
||||
}}</v-list-subheader>
|
||||
|
||||
@@ -57,7 +57,6 @@ async function logout() {
|
||||
'mx-2': smAndDown || activeSettingsDrawer,
|
||||
'my-2': !smAndDown || activeSettingsDrawer,
|
||||
'drawer-mobile': smAndDown,
|
||||
'drawer-desktop': !smAndDown,
|
||||
}"
|
||||
class="bg-surface pa-1"
|
||||
style="height: unset"
|
||||
@@ -115,26 +114,15 @@ async function logout() {
|
||||
append-icon="mdi-security"
|
||||
>{{ t("common.administration") }}
|
||||
</v-list-item>
|
||||
<template v-if="smAndDown && scopes.includes('me.write')">
|
||||
<v-list-item
|
||||
@click="logout"
|
||||
append-icon="mdi-location-exit"
|
||||
rounded
|
||||
class="bg-toplayer border-sm text-romm-red border-romm-red mt-1"
|
||||
>{{ t("common.logout") }}</v-list-item
|
||||
>
|
||||
</template>
|
||||
</v-list>
|
||||
<template v-if="!smAndDown && scopes.includes('me.write')" #append>
|
||||
<v-list class="pa-0">
|
||||
<v-list-item
|
||||
@click="logout"
|
||||
append-icon="mdi-location-exit"
|
||||
rounded
|
||||
class="bg-toplayer border-sm text-romm-red border-romm-red"
|
||||
>{{ t("common.logout") }}</v-list-item
|
||||
>
|
||||
</v-list>
|
||||
<template v-if="scopes.includes('me.write')" #append>
|
||||
<v-btn
|
||||
@click="logout"
|
||||
append-icon="mdi-location-exit"
|
||||
block
|
||||
class="bg-toplayer text-romm-red"
|
||||
>{{ t("common.logout") }}</v-btn
|
||||
>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
@@ -30,12 +30,12 @@ watch(files, (newList) => {
|
||||
absolute
|
||||
:location="xs ? 'bottom' : 'bottom right'"
|
||||
class="mb-4 mr-4"
|
||||
color="tooltip"
|
||||
color="toplayer"
|
||||
>
|
||||
<v-list>
|
||||
<v-list class="bg-toplayer pa-0">
|
||||
<v-list-item
|
||||
v-for="file in files"
|
||||
class="py-2 px-4"
|
||||
class="py-2 px-4 bg-toplayer"
|
||||
:disabled="file.finished && !file.failed"
|
||||
>
|
||||
<template v-if="file.failed">
|
||||
@@ -43,7 +43,7 @@ watch(files, (newList) => {
|
||||
{{ file.filename }}
|
||||
<v-icon :icon="`mdi-close`" :color="`red`" class="mx-2" />
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-red mt-1">
|
||||
<v-list-item-subtitle v-if="file.failureReason" class="text-red mt-1">
|
||||
{{ file.failureReason }}
|
||||
</v-list-item-subtitle>
|
||||
</template>
|
||||
@@ -82,12 +82,12 @@ watch(files, (newList) => {
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div class="text-center">
|
||||
<div class="bg-surface text-center">
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
class="my-2"
|
||||
color="primary"
|
||||
variant="text"
|
||||
:disabled="!files.some((f) => f.finished || f.failed)"
|
||||
@click="clearFinished"
|
||||
>
|
||||
|
||||
@@ -30,6 +30,7 @@ const heartbeatStore = storeHeartbeat();
|
||||
#container {
|
||||
background-image: url("/assets/auth_background.svg");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
max-width: 100vw;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"copy-link": "Download-Link kopieren",
|
||||
"cant-copy-link": "Link kann nicht in Zwischenablage kopiert werden. Bitte manuell kopieren.",
|
||||
"details": "Details",
|
||||
"manual": "Benutzerhandbuch",
|
||||
"personal": "Persönlich",
|
||||
"version": "Version",
|
||||
"default": "Standard",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"copy-link": "Copy download link",
|
||||
"cant-copy-link": "Can't copy link to clipboard, copy it manually",
|
||||
"details": "Details",
|
||||
"manual": "User manual",
|
||||
"personal": "Personal",
|
||||
"version": "Version",
|
||||
"default": "Default",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"copy-link": "Copy download link",
|
||||
"cant-copy-link": "Can't copy link to clipboard, copy it manually",
|
||||
"details": "Details",
|
||||
"manual": "User manual",
|
||||
"personal": "Personal",
|
||||
"version": "Version",
|
||||
"default": "Default",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"copy-link": "Copiar link de descarga",
|
||||
"cant-copy-link": "No se pudo copiar el link al portapapeles, copialo manualmente",
|
||||
"details": "Detalles",
|
||||
"manual": "Manual de usuario",
|
||||
"personal": "Personal",
|
||||
"version": "Versión",
|
||||
"default": "Por defecto",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"copy-link": "Copier le lien de téléchargement",
|
||||
"cant-copy-link": "Impossible de copier le lien dans le presse-papiers, copiez-le manuellement",
|
||||
"details": "Détails",
|
||||
"manual": "Manuel utilisateur",
|
||||
"personal": "Personnel",
|
||||
"version": "Version",
|
||||
"default": "Par défaut",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"copy-link": "다운로드 링크 복사",
|
||||
"cant-copy-link": "링크를 클립보드에 복사할 수 없습니다. 수동으로 복사합니다",
|
||||
"details": "자세히",
|
||||
"manual": "사용자 설명서",
|
||||
"personal": "개인",
|
||||
"version": "버전",
|
||||
"default": "기본으로",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"copy-link": "Copiar link de download",
|
||||
"cant-copy-link": "Não é possível copiar o link para a área de transferência, copie manualmente",
|
||||
"details": "Detalhes",
|
||||
"manual": "Manual do usuário",
|
||||
"personal": "Pessoal",
|
||||
"version": "Versão",
|
||||
"default": "Padrão",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"copy-link": "Скопировать ссылку для скачивания",
|
||||
"cant-copy-link": "Не удается скопировать ссылку в буфер обмена, скопируйте ее вручную",
|
||||
"details": "Детали",
|
||||
"manual": "Руководство пользователя",
|
||||
"personal": "Личное",
|
||||
"version": "Версия",
|
||||
"default": "По умолчанию",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"copy-link": "复制下载链接",
|
||||
"cant-copy-link": "无法将链接复制到剪贴板,请手动复制",
|
||||
"details": "详情",
|
||||
"manual": "用户手册",
|
||||
"personal": "私人",
|
||||
"version": "版本",
|
||||
"default": "默认",
|
||||
|
||||
@@ -155,6 +155,7 @@ async function updateRom({
|
||||
const formData = new FormData();
|
||||
if (rom.igdb_id) formData.append("igdb_id", rom.igdb_id.toString());
|
||||
if (rom.moby_id) formData.append("moby_id", rom.moby_id.toString());
|
||||
if (rom.ss_id) formData.append("ss_id", rom.ss_id.toString());
|
||||
formData.append("name", rom.name || "");
|
||||
formData.append("fs_name", rom.fs_name);
|
||||
formData.append("summary", rom.summary || "");
|
||||
@@ -170,17 +171,44 @@ async function updateRom({
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteRoms({
|
||||
roms,
|
||||
deleteFromFs = [],
|
||||
async function uploadManuals({
|
||||
romId,
|
||||
filesToUpload,
|
||||
}: {
|
||||
roms: SimpleRom[];
|
||||
deleteFromFs: number[];
|
||||
}): Promise<{ data: MessageResponse }> {
|
||||
return api.post("/roms/delete", {
|
||||
roms: roms.map((r) => r.id),
|
||||
delete_from_fs: deleteFromFs,
|
||||
romId: number;
|
||||
filesToUpload: File[];
|
||||
}): Promise<PromiseSettledResult<unknown>[]> {
|
||||
const heartbeat = storeHeartbeat();
|
||||
const uploadStore = storeUpload();
|
||||
|
||||
console.log(filesToUpload);
|
||||
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/${romId}/manuals`, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
"X-Upload-Filename": file.name,
|
||||
},
|
||||
timeout: heartbeat.value.FRONTEND.UPLOAD_TIMEOUT * 1000,
|
||||
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 updateUserRomProps({
|
||||
@@ -201,6 +229,19 @@ async function updateUserRomProps({
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteRoms({
|
||||
roms,
|
||||
deleteFromFs = [],
|
||||
}: {
|
||||
roms: SimpleRom[];
|
||||
deleteFromFs: number[];
|
||||
}): Promise<{ data: MessageResponse }> {
|
||||
return api.post("/roms/delete", {
|
||||
roms: roms.map((r) => r.id),
|
||||
delete_from_fs: deleteFromFs,
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
uploadRoms,
|
||||
getRoms,
|
||||
@@ -210,6 +251,7 @@ export default {
|
||||
downloadRom,
|
||||
searchRom,
|
||||
updateRom,
|
||||
deleteRoms,
|
||||
uploadManuals,
|
||||
updateUserRomProps,
|
||||
deleteRoms,
|
||||
};
|
||||
|
||||
@@ -25,6 +25,11 @@ export default defineStore("heartbeat", {
|
||||
value: "moby",
|
||||
disabled: !this.value.METADATA_SOURCES?.MOBY_API_ENABLED,
|
||||
},
|
||||
{
|
||||
name: "Screenscraper",
|
||||
value: "ss",
|
||||
disabled: !this.value.METADATA_SOURCES?.SS_API_ENABLED,
|
||||
},
|
||||
]).value.filter((s) => !s.disabled);
|
||||
},
|
||||
reset() {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkC3kaSTbQWt4N.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F,
|
||||
U+FE2E-FE2F;
|
||||
unicode-range:
|
||||
U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
@@ -41,8 +41,8 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBXkaSTbQWt4N.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1,
|
||||
U+03A3-03FF;
|
||||
unicode-range:
|
||||
U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* math */
|
||||
@font-face {
|
||||
@@ -53,9 +53,10 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkenkaSTbQWt4N.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315,
|
||||
U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A,
|
||||
U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6,
|
||||
unicode-range:
|
||||
U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A,
|
||||
U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346,
|
||||
U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6,
|
||||
U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043,
|
||||
U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C,
|
||||
U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121,
|
||||
@@ -75,20 +76,21 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkaHkaSTbQWt4N.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0,
|
||||
U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0,
|
||||
U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A,
|
||||
U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB,
|
||||
U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C,
|
||||
U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3,
|
||||
U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F,
|
||||
U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D,
|
||||
U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2,
|
||||
U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3,
|
||||
U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442,
|
||||
U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3,
|
||||
U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6,
|
||||
U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB,
|
||||
unicode-range:
|
||||
U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4,
|
||||
U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3,
|
||||
U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF,
|
||||
U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF,
|
||||
U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0,
|
||||
U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F,
|
||||
U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315,
|
||||
U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382,
|
||||
U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6,
|
||||
U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7,
|
||||
U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444,
|
||||
U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0,
|
||||
U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA,
|
||||
U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB,
|
||||
U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513,
|
||||
U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D,
|
||||
U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC,
|
||||
@@ -108,9 +110,10 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCXkaSTbQWt4N.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169,
|
||||
U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323,
|
||||
U+0329, U+1EA0-1EF9, U+20AB;
|
||||
unicode-range:
|
||||
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
|
||||
U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
|
||||
U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
@@ -121,9 +124,10 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkCHkaSTbQWt4N.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
|
||||
U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
|
||||
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
unicode-range:
|
||||
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
|
||||
U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
|
||||
U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
@@ -134,9 +138,10 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO5CnqEu92Fr1Mu53ZEC9_Vu3r1gIhOszmkBnkaSTbQWg.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
|
||||
U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
@@ -147,8 +152,8 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3GUBHMdazTgWw.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F,
|
||||
U+FE2E-FE2F;
|
||||
unicode-range:
|
||||
U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
@@ -181,8 +186,8 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3-UBHMdazTgWw.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1,
|
||||
U+03A3-03FF;
|
||||
unicode-range:
|
||||
U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* math */
|
||||
@font-face {
|
||||
@@ -193,9 +198,10 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMawCUBHMdazTgWw.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315,
|
||||
U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A,
|
||||
U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6,
|
||||
unicode-range:
|
||||
U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A,
|
||||
U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346,
|
||||
U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6,
|
||||
U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043,
|
||||
U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C,
|
||||
U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121,
|
||||
@@ -215,20 +221,21 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMaxKUBHMdazTgWw.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0,
|
||||
U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0,
|
||||
U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A,
|
||||
U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB,
|
||||
U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C,
|
||||
U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3,
|
||||
U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F,
|
||||
U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D,
|
||||
U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2,
|
||||
U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3,
|
||||
U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442,
|
||||
U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3,
|
||||
U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6,
|
||||
U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB,
|
||||
unicode-range:
|
||||
U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4,
|
||||
U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3,
|
||||
U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF,
|
||||
U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF,
|
||||
U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0,
|
||||
U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F,
|
||||
U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315,
|
||||
U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382,
|
||||
U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6,
|
||||
U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7,
|
||||
U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444,
|
||||
U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0,
|
||||
U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA,
|
||||
U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB,
|
||||
U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513,
|
||||
U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D,
|
||||
U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC,
|
||||
@@ -248,9 +255,10 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3OUBHMdazTgWw.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169,
|
||||
U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323,
|
||||
U+0329, U+1EA0-1EF9, U+20AB;
|
||||
unicode-range:
|
||||
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
|
||||
U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
|
||||
U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
@@ -261,9 +269,10 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBHMdazTgWw.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
|
||||
U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
|
||||
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
unicode-range:
|
||||
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
|
||||
U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB,
|
||||
U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
@@ -274,7 +283,8 @@
|
||||
font-display: swap;
|
||||
src: url(/assets/fonts/roboto/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBHMdazQ.woff2)
|
||||
format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212,
|
||||
U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@ const metadataOptions = computed(() => [
|
||||
logo_path: "/assets/scrappers/moby.png",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.MOBY_API_ENABLED,
|
||||
},
|
||||
{
|
||||
name: "ScreenScrapper",
|
||||
value: "ss",
|
||||
logo_path: "/assets/scrappers/ss.png",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.SS_API_ENABLED,
|
||||
},
|
||||
{
|
||||
name: "SteamgridDB",
|
||||
value: "sgdb",
|
||||
|
||||
@@ -5,6 +5,7 @@ import BackgroundHeader from "@/components/Details/BackgroundHeader.vue";
|
||||
import FileInfo from "@/components/Details/Info/FileInfo.vue";
|
||||
import GameInfo from "@/components/Details/Info/GameInfo.vue";
|
||||
import Personal from "@/components/Details/Personal.vue";
|
||||
import PdfViewer from "@/components/Details/PDFViewer.vue";
|
||||
import RelatedGames from "@/components/Details/RelatedGames.vue";
|
||||
import Saves from "@/components/Details/Saves.vue";
|
||||
import States from "@/components/Details/States.vue";
|
||||
@@ -27,6 +28,7 @@ const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const tab = ref<
|
||||
| "details"
|
||||
| "manual"
|
||||
| "saves"
|
||||
| "states"
|
||||
| "personal"
|
||||
@@ -40,6 +42,7 @@ const noRomError = ref(false);
|
||||
const romsStore = storeRoms();
|
||||
const { currentRom, gettingRoms } = storeToRefs(romsStore);
|
||||
|
||||
// Functions
|
||||
async function fetchDetails() {
|
||||
gettingRoms.value = true;
|
||||
await romApi
|
||||
@@ -115,6 +118,9 @@ watch(
|
||||
:class="{ 'mt-4': smAndDown }"
|
||||
>
|
||||
<v-tab value="details"> {{ t("rom.details") }} </v-tab>
|
||||
<v-tab value="manual" v-if="currentRom.has_manual">
|
||||
{{ t("rom.manual") }}
|
||||
</v-tab>
|
||||
<v-tab value="saves"> {{ t("common.saves") }} </v-tab>
|
||||
<v-tab value="states"> {{ t("common.states") }} </v-tab>
|
||||
<v-tab value="personal">
|
||||
@@ -152,6 +158,9 @@ watch(
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="manual">
|
||||
<pdf-viewer v-if="currentRom.has_manual" :rom="currentRom" />
|
||||
</v-window-item>
|
||||
<v-window-item value="saves">
|
||||
<saves :rom="currentRom" />
|
||||
</v-window-item>
|
||||
|
||||
@@ -30,11 +30,17 @@ const metadataOptions = computed(() => [
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.IGDB_API_ENABLED,
|
||||
},
|
||||
{
|
||||
name: "MobyGames",
|
||||
name: "Mobygames",
|
||||
value: "moby",
|
||||
logo_path: "/assets/scrappers/moby.png",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.MOBY_API_ENABLED,
|
||||
},
|
||||
{
|
||||
name: "Screenscraper",
|
||||
value: "ss",
|
||||
logo_path: "/assets/scrappers/ss.png",
|
||||
disabled: !heartbeat.value.METADATA_SOURCES?.SS_API_ENABLED,
|
||||
},
|
||||
]);
|
||||
// Use the computed metadataOptions to filter out disabled sources
|
||||
const metadataSources = ref(metadataOptions.value.filter((s) => !s.disabled));
|
||||
@@ -199,11 +205,7 @@ async function stopScan() {
|
||||
<template #chip="{ item }">
|
||||
<v-chip>
|
||||
<v-avatar class="mr-2" size="15" rounded="1">
|
||||
<v-img
|
||||
:src="`/assets/scrappers/${item.raw.name
|
||||
.slice(0, 4)
|
||||
.toLowerCase()}.png`"
|
||||
/>
|
||||
<v-img :src="item.raw.logo_path" />
|
||||
</v-avatar>
|
||||
{{ item.raw.name }}
|
||||
</v-chip>
|
||||
@@ -296,76 +298,85 @@ async function stopScan() {
|
||||
/>
|
||||
|
||||
<!-- Scan log -->
|
||||
<v-card elevation="0" class="bg-surface mx-auto mt-2 mb-14" max-width="800">
|
||||
<v-card-text class="pa-0">
|
||||
<v-expansion-panels v-model="panels" multiple flat variant="accordion">
|
||||
<v-expansion-panel
|
||||
v-for="platform in scanningPlatforms"
|
||||
:key="platform.id"
|
||||
>
|
||||
<v-expansion-panel-title>
|
||||
<v-list-item class="pa-0">
|
||||
<template #prepend>
|
||||
<v-avatar size="40">
|
||||
<platform-icon
|
||||
:key="platform.slug"
|
||||
:slug="platform.slug"
|
||||
:name="platform.name"
|
||||
:fs-slug="platform.fs_slug"
|
||||
/>
|
||||
</v-avatar>
|
||||
</template>
|
||||
{{ platform.name }}
|
||||
<template #append>
|
||||
<v-chip class="ml-3" color="primary" size="x-small" label>{{
|
||||
platform.roms.length
|
||||
}}</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text class="bg-toplayer">
|
||||
<rom-list-item
|
||||
v-for="rom in platform.roms"
|
||||
class="pa-4"
|
||||
:rom="rom"
|
||||
with-link
|
||||
with-filename
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-card
|
||||
elevation="0"
|
||||
class="bg-surface mx-auto mt-2 mb-14"
|
||||
max-width="800"
|
||||
>
|
||||
<v-card-text class="pa-0">
|
||||
<v-expansion-panels
|
||||
v-model="panels"
|
||||
multiple
|
||||
flat
|
||||
variant="accordion"
|
||||
>
|
||||
<v-expansion-panel
|
||||
v-for="platform in scanningPlatforms"
|
||||
:key="platform.id"
|
||||
>
|
||||
<template #append-body>
|
||||
<v-chip
|
||||
v-if="!rom.igdb_id && !rom.moby_id"
|
||||
color="red"
|
||||
size="x-small"
|
||||
label
|
||||
>Not identified<v-icon class="ml-1">mdi-close</v-icon></v-chip
|
||||
<v-expansion-panel-title>
|
||||
<v-list-item class="pa-0">
|
||||
<template #prepend>
|
||||
<v-avatar rounded="0" size="40">
|
||||
<platform-icon
|
||||
:key="platform.slug"
|
||||
:slug="platform.slug"
|
||||
:name="platform.name"
|
||||
/>
|
||||
</v-avatar>
|
||||
</template>
|
||||
{{ platform.name }}
|
||||
<template #append>
|
||||
<v-chip class="ml-3" color="primary" size="x-small" label>{{
|
||||
platform.roms.length
|
||||
}}</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text class="bg-toplayer">
|
||||
<rom-list-item
|
||||
v-for="rom in platform.roms"
|
||||
class="pa-4"
|
||||
:rom="rom"
|
||||
with-link
|
||||
with-filename
|
||||
>
|
||||
</template>
|
||||
</rom-list-item>
|
||||
<v-list-item
|
||||
v-if="platform.roms.length == 0"
|
||||
class="text-center my-2"
|
||||
>
|
||||
{{ t("scan.no-new-roms") }}
|
||||
</v-list-item>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<template #append-body>
|
||||
<v-chip
|
||||
v-if="!rom.igdb_id && !rom.moby_id && !rom.ss_id"
|
||||
color="red"
|
||||
size="x-small"
|
||||
label
|
||||
>Not identified<v-icon class="ml-1"
|
||||
>mdi-close</v-icon
|
||||
></v-chip
|
||||
>
|
||||
</template>
|
||||
</rom-list-item>
|
||||
<v-list-item
|
||||
v-if="platform.roms.length == 0"
|
||||
class="text-center my-2"
|
||||
>
|
||||
{{ t("scan.no-new-roms") }}
|
||||
</v-list-item>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Scan stats -->
|
||||
<div
|
||||
v-if="scanningPlatforms.length > 0"
|
||||
class="text-caption position-fixed d-flex w-100 m-1 justify-center"
|
||||
style="bottom: 0.5rem"
|
||||
>
|
||||
<v-chip variant="outlined" color="toplayer" class="px-2 py-5 bg-background">
|
||||
<v-chip
|
||||
v-if="scanningPlatforms.length > 0"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
size="small"
|
||||
class="mr-1 my-1"
|
||||
>
|
||||
<v-chip color="primary" text-color="white" size="small" class="mr-1 my-1">
|
||||
<v-icon left>mdi-controller</v-icon>
|
||||
<span v-if="xs" class="ml-2">{{
|
||||
t("scan.platforms-scanned-n", scanningPlatforms.length)
|
||||
|
||||
Reference in New Issue
Block a user