From c7fa0ca3f1285d2c33730990d4da957d0b57f7ba Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Mon, 20 Jul 2026 23:01:06 -0500 Subject: [PATCH] Normalize drum part pointer identities --- lib/sloppak.py | 15 ++++++++++++--- tests/test_sloppak_drum_parts.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/sloppak.py b/lib/sloppak.py index 3d8e2c5..71fb913 100644 --- a/lib/sloppak.py +++ b/lib/sloppak.py @@ -790,14 +790,23 @@ def _resolve_drum_parts( primary_name = None extra_parts: list[dict] = [] seen_rels: set[str] = set() + # Use the same canonical, traversal-safe identity as zip member lookup so + # equivalent spellings ("x.json", "./x.json", or backslashes) identify + # one file. Otherwise an alias pointer can reload and duplicate the primary. + primary_rel_key = ( + _zip_member_key(drum_tab_rel.strip()) + if isinstance(drum_tab_rel, str) and drum_tab_rel.strip() else None + ) for entry in drum_pointer_entries: rel = str(entry.get("drum_tab") or "").strip() - if not rel or rel in seen_rels: + rel_key = _zip_member_key(rel) if rel else None + rel_identity = rel_key or rel + if not rel or rel_identity in seen_rels: continue - seen_rels.add(rel) + seen_rels.add(rel_identity) entry_id = str(entry.get("id") or "").strip() entry_name = str(entry.get("name") or "").strip() - if isinstance(drum_tab_rel, str) and rel == drum_tab_rel.strip(): + if primary_rel_key is not None and rel_key == primary_rel_key: if entry_id: primary_id = entry_id if entry_name: diff --git a/tests/test_sloppak_drum_parts.py b/tests/test_sloppak_drum_parts.py index ee18a63..442382a 100644 --- a/tests/test_sloppak_drum_parts.py +++ b/tests/test_sloppak_drum_parts.py @@ -118,6 +118,24 @@ def test_two_parts_resolve_primary_first_with_alias_identity(tmp_path: Path): assert loaded.drum_parts[1]["drum_tab"]["hits"][0]["t"] == 2.0 +def test_primary_pointer_equivalent_path_is_not_duplicated(tmp_path: Path): + manifest = { + "drum_tab": "drum_tab.json", + "arrangements": [ + {"id": "lead", "name": "Lead", "file": "arrangements/lead.json"}, + {"id": "kit", "name": "Live Kit", "type": "drums", + "drum_tab": "./drum_tab.json"}, + ], + } + pak = _write_pak(tmp_path, manifest, {"drum_tab.json": _tab("Drums")}) + loaded = _load(pak, tmp_path) + assert loaded.drum_parts is not None + assert [(p["id"], p["name"]) for p in loaded.drum_parts] == [ + ("kit", "Live Kit"), + ] + assert loaded.drum_parts[0]["drum_tab"] is loaded.drum_tab + + def test_legacy_single_drum_pack_gets_a_one_part_list(tmp_path: Path): pak = _write_pak(tmp_path, {"drum_tab": "drum_tab.json"}, { "drum_tab.json": _tab("Drums"),