diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index b9f013d4f..3f8af1a84 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -295,20 +295,17 @@ async def _identify_rom( calculate_hashes = not cm.get_config().SKIP_HASH_CALCULATION if calculate_hashes: log.debug(f"Calculating file hashes for {rom.fs_name}...") - ( - rom_files, - rom_crc_c, - rom_md5_h, - rom_sha1_h, - rom_ra_h, - ) = await fs_rom_handler.get_rom_files(rom, calculate_hashes=calculate_hashes) + + parsed_rom_files = await fs_rom_handler.get_rom_files( + rom, calculate_hashes=calculate_hashes + ) fs_rom.update( { - "files": rom_files, - "crc_hash": rom_crc_c, - "md5_hash": rom_md5_h, - "sha1_hash": rom_sha1_h, - "ra_hash": rom_ra_h, + "files": parsed_rom_files.rom_files, + "crc_hash": parsed_rom_files.crc_hash, + "md5_hash": parsed_rom_files.md5_hash, + "sha1_hash": parsed_rom_files.sha1_hash, + "ra_hash": parsed_rom_files.ra_hash, } ) diff --git a/backend/handler/filesystem/roms_handler.py b/backend/handler/filesystem/roms_handler.py index 998fe4f07..5c6401f59 100644 --- a/backend/handler/filesystem/roms_handler.py +++ b/backend/handler/filesystem/roms_handler.py @@ -303,6 +303,15 @@ class ParsedTags: other_tags: list[str] +@dataclass(frozen=True) +class ParsedRomFiles: + rom_files: list[RomFile] + crc_hash: str + md5_hash: str + sha1_hash: str + ra_hash: str + + class FSRomsHandler(FSHandler): def __init__(self) -> None: super().__init__(base_path=LIBRARY_BASE_PATH) @@ -415,7 +424,7 @@ class FSRomsHandler(FSHandler): async def get_rom_files( self, rom: Rom, calculate_hashes: bool = True - ) -> tuple[list[RomFile], str, str, str, str]: + ) -> ParsedRomFiles: from adapters.services.rahasher import RAHasherService from handler.metadata import meta_ra_handler @@ -574,20 +583,20 @@ class FSRomsHandler(FSHandler): ) ) - return ( - rom_files, - crc32_to_hex(rom_crc_c) if rom_crc_c != DEFAULT_CRC_C else "", - ( + return ParsedRomFiles( + rom_files=rom_files, + crc_hash=crc32_to_hex(rom_crc_c) if rom_crc_c != DEFAULT_CRC_C else "", + md5_hash=( rom_md5_h.hexdigest() if rom_md5_h and rom_md5_h.digest() != DEFAULT_MD5_H_DIGEST else "" ), - ( + sha1_hash=( rom_sha1_h.hexdigest() if rom_sha1_h and rom_sha1_h.digest() != DEFAULT_SHA1_H_DIGEST else "" ), - rom_ra_h, + ra_hash=rom_ra_h, ) def _calculate_rom_hashes( diff --git a/backend/tests/handler/filesystem/test_roms_handler.py b/backend/tests/handler/filesystem/test_roms_handler.py index 7599f43ec..1cfe8c09b 100644 --- a/backend/tests/handler/filesystem/test_roms_handler.py +++ b/backend/tests/handler/filesystem/test_roms_handler.py @@ -347,20 +347,20 @@ class TestFSRomsHandler: m.setattr("handler.filesystem.roms_handler.cm.get_config", lambda: config) m.setattr("os.path.exists", lambda x: False) # Normal structure - rom_files, crc_hash, md5_hash, sha1_hash, ra_hash = ( - await handler.get_rom_files(rom_single) + parsed_rom_files = await handler.get_rom_files(rom_single) + + assert len(parsed_rom_files.rom_files) == 1 + assert isinstance(parsed_rom_files.rom_files[0], RomFile) + assert parsed_rom_files.rom_files[0].file_name == "Paper Mario (USA).z64" + assert parsed_rom_files.rom_files[0].file_path == "n64/roms" + assert parsed_rom_files.rom_files[0].file_size_bytes > 0 + + assert parsed_rom_files.crc_hash == "13263b35" + assert parsed_rom_files.md5_hash == "f1c2e022870405e373720e14fa6ab4a0" + assert ( + parsed_rom_files.sha1_hash == "91fe2e94ca7d01531002e99199bd8943c9d6e992" ) - assert len(rom_files) == 1 - assert isinstance(rom_files[0], RomFile) - assert rom_files[0].file_name == "Paper Mario (USA).z64" - assert rom_files[0].file_path == "n64/roms" - assert rom_files[0].file_size_bytes > 0 - - assert crc_hash == "13263b35" - assert md5_hash == "f1c2e022870405e373720e14fa6ab4a0" - assert sha1_hash == "91fe2e94ca7d01531002e99199bd8943c9d6e992" - @pytest.mark.asyncio async def test_get_rom_files_multi_rom( self, handler: FSRomsHandler, rom_multi, config @@ -370,17 +370,15 @@ class TestFSRomsHandler: m.setattr("handler.filesystem.roms_handler.cm.get_config", lambda: config) m.setattr("os.path.exists", lambda x: False) # Normal structure - rom_files, crc_hash, md5_hash, sha1_hash, ra_hash = ( - await handler.get_rom_files(rom_multi) - ) + parsed_rom_files = await handler.get_rom_files(rom_multi) - assert len(rom_files) >= 2 # Should have multiple parts + assert len(parsed_rom_files.rom_files) >= 2 # Should have multiple parts - file_names = [rf.file_name for rf in rom_files] + file_names = [rf.file_name for rf in parsed_rom_files.rom_files] assert "Super Mario 64 (J) (Rev A) [Part 1].z64" in file_names assert "Super Mario 64 (J) (Rev A) [Part 2].z64" in file_names - for rom_file in rom_files: + for rom_file in parsed_rom_files.rom_files: assert isinstance(rom_file, RomFile) assert rom_file.file_size_bytes > 0 assert rom_file.last_modified is not None @@ -616,17 +614,15 @@ class TestFSRomsHandler: self, handler: FSRomsHandler, rom_single_nested ): """Test that only top-level files contribute to main ROM hash calculation""" - rom_files, rom_crc, rom_md5, rom_sha1, rom_ra = await handler.get_rom_files( - rom_single_nested - ) + parsed_rom_files = await handler.get_rom_files(rom_single_nested) # Verify we have multiple files (base game + translation) - assert len(rom_files) == 2 + assert len(parsed_rom_files.rom_files) == 2 base_game_rom_file = None translation_rom_file = None - for rom_file in rom_files: + for rom_file in parsed_rom_files.rom_files: if rom_file.file_name == "Sonic (EU) [T].n64": base_game_rom_file = rom_file elif rom_file.file_name == "Sonic (EU) [T-En].z64": @@ -643,17 +639,17 @@ class TestFSRomsHandler: # (this verifies that the translation is not included in the main hash) assert ( - rom_md5 == base_game_rom_file.md5_hash + parsed_rom_files.md5_hash == base_game_rom_file.md5_hash ), "Main ROM hash should include base game file" assert ( - rom_md5 != translation_rom_file.md5_hash + parsed_rom_files.md5_hash != translation_rom_file.md5_hash ), "Main ROM hash should not include translation file" assert ( - rom_sha1 == base_game_rom_file.sha1_hash + parsed_rom_files.sha1_hash == base_game_rom_file.sha1_hash ), "Main ROM hash should include base game file" assert ( - rom_sha1 != translation_rom_file.sha1_hash + parsed_rom_files.sha1_hash != translation_rom_file.sha1_hash ), "Main ROM hash should not include translation file" @pytest.mark.asyncio @@ -696,20 +692,24 @@ class TestFSRomsHandler: ) # Run the hashing process - rom_files, crc_hash, md5_hash, sha1_hash, _ = await test_handler.get_rom_files( - rom - ) + parsed_rom_files = await test_handler.get_rom_files(rom) # Assert that only SHA1 is populated, and it's from the header - assert len(rom_files) == 1 - assert sha1_hash == internal_sha1, "SHA1 should be from CHD v5 header" - assert rom_files[0].sha1_hash == internal_sha1 + assert len(parsed_rom_files.rom_files) == 1 + assert ( + parsed_rom_files.sha1_hash == internal_sha1 + ), "SHA1 should be from CHD v5 header" + assert parsed_rom_files.rom_files[0].sha1_hash == internal_sha1 # CRC32 and MD5 should be empty/zero (not calculated) - assert crc_hash == "", f"CRC hash should be empty, got: {crc_hash}" - assert md5_hash == "", f"MD5 hash should be empty, got: {md5_hash}" - assert rom_files[0].crc_hash == "" - assert rom_files[0].md5_hash == "" + assert ( + parsed_rom_files.crc_hash == "" + ), f"CRC hash should be empty, got: {parsed_rom_files.crc_hash}" + assert ( + parsed_rom_files.md5_hash == "" + ), f"MD5 hash should be empty, got: {parsed_rom_files.md5_hash}" + assert parsed_rom_files.rom_files[0].crc_hash == "" + assert parsed_rom_files.rom_files[0].md5_hash == "" @pytest.mark.asyncio async def test_get_rom_files_with_non_v5_chd_fallback_to_std_hashing( @@ -747,20 +747,24 @@ class TestFSRomsHandler: ) # Run the hashing process - rom_files, crc_hash, md5_hash, sha1_hash, _ = await test_handler.get_rom_files( - rom - ) + parsed_rom_files = await test_handler.get_rom_files(rom) # All hashes should be populated (calculated from file content) - assert len(rom_files) == 1 - assert crc_hash != "", "CRC hash should be calculated for non-v5 CHD" - assert md5_hash != "", "MD5 hash should be calculated for non-v5 CHD" - assert sha1_hash != "", "SHA1 hash should be calculated for non-v5 CHD" + assert len(parsed_rom_files.rom_files) == 1 + assert ( + parsed_rom_files.crc_hash != "" + ), "CRC hash should be calculated for non-v5 CHD" + assert ( + parsed_rom_files.md5_hash != "" + ), "MD5 hash should be calculated for non-v5 CHD" + assert ( + parsed_rom_files.sha1_hash != "" + ), "SHA1 hash should be calculated for non-v5 CHD" # Verify they're actual hash values (not from an internal header) - assert rom_files[0].crc_hash == crc_hash - assert rom_files[0].md5_hash == md5_hash - assert rom_files[0].sha1_hash == sha1_hash + assert parsed_rom_files.rom_files[0].crc_hash == parsed_rom_files.crc_hash + assert parsed_rom_files.rom_files[0].md5_hash == parsed_rom_files.md5_hash + assert parsed_rom_files.rom_files[0].sha1_hash == parsed_rom_files.sha1_hash class TestExtractCHDHash: