sorter: drop non-warhammer game systems (default: Middle-Earth)
The collection is warhammer-only; everything else stays in _unsorted even when names match the catalog. The catalog snapshot keeps all systems - the filter is a sorter default (DROPPED_SYSTEMS, applied in load_catalog and after keyword classification), adjustable with --drop-system/--keep-system.
This commit is contained in:
+26
-2
@@ -32,9 +32,15 @@ UNSORTED = "_unsorted"
|
|||||||
UNCLASSIFIED_CLASS = "Unclassified"
|
UNCLASSIFIED_CLASS = "Unclassified"
|
||||||
# A file whose system the keyword tables could not identify only gets a
|
# A file whose system the keyword tables could not identify only gets a
|
||||||
# catalog unit when the fuzzy hit is strong - otherwise it would wander
|
# catalog unit when the fuzzy hit is strong - otherwise it would wander
|
||||||
# into other games' sections (Middle-earth, Blood Bowl, ...) on a guess.
|
# into other games' sections (Blood Bowl, ...) on a guess.
|
||||||
CROSS_SYSTEM_MIN = 0.75
|
CROSS_SYSTEM_MIN = 0.75
|
||||||
|
|
||||||
|
# Game systems whose sections the sorter must never place into. The
|
||||||
|
# collection is warhammer-only: everything else (Middle-earth proxies,
|
||||||
|
# ...) stays in _unsorted even when a name matches the catalog. The
|
||||||
|
# catalog snapshot itself keeps all systems; the filter lives here.
|
||||||
|
DROPPED_SYSTEMS = frozenset({"Middle-Earth"})
|
||||||
|
|
||||||
# Weak acceptance tier: when nothing reached the normal threshold, a hit
|
# Weak acceptance tier: when nothing reached the normal threshold, a hit
|
||||||
# at least this strong is accepted but flagged low confidence ("[w]" in
|
# at least this strong is accepted but flagged low confidence ("[w]" in
|
||||||
# the plan, confidence field in the audit, listed in
|
# the plan, confidence field in the audit, listed in
|
||||||
@@ -178,9 +184,14 @@ def classify_item(item: SortItem) -> None:
|
|||||||
# faction group layer (GW naming, shared with warhammer_data)
|
# faction group layer (GW naming, shared with warhammer_data)
|
||||||
|
|
||||||
|
|
||||||
def load_catalog(path: Path) -> list[dict]:
|
def load_catalog(path: Path,
|
||||||
|
dropped: frozenset[str] | None = DROPPED_SYSTEMS) -> list[dict]:
|
||||||
|
"""Load catalog entries, dropping non-collected game systems by default
|
||||||
|
(pass dropped=frozenset() to keep everything)."""
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
entries = data["entries"] if isinstance(data, dict) else data
|
entries = data["entries"] if isinstance(data, dict) else data
|
||||||
|
if dropped:
|
||||||
|
entries = [e for e in entries if e.get("system") not in dropped]
|
||||||
for e in entries:
|
for e in entries:
|
||||||
e["_clean"] = _clean_name(e["name"])
|
e["_clean"] = _clean_name(e["name"])
|
||||||
e["_clean_aliases"] = [_clean_name(a) for a in e.get("aliases", [])]
|
e["_clean_aliases"] = [_clean_name(a) for a in e.get("aliases", [])]
|
||||||
@@ -672,6 +683,17 @@ def _display_name(unit: str) -> str:
|
|||||||
# plan / apply
|
# plan / apply
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _drop_dropped_system(item: SortItem) -> None:
|
||||||
|
"""Dropped systems (see DROPPED_SYSTEMS) count as unclassified: the
|
||||||
|
collection does not cover them, so their names must not place files
|
||||||
|
into those sections. Called after keyword classification, before
|
||||||
|
matching - the item can still be re-homed by other signals."""
|
||||||
|
if item.system in DROPPED_SYSTEMS:
|
||||||
|
item.system = "Unknown"
|
||||||
|
item.group = item.faction = UNSORTED
|
||||||
|
item.subfaction = None
|
||||||
|
|
||||||
|
|
||||||
def sort_plan(
|
def sort_plan(
|
||||||
files: list[Path],
|
files: list[Path],
|
||||||
library: Path,
|
library: Path,
|
||||||
@@ -716,6 +738,7 @@ def sort_plan(
|
|||||||
if unit_name is None:
|
if unit_name is None:
|
||||||
item = inspect(f)
|
item = inspect(f)
|
||||||
classify_item(item)
|
classify_item(item)
|
||||||
|
_drop_dropped_system(item)
|
||||||
match_catalog(item, catalog, threshold)
|
match_catalog(item, catalog, threshold)
|
||||||
if root is not None:
|
if root is not None:
|
||||||
# manual trees: let the real ancestor dir names compete
|
# manual trees: let the real ancestor dir names compete
|
||||||
@@ -741,6 +764,7 @@ def sort_plan(
|
|||||||
clean=_clean_name(unit_name), inner_names=listing,
|
clean=_clean_name(unit_name), inner_names=listing,
|
||||||
)
|
)
|
||||||
classify_item(probe)
|
classify_item(probe)
|
||||||
|
_drop_dropped_system(probe)
|
||||||
match_catalog(probe, catalog, threshold)
|
match_catalog(probe, catalog, threshold)
|
||||||
fields = {k: getattr(probe, k) for k in (
|
fields = {k: getattr(probe, k) for k in (
|
||||||
"system", "group", "faction", "subfaction", "klass",
|
"system", "group", "faction", "subfaction", "klass",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dlphelper.sorter import (
|
from dlphelper.sorter import (
|
||||||
|
DROPPED_SYSTEMS,
|
||||||
format_plan,
|
format_plan,
|
||||||
load_catalog,
|
load_catalog,
|
||||||
sort_plan,
|
sort_plan,
|
||||||
@@ -57,8 +58,20 @@ def main() -> None:
|
|||||||
"sort whole extraction directories as units")
|
"sort whole extraction directories as units")
|
||||||
parser.add_argument("--json", action="store_true",
|
parser.add_argument("--json", action="store_true",
|
||||||
help="print the plan as JSON instead of text")
|
help="print the plan as JSON instead of text")
|
||||||
|
parser.add_argument("--drop-system", action="append", default=[],
|
||||||
|
metavar="NAME",
|
||||||
|
help="never place into this game system "
|
||||||
|
"(default-dropped: Middle-Earth)")
|
||||||
|
parser.add_argument("--keep-system", action="append", default=[],
|
||||||
|
metavar="NAME",
|
||||||
|
help="re-enable a default-dropped game system")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
dropped = frozenset(DROPPED_SYSTEMS - set(args.keep_system)
|
||||||
|
| set(args.drop_system))
|
||||||
|
if args.keep_system or args.drop_system:
|
||||||
|
log.info("System filter adjusted: dropping %s", ", ".join(sorted(dropped)) or "nothing")
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO,
|
logging.basicConfig(level=logging.INFO,
|
||||||
format="%(asctime)s %(levelname)-7s %(message)s",
|
format="%(asctime)s %(levelname)-7s %(message)s",
|
||||||
datefmt="%H:%M:%S")
|
datefmt="%H:%M:%S")
|
||||||
@@ -69,7 +82,9 @@ def main() -> None:
|
|||||||
# hidden trees like .broken/ must never reach the plan
|
# hidden trees like .broken/ must never reach the plan
|
||||||
if not any(part.startswith(".") for part in p.relative_to(source).parts)
|
if not any(part.startswith(".") for part in p.relative_to(source).parts)
|
||||||
)
|
)
|
||||||
catalog = load_catalog(args.catalog)
|
catalog = load_catalog(args.catalog, dropped=dropped)
|
||||||
|
if dropped:
|
||||||
|
log.info("Dropped systems: %s", ", ".join(sorted(dropped)))
|
||||||
audit = args.audit or (args.library / "sorter_applied.jsonl")
|
audit = args.audit or (args.library / "sorter_applied.jsonl")
|
||||||
|
|
||||||
provenance = None
|
provenance = None
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ CATALOG = {
|
|||||||
{"name": "Dark Reapers", "system": "Warhammer 40,000",
|
{"name": "Dark Reapers", "system": "Warhammer 40,000",
|
||||||
"group": "Xenos Armies", "faction": "Aeldari",
|
"group": "Xenos Armies", "faction": "Aeldari",
|
||||||
"class": "Aspect Warriors", "aliases": []},
|
"class": "Aspect Warriors", "aliases": []},
|
||||||
|
{"name": "Mordor Orcs", "system": "Middle-Earth",
|
||||||
|
"group": "The Lord of the Rings™ - Evil", "faction": "Mordor",
|
||||||
|
"class": "Warriors", "aliases": []},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +181,27 @@ def test_tokenless_needs_bonus():
|
|||||||
shutil.rmtree(root); shutil.rmtree(library)
|
shutil.rmtree(root); shutil.rmtree(library)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dropped_system_mordor():
|
||||||
|
"""Middle-earth is dropped: a 'Mordor' pack lands in _unsorted, and
|
||||||
|
its inner dirs cannot adopt the (filtered) Mordor Orcs entry."""
|
||||||
|
root = Path(tempfile.mkdtemp(prefix="sorter_test_src_"))
|
||||||
|
write(root / "Mordor pack" / "boy.stl")
|
||||||
|
library = make_lib()
|
||||||
|
items = sort_plan([root / "Mordor pack" / "boy.stl"], library,
|
||||||
|
get_catalog(), provenance={"Mordor pack": "Mordor pack.zip"},
|
||||||
|
root=root)
|
||||||
|
it = items[0]
|
||||||
|
assert "Middle-Earth" not in str(it.target), it.target
|
||||||
|
assert "_unsorted/Mordor pack" in str(it.target).replace("\\", "/")
|
||||||
|
shutil.rmtree(root); shutil.rmtree(library)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_catalog_filters_dropped():
|
||||||
|
cat = load_catalog_path()
|
||||||
|
assert not any(e["system"] == "Middle-Earth" for e in cat)
|
||||||
|
assert any(e["name"] == "Warp Spiders" for e in cat)
|
||||||
|
|
||||||
|
|
||||||
def test_nonprov_rehome():
|
def test_nonprov_rehome():
|
||||||
"""Loose noise file inside a named dir re-homes to the dir's unit."""
|
"""Loose noise file inside a named dir re-homes to the dir's unit."""
|
||||||
root = Path(tempfile.mkdtemp(prefix="sorter_test_src_"))
|
root = Path(tempfile.mkdtemp(prefix="sorter_test_src_"))
|
||||||
@@ -222,7 +246,8 @@ def main():
|
|||||||
catalog = load_catalog_path()
|
catalog = load_catalog_path()
|
||||||
for fn in (test_prov_dir_rehome, test_prov_weak_tier, test_nonprov_rehome,
|
for fn in (test_prov_dir_rehome, test_prov_weak_tier, test_nonprov_rehome,
|
||||||
test_cross_system_needs_token_containment, test_apply_weak_doc,
|
test_cross_system_needs_token_containment, test_apply_weak_doc,
|
||||||
test_weak_needs_shared_token, test_tokenless_needs_bonus):
|
test_weak_needs_shared_token, test_tokenless_needs_bonus,
|
||||||
|
test_dropped_system_mordor, test_load_catalog_filters_dropped):
|
||||||
fn()
|
fn()
|
||||||
print("OK", fn.__name__)
|
print("OK", fn.__name__)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user