mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 06:46:00 +00:00
Merge sort_name into name_sort_key with custom-override flag
Collapse the separate `sort_name` column into `name_sort_key`, which is now
the single user-settable sort field: always normalized and indexed for fast
ordering, derived from `name` by default, and overridable. A new
`name_sort_key_custom` boolean marks user/metadata overrides so they survive
renames and rescans.
- Drop the `roms.sort_name` column; repurpose migration 0085 to add
`name_sort_key_custom`.
- Derive the key via `@validates("name")` unless pinned custom; the edit
dialog, unmatch flow, and ES-DE gamelist <sortname> set custom keys.
- update_rom / scan_rom keep the columns in sync explicitly (bulk update and
construction bypass / reorder the validator).
- Frontend: edit field drives name_sort_key (empty when auto), api sends the
override only when custom, regenerated types updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1010,8 +1010,8 @@ class DBRomsHandler(DBBaseHandler):
|
||||
order_attr = Rom.name
|
||||
|
||||
# Use indexed `name_sort_key` to have fast access to names without
|
||||
# articles (the, a, an) and leading digits. The key already folds in
|
||||
# `sort_name` (falling back to `name`) at write time.
|
||||
# articles (the, a, an) and leading digits. The key is derived from
|
||||
# `name` at write time, or holds a custom override when one is set.
|
||||
if order_attr is Rom.name:
|
||||
order_attr = Rom.name_sort_key
|
||||
|
||||
@@ -1206,19 +1206,16 @@ class DBRomsHandler(DBBaseHandler):
|
||||
) -> Rom:
|
||||
# Bulk update() bypasses the ORM @validates hooks, so keep the
|
||||
# columns derived from name / fs_name in sync explicitly.
|
||||
if "name" in data or "sort_name" in data:
|
||||
if "name" in data and "sort_name" in data:
|
||||
effective = data["sort_name"] or data["name"]
|
||||
else:
|
||||
# Only one of the two changed; read the other from the row so
|
||||
# the key reflects the effective `sort_name or name`.
|
||||
existing = session.query(Rom).filter_by(id=id).one()
|
||||
name = data["name"] if "name" in data else existing.name
|
||||
sort_name = (
|
||||
data["sort_name"] if "sort_name" in data else existing.sort_name
|
||||
)
|
||||
effective = sort_name or name
|
||||
data = {**data, "name_sort_key": compute_name_sort_key(effective)}
|
||||
if "name_sort_key" in data:
|
||||
# An explicit sort key was supplied (custom override or a revert to
|
||||
# the derived value). Trust it; mark it custom unless told otherwise.
|
||||
data = {"name_sort_key_custom": True, **data}
|
||||
elif "name" in data:
|
||||
# Name changed without an explicit key: recompute the derived key,
|
||||
# but never clobber a row pinned to a custom sort key.
|
||||
existing = session.query(Rom).filter_by(id=id).one()
|
||||
if not existing.name_sort_key_custom:
|
||||
data = {**data, "name_sort_key": compute_name_sort_key(data["name"])}
|
||||
|
||||
if "fs_name" in data:
|
||||
parts = compute_file_name_parts(data["fs_name"])
|
||||
|
||||
@@ -52,7 +52,8 @@ MULTIPLE_SPACE_PATTERN = re.compile(r"\s+")
|
||||
|
||||
class BaseRom(TypedDict):
|
||||
name: NotRequired[str]
|
||||
sort_name: NotRequired[str | None]
|
||||
name_sort_key: NotRequired[str | None]
|
||||
name_sort_key_custom: NotRequired[bool]
|
||||
summary: NotRequired[str]
|
||||
url_cover: NotRequired[str]
|
||||
url_screenshots: NotRequired[list[str]]
|
||||
|
||||
@@ -14,7 +14,7 @@ from config.config_manager import config_manager as cm
|
||||
from handler.filesystem import fs_platform_handler, fs_resource_handler
|
||||
from logger.logger import log
|
||||
from models.platform import Platform
|
||||
from models.rom import Rom
|
||||
from models.rom import Rom, compute_name_sort_key
|
||||
|
||||
from .base_handler import BaseRom, MetadataHandler
|
||||
|
||||
@@ -420,7 +420,12 @@ class GamelistHandler(MetadataHandler):
|
||||
rom_data = GamelistRom(
|
||||
gamelist_id=str(uuid.uuid4()),
|
||||
name=name,
|
||||
sort_name=sort_name or name,
|
||||
# A gamelist <sortname> tag becomes a custom sort key; the
|
||||
# derived-from-name default is used when it is absent.
|
||||
name_sort_key=(
|
||||
compute_name_sort_key(sort_name) if sort_name else None
|
||||
),
|
||||
name_sort_key_custom=bool(sort_name),
|
||||
summary=summary,
|
||||
regions=regions,
|
||||
languages=languages,
|
||||
|
||||
@@ -310,6 +310,19 @@ async def scan_firmware(
|
||||
return Firmware(**firmware_attrs)
|
||||
|
||||
|
||||
def _build_rom(rom_attrs: dict[str, Any]) -> Rom:
|
||||
"""Construct a Rom, applying a custom ``name_sort_key`` after construction so
|
||||
the override survives the ``@validates('name')`` derivation regardless of the
|
||||
order in which ``name`` and the sort-key fields are passed."""
|
||||
name_sort_key = rom_attrs.pop("name_sort_key", None)
|
||||
name_sort_key_custom = rom_attrs.pop("name_sort_key_custom", False)
|
||||
rom = Rom(**rom_attrs)
|
||||
if name_sort_key_custom and name_sort_key:
|
||||
rom.name_sort_key = name_sort_key
|
||||
rom.name_sort_key_custom = True
|
||||
return rom
|
||||
|
||||
|
||||
async def scan_rom(
|
||||
scan_type: ScanType,
|
||||
platform: Platform,
|
||||
@@ -335,7 +348,6 @@ async def scan_rom(
|
||||
"sha1_hash": rom.sha1_hash,
|
||||
"ra_hash": rom.ra_hash,
|
||||
"fs_size_bytes": rom.fs_size_bytes,
|
||||
"sort_name": None,
|
||||
}
|
||||
|
||||
# Check if files have been parsed and hashed
|
||||
@@ -356,7 +368,8 @@ async def scan_rom(
|
||||
rom_attrs.update(
|
||||
{
|
||||
"name": rom.name,
|
||||
"sort_name": rom.sort_name,
|
||||
"name_sort_key": rom.name_sort_key,
|
||||
"name_sort_key_custom": rom.name_sort_key_custom,
|
||||
"slug": rom.slug,
|
||||
"summary": rom.summary,
|
||||
"url_cover": rom.url_cover,
|
||||
@@ -1003,7 +1016,7 @@ async def scan_rom(
|
||||
f"{hl(rom_attrs['fs_name'])} not identified {emoji.EMOJI_CROSS_MARK}",
|
||||
extra=LOGGER_MODULE_NAME,
|
||||
)
|
||||
return Rom(**rom_attrs)
|
||||
return _build_rom(rom_attrs)
|
||||
|
||||
async def fetch_sgdb_details(playmatch_rom: PlaymatchRomMatch) -> SGDBRom:
|
||||
"""Fetch SteamGridDB details for the ROM."""
|
||||
@@ -1079,7 +1092,7 @@ async def scan_rom(
|
||||
)
|
||||
|
||||
rom_attrs["missing_from_fs"] = False
|
||||
return Rom(**rom_attrs)
|
||||
return _build_rom(rom_attrs)
|
||||
|
||||
|
||||
async def _scan_asset(file_name: str, asset_path: str, should_hash: bool = False):
|
||||
|
||||
Reference in New Issue
Block a user