Skip to content

Commit 3d31328

Browse files
committed
Add opt-in files/siblings expansion to GET /api/roms
1 parent e5d48dc commit 3d31328

6 files changed

Lines changed: 133 additions & 13 deletions

File tree

backend/endpoints/responses/rom.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import NotRequired, TypedDict, get_type_hints
66

77
from fastapi import Request
8-
from pydantic import ConfigDict, computed_field, field_validator
8+
from pydantic import ConfigDict, Field, computed_field, field_validator
99

1010
from endpoints.responses.assets import SaveSchema, ScreenshotSchema, StateSchema
1111
from handler.metadata.flashpoint_handler import FlashpointMetadata
@@ -334,16 +334,52 @@ def sort_comparator(self) -> str:
334334
.lower()
335335
)
336336

337+
@classmethod
338+
def from_rom(cls, rom: Rom) -> SiblingRomSchema:
339+
return cls(
340+
id=rom.id,
341+
name=rom.name,
342+
fs_name_no_tags=rom.fs_name_no_tags,
343+
fs_name_no_ext=rom.fs_name_no_ext,
344+
)
345+
337346

338347
class SimpleRomSchema(RomSchema):
339348
sibling_ids: list[int]
349+
# `files` binds to a dedicated attribute via validation alias rather than the
350+
# `Rom.files` relationship: that keeps the full file list out of the default
351+
# gallery payload, and avoids tripping the relationship's lazy="raise" in
352+
# factory contexts where files aren't loaded. Populated only when requested.
353+
files: list[RomFileSchema] = Field(
354+
default_factory=list, validation_alias="included_files"
355+
)
356+
siblings: list[SiblingRomSchema] = Field(default_factory=list)
357+
358+
@field_validator("files")
359+
def sort_files(cls, v: list[RomFileSchema]) -> list[RomFileSchema]:
360+
return sorted(v, key=lambda x: x.file_name)
361+
362+
@field_validator("siblings")
363+
def sort_siblings(cls, v: list[SiblingRomSchema]) -> list[SiblingRomSchema]:
364+
return sorted(v, key=lambda x: x.sort_comparator)
340365

341366
@classmethod
342367
def from_orm_with_request(
343-
cls, db_rom: Rom, request: Request, sibling_ids: list[int] | None = None
368+
cls,
369+
db_rom: Rom,
370+
request: Request,
371+
sibling_ids: list[int] | None = None,
372+
include_files: bool = False,
373+
include_siblings: bool = False,
344374
) -> SimpleRomSchema:
345375
db_rom = cls.populate_properties(db_rom, request)
346376
db_rom.sibling_ids = sibling_ids or [] # type: ignore
377+
db_rom.included_files = db_rom.files if include_files else [] # type: ignore
378+
db_rom.siblings = ( # type: ignore
379+
[SiblingRomSchema.from_rom(s) for s in db_rom.sibling_roms]
380+
if include_siblings
381+
else []
382+
)
347383
return cls.model_validate(db_rom)
348384

349385
@classmethod
@@ -396,15 +432,7 @@ def from_orm_with_request(cls, db_rom: Rom, request: Request) -> DetailedRomSche
396432
db_rom = cls.populate_properties(db_rom, request)
397433

398434
sorted_siblings = sorted(
399-
(
400-
SiblingRomSchema(
401-
id=s.id,
402-
name=s.name,
403-
fs_name_no_tags=s.fs_name_no_tags,
404-
fs_name_no_ext=s.fs_name_no_ext,
405-
)
406-
for s in db_rom.sibling_roms
407-
),
435+
(SiblingRomSchema.from_rom(s) for s in db_rom.sibling_roms),
408436
key=lambda x: x.sort_comparator,
409437
)
410438
db_rom.siblings = sorted_siblings # type: ignore

backend/endpoints/roms/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,14 @@ def get_roms(
482482
description="Filter roms updated after this datetime (ISO 8601 format with timezone information)."
483483
),
484484
] = None,
485+
with_files: Annotated[
486+
bool,
487+
Query(description="Whether to include each rom's file entries."),
488+
] = False,
489+
with_siblings: Annotated[
490+
bool,
491+
Query(description="Whether to include each rom's sibling roms."),
492+
] = False,
485493
) -> CustomLimitOffsetPage[SimpleRomSchema]:
486494
"""Retrieve roms."""
487495
unfiltered_query, order_by_attr = db_rom_handler.get_roms_query(
@@ -580,6 +588,8 @@ def _transform(items: Sequence[Rom]) -> list[SimpleRomSchema]:
580588
db_rom=item,
581589
request=request,
582590
sibling_ids=sibling_ids_by_rom.get(item.id, []),
591+
include_files=with_files,
592+
include_siblings=with_siblings,
583593
)
584594
for item in items
585595
]

backend/endpoints/sockets/scan.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ async def _identify_rom(
361361
"rom_user",
362362
"last_modified",
363363
"files",
364+
"siblings",
364365
}
365366
),
366367
)
@@ -480,7 +481,14 @@ async def _identify_rom(
480481
await socket_manager.emit(
481482
"scan:scanning_rom",
482483
SimpleRomSchema.from_orm_with_factory(_added_rom).model_dump(
483-
exclude={"created_at", "updated_at", "rom_user", "last_modified", "files"}
484+
exclude={
485+
"created_at",
486+
"updated_at",
487+
"rom_user",
488+
"last_modified",
489+
"files",
490+
"siblings",
491+
}
484492
),
485493
)
486494

backend/handler/scan_handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ async def fetch_hasheous_hash_match() -> HasheousRom:
437437
"rom_user",
438438
"last_modified",
439439
"files",
440+
"siblings",
440441
}
441442
),
442443
},

backend/tests/endpoints/roms/test_rom.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from fastapi import status
55
from fastapi.testclient import TestClient
66

7+
from handler.database import db_rom_handler
78
from handler.filesystem.resources_handler import FSResourcesHandler
89
from handler.filesystem.roms_handler import FSRomsHandler
910
from handler.metadata.flashpoint_handler import FlashpointHandler, FlashpointRom
@@ -14,7 +15,8 @@
1415
from handler.metadata.ra_handler import RAGameRom, RAHandler
1516
from handler.metadata.ss_handler import SSHandler, SSRom
1617
from models.platform import Platform
17-
from models.rom import Rom
18+
from models.rom import Rom, RomFile
19+
from models.user import User
1820

1921
MOCK_IGDB_ID = 11111
2022
MOCK_MOBY_ID = 22222
@@ -57,6 +59,73 @@ def test_get_all_roms(
5759
items = body["items"]
5860
assert len(items) == 1
5961
assert items[0]["id"] == rom.id
62+
assert items[0]["files"] == []
63+
assert items[0]["siblings"] == []
64+
65+
66+
def test_get_all_roms_with_files(
67+
client: TestClient, access_token: str, rom: Rom, platform: Platform
68+
):
69+
db_rom_handler.add_rom_file(
70+
RomFile(
71+
rom_id=rom.id,
72+
file_name="test_rom.zip",
73+
file_path=f"{platform.slug}/roms",
74+
file_size_bytes=1024,
75+
last_modified=1700000000.0,
76+
)
77+
)
78+
79+
response = client.get(
80+
"/api/roms",
81+
headers={"Authorization": f"Bearer {access_token}"},
82+
params={"platform_id": platform.id, "with_files": True},
83+
)
84+
assert response.status_code == status.HTTP_200_OK
85+
86+
item = response.json()["items"][0]
87+
assert item["id"] == rom.id
88+
assert len(item["files"]) == 1
89+
assert item["files"][0]["file_name"] == "test_rom.zip"
90+
# with_files alone must not pull in siblings.
91+
assert item["siblings"] == []
92+
93+
94+
def test_get_all_roms_with_siblings(
95+
client: TestClient, access_token: str, platform: Platform, admin_user: User
96+
):
97+
siblings = [
98+
db_rom_handler.add_rom(
99+
Rom(
100+
platform_id=platform.id,
101+
igdb_id=424242,
102+
name=name,
103+
slug=slug,
104+
fs_name=f"{slug}.zip",
105+
fs_name_no_tags=slug,
106+
fs_name_no_ext=slug,
107+
fs_extension="zip",
108+
fs_path=f"{platform.slug}/roms",
109+
)
110+
)
111+
for name, slug in (("Game A", "game_a"), ("Game B", "game_b"))
112+
]
113+
for sibling in siblings:
114+
db_rom_handler.add_rom_user(rom_id=sibling.id, user_id=admin_user.id)
115+
116+
response = client.get(
117+
"/api/roms",
118+
headers={"Authorization": f"Bearer {access_token}"},
119+
params={"platform_id": platform.id, "with_siblings": True},
120+
)
121+
assert response.status_code == status.HTTP_200_OK
122+
123+
items = {item["id"]: item for item in response.json()["items"]}
124+
rom_a, rom_b = siblings
125+
assert [s["id"] for s in items[rom_a.id]["siblings"]] == [rom_b.id]
126+
assert [s["id"] for s in items[rom_b.id]["siblings"]] == [rom_a.id]
127+
# with_siblings alone must not pull in files.
128+
assert items[rom_a.id]["files"] == []
60129

61130

62131
@patch.object(FSRomsHandler, "rename_fs_rom")

frontend/src/__generated__/models/SimpleRomSchema.ts

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)