Commit Graph
53 Commits
Author SHA1 Message Date
Ben_Kosytorz 93b86d34af gwshop: fix primary-branch tie-break and refresh the committed catalog
- src/gwshop.rs: pick the FIRST longest facet path on ties, matching the
  original Python max(paths, key=len). Rust's max_by_key returned the last,
  which silently reclassified ~2298 entries for products faceted under
  several branches (e.g. a generic kit listed per chapter). Adds a regression
  test.
- AGENTS.md: document the first-longest tie-break and the facet-partition
  coverage shortfall (non-system merchandise left out; no model kits).
- data/gwshop_catalog.json: regenerate with the fixed binary (3491 -> 3459
  entries; 34 removed, 2 added, 188 structural + 785 collections changes).
2026-10-11 13:10:24 +02:00
Ben_Kosytorz bc165093e2 docs: record the sorter's default (non-provenance) mode
- docs/SORTING.md: the extraction section now shows the default sort command (no --provenance) and presents --provenance as an optional mode.
- AGENTS.md 7: note that sorting runs in the default mode and that mixing the two modes creates duplicate placements.
Docs-only change; no code touched.
2026-10-11 12:55:30 +02:00
Ben_Kosytorz 6c6a3273bb refactor: flatten the crate to the repo root and drop the python version
The Rust rewrite has replaced the original python implementation, so the transition layout is no longer needed:

- move version-rust/{Cargo.toml,Cargo.lock,README.md,.gitignore,src,data} to the repo root; the crate is now the repo.
- move docs/SORTING.md and data/gwshop_catalog.json to the root.
- remove version-python/ entirely, plus the dev-only version-rust/tools/dump_warhammer_data.py (its only job was regenerating data/warhammer_data.json from python).
- rewrite AGENTS.md and README.md for the single-project layout; strip all version-python/version-rust references from code comments.
- fix the gwshop credential lookup: algolia.json now sits at CARGO_MANIFEST_DIR (crate root == repo root), not its parent.
- restore the credentials/run-output ignores in .gitignore (algolia.json, .env, *.token, downloads/, state.json, manifest.jsonl, ...).
- docs/SORTING.md: retarget the extraction/sorter/dlp examples at the dlphelper binary instead of the deleted python scripts.

cargo test: 86/86 green.
2026-10-11 09:22:41 +02:00
Ben_Kosytorz 356b9089b7 docs(rust): record the 6d update run and 0807 folder-page expansion 2026-10-11 08:54:58 +02:00
Ben_Kosytorz 3b3c4b57ea feat(rust): expand 0807.st folder pages (/f/<id>) into their files 2026-10-11 07:11:49 +02:00
Ben_Kosytorz bfc25bc7b6 docs(rust): note the .tgz extraction extension 2026-10-11 01:00:40 +02:00
Ben_Kosytorz 550062f46b feat(rust): extract .tgz archives (Rust-only short suffix) 2026-10-10 23:52:47 +02:00
Ben_Kosytorz b8fdf508c8 fix(rust): first-max tie-breaking in dir matching + python-style score rounding
Two divergences found by diffing the Rust sorter plan against the python
reference on a real 14k-file tree (both 99.82% identical):

- compute_match_name used max_by (LAST max) where python max() keeps the
  FIRST; a same-faction dir hit and a cross-faction hit scored equally and
  the wrong entry won (25 Deathwatch files).
- --json score used round() instead of round_ties_even(), printing 0.813
  where python prints 0.812.

After the fix the plans are identical: 14025/14025 targets, kinds and
scores, zero differences.
2026-10-10 22:11:26 +02:00
Ben_Kosytorz 81972afd22 fix(rust): sniff container magic before extension in the extractor
Real 7z archives named .zip (seen live: 'Heresy Lab - HL068 Utela Justina.zip')
were quarantined because dispatch trusted the extension. python's 7z CLI
sniffs content; now so do we for 7z/zip/rar, keeping extension logic for
gz/bz2/xz/tar where the tar-inside decision needs the name.
2026-10-10 20:12:32 +02:00
Ben_Kosytorz ef5cf04c52 feat(rust): never download .torrent links or torrent files in gofile content
User policy: the channel hosts user-created torrents (one ~2.5 TB); this
tool is for direct-download packs. Torrent links are recorded as done with
a note so they never re-download.
2026-10-10 09:17:00 +02:00
Ben_Kosytorz eea1260f13 docs(rust): live validation of export/download/extract paths 2026-10-10 01:00:51 +02:00
Ben_Kosytorz 53da8dc3c2 feat(rust): dlp --since — filter URLs by message timestamp
Rust-only extension (no python counterpart): --since 5d / 12h / 2026-10-04
keeps only URLs posted at/after the cutoff. URLs without a timestamp
(e.g. --urls-file lists) are kept with a warning.
2026-10-09 23:38:29 +02:00
Ben_Kosytorz cd061b859d docs(rust): point the sorter at docs/SORTING.md 2026-10-09 15:32:27 +02:00
Ben_Kosytorz 55d32379ed feat(rust): port dlp.py orchestration + unified CLI (clap subcommands)
- orchestrate.rs: process_channel, run_gofile (re-auth on AuthError,
  no-retry branches for dead/password/empty, cooldowns), run_0807,
  subscriptions, --assume-done (with note), failed_urls export
- main.rs: dlp / catalogue / sorter / extract / gwshop subcommands,
  token resolution (--token -> $DISCORD_TOKEN -> hidden prompt)
- globalstore: mark() gains a note field (python note= kwarg)
- README: port status, CLI, deviations, untested live paths

77 tests green.
2026-10-09 15:30:27 +02:00
Ben_Kosytorz 1d3f83d950 feat(rust): port gwshop.py — Algolia catalog dump
- facet-partitioned full-index retrieval (search-only key, /query only),
  retries (2/5/15s), credentials from env or gitignored algolia.json
- normalize/build_catalog: primary = longest GameSystemsRoot path,
  system/group/faction/subfaction/class expansion, objectID dedup
- 5 offline tests (hierarchy paths verified against python)
2026-10-09 14:48:49 +02:00
Ben_Kosytorz 964400cb1e feat(rust): port sorter.py — fuzzy matching, plan, hardlink apply
- src/sorter.rs: inspect (zip/7z listing via crates, HTML-page detection),
  keyword classification with inner-name votes, catalog load with dropped
  systems, fuzzy matching on the CPython-difflib port (containment boost,
  generic-token stoplist, tokenless bonus, weak tier, cross-system gates),
  deepest-dir re-homing incl. provenance unit mode, build_target,
  hardlink-apply with audit + low-confidence doc, format_plan
- all 9 python test_sorter_units.py cases ported and passing
- parity fixes found while porting: inner-name votes REPLACE (not max-keep)
  the outer score below threshold; python max() keeps first maximal vote;
  ratio cache keyed with allow_single; 512-byte head read for HTML sniff
2026-10-09 14:44:09 +02:00
Ben_Kosytorz 137efa7e6c feat(rust): port discord, gofile, zero807, extract_all and catalogue
Parallel module ports (behavior parity with the python versions):

- src/discord.rs: DCE docker export (token via env only), base64/plain URL
  extraction with grab.sh-compatible regexes, host classification; 11 tests
- src/gofile.rs: GoFile API client — one guest account per run, linear
  backoff on account creation, sha256 website token, recursive content walk,
  streaming .part downloads; 7 tests (offline goldens from python hashlib)
- src/zero807.rs: curl downloader with UA+Referer contract, dead-link
  detection; 4 tests
- src/extract.rs: recursive extraction (sevenz-rust2 for 7z, zip crate,
  unrar for rar, system tar/gzip/bzip2/xz for tarballs), nested BFS
  expansion, .broken quarantine, extract_manifest.jsonl, idempotent re-runs;
  8 tests
- src/catalogue.rs: keyword classifier over the dumped taxonomy, manifest/
  directory scanning, overrides, md+jsonl reports, audited moves; 7 tests
  incl. all 17 python test cases
2026-10-09 14:17:02 +02:00
Ben_Kosytorz 519e7904dc feat(rust): difflib port + taxonomy data dump
- src/difflib.rs: faithful port of CPython difflib.SequenceMatcher
  (Ratcliff/Obershelp ratio incl. real_quick_ratio/quick_ratio and autojunk),
  validated against python-computed test vectors with exact float equality
- data/warhammer_data.json: taxonomy tables dumped from the python module
  (tools/dump_warhammer_data.py), parsed order-preserving in
  src/warhammer_data.rs (python dict insert-order + last-value-wins semantics)
- deps for the module ports: clap, reqwest, url, base64, regex, sha2, hex,
  sevenz-rust2, unrar, zip, indexmap, rand, rpassword
2026-10-09 13:44:28 +02:00
Ben_Kosytorz 59a89fa06a feat(rust): port state.py, globalstore.py and manifest appending
- src/state.rs: per-run state.json (status/attempts/error/files/updated),
  corrupt-file -> *.json.bak restart, atomic tmp+rename save, mark() parity
  (attempts increments, None fields never clear existing values)
- src/globalstore.rs: cross-run urls.json at XDG_DATA_HOME or
  ~/.local/share/dlphelper/urls.json, disabled-store no-op
- src/manifest.rs: append_manifest as compact JSONL, UTF-8
- unknown/extra JSON fields are preserved on round-trip so both versions can
  read each other's artifacts (schema contract per AGENTS.md)
- 14 unit tests
2026-10-09 13:27:59 +02:00
Ben_Kosytorz 271fb3bf68 chore: start transition layout — python into version-python/, scaffold version-rust/
- git mv of all existing files into version-python/ (history preserved)
- version-rust/: minimal cargo scaffold (edition 2024), no deps yet
- root AGENTS.md documents the transition rules and artifact/schema contract
- main is untouched; the python layout there stays as-is
2026-10-09 13:18:31 +02:00
Ben_Kosytorz 8f4cc55bb4 docs: audit + low-confidence doc live in <library>/../_logs/ now 2026-10-08 14:22:40 +02:00
Ben_Kosytorz 8f3a91bcd4 sorter: write audit + low-confidence doc beside the library, never inside it
The library may be a seeded torrent whose file set is frozen; the
sorter_applied.jsonl / sorter_low_confidence.txt bookkeeping now goes
to <library>/../_logs/ by default. Explicit --audit still wins.
2026-10-08 14:18:40 +02:00
Ben_Kosytorz 390ce8b024 docs: probe votes on full inner paths; Iron Hands mega-bucket fix documented 2026-10-07 11:41:27 +02:00
Ben_Kosytorz f0c51503d9 sorter: weapon/gun/turret/carriage/upgrade are generic tokens - not match anchors 2026-10-07 09:49:06 +02:00
Ben_Kosytorz 9133b43831 sorter: probe votes on full inner paths (dir names count); no single-token containment boost for inner votes; catalog class wins on unit adoption; confidence copied from probe 2026-10-07 09:46:36 +02:00
Ben_Kosytorz fcec027473 docs: non-warhammer system drop policy (Middle-Earth default-dropped) 2026-10-07 08:04:09 +02:00
Ben_Kosytorz 0439071f70 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.
2026-10-07 08:03:05 +02:00
Ben_Kosytorz e13412a5fe sorter: weak tier and tokenless adoption need a shared rare token; pool scan takes best accepted entry 2026-10-07 06:08:40 +02:00
Ben_Kosytorz a995937c4b sorter: cross-system name adoption needs token containment; sys-retry keeps the higher score 2026-10-06 23:44:00 +02:00
Ben_Kosytorz 7ccaf75a41 sorter: deepest-matching-dir re-homing + weak 0.50 confidence tier
Inner directory names now compete with the pack/file name: a subtree
under a dir whose name matches the catalog better than the pack-level
resolution is re-homed there (fixes multi-unit sculptor packs like
'Retro Elvez' dumping everything into one keyword class). Junk dir
names (files/images/HEADS/2/...) never match. Hits between 0.50 and the
threshold are accepted as low-confidence: [w] in the plan, confidence
in the audit, listed in sorter_low_confidence.txt. Works in both
provenance and manual-tree mode.
2026-10-06 23:07:26 +02:00
Ben_Kosytorz c3562f63d2 docs: first full extraction + sort run - CIFS inode gotcha, cp437 fallback, .broken/RETRY_URLS policy 2026-10-06 18:58:55 +02:00
Ben_Kosytorz 14ffc9e918 sorter: unit-mode inner_rel must carry dirs only, not duplicate the file name 2026-10-06 17:15:45 +02:00
Ben_Kosytorz 2c6e7b4d34 sorter: skip hidden trees (.broken) in the plan; treat extract_manifest.jsonl as run artifact 2026-10-06 16:52:55 +02:00
Ben_Kosytorz 2b5dadc0a8 extract_all: CIFS-safe fallback extraction via /var/tmp with cp437 filename repair; delete macOS AppleDouble junk 2026-10-06 16:51:33 +02:00
Ben_Kosytorz 624b11cc0f extract_all: resolve archive-stem/file name collisions with a _dup destination instead of crashing 2026-10-05 19:56:26 +02:00
Ben_Kosytorz 7a046a3d65 extract_all: nested extraction failures must quarantine relative to out root 2026-10-05 13:51:39 +02:00
Ben_Kosytorz 04baa2fd2a extract_all: tolerate non-UTF8 7z output, manifest-based skip, final stranded-archive sweep; same decode fix in sorter list_7z and zero807 2026-10-05 13:46:55 +02:00
Ben_Kosytorz 86fcb3aecc extract stage + system-locked matching + provenance unit mode
- tools/extract_all.py: recursive extraction of all archives (incl.
  nested) into a plain-file tree; nested archives are deleted after
  successful extraction, plain files are hardlinked, failures go to
  .broken/, extract_manifest.jsonl records archive->dest->unit.
- sorter --provenance: classify each extraction directory once under
  the archive's name (files of one archive are one unit; inner
  structure kept), so loose STLs sort with their model set.
- matching discipline: a fuzzy match never leaves the file's system;
  system-less files need CROSS_SYSTEM_MIN (0.75) for a catalog unit,
  and a same-system faction jump below 0.75 is rejected. Fixes 40k
  files landing in Middle-earth/Blood Bowl/AoS buckets.
- speed for big trees: lru_cache on keyword matchers and pair scores,
  quick_ratio pruning; keywords: night lord, csm.
2026-10-05 08:09:22 +02:00
Ben_Kosytorz 7e28518a52 gwshop: count dump coverage by unique objectID (review fixes)
Summed nbHits across facet branches double-counted products listed in
several branches; coverage is now per unique objectID, with a warning
about records left out of the dump. AGENTS.md documents the live
count (4183 records, 3491 via facet).
2026-10-05 07:34:29 +02:00
Ben_Kosytorz da5f1f7315 sorter: outer file name wins over inner archive names in fuzzy match
Best-pool scoring used to always blend the archive's inner file names
into the fuzzy score (0.95 discount), which dragged clean-named files
into wrong units (e.g. 'Keeper of Secrets & Shalaxi Helbane.7z' ended
in Orks/Boyz). Now the outer name decides when it scores >= threshold;
inner names only vote as fallback (hash-named gofile archives rely on
that). Also: url/failed-list run artifacts are excluded from sorting,
and two plan-inspection helpers under tools/.
2026-10-05 07:34:29 +02:00
Ben_Kosytorz ecf01df13b AGENTS.md: document the working GW/Algolia catalog source
The §7.3 catalog-source notes still described the Algolia route as a
future 'promising path'. Now documented as implemented, with the full
method (WAF findings, search-only key in gitignored algolia.json, index
name, /browse 403 + 1000-hit pagination cap, GameSystemsRoot facet
partitioning, snapshot committed as data/gwshop_catalog.json and how to
refresh it) and the GW-naming switch in §7.3 + the module list.
2026-10-04 22:08:02 +02:00
Ben_Kosytorz 4bbaefb45a Switch library hierarchy to GW webshop taxonomy
- warhammer_data.py: GW naming throughout - Space Marine chapters are
  their own factions under a 'Space Marines' group, systems match the
  shop (Age of Sigmar, The Horus Heresy, Other Games, The Old World,
  Middle-Earth), classes are GW shop categories (Battleline, Infantry,
  Vehicle, Walker, Flyer, Monster, Mounted, Hero, Titanic), group map
  extended to AoS grand alliances / Other Games / Old World / Middle-Earth
- gwshop.py: hierarchy remap - GW depth-4 paths are system/group/faction/
  class (the shop's last level is the class layer), depth-5 sources keep
  a subfaction before it; catalog snapshot regenerated accordingly
- sorter.py: catalog entry is hierarchy-authoritative (group, class, and
  faction when GW moved a unit or keyword tables are stale); generic GW
  branches ('Unit Type') and a Battleline default never degrade a
  confident keyword classification; whole-catalog retry when the faction
  pool has no confident match (handles units GW moved between factions);
  whole-catalog matching also runs when keyword classification found no
  faction; invalid (HTML) items never fuzzy-match; default catalog is
  now data/gwshop_catalog.json
- catalogue.py: --apply moves include the group layer, same structure
  as the sorter
- docs/SORTING.md: hierarchy rewritten to GW naming with examples
- tests: expectations updated to GW naming (chapters, unit types)
- verified live: 4/4 real files in the test batch now catalog-match,
  e.g. The Twin Lance -> 40k/Xenos Armies/T'au Empire/Epic Hero (GW's
  current placement) instead of the stale Drukhari keyword
2026-10-04 22:05:31 +02:00
Ben_Kosytorz 5fd5dacaf0 Add GW webshop catalog source (Algolia): dlphelper.gwshop + full dump
- warhammer.com shop HTML/listing endpoints are behind AWS WAF (405/202
  goku challenge) for non-browser clients; findings recorded in AGENTS.md
- the shop search runs on Algolia; public search-only key lets us dump the
  whole product index with plain requests (no browser, no captcha)
- gwshop.py: credential loading (env or gitignored algolia.json), index
  listing, facet-partitioned full retrieval (search-only key cannot /browse
  and pagination caps at 1000, so the index is partitioned on the
  GameSystemsRoot hierarchy facet), normalization into the sorter's
  source-agnostic catalog schema (system/group/faction/subfaction/class)
- data/gwshop_catalog.json: snapshot of prod-lazarus-product-en-ww,
  3491 unique products, every product with full hierarchy path
- tools/: one-shot browse + catalog builder + shape inspector
2026-10-04 21:48:58 +02:00
Ben_Kosytorz cccf10a308 Sorter: fuzzysort files into the miniset-aligned library via hardlinks
- docs/SORTING.md: target hierarchy (game > faction group > faction >
  subfaction > class > unit, miniset.net naming), canonical-name policy,
  hardlink policy, unzip outlook
- data/library_catalog.json: canonical catalog (name/aliases/subfaction/class)
- dlphelper/sorter.py: inspect (zip via zipfile, 7z via 7z l), keyword
  classification incl. inner-name voting for hash-named archives, fuzzy
  matching (difflib), plan/apply split, hardlink preferred with copy
  fallback, append-only audit, existing targets skipped, run artifacts
  filtered, HTML-page detection for bad downloads
- sorter.py CLI (plan by default, --apply to act)
2026-10-04 21:17:52 +02:00
Ben_Kosytorz acb3415878 Catalogue: classify collection into system/faction/force-org class
- dlphelper/warhammer_data.py: keyword taxonomy (40k factions incl. Chaos and
  Xenos super-faction units, plus AoS/Heresy/Kill Team/Necromunda/Epic/OW)
- dlphelper/catalogue.py: manifest+directory scanner, conservative keyword
  classifier (plural-tolerant word boundaries, longest match wins), markdown
  and jsonl reports, overrides support, audited --apply moves
- catalogue.py CLI; tests/test_catalogue.py offline classifier test
- real-data check on the user's dirs: 1624/3333 files classified so far
2026-10-04 20:25:21 +02:00
Ben_Kosytorz 64abd7126f GoFileEmpty: no retries for content that exists but is empty; add tools/debug_gofile.py for raw content-API inspection 2026-10-04 19:16:08 +02:00
Ben_Kosytorz 4f4270fd28 Match gofile 'error-notFound' status (API prefixes dead-link status with error-) 2026-10-04 19:03:54 +02:00
Ben_Kosytorz 413672f114 Dead links (gofile notFound/404, 0807 404/410) fail immediately without retry 2026-10-04 18:54:08 +02:00
Ben_Kosytorz 9d47c29495 Fix gofile website-token crash: int(time()) -> int(time.time()) 2026-10-04 18:49:50 +02:00
Ben_Kosytorz edf6d46cd2 Add --limit (per host class, on pending URLs) and --shuffle for test runs 2026-10-04 18:45:13 +02:00
Ben_Kosytorz c8bb882d9d Fix DCE token env var: v2.48 reads DISCORD_TOKEN, not TOKEN (both now set) 2026-10-04 18:36:47 +02:00
Ben_Kosytorz 72ef7bc465 Add subscription mode, global cross-run URL store and --assume-done import
- dlphelper/globalstore.py: ~/.local/share/dlphelper/urls.json records every
  successfully downloaded URL once, regardless of OUT_DIR - reposts in other
  channels and re-runs of moved channels no longer re-download
- --subs subscriptions.json: batch mode over a configurable channel list;
  re-exports each channel by default (new links get picked up), --no-fresh
  reuses cached exports
- --assume-done <file>: manual import step that marks old URL lists as done
  in the global store (deliberate, unverified, never automatic)
- lazy downloader startup: no gofile guest account / no curl runs when all
  URLs are already done; token prompt skipped in --urls-file mode
- write_failed_urls creates OUT_DIR if needed
2026-10-04 17:19:11 +02:00
Ben_Kosytorz db8a1c1b52 Initial version: dlp.py orchestrates Discord export, link extraction and host-aware downloads
- dlphelper/discord.py: DCE docker export (JSON) + base64/plain URL extraction
  (extraction regex kept compatible with parts/grab.sh)
- dlphelper/gofile.py: GoFile client adapted from Lysagxra/GoFileDownloader's
  API scheme, but ONE guest account per run and per-file failure tracking
  instead of sys.exit - fixes the rate-limit death after ~6 downloads
- dlphelper/zero807.py: curl downloader with the UA+Referer contract 0807.st
  requires; .part files for resumable downloads
- dlphelper/state.py: state.json (resume/skip) + manifest.jsonl (torrent stage
  later) + failed_urls.txt
- tools/new_repo.py: Gitea repo helper reading ~/.git-credentials itself
- AGENTS.md with agent rules, README, offline extraction test
2026-10-04 14:06:46 +02:00