mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 21:43:23 -05:00
fix: order same-named files by their owner's copy
This commit is contained in:
1 parent
f625039190
commit
8e532db6aa
5 files changed
+73
-4
No files matched your search
File renamed without changes.
+34
-2
@@ -468,6 +468,36 @@ def _resolve_agnostic(file_entry: dict, files_db: dict, has_strong_hash: bool):
|
||||
return None
|
||||
|
||||
|
||||
def _affinity_tokens(text: str) -> set[str]:
|
||||
return {
|
||||
token
|
||||
for token in (re.sub(r"[^0-9a-z]", "", part.casefold()) for part in text.split("/"))
|
||||
if token
|
||||
}
|
||||
|
||||
|
||||
def _by_affinity(paths: list[str], file_entry: dict, dest_hint: str) -> list[str]:
|
||||
"""Same-named candidates, the ones stored with the file's owner first.
|
||||
|
||||
A name step without a tie-break took whichever path sorted first: the
|
||||
TheXTech coin.ogg for Syobon Action, a Hurrican sound for ARMSX2. The
|
||||
destination's directories and the profile that asks name where its own
|
||||
copy lives; the order is otherwise kept.
|
||||
"""
|
||||
hint = dest_hint or file_entry.get("path") or file_entry.get("destination") or ""
|
||||
wanted = _affinity_tokens(hint.rsplit("/", 1)[0] if "/" in hint else "")
|
||||
for owner in (file_entry.get("source_profile"), file_entry.get("source_emulator")):
|
||||
if owner:
|
||||
wanted |= _affinity_tokens(str(owner).replace(" ", ""))
|
||||
if not wanted or len(paths) < 2:
|
||||
return paths
|
||||
|
||||
def score(path: str) -> int:
|
||||
return len(wanted & _affinity_tokens(path.rsplit("/", 1)[0]))
|
||||
|
||||
return sorted(paths, key=score, reverse=True)
|
||||
|
||||
|
||||
def resolve_local_file(
|
||||
file_entry: dict,
|
||||
db: dict,
|
||||
@@ -737,6 +767,7 @@ def resolve_local_file(
|
||||
if candidates:
|
||||
if zipped_file:
|
||||
candidates = [p for p in candidates if ".zip" in os.path.basename(p)]
|
||||
candidates = _by_affinity(candidates, file_entry, dest_hint)
|
||||
primary = [p for p in candidates if "/.variants/" not in p]
|
||||
if primary or candidates:
|
||||
return (primary[0] if primary else candidates[0]), "name_exact"
|
||||
@@ -795,8 +826,9 @@ def resolve_local_file(
|
||||
return (primary[0] if primary else valid[0][0]), "hash_mismatch"
|
||||
# No candidate contains the zipped_file -fall through to step 5
|
||||
elif not unsourceable:
|
||||
primary = [c[0] for c in candidates if "/.variants/" not in c[0]]
|
||||
return (primary[0] if primary else candidates[0][0]), "hash_mismatch"
|
||||
ordered = _by_affinity([c[0] for c in candidates], file_entry, dest_hint)
|
||||
primary = [c for c in ordered if "/.variants/" not in c]
|
||||
return (primary[0] if primary else ordered[0]), "hash_mismatch"
|
||||
|
||||
# 5. zipped_file content match via pre-built index (last resort:
|
||||
# matches inner ROM MD5 across ALL ZIPs in the repo, so only use
|
||||
|
||||
@@ -1437,7 +1437,7 @@ def generate_emulator_pack(
|
||||
|
||||
dest_hint = fe.get("path", "")
|
||||
local_path, status = resolve_file(
|
||||
fe,
|
||||
{**fe, "source_profile": emu_name},
|
||||
db,
|
||||
bios_dir,
|
||||
zip_contents,
|
||||
|
||||
+1
-1
@@ -1382,7 +1382,7 @@ def verify_emulator(
|
||||
|
||||
dest_hint = file_entry.get("path", "")
|
||||
local_path, resolve_status = resolve_local_file(
|
||||
file_entry,
|
||||
{**file_entry, "source_profile": emu_name},
|
||||
db,
|
||||
zip_contents,
|
||||
dest_hint=dest_hint,
|
||||
|
||||
@@ -194,6 +194,43 @@ class ReleaseAssetIsNotANameMatch(unittest.TestCase):
|
||||
fetch.assert_not_called()
|
||||
|
||||
|
||||
class NameStepPrefersTheOwnersCopy(unittest.TestCase):
|
||||
"""Same-named files are ordered by where the asking profile keeps its own.
|
||||
|
||||
The name step took the first path in index order: Syobon Action's
|
||||
coin.ogg came from TheXTech's sound folder while its own copy sat in
|
||||
bios/Other/syobonaction/.
|
||||
"""
|
||||
|
||||
def test_the_copy_stored_with_the_profile_wins(self):
|
||||
import tempfile
|
||||
|
||||
from common import resolve_local_file
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
previous = os.getcwd()
|
||||
os.chdir(tmp)
|
||||
self.addCleanup(os.chdir, previous)
|
||||
other = Path("bios/Game Engines/TheXTech/sound/coin.ogg")
|
||||
own = Path("bios/Other/syobonaction/coin.ogg")
|
||||
for path, data in ((other, b"a" * 10), (own, b"b" * 12)):
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_bytes(data)
|
||||
files = {
|
||||
"1" * 40: {"path": str(other), "name": "coin.ogg", "size": 10},
|
||||
"2" * 40: {"path": str(own), "name": "coin.ogg", "size": 12},
|
||||
}
|
||||
db = {"files": files, "indexes": {
|
||||
"by_name": {"coin.ogg": ["1" * 40, "2" * 40]},
|
||||
"by_md5": {}, "by_crc32": {}, "by_path_suffix": {}}}
|
||||
entry = {"name": "coin.ogg", "path": "SyobonAction/SE/coin.ogg"}
|
||||
path, status = resolve_local_file(entry, db, dest_hint=entry["path"])
|
||||
self.assertEqual((path, status), (str(own), "name_exact"))
|
||||
entry = {"name": "coin.ogg", "path": "SE/coin.ogg", "source_profile": "syobonaction"}
|
||||
path, _status = resolve_local_file(entry, db, dest_hint=entry["path"])
|
||||
self.assertEqual(path, str(own))
|
||||
|
||||
|
||||
class CollectionCarriesNoGameDataHomonym(unittest.TestCase):
|
||||
def test_the_profiles_resolve_no_game_data_to_another_game(self):
|
||||
database = REPO_ROOT / "database.json"
|
||||
|
||||
Reference in new issue
Block a user