mirror of
https://github.com/rommapp/romm.git
synced 2026-10-12 02:21:58 +00:00
Merge pull request #4854 from rommapp/fix/rom-file-category-anchor
fix(scan): anchor rom file category to the folder below the rom root
This commit is contained in:
@@ -149,12 +149,15 @@ def category_matches(category: str, path_parts: list[str]) -> bool:
|
||||
|
||||
|
||||
def category_for_path_parts(path_parts_lower: list[str]) -> RomFileCategory | None:
|
||||
"""The file category a folder path implies, from its lowercased parts."""
|
||||
"""The file category of a folder, from its lowercased parts below the ROM root."""
|
||||
# Only the top folder counts, so a dump's inner `content/game` never matches.
|
||||
if not path_parts_lower:
|
||||
return None
|
||||
return next(
|
||||
(
|
||||
category
|
||||
for category in RomFileCategory
|
||||
if category_matches(category.value, path_parts_lower)
|
||||
if category_matches(category.value, path_parts_lower[:1])
|
||||
),
|
||||
None,
|
||||
)
|
||||
@@ -515,6 +518,17 @@ class FSRomsHandler(FSHandler):
|
||||
|
||||
return kept_roms
|
||||
|
||||
@staticmethod
|
||||
def _folder_category(rom: Rom, rom_path: Path) -> RomFileCategory | None:
|
||||
"""The category a file's folder gives it, relative to the ROM's own root."""
|
||||
rom_root = Path(rom.full_path)
|
||||
rom_relative_parts = (
|
||||
rom_path.relative_to(rom_root).parts
|
||||
if rom_path.is_relative_to(rom_root)
|
||||
else ()
|
||||
)
|
||||
return category_for_path_parts(list(map(str.lower, rom_relative_parts)))
|
||||
|
||||
def _build_rom_file(
|
||||
self,
|
||||
rom: Rom,
|
||||
@@ -526,10 +540,7 @@ class FSRomsHandler(FSHandler):
|
||||
archive_members: list[dict[str, Any]] | None = None,
|
||||
) -> RomFile:
|
||||
abs_file_path = Path(self.base_path, rom_path, file_name)
|
||||
|
||||
matching_category = category_for_path_parts(
|
||||
list(map(str.lower, rom_path.parts))
|
||||
)
|
||||
matching_category = self._folder_category(rom, rom_path)
|
||||
|
||||
track_meta = None
|
||||
if matching_category == RomFileCategory.SOUNDTRACK:
|
||||
@@ -732,6 +743,8 @@ class FSRomsHandler(FSHandler):
|
||||
hashable=hashable_platform,
|
||||
)
|
||||
):
|
||||
# Title id extraction below may settle a content category.
|
||||
row.category = self._folder_category(rom, rel_dir)
|
||||
rom_files.append(row)
|
||||
_record_title_id_source(abs_file_path, row)
|
||||
continue
|
||||
|
||||
@@ -24,6 +24,7 @@ from handler.filesystem.roms_handler import (
|
||||
FileHash,
|
||||
FSRomsHandler,
|
||||
_TitleIdSource,
|
||||
category_for_path_parts,
|
||||
category_matches,
|
||||
mtime_matches,
|
||||
)
|
||||
@@ -561,7 +562,7 @@ class TestFSRomsHandler:
|
||||
def test_build_rom_file_with_category(self, rom_multi: Rom, handler: FSRomsHandler):
|
||||
"""Test _build_rom_file with category detection"""
|
||||
# Test with DLC category
|
||||
rom_path = Path(rom_multi.fs_path, "dlc")
|
||||
rom_path = Path(rom_multi.full_path, "dlc")
|
||||
file_name = "test_dlc.n64"
|
||||
file_hash = FileHash(
|
||||
{
|
||||
@@ -3482,6 +3483,69 @@ class TestIncrementalRomFiles:
|
||||
assert by_name["part1 (Hack).bin"].category == RomFileCategory.HACK
|
||||
assert by_name["fix.ips"].category == RomFileCategory.PATCH
|
||||
|
||||
async def test_category_comes_from_the_folder_below_the_rom_root(
|
||||
self, handler: FSRomsHandler, platform: Platform
|
||||
):
|
||||
rom = self._rom(platform)
|
||||
self._write(handler, f"{self.ROM_DIR}/dlc/foo.bin", b"dlc")
|
||||
self._write(handler, f"{self.ROM_DIR}/update/content/game/x.bin", b"update")
|
||||
self._write(
|
||||
handler,
|
||||
f"{self.ROM_DIR}/Sonic [Update]/content/Game/Stats/archive.bin",
|
||||
b"stats",
|
||||
)
|
||||
self._write(handler, f"{self.ROM_DIR}/content/CMCmn/demo/effect.szs", b"fx")
|
||||
|
||||
parsed = await handler.get_rom_files(rom)
|
||||
|
||||
by_name = {f.file_name: f for f in parsed.rom_files}
|
||||
assert by_name["foo.bin"].category == RomFileCategory.DLC
|
||||
assert by_name["x.bin"].category == RomFileCategory.UPDATE
|
||||
assert by_name["archive.bin"].category is None
|
||||
assert by_name["effect.szs"].category is None
|
||||
|
||||
async def test_reused_row_takes_its_folder_category(
|
||||
self, handler: FSRomsHandler, platform: Platform
|
||||
):
|
||||
rom = self._rom(platform)
|
||||
rel_dir = f"{self.ROM_DIR}/Sonic [Update]/content/Game"
|
||||
st = self._write(handler, f"{rel_dir}/archive.bin", b"stats")
|
||||
row = self._row(rom, rel_dir, "archive.bin", st)
|
||||
row.category = RomFileCategory.GAME
|
||||
|
||||
parsed = await handler.get_rom_files(rom, existing_files=[row])
|
||||
|
||||
assert parsed.rom_files == [row]
|
||||
assert row.category is None
|
||||
|
||||
async def test_rom_folder_named_like_a_category_is_not_a_category(
|
||||
self, handler: FSRomsHandler
|
||||
):
|
||||
platform = Platform(name="Nintendo 64", slug="n64", fs_slug="n64")
|
||||
rom = self._rom(platform, fs_name="Demo")
|
||||
self._write(handler, "n64/roms/Demo/game.n64", b"game")
|
||||
|
||||
parsed = await handler.get_rom_files(rom)
|
||||
|
||||
assert parsed.rom_files[0].category is None
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"parts, expected",
|
||||
[
|
||||
([], None),
|
||||
(["dlc"], RomFileCategory.DLC),
|
||||
(["update", "x"], RomFileCategory.UPDATE),
|
||||
(["patches", "v2"], RomFileCategory.PATCH),
|
||||
(["update", "content", "game"], RomFileCategory.UPDATE),
|
||||
(["some game [update]", "content", "game", "stats"], None),
|
||||
(["content", "cmcmn", "demo"], None),
|
||||
],
|
||||
)
|
||||
def test_category_for_path_parts(
|
||||
self, parts: list[str], expected: RomFileCategory | None
|
||||
):
|
||||
assert category_for_path_parts(parts) == expected
|
||||
|
||||
def test_category_matches_plural_forms(self):
|
||||
assert category_matches("patch", ["patches"])
|
||||
assert category_matches("hack", ["hacks"])
|
||||
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
|
||||
from handler.database import db_rom_handler
|
||||
from handler.filesystem import fs_rom_handler
|
||||
from handler.filesystem.roms_handler import ParsedRomFiles
|
||||
from handler.filesystem.roms_handler import ParsedRomFiles, category_for_path_parts
|
||||
from handler.rom_files import refresh_rom_files
|
||||
from models.platform import Platform
|
||||
from models.rom import (
|
||||
@@ -64,6 +64,9 @@ def _folder_rom(
|
||||
rom_id=rom.id,
|
||||
file_name=Path(rel).name,
|
||||
file_path=str(Path(rom.fs_path, FOLDER, rel).parent),
|
||||
category=category_for_path_parts(
|
||||
[part.lower() for part in Path(rel).parent.parts]
|
||||
),
|
||||
file_size_bytes=st.st_size,
|
||||
last_modified=st.st_mtime,
|
||||
crc_hash="row-crc",
|
||||
|
||||
Reference in New Issue
Block a user