Skip to content

Commit 2997f98

Browse files
committed
Merge remote-tracking branch 'origin/master' into feat/backend-disconnect-warning
2 parents a4d31d3 + aa044b7 commit 2997f98

52 files changed

Lines changed: 574 additions & 53 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/pytest.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ jobs:
8080
ROMM_DB_DRIVER: ${{ matrix.db }}
8181
REDIS_HOST: 127.0.0.1
8282
REDIS_PORT: ${{ job.services.valkey.ports['6379'] }}
83+
HYPOTHESIS_PROFILE: ci
8384
run: |
8485
cd backend
8586
uv run pytest -vv --maxfail=10 --junitxml=pytest-report.xml --cov --cov-report xml:coverage.xml --cov-config=.coveragerc .

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ romm_mock
5454
backend/romm_test/resources
5555
backend/romm_test/logs
5656
.pytest_cache
57+
.hypothesis
5758
/romm_test
5859
.coverage
5960
pytest-report.xml

backend/alembic/versions/0084_roms_search_sort_indexes.py renamed to backend/alembic/versions/0084_add_roms_search_index.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
downgrade() drops every object created here in reverse order, leaving the
1818
pg_trgm extension in place since other objects may depend on it.
1919
20-
Revision ID: 0084_roms_search_sort_indexes
20+
Revision ID: 0084_add_roms_search_index
2121
Revises: 0083_rom_category_soundtrack
2222
Create Date: 2026-06-16 00:00:00.000000
2323
@@ -30,7 +30,7 @@
3030
from utils.database import is_mariadb, is_mysql, is_postgresql
3131

3232
# revision identifiers, used by Alembic.
33-
revision = "0084_roms_search_sort_indexes"
33+
revision = "0084_add_roms_search_index"
3434
down_revision = "0083_rom_category_soundtrack"
3535
branch_labels = None
3636
depends_on = None
@@ -87,6 +87,7 @@ def upgrade() -> None:
8787
sa.String(length=NAME_SORT_KEY_MAX_LENGTH),
8888
nullable=True,
8989
),
90+
if_not_exists=True,
9091
)
9192

9293
roms = sa.table(
@@ -111,15 +112,17 @@ def upgrade() -> None:
111112
[{"_id": row.id, "_key": compute_name_sort_key(row.name)} for row in batch],
112113
)
113114

114-
op.create_index("idx_roms_name_sort_key", "roms", ["name_sort_key"])
115+
op.create_index(
116+
"idx_roms_name_sort_key", "roms", ["name_sort_key"], if_not_exists=True
117+
)
115118

116119

117120
def downgrade() -> None:
118121
bind = op.get_bind()
119122

120123
# 3. name_sort_key column and its index.
121-
op.drop_index("idx_roms_name_sort_key", table_name="roms")
122-
op.drop_column("roms", "name_sort_key")
124+
op.drop_index("idx_roms_name_sort_key", table_name="roms", if_exists=True)
125+
op.drop_column("roms", "name_sort_key", if_exists=True)
123126

124127
# 2. Plain index on roms.name.
125128
with op.batch_alter_table("roms", schema=None) as batch_op:

backend/alembic/versions/0085_rom_category_screenshot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Update rom_file.category column enum to include screenshots.
22
33
Revision ID: 0085_rom_category_screenshot
4-
Revises: 0084_roms_search_sort_indexes
4+
Revises: 0084_add_roms_search_index
55
Create Date: 2026-06-17 00:00:00.000000
66
77
"""
@@ -13,7 +13,7 @@
1313

1414
# revision identifiers, used by Alembic.
1515
revision = "0085_rom_category_screenshot"
16-
down_revision = "0084_roms_search_sort_indexes"
16+
down_revision = "0084_add_roms_search_index"
1717
branch_labels = None
1818
depends_on = None
1919

backend/endpoints/responses/rom.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ class RomSchema(BaseModel):
277277
fs_size_bytes: int
278278

279279
name: str | None
280+
name_sort_key: str | None
280281
slug: str | None
281282
summary: str | None
282283

backend/endpoints/roms/__init__.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
from logger.formatter import BLUE
6464
from logger.formatter import highlight as hl
6565
from logger.logger import log
66-
from models.rom import Rom, RomUserStatus
66+
from models.rom import Rom, RomUserStatus, compute_name_sort_key
6767
from utils.background_tasks import fire_and_forget
6868
from utils.database import safe_int, safe_str_to_bool
6969
from utils.filesystem import sanitize_filename
@@ -135,6 +135,7 @@ class RomUpdateForm(BaseModel):
135135
default=None, description="Raw manual metadata as JSON string."
136136
)
137137
name: str | None = None
138+
name_sort_key: str | None = None
138139
summary: str | None = None
139140
fs_name: str | None = None
140141
url_cover: str | None = None
@@ -194,6 +195,7 @@ async def parse_rom_update_form(
194195
raw_hltb_metadata: str | None = Form(default=None),
195196
raw_manual_metadata: str | None = Form(default=None),
196197
name: str | None = Form(default=None),
198+
name_sort_key: str | None = Form(default=None),
197199
summary: str | None = Form(default=None),
198200
fs_name: str | None = Form(default=None),
199201
url_cover: str | None = Form(default=None),
@@ -222,6 +224,7 @@ async def parse_rom_update_form(
222224
"raw_hltb_metadata": raw_hltb_metadata,
223225
"raw_manual_metadata": raw_manual_metadata,
224226
"name": name,
227+
"name_sort_key": name_sort_key,
225228
"summary": summary,
226229
"fs_name": fs_name,
227230
"url_cover": url_cover,
@@ -1207,6 +1210,7 @@ async def update_rom(
12071210
"hltb_id": None,
12081211
"libretro_id": None,
12091212
"name": rom.fs_name,
1213+
"name_sort_key": compute_name_sort_key(rom.fs_name),
12101214
"summary": "",
12111215
"url_screenshots": [],
12121216
"path_screenshots": [],
@@ -1391,15 +1395,27 @@ async def update_rom(
13911395
log.error(f"Invalid screenshot URL in update_rom: {str(e)}")
13921396
raise HTTPException(status_code=400, detail=str(e)) from e
13931397

1398+
name_value = form_data.name if "name" in provided_fields else rom.name
13941399
cleaned_data.update(
13951400
{
1396-
"name": form_data.name if "name" in provided_fields else rom.name,
1401+
"name": name_value,
13971402
"summary": (
13981403
form_data.summary if "summary" in provided_fields else rom.summary
13991404
),
14001405
}
14011406
)
14021407

1408+
if "name_sort_key" in provided_fields:
1409+
# The edit form always echoes the current key back, so only act when the
1410+
# user actually changed it: a value is a custom override, an empty value
1411+
# reverts to deriving from the (possibly new) name. When unchanged, leave
1412+
# it out so update_rom re-derives only if the stored key isn't custom.
1413+
submitted = (form_data.name_sort_key or "").strip()
1414+
if submitted != (rom.name_sort_key or "").strip():
1415+
cleaned_data["name_sort_key"] = compute_name_sort_key(
1416+
submitted or name_value
1417+
)
1418+
14031419
new_fs_name = str(form_data.fs_name or rom.fs_name)
14041420
new_fs_name = sanitize_filename(new_fs_name)
14051421
cleaned_data.update({"fs_name": new_fs_name})

backend/endpoints/roms/screenshot.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,15 @@
1919
from logger.logger import log
2020
from models.rom import RomFile, RomFileCategory
2121
from utils.router import APIRouter
22+
from utils.screenshots import (
23+
ALLOWED_SCREENSHOT_EXTENSIONS,
24+
is_allowed_screenshot_file,
25+
)
2226

2327
router = APIRouter()
2428

2529
SCREENSHOT_FOLDER = "screenshots"
2630

27-
# Image formats every modern browser can decode. Mirrors the frontend's
28-
# screenshot gallery filter (MediaTab IMAGE_EXTENSIONS).
29-
ALLOWED_SCREENSHOT_EXTENSIONS = frozenset(
30-
{".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".avif"}
31-
)
32-
33-
34-
def is_allowed_screenshot_file(file_name: str) -> bool:
35-
_, ext = os.path.splitext(file_name)
36-
return ext.lower() in ALLOWED_SCREENSHOT_EXTENSIONS
37-
3831

3932
@protected_route(
4033
router.post,

backend/endpoints/screenshots.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import os
21
from typing import Annotated
32

43
from fastapi import Body, File, HTTPException
@@ -18,6 +17,10 @@
1817
from logger.logger import log
1918
from utils.filesystem import sanitize_filename
2019
from utils.router import APIRouter
20+
from utils.screenshots import (
21+
ALLOWED_SCREENSHOT_EXTENSIONS,
22+
is_allowed_screenshot_file,
23+
)
2124

2225
router = APIRouter(
2326
prefix="/screenshots",
@@ -26,16 +29,6 @@
2629

2730
SCREENSHOT_FILE_UPLOAD = File(..., description="Screenshot file to upload.")
2831

29-
# Image formats every modern browser can decode.
30-
ALLOWED_SCREENSHOT_EXTENSIONS = frozenset(
31-
{".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".avif"}
32-
)
33-
34-
35-
def _is_allowed_screenshot_file(file_name: str) -> bool:
36-
_, ext = os.path.splitext(file_name)
37-
return ext.lower() in ALLOWED_SCREENSHOT_EXTENSIONS
38-
3932

4033
@protected_route(router.post, "", [Scope.ASSETS_WRITE])
4134
async def add_screenshot(
@@ -75,7 +68,7 @@ async def add_screenshot(
7568
detail=f"Invalid screenshot filename: {str(exc)}",
7669
) from exc
7770

78-
if not _is_allowed_screenshot_file(sanitized_screenshot_filename):
71+
if not is_allowed_screenshot_file(sanitized_screenshot_filename):
7972
raise HTTPException(
8073
status_code=status.HTTP_400_BAD_REQUEST,
8174
detail=(

backend/handler/database/roms_handler.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,7 +1010,8 @@ def get_roms_query(
10101010
order_attr = Rom.name
10111011

10121012
# Use indexed `name_sort_key` to have fast access to names without
1013-
# articles (the, a, an) and leading digits
1013+
# articles (the, a, an) and leading digits. The key is derived from
1014+
# `name` at write time, or holds a custom override when one is set.
10141015
if order_attr is Rom.name:
10151016
order_attr = Rom.name_sort_key
10161017

@@ -1203,10 +1204,16 @@ def update_rom(
12031204
data: dict,
12041205
session: Session = None, # type: ignore
12051206
) -> Rom:
1206-
# Bulk update() bypasses the ORM @validates hooks, so keep the
1207-
# columns derived from name / fs_name in sync explicitly.
1208-
if "name" in data:
1209-
data = {**data, "name_sort_key": compute_name_sort_key(data["name"])}
1207+
if "name" in data and "name_sort_key" not in data:
1208+
# Re-derive the key from the new name, but only when the stored key
1209+
# is still the derived value (i.e. not a manual override). Mirrors
1210+
# the `@validates` logic, which the bulk update() bypasses.
1211+
existing = session.query(Rom).filter_by(id=id).one()
1212+
if (
1213+
existing.name_sort_key is None
1214+
or existing.name_sort_key == compute_name_sort_key(existing.name)
1215+
):
1216+
data = {**data, "name_sort_key": compute_name_sort_key(data["name"])}
12101217

12111218
if "fs_name" in data:
12121219
parts = compute_file_name_parts(data["fs_name"])

backend/handler/metadata/base_handler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252

5353
class BaseRom(TypedDict):
5454
name: NotRequired[str]
55+
name_sort_key: NotRequired[str | None]
5556
summary: NotRequired[str]
5657
url_cover: NotRequired[str]
5758
url_screenshots: NotRequired[list[str]]

0 commit comments

Comments
 (0)