From 74fc4fe8478418c3995eae787c425df152285068 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 2 Aug 2026 19:01:09 -0400 Subject: [PATCH] RPGs become local library citizens: identified, enriched, never uploaded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the board-game search (and truncation heads) runs dry, resolve falls back to type=rpgitem — the geekdo database is shared, so the same API, token, cache, and classification machinery apply. Matched rpgitems flow through review and enrich normally but diff routes them to a local_only bucket, structurally outside to_add/to_update: their collections live on RPGGeek, beyond this pipeline's write scope. The library page gains an All/Board games/RPGs filter and an "RPG · local only" badge; the catalog tags them too. Fixture generators write blanket empty rpgitem stubs for every known query (the fallback fires for every unmatched title) with real synthetic entries for Alice Is Missing. Data: both Alice rows re-resolved from unmatched to auto rpgitem matches. First diff since the audit reworks also lands their real-data consequences: Dungeon! gains its TSR edition update on a versionless copy the old claim ordering missed, to_add rows carry unioned reshoot provenance, and the Herbaceous typo row's survivor is now the correctly-spelled title. Co-Authored-By: Claude Fable 5 --- .claude/skills/bgg-api/SKILL.md | 1 + CLAUDE.md | 1 + README.md | 4 ++ data/matches.csv | 72 +++++++++---------- data/to_add.csv | 18 ++--- data/to_update.csv | 1 + scripts/write_photo_fixtures.py | 17 +++++ scripts/write_stub_fixtures.py | 4 ++ src/bggpipe/diff.py | 11 ++- src/bggpipe/resolve.py | 24 ++++++- src/bggpipe/static/app.css | 3 + src/bggpipe/templates/pages/catalog.html | 3 +- src/bggpipe/templates/pages/library.html | 27 +++++-- src/bggpipe/webreview.py | 1 + ...MINUTE-DUNGEON-type=rpgitem_58faea4564.xml | 1 + ...oic-Age---Featuring-New-Civ_7c5116c957.xml | 1 + ...the-Heroic-Age-type=rpgitem_8704f88656.xml | 1 + ...D-CIVILIZATION-type=rpgitem_436f8cfac3.xml | 1 + ...E-PLAYING-GAME-type=rpgitem_8efe1bd6f0.xml | 1 + ...T-ROLE-PLAYING-type=rpgitem_daff8eceef.xml | 1 + ...ALLS-EXPANSION-type=rpgitem_82261134d9.xml | 1 + ...ICE-IS-MISSING-type=rpgitem_088ab34fc2.xml | 1 + ...query=ALICE-IS-type=rpgitem_f2de3eea31.xml | 1 + ...of-Renaissance-type=rpgitem_83ec5d7426.xml | 1 + ...t-of-the-Zzyzx-type=rpgitem_288971e44c.xml | 1 + ...query=Blorvath-type=rpgitem_ad250413dc.xml | 1 + ...uery=Britannia-type=rpgitem_78177b4e85.xml | 1 + ...ery=CAT-CRIMES-type=rpgitem_3d56b6309b.xml | 1 + ...of-History-8000-BC-to-250-B_4292e34c30.xml | 1 + ...the-Heroic-Age-type=rpgitem_05fdc9d88b.xml | 1 + ...y=CIVILIZATION-type=rpgitem_5e9e2b0726.xml | 1 + ...SMIC-ENCOUNTER-type=rpgitem_14e74aa71f.xml | 1 + ...-International-type=rpgitem_957f2522ae.xml | 1 + ...ainst-Humanity-type=rpgitem_04b2bcdf24.xml | 1 + ...ch_query=Catan-type=rpgitem_63fb6e5035.xml | 1 + ...query=Citadels-type=rpgitem_22dea38263.xml | 1 + ...-Time-26-Space-type=rpgitem_48015c5934.xml | 1 + ...ery=DUNGEON-21-type=rpgitem_95d15d03fd.xml | 1 + ...uery=Diplomacy-type=rpgitem_0747a2387d.xml | 1 + ...ery=Dungeon-21-type=rpgitem_3bf546910a.xml | 1 + ...LODING-KITTENS-type=rpgitem_49ed26a640.xml | 1 + ...query=FLAT-TOP-type=rpgitem_c1e66fb254.xml | 1 + ...uery=Hebarceos-type=rpgitem_07740d9a32.xml | 1 + ...ery=Herbaceous-type=rpgitem_d78832e3ed.xml | 1 + ...y-of-the-World-type=rpgitem_7928f17f52.xml | 1 + ...=Joking-Hazard-type=rpgitem_3550758066.xml | 1 + ...ery=PATCH-WORK-type=rpgitem_afb691fd45.xml | 1 + ...h_query=ROYALS-type=rpgitem_740855ed66.xml | 1 + ...uery=SANTORINI-type=rpgitem_b3713fc18f.xml | 1 + ...h_query=SCRAWL-type=rpgitem_28917ca749.xml | 1 + ...-OF-NOTTINGHAM-type=rpgitem_77cf1f8445.xml | 1 + ..._query=SKIP-BO-type=rpgitem_3f9e6315a6.xml | 1 + ...gical-Conflict-type=rpgitem_2ba94ce51c.xml | 1 + ...query=SORCERER-type=rpgitem_0a90b76555.xml | 1 + ...in-the-25th-Century-type=rp_d8afe77786.xml | 1 + ...TARFORCE-ALPHA-type=rpgitem_5fc9118ed9.xml | 1 + ...er-Nonsense-21-type=rpgitem_f01cc4ca29.xml | 1 + ...pean-Expansion-type=rpgitem_38f0ad0fff.xml | 1 + ...ingspan-Europe-type=rpgitem_4387722048.xml | 1 + ...query=Wingspan-type=rpgitem_f93bee11fc.xml | 1 + ..._query=Wiz-War-type=rpgitem_3adf0a77c6.xml | 1 + ...=a-Gentle-Rain-type=rpgitem_a3470f199b.xml | 1 + .../thing_id=400001-versions=1_6c87ca2e4a.xml | 1 + .../thing_id=400002-versions=1_a82fe6afff.xml | 1 + tests/test_diff.py | 8 +++ tests/test_resolve.py | 13 ++++ 66 files changed, 204 insertions(+), 54 deletions(-) create mode 100644 tests/fixtures/bgg_cache/search_query=5-MINUTE-DUNGEON-type=rpgitem_58faea4564.xml create mode 100644 tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-Game-Expansion-of-the-Heroic-Age---Featuring-New-Civ_7c5116c957.xml create mode 100644 tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-Game-Expansion-of-the-Heroic-Age-type=rpgitem_8704f88656.xml create mode 100644 tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-type=rpgitem_436f8cfac3.xml create mode 100644 tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-A-SILENT-ROLE-PLAYING-GAME-type=rpgitem_8efe1bd6f0.xml create mode 100644 tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-A-SILENT-ROLE-PLAYING-type=rpgitem_daff8eceef.xml create mode 100644 tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-SILENT-FALLS-EXPANSION-type=rpgitem_82261134d9.xml create mode 100644 tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-type=rpgitem_088ab34fc2.xml create mode 100644 tests/fixtures/bgg_cache/search_query=ALICE-IS-type=rpgitem_f2de3eea31.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Age-of-Renaissance-type=rpgitem_83ec5d7426.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Blorvath-3A-Quest-of-the-Zzyzx-type=rpgitem_288971e44c.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Blorvath-type=rpgitem_ad250413dc.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Britannia-type=rpgitem_78177b4e85.xml create mode 100644 tests/fixtures/bgg_cache/search_query=CAT-CRIMES-type=rpgitem_3d56b6309b.xml create mode 100644 tests/fixtures/bgg_cache/search_query=CIVILIZATION-Game-of-the-Heroic-Age---The-Dawn-of-History-8000-BC-to-250-B_4292e34c30.xml create mode 100644 tests/fixtures/bgg_cache/search_query=CIVILIZATION-Game-of-the-Heroic-Age-type=rpgitem_05fdc9d88b.xml create mode 100644 tests/fixtures/bgg_cache/search_query=CIVILIZATION-type=rpgitem_5e9e2b0726.xml create mode 100644 tests/fixtures/bgg_cache/search_query=COSMIC-ENCOUNTER-type=rpgitem_14e74aa71f.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Caf-C3-A9-International-type=rpgitem_957f2522ae.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Cards-Against-Humanity-type=rpgitem_04b2bcdf24.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Catan-type=rpgitem_63fb6e5035.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Citadels-type=rpgitem_22dea38263.xml create mode 100644 tests/fixtures/bgg_cache/search_query=DOCTOR-WHO-The-Game-of-Time-26-Space-type=rpgitem_48015c5934.xml create mode 100644 tests/fixtures/bgg_cache/search_query=DUNGEON-21-type=rpgitem_95d15d03fd.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Diplomacy-type=rpgitem_0747a2387d.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Dungeon-21-type=rpgitem_3bf546910a.xml create mode 100644 tests/fixtures/bgg_cache/search_query=EXPLODING-KITTENS-type=rpgitem_49ed26a640.xml create mode 100644 tests/fixtures/bgg_cache/search_query=FLAT-TOP-type=rpgitem_c1e66fb254.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Hebarceos-type=rpgitem_07740d9a32.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Herbaceous-type=rpgitem_d78832e3ed.xml create mode 100644 tests/fixtures/bgg_cache/search_query=History-of-the-World-type=rpgitem_7928f17f52.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Joking-Hazard-type=rpgitem_3550758066.xml create mode 100644 tests/fixtures/bgg_cache/search_query=PATCH-WORK-type=rpgitem_afb691fd45.xml create mode 100644 tests/fixtures/bgg_cache/search_query=ROYALS-type=rpgitem_740855ed66.xml create mode 100644 tests/fixtures/bgg_cache/search_query=SANTORINI-type=rpgitem_b3713fc18f.xml create mode 100644 tests/fixtures/bgg_cache/search_query=SCRAWL-type=rpgitem_28917ca749.xml create mode 100644 tests/fixtures/bgg_cache/search_query=SHERIFF-OF-NOTTINGHAM-type=rpgitem_77cf1f8445.xml create mode 100644 tests/fixtures/bgg_cache/search_query=SKIP-BO-type=rpgitem_3f9e6315a6.xml create mode 100644 tests/fixtures/bgg_cache/search_query=SORCERER-The-Game-of-Magical-Conflict-type=rpgitem_2ba94ce51c.xml create mode 100644 tests/fixtures/bgg_cache/search_query=SORCERER-type=rpgitem_0a90b76555.xml create mode 100644 tests/fixtures/bgg_cache/search_query=STARFORCE-ALPHA-CENTAURI-Interstellar-Conflict-in-the-25th-Century-type=rp_d8afe77786.xml create mode 100644 tests/fixtures/bgg_cache/search_query=STARFORCE-ALPHA-type=rpgitem_5fc9118ed9.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Utter-Nonsense-21-type=rpgitem_f01cc4ca29.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Wingspan-3A-European-Expansion-type=rpgitem_38f0ad0fff.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Wingspan-Europe-type=rpgitem_4387722048.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Wingspan-type=rpgitem_f93bee11fc.xml create mode 100644 tests/fixtures/bgg_cache/search_query=Wiz-War-type=rpgitem_3adf0a77c6.xml create mode 100644 tests/fixtures/bgg_cache/search_query=a-Gentle-Rain-type=rpgitem_a3470f199b.xml create mode 100644 tests/fixtures/bgg_cache/thing_id=400001-versions=1_6c87ca2e4a.xml create mode 100644 tests/fixtures/bgg_cache/thing_id=400002-versions=1_a82fe6afff.xml diff --git a/.claude/skills/bgg-api/SKILL.md b/.claude/skills/bgg-api/SKILL.md index c9245bd..00c0af7 100644 --- a/.claude/skills/bgg-api/SKILL.md +++ b/.claude/skills/bgg-api/SKILL.md @@ -25,6 +25,7 @@ BGG" logo linking back to boardgamegeek.com. ## Endpoints (XML API2 — the only sanctioned read API) - Search: `https://boardgamegeek.com/xmlapi2/search?query=&type=boardgame,boardgameexpansion` +- RPG search: same endpoint with `type=rpgitem` — the geekdo database is shared across BGG/RPGGeek, so the same API and token serve RPG products. bggpipe uses this as a fallback for titles absent from the board-game types; matched rpgitems are LOCAL-ONLY (enriched, browsable, never uploaded — RPG collections live on rpggeek.com, outside this pipeline's write scope). - Thing (details/stats): `https://boardgamegeek.com/xmlapi2/thing?id=<id1,id2,...>&stats=1` — accepts comma-separated IDs; batch (~20) to reduce request count. - Thing versions: `https://boardgamegeek.com/xmlapi2/thing?id=<id>&versions=1` — lists every published edition/version of a game, each with its own version id, name, publisher, year, and language. Used to match photo edition cues to a concrete version. - Collection: `https://boardgamegeek.com/xmlapi2/collection?username=<user>&own=1` — add `&version=1` to include version info on collection items. diff --git a/CLAUDE.md b/CLAUDE.md index 206c8a5..1106766 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,7 @@ Full design lives in `bgg-shelf-pipeline-spec.md` (read it before changing pipel - Base game vs. expansion vs. new edition is the top failure mode — bias matching toward `ambiguous` over auto-match ("Wingspan Europe" must not match base Wingspan). - Editions/versions matter: Eric owns multiple editions of some games — each is a separate collection entry (keyed by `collid` on BGG). Never guess a version: no legible cues → `version_unknown` and a version-less collection entry. - Normalize titles (casefold, strip punctuation/articles, special chars like é/&/:) identically on both sides of a match; dedupe across photos but keep `source_photos` provenance. +- **RPGs are local-only citizens**: when the board-game search runs dry, resolve falls back to `type=rpgitem` (same geekdo API/token). Matched rpgitems enrich into the library but diff routes them to `local_only` — they must never reach `to_add.csv`/upload (their collection lives on RPGGeek, out of scope). - Detailed BGG API behavior (202 queueing, collection-endpoint quirks, endpoints): use the `bgg-api` skill. **If the spec's BGG behavior changes, update the `bgg-api` skill to match** — they must not drift. ## Git diff --git a/README.md b/README.md index c063618..9a30538 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,10 @@ uv run bggpipe enrich # full metadata → data/games.json Each stage skips work it has already done; `--force`/`--refresh` flags redo it. `review` without `--web` runs in the terminal instead. `upload` also has `--retry-failed`, `--verify` (re-fetches your collection and cross-checks the log), and runs a **headed** browser by default — BGG's Cloudflare check blocks headless ones, and a first login may need one human click before the session is saved locally and reused. +### RPGs on your shelves + +Tabletop RPGs aren't in BGG's board-game database — they live on RPGGeek, which shares the same underlying API. When a title isn't found as a board game, bggpipe retries as an RPG: matches are identified, enriched, and browsable in the library (filter: RPGs), but they stay **local only** — they're never uploaded, since your BGG collection can't hold them. + ### Taking good shelf photos Straight-on, one shelf (or part of one) per shot, close enough that spine text is legible to a human. If you can't read it, the model can't either. Overlap between shots is fine: duplicate reads are deduped automatically, with the merge shown (and veto-able) in review. Boxes the model spots but can't identify become retake prompts in `unidentified.json` and the review UI's "reshoot" list: photograph those boxes up close, drop the new photo in `photos/`, and run `extract` again. diff --git a/data/matches.csv b/data/matches.csv index 69a9d4f..62ee227 100644 --- a/data/matches.csv +++ b/data/matches.csv @@ -1,36 +1,36 @@ -title_raw,bgg_id,bgg_name,year,type,match_status,version_id,version_name,version_status,candidates_json,version_candidates_json,source_photos,merged_into -ROYALS,165986,Royals,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 165986, ""name"": ""Royals"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg, -CAT CRIMES,235096,Cat Crimes,2017,boardgame,auto,360982,English edition,version_auto,"[{""bgg_id"": 235096, ""name"": ""Cat Crimes"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 360982, ""name"": ""English edition"", ""year"": 2017, ""publishers"": [""ThinkFun""], ""languages"": [""English""], ""score"": 3}]",IMG_4499.jpeg, -COSMIC ENCOUNTER,40529,Cosmic Encounter,2008,boardgame,auto,,,version_unknown,"[{""bgg_id"": 39, ""name"": ""Cosmic Encounter"", ""year"": 1977, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 7000, ""rank"": 800}, {""bgg_id"": 40529, ""name"": ""Cosmic Encounter"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 75000, ""rank"": 150}]",[],IMG_4499.jpeg, -SHERIFF OF NOTTINGHAM,157969,Sheriff of Nottingham,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 157969, ""name"": ""Sheriff of Nottingham"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg, -5 MINUTE DUNGEON,207830,5-Minute Dungeon,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 207830, ""name"": ""5-Minute Dungeon"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg, -Britannia,240,Britannia,1986,boardgame,auto,25668,Avalon Hill English edition,version_auto,"[{""bgg_id"": 240, ""name"": ""Britannia"", ""year"": 1986, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 25668, ""name"": ""Avalon Hill English edition"", ""year"": 1986, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg, -CIVILIZATION Game of the Heroic Age - The Dawn of History 8000 BC to 250 BC,71,Civilization,1980,boardgame,auto,71001,Avalon Hill English edition,version_auto,"[{""bgg_id"": 71, ""name"": ""Civilization"", ""year"": 1980, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 31.2, ""owned"": null, ""rank"": null}]","[{""version_id"": 71001, ""name"": ""Avalon Hill English edition"", ""year"": 1981, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg, -"ADVANCED CIVILIZATION Game Expansion of the Heroic Age - Featuring New Civilization, Commodity, and Calamity Cards",177,Advanced Civilization,1991,boardgameexpansion,auto,177001,Avalon Hill English edition,version_auto,"[{""bgg_id"": 177, ""name"": ""Advanced Civilization"", ""year"": 1991, ""type"": ""boardgameexpansion"", ""exact"": true, ""fuzzy"": 33.1, ""owned"": null, ""rank"": null}]","[{""version_id"": 177001, ""name"": ""Avalon Hill English edition"", ""year"": 1991, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg, -DOCTOR WHO The Game of Time & Space,3090,Doctor Who: The Game of Time & Space,1980,boardgame,auto,309001,Games Workshop English edition,version_auto,"[{""bgg_id"": 3090, ""name"": ""Doctor Who: The Game of Time & Space"", ""year"": 1980, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 309001, ""name"": ""Games Workshop English edition"", ""year"": 1980, ""publishers"": [""Games Workshop Ltd.""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg, -SORCERER The Game of Magical Conflict,3585,Sorcerer,1975,boardgame,auto,358501,SPI Designer's Edition,version_auto,"[{""bgg_id"": 3585, ""name"": ""Sorcerer"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 39.0, ""owned"": 900, ""rank"": 5000}, {""bgg_id"": 244115, ""name"": ""Sorcerer"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 39.0, ""owned"": 6000, ""rank"": 1500}]","[{""version_id"": 358501, ""name"": ""SPI Designer's Edition"", ""year"": 1975, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 358502, ""name"": ""SPI folio edition"", ""year"": 1975, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg, -STARFORCE ALPHA CENTAURI Interstellar Conflict in the 25th Century,2524,StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century,1974,boardgame,auto,252401,SPI Designer's Edition,version_auto,"[{""bgg_id"": 2524, ""name"": ""StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century"", ""year"": 1974, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 252401, ""name"": ""SPI Designer's Edition"", ""year"": 1974, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 252402, ""name"": ""SPI folio edition"", ""year"": 1974, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg, -SKIP-BO,1078,Skip-Bo,1967,boardgame,auto,,,version_unknown,"[{""bgg_id"": 1078, ""name"": ""Skip-Bo"", ""year"": 1967, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg, -a Gentle Rain,268163,A Gentle Rain,2019,boardgame,auto,268164,Bloom Edition,version_auto,"[{""bgg_id"": 268163, ""name"": ""A Gentle Rain"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 268164, ""name"": ""Bloom Edition"", ""year"": 2021, ""publishers"": [""Mondo Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg, -PATCH WORK,163412,Patchwork,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 163412, ""name"": ""Patchwork"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 94.7, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg, -SANTORINI,194655,Santorini,2016,boardgame,auto,,,version_unknown,"[{""bgg_id"": 194655, ""name"": ""Santorini"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4502.jpeg, -Utter Nonsense!,181254,Utter Nonsense,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 181254, ""name"": ""Utter Nonsense"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg, -Cards Against Humanity,50381,Cards Against Humanity,2009,boardgame,auto,,,version_unknown,"[{""bgg_id"": 50381, ""name"": ""Cards Against Humanity"", ""year"": 2009, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg, -Joking Hazard,193621,Joking Hazard,2016,boardgame,auto,19362101,First edition,version_auto,"[{""bgg_id"": 193621, ""name"": ""Joking Hazard"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 19362101, ""name"": ""First edition"", ""year"": 2016, ""publishers"": [""Cyanide & Happiness""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg;IMG_4502.jpeg, -EXPLODING KITTENS,172225,Exploding Kittens,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 172225, ""name"": ""Exploding Kittens"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg, -Hebarceos,195314,Herbaceous,2017,boardgame,merged,,,version_unknown,[],[],IMG_4502.jpeg,Herbaceous -Wiz-War,104710,Wiz-War,2012,boardgame,approved,,,version_unknown,"[{""bgg_id"": 1218, ""name"": ""Wiz-War"", ""year"": 1983, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 4000, ""rank"": 2500}, {""bgg_id"": 104710, ""name"": ""Wiz-War"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 8000, ""rank"": 1200}]",[],IMG_4502.jpeg, -SCRAWL,218866,Scrawl,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 218866, ""name"": ""Scrawl"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg, -FLAT TOP,2529,Flat Top,1977,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2529, ""name"": ""Flat Top"", ""year"": 1977, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg, -Diplomacy,483,Diplomacy,1959,boardgame,auto,,,version_unknown,"[{""bgg_id"": 483, ""name"": ""Diplomacy"", ""year"": 1959, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg, -Age of Renaissance,26,Age of Renaissance,1996,boardgame,auto,2601,Avalon Hill English edition,version_auto,"[{""bgg_id"": 26, ""name"": ""Age of Renaissance"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 2601, ""name"": ""Avalon Hill English edition"", ""year"": 1996, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg, -History of the World,224,History of the World,1991,boardgame,auto,22401,Avalon Hill English edition,version_auto,"[{""bgg_id"": 224, ""name"": ""History of the World"", ""year"": 1991, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 22401, ""name"": ""Avalon Hill English edition"", ""year"": 1993, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg, -DUNGEON!,1339,Dungeon!,1975,boardgame,auto,133903,TSR New Dungeon edition,version_approved,"[{""bgg_id"": 1339, ""name"": ""Dungeon!"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 133901, ""name"": ""TSR first edition"", ""year"": 1975, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133902, ""name"": ""TSR second edition"", ""year"": 1980, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133903, ""name"": ""TSR New Dungeon edition"", ""year"": 1989, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg, -CAT CRIMES,235096,Cat Crimes,2017,boardgame,merged,360982,English edition,version_auto,"[{""bgg_id"": 235096, ""name"": ""Cat Crimes"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 360982, ""name"": ""English edition"", ""year"": 2017, ""publishers"": [""ThinkFun""], ""languages"": [""English""], ""score"": 3}]",IMG_4499.jpeg;IMG_4505.jpeg,CAT CRIMES -a Gentle Rain,268163,A Gentle Rain,2019,boardgame,merged,268164,Bloom Edition,version_auto,"[{""bgg_id"": 268163, ""name"": ""A Gentle Rain"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 268164, ""name"": ""Bloom Edition"", ""year"": 2021, ""publishers"": [""Mondo Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg;IMG_4507.jpeg,a Gentle Rain -SANTORINI,194655,Santorini,2016,boardgame,merged,,,version_unknown,"[{""bgg_id"": 194655, ""name"": ""Santorini"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4502.jpeg;IMG_4506.jpeg;IMG_4507.jpeg,SANTORINI -Utter Nonsense!,181254,Utter Nonsense,2015,boardgame,merged,,,version_unknown,"[{""bgg_id"": 181254, ""name"": ""Utter Nonsense"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg,Utter Nonsense! -EXPLODING KITTENS,172225,Exploding Kittens,2015,boardgame,merged,,,version_unknown,"[{""bgg_id"": 172225, ""name"": ""Exploding Kittens"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4505.jpeg,EXPLODING KITTENS -Herbaceous,195314,Herbaceous,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 195314, ""name"": ""Herbaceous"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4507.jpeg, -ALICE IS MISSING: SILENT FALLS EXPANSION,,,,,unmatched,,,,[],[],IMG_4566.jpeg, -ALICE IS MISSING: A SILENT ROLE PLAYING GAME,,,,,unmatched,,,,[],[],IMG_4566.jpeg, +title_raw,bgg_id,bgg_name,year,type,match_status,version_id,version_name,version_status,candidates_json,version_candidates_json,source_photos,merged_into,dedupe_veto +ROYALS,165986,Royals,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 165986, ""name"": ""Royals"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg,, +CAT CRIMES,235096,Cat Crimes,2017,boardgame,auto,360982,English edition,version_auto,"[{""bgg_id"": 235096, ""name"": ""Cat Crimes"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 360982, ""name"": ""English edition"", ""year"": 2017, ""publishers"": [""ThinkFun""], ""languages"": [""English""], ""score"": 3}]",IMG_4499.jpeg;IMG_4505.jpeg,, +COSMIC ENCOUNTER,40529,Cosmic Encounter,2008,boardgame,auto,,,version_unknown,"[{""bgg_id"": 39, ""name"": ""Cosmic Encounter"", ""year"": 1977, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 7000, ""rank"": 800}, {""bgg_id"": 40529, ""name"": ""Cosmic Encounter"", ""year"": 2008, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 75000, ""rank"": 150}]",[],IMG_4499.jpeg,, +SHERIFF OF NOTTINGHAM,157969,Sheriff of Nottingham,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 157969, ""name"": ""Sheriff of Nottingham"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg,, +5 MINUTE DUNGEON,207830,5-Minute Dungeon,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 207830, ""name"": ""5-Minute Dungeon"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4499.jpeg,, +Britannia,240,Britannia,1986,boardgame,auto,25668,Avalon Hill English edition,version_auto,"[{""bgg_id"": 240, ""name"": ""Britannia"", ""year"": 1986, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 25668, ""name"": ""Avalon Hill English edition"", ""year"": 1986, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,, +CIVILIZATION Game of the Heroic Age - The Dawn of History 8000 BC to 250 BC,71,Civilization,1980,boardgame,auto,71001,Avalon Hill English edition,version_auto,"[{""bgg_id"": 71, ""name"": ""Civilization"", ""year"": 1980, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 31.2, ""owned"": null, ""rank"": null}]","[{""version_id"": 71001, ""name"": ""Avalon Hill English edition"", ""year"": 1981, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,, +"ADVANCED CIVILIZATION Game Expansion of the Heroic Age - Featuring New Civilization, Commodity, and Calamity Cards",177,Advanced Civilization,1991,boardgameexpansion,auto,177001,Avalon Hill English edition,version_auto,"[{""bgg_id"": 177, ""name"": ""Advanced Civilization"", ""year"": 1991, ""type"": ""boardgameexpansion"", ""exact"": true, ""fuzzy"": 33.1, ""owned"": null, ""rank"": null}]","[{""version_id"": 177001, ""name"": ""Avalon Hill English edition"", ""year"": 1991, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,, +DOCTOR WHO The Game of Time & Space,3090,Doctor Who: The Game of Time & Space,1980,boardgame,auto,309001,Games Workshop English edition,version_auto,"[{""bgg_id"": 3090, ""name"": ""Doctor Who: The Game of Time & Space"", ""year"": 1980, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 309001, ""name"": ""Games Workshop English edition"", ""year"": 1980, ""publishers"": [""Games Workshop Ltd.""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,, +SORCERER The Game of Magical Conflict,3585,Sorcerer,1975,boardgame,auto,358501,SPI Designer's Edition,version_auto,"[{""bgg_id"": 3585, ""name"": ""Sorcerer"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 39.0, ""owned"": 900, ""rank"": 5000}, {""bgg_id"": 244115, ""name"": ""Sorcerer"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 39.0, ""owned"": 6000, ""rank"": 1500}]","[{""version_id"": 358501, ""name"": ""SPI Designer's Edition"", ""year"": 1975, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 358502, ""name"": ""SPI folio edition"", ""year"": 1975, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,, +STARFORCE ALPHA CENTAURI Interstellar Conflict in the 25th Century,2524,StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century,1974,boardgame,auto,252401,SPI Designer's Edition,version_auto,"[{""bgg_id"": 2524, ""name"": ""StarForce 'Alpha Centauri': Interstellar Conflict in the 25th Century"", ""year"": 1974, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 252401, ""name"": ""SPI Designer's Edition"", ""year"": 1974, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 252402, ""name"": ""SPI folio edition"", ""year"": 1974, ""publishers"": [""Simulations Publications, Inc. (SPI)""], ""languages"": [""English""], ""score"": 3}]",IMG_4500.jpeg,, +SKIP-BO,1078,Skip-Bo,1967,boardgame,auto,,,version_unknown,"[{""bgg_id"": 1078, ""name"": ""Skip-Bo"", ""year"": 1967, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4542.jpeg,, +a Gentle Rain,268163,A Gentle Rain,2019,boardgame,auto,268164,Bloom Edition,version_auto,"[{""bgg_id"": 268163, ""name"": ""A Gentle Rain"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 268164, ""name"": ""Bloom Edition"", ""year"": 2021, ""publishers"": [""Mondo Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg;IMG_4507.jpeg,, +PATCH WORK,163412,Patchwork,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 163412, ""name"": ""Patchwork"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 94.7, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg,, +SANTORINI,194655,Santorini,2016,boardgame,auto,,,version_unknown,"[{""bgg_id"": 194655, ""name"": ""Santorini"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4502.jpeg;IMG_4506.jpeg;IMG_4507.jpeg,, +Utter Nonsense!,181254,Utter Nonsense,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 181254, ""name"": ""Utter Nonsense"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg,, +Cards Against Humanity,50381,Cards Against Humanity,2009,boardgame,auto,,,version_unknown,"[{""bgg_id"": 50381, ""name"": ""Cards Against Humanity"", ""year"": 2009, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg,, +Joking Hazard,193621,Joking Hazard,2016,boardgame,auto,19362101,First edition,version_auto,"[{""bgg_id"": 193621, ""name"": ""Joking Hazard"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 19362101, ""name"": ""First edition"", ""year"": 2016, ""publishers"": [""Cyanide & Happiness""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg;IMG_4502.jpeg,, +EXPLODING KITTENS,172225,Exploding Kittens,2015,boardgame,auto,,,version_unknown,"[{""bgg_id"": 172225, ""name"": ""Exploding Kittens"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4505.jpeg,, +Hebarceos,195314,Herbaceous,2017,boardgame,merged,,,version_unknown,[],[],IMG_4502.jpeg,Herbaceous, +Wiz-War,104710,Wiz-War,2012,boardgame,approved,,,version_unknown,"[{""bgg_id"": 1218, ""name"": ""Wiz-War"", ""year"": 1983, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 4000, ""rank"": 2500}, {""bgg_id"": 104710, ""name"": ""Wiz-War"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 8000, ""rank"": 1200}]",[],IMG_4502.jpeg;IMG_4504.jpeg;IMG_4528.jpeg,, +SCRAWL,218866,Scrawl,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 218866, ""name"": ""Scrawl"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,, +FLAT TOP,2529,Flat Top,1977,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2529, ""name"": ""Flat Top"", ""year"": 1977, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,, +Diplomacy,483,Diplomacy,1959,boardgame,auto,,,version_unknown,"[{""bgg_id"": 483, ""name"": ""Diplomacy"", ""year"": 1959, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,, +Age of Renaissance,26,Age of Renaissance,1996,boardgame,auto,2601,Avalon Hill English edition,version_auto,"[{""bgg_id"": 26, ""name"": ""Age of Renaissance"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 2601, ""name"": ""Avalon Hill English edition"", ""year"": 1996, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,, +History of the World,224,History of the World,1991,boardgame,auto,22401,Avalon Hill English edition,version_auto,"[{""bgg_id"": 224, ""name"": ""History of the World"", ""year"": 1991, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 22401, ""name"": ""Avalon Hill English edition"", ""year"": 1993, ""publishers"": [""The Avalon Hill Game Co""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,, +DUNGEON!,1339,Dungeon!,1975,boardgame,auto,133903,TSR New Dungeon edition,version_approved,"[{""bgg_id"": 1339, ""name"": ""Dungeon!"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 133901, ""name"": ""TSR first edition"", ""year"": 1975, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133902, ""name"": ""TSR second edition"", ""year"": 1980, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133903, ""name"": ""TSR New Dungeon edition"", ""year"": 1989, ""publishers"": [""TSR""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,, +CAT CRIMES,235096,Cat Crimes,2017,boardgame,merged,360982,English edition,version_auto,"[{""bgg_id"": 235096, ""name"": ""Cat Crimes"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 360982, ""name"": ""English edition"", ""year"": 2017, ""publishers"": [""ThinkFun""], ""languages"": [""English""], ""score"": 3}]",IMG_4499.jpeg;IMG_4505.jpeg,CAT CRIMES, +a Gentle Rain,268163,A Gentle Rain,2019,boardgame,merged,268164,Bloom Edition,version_auto,"[{""bgg_id"": 268163, ""name"": ""A Gentle Rain"", ""year"": 2019, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 268164, ""name"": ""Bloom Edition"", ""year"": 2021, ""publishers"": [""Mondo Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4501.jpeg;IMG_4507.jpeg,a Gentle Rain, +SANTORINI,194655,Santorini,2016,boardgame,merged,,,version_unknown,"[{""bgg_id"": 194655, ""name"": ""Santorini"", ""year"": 2016, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4502.jpeg;IMG_4506.jpeg;IMG_4507.jpeg,SANTORINI, +Utter Nonsense!,181254,Utter Nonsense,2015,boardgame,merged,,,version_unknown,"[{""bgg_id"": 181254, ""name"": ""Utter Nonsense"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg,Utter Nonsense!, +EXPLODING KITTENS,172225,Exploding Kittens,2015,boardgame,merged,,,version_unknown,"[{""bgg_id"": 172225, ""name"": ""Exploding Kittens"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4501.jpeg;IMG_4505.jpeg,EXPLODING KITTENS, +Herbaceous,195314,Herbaceous,2017,boardgame,auto,,,version_unknown,"[{""bgg_id"": 195314, ""name"": ""Herbaceous"", ""year"": 2017, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4507.jpeg,, +ALICE IS MISSING: SILENT FALLS EXPANSION,400002,Alice Is Missing: Silent Falls Expansion,2023,rpgitem,auto,,,version_unknown,"[{""bgg_id"": 400002, ""name"": ""Alice Is Missing: Silent Falls Expansion"", ""year"": 2023, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4566.jpeg,, +ALICE IS MISSING: A SILENT ROLE PLAYING GAME,400001,Alice Is Missing: A Silent Role Playing Game,2020,rpgitem,auto,,,version_unknown,"[{""bgg_id"": 400001, ""name"": ""Alice Is Missing: A Silent Role Playing Game"", ""year"": 2020, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4566.jpeg,, diff --git a/data/to_add.csv b/data/to_add.csv index 45fd2f7..b680301 100644 --- a/data/to_add.csv +++ b/data/to_add.csv @@ -1,9 +1,9 @@ -bgg_id,bgg_name,year,type,version_id,version_name,title_raw,source_photos -235096,Cat Crimes,2017,boardgame,360982,English edition,CAT CRIMES,IMG_4499.jpeg -1078,Skip-Bo,1967,boardgame,,,SKIP-BO,IMG_4501.jpeg -268163,A Gentle Rain,2019,boardgame,268164,Bloom Edition,a Gentle Rain,IMG_4501.jpeg -163412,Patchwork,2014,boardgame,,,PATCH WORK,IMG_4501.jpeg -181254,Utter Nonsense,2015,boardgame,,,Utter Nonsense!,IMG_4501.jpeg -50381,Cards Against Humanity,2009,boardgame,,,Cards Against Humanity,IMG_4501.jpeg -195314,Herbaceous,2017,boardgame,,,Hebarceos,IMG_4502.jpeg -218866,Scrawl,2017,boardgame,,,SCRAWL,IMG_4502.jpeg +bgg_id,bgg_name,year,type,version_id,version_name,title_raw,source_photos,second_copy +235096,Cat Crimes,2017,boardgame,360982,English edition,CAT CRIMES,IMG_4499.jpeg;IMG_4505.jpeg, +268163,A Gentle Rain,2019,boardgame,268164,Bloom Edition,a Gentle Rain,IMG_4501.jpeg;IMG_4507.jpeg, +1078,Skip-Bo,1967,boardgame,,,SKIP-BO,IMG_4501.jpeg;IMG_4542.jpeg, +163412,Patchwork,2014,boardgame,,,PATCH WORK,IMG_4501.jpeg, +181254,Utter Nonsense,2015,boardgame,,,Utter Nonsense!,IMG_4501.jpeg;IMG_4507.jpeg;IMG_4556.jpeg, +50381,Cards Against Humanity,2009,boardgame,,,Cards Against Humanity,IMG_4501.jpeg, +218866,Scrawl,2017,boardgame,,,SCRAWL,IMG_4502.jpeg, +195314,Herbaceous,2017,boardgame,,,Herbaceous,IMG_4502.jpeg;IMG_4507.jpeg, diff --git a/data/to_update.csv b/data/to_update.csv index 4767a43..0f2c24a 100644 --- a/data/to_update.csv +++ b/data/to_update.csv @@ -8,3 +8,4 @@ collid,bgg_id,bgg_name,version_id,version_name 53430387,193621,Joking Hazard,19362101,First edition 53429831,26,Age of Renaissance,2601,Avalon Hill English edition 53429816,224,History of the World,22401,Avalon Hill English edition +53430097,1339,Dungeon!,133903,TSR New Dungeon edition diff --git a/scripts/write_photo_fixtures.py b/scripts/write_photo_fixtures.py index 7b013d2..25c882e 100644 --- a/scripts/write_photo_fixtures.py +++ b/scripts/write_photo_fixtures.py @@ -110,6 +110,19 @@ SEARCHES: dict[str, list[tuple]] = { "ALICE IS": [], } +# rpgitem-search results for titles that live on RPGGeek, not BGG. Ids are +# SYNTHETIC like everything else here; the marker guards them. Queries not +# listed get an empty rpgitem stub automatically. +RPG = "rpgitem" +RPG_SEARCHES: dict[str, list[tuple]] = { + "ALICE IS MISSING: A SILENT ROLE PLAYING GAME": [ + (400001, "Alice Is Missing: A Silent Role Playing Game", 2020, RPG, "primary") + ], + "ALICE IS MISSING: SILENT FALLS EXPANSION": [ + (400002, "Alice Is Missing: Silent Falls Expansion", 2023, RPG, "primary") + ], +} + # ids-param -> [(bgg_id, name, year, type, owned, rank, [publishers])] THING_STATS: dict[str, list[tuple]] = { "39,40529": [ @@ -144,6 +157,8 @@ THING_STATS: dict[str, list[tuple]] = { # bgg_id -> [(version_id, name, year, [publishers], [languages])] VERSIONS: dict[int, list[tuple]] = { + 400001: [], # Alice Is Missing (rpgitem): no cataloged versions in stubs + 400002: [], 235096: [(360982, "English edition", 2017, ["ThinkFun"], ["English"])], 240: [ ( @@ -344,6 +359,8 @@ def main() -> None: for query, results in SEARCHES.items(): key = cache_key("search", {"query": query, "type": SEARCH_TYPES}) files[key] = search_xml(results) + rpg_key = cache_key("search", {"query": query, "type": "rpgitem"}) + files[rpg_key] = search_xml(RPG_SEARCHES.get(query, [])) for ids, things in THING_STATS.items(): files[cache_key("thing", {"id": ids, "stats": "1"})] = stats_xml(things) for bgg_id, versions in VERSIONS.items(): diff --git a/scripts/write_stub_fixtures.py b/scripts/write_stub_fixtures.py index 03a1441..249038b 100644 --- a/scripts/write_stub_fixtures.py +++ b/scripts/write_stub_fixtures.py @@ -106,6 +106,10 @@ def main() -> None: key = cache_key("search", {"query": query, "type": SEARCH_TYPES}) total = items.count("<item ") (FIXTURE_CACHE / key).write_text(f'<items total="{total}">{items}</items>') + # resolve falls back to an rpgitem search whenever the board-game + # search runs dry: every known query needs an (empty) answer there + rpg_key = cache_key("search", {"query": query, "type": "rpgitem"}) + (FIXTURE_CACHE / rpg_key).write_text('<items total="0"></items>') for (ids, flavor), xml in THINGS.items(): key = cache_key("thing", {"id": ids, flavor: "1"}) (FIXTURE_CACHE / key).write_text(xml) diff --git a/src/bggpipe/diff.py b/src/bggpipe/diff.py index 1e0f165..9ba3cbc 100644 --- a/src/bggpipe/diff.py +++ b/src/bggpipe/diff.py @@ -62,6 +62,7 @@ class DiffResult: to_add: list[dict] = field(default_factory=list) to_update: list[dict] = field(default_factory=list) already_owned: list[str] = field(default_factory=list) # title_raw + local_only: list[str] = field(default_factory=list) # RPGs etc: never uploaded second_copies: list[str] = field(default_factory=list) # notes for adds disagreements: list[str] = field(default_factory=list) # report-only unseen: list[CollectionItem] = field(default_factory=list) @@ -137,6 +138,11 @@ def compute_diff(rows: list[dict], collection: list[CollectionItem]) -> DiffResu if status not in RECOGNIZED_MATCH_STATUSES or not row["bgg_id"]: result.pending.append(row["title_raw"]) continue + if row["type"] == "rpgitem": + # a real, identified game — but it lives on RPGGeek, not in the + # BGG collection: a library citizen only, never queued + result.local_only.append(row["title_raw"]) + continue result.recognized += 1 recognized.append(row) if by_object.get(int(row["bgg_id"])): @@ -309,11 +315,14 @@ def run_diff(cfg: Config, *, client: BGGClient | None = None) -> DiffResult: atomic_write_csv(cfg.to_update_path, TO_UPDATE_COLUMNS, result.to_update) merged_note = f" · {result.merged} merged duplicate(s)" if result.merged else "" + local_note = ( + f" · {len(result.local_only)} local-only (RPGs)" if result.local_only else "" + ) typer.echo( f"\n{result.recognized} recognized · {len(result.already_owned)} already " f"owned · {len(result.to_add)} to add · {len(result.to_update)} version " f"update(s) · {len(result.pending)} pending review · " - f"{result.rejected} rejected{merged_note}" + f"{result.rejected} rejected{merged_note}{local_note}" ) if result.second_copies: typer.echo("\nSecond copies (verify these on the dry run before upload):") diff --git a/src/bggpipe/resolve.py b/src/bggpipe/resolve.py index 13d6d38..76e86ab 100644 --- a/src/bggpipe/resolve.py +++ b/src/bggpipe/resolve.py @@ -207,7 +207,11 @@ def _truncation_heads(title_raw: str) -> list[str]: def _plausible_candidates( - client: BGGClient, entry: TitleEntry, query: str, head_normalized: str = "" + client: BGGClient, + entry: TitleEntry, + query: str, + head_normalized: str = "", + types: str | None = None, ) -> list[Candidate]: """Search BGG and keep plausible candidates, one per id: exact-normalized or fuzzy>=90 against the FULL title, or — on truncated retries — exact @@ -218,7 +222,8 @@ def _plausible_candidates( if not entry.title_normalized: return [] by_id: dict[int, Candidate] = {} - for result in client.search(query): + results = client.search(query, types) if types else client.search(query) + for result in results: norm = normalize_title(result.name) exact = norm == entry.title_normalized fuzzy = fuzz.token_sort_ratio(norm, entry.title_normalized) @@ -396,6 +401,21 @@ def resolve_entry(client: BGGClient, entry: TitleEntry) -> MatchRow: cands = _plausible_candidates(client, entry, head, normalize_title(head)) if cands: break + if not cands: + # Not a board game? RPGs live in the same geekdo database under + # type=rpgitem (same API, same token). A hit becomes a LOCAL + # library citizen: identified and enriched, never uploaded (diff + # routes rpgitem rows to local_only). + cands = _plausible_candidates( + client, entry, entry.title_raw, types="rpgitem" + ) + if not cands: + for head in _truncation_heads(entry.title_raw): + cands = _plausible_candidates( + client, entry, head, normalize_title(head), types="rpgitem" + ) + if cands: + break row = _classify(client, entry, cands) if row.match_status == "auto": resolve_version(client, entry, row) diff --git a/src/bggpipe/static/app.css b/src/bggpipe/static/app.css index 62343d4..3057979 100644 --- a/src/bggpipe/static/app.css +++ b/src/bggpipe/static/app.css @@ -428,6 +428,9 @@ button.danger { color: var(--stop-ink); border-color: var(--stop); background: # padding: .15rem .5rem; border-radius: 6px; } .filterbar { display: flex; gap: .6rem; margin-bottom: 1rem; flex-wrap: wrap; } +.filterbar button[aria-pressed="true"] { + background: var(--accent); border-color: var(--ink); color: #fff; +} .filterbar input[type=search] { font: inherit; padding: .35rem .7rem; min-width: 16rem; border: var(--line); border-radius: var(--radius); background: #fff; diff --git a/src/bggpipe/templates/pages/catalog.html b/src/bggpipe/templates/pages/catalog.html index 7e1f1ca..ceca5c9 100644 --- a/src/bggpipe/templates/pages/catalog.html +++ b/src/bggpipe/templates/pages/catalog.html @@ -21,7 +21,8 @@ function render() { <td class="t">${esc(c.title_raw)}</td> <td>${statusChip(c)}</td> <td class="meta">${c.bgg_name ? esc(c.bgg_name) + (c.bgg_id ? " · " + esc(c.bgg_id) : "") : ""} - ${c.version_name ? " · " + esc(c.version_name) : ""}</td> + ${c.version_name ? " · " + esc(c.version_name) : ""} + ${c.type === "rpgitem" ? ` <span class="chip open">RPG · local only</span>` : ""}</td> <td class="meta">${c.photos.map(p => `<a href="/photos/view/${encodeURIComponent(p)}">${esc(p)}</a>` ).join(", ")}</td> diff --git a/src/bggpipe/templates/pages/library.html b/src/bggpipe/templates/pages/library.html index 38394fb..a6642fe 100644 --- a/src/bggpipe/templates/pages/library.html +++ b/src/bggpipe/templates/pages/library.html @@ -1,12 +1,16 @@ <h1>Library</h1> <div class="pagebar"><span id="libcount"></span></div> -<div class="filterbar"> +<div class="filterbar" role="group" aria-label="filter by kind"> <input type="search" id="libsearch" placeholder="search your games…" aria-label="search library"> + <button type="button" data-kind="" aria-pressed="true">All</button> + <button type="button" data-kind="boardgame" aria-pressed="false">Board games</button> + <button type="button" data-kind="rpgitem" aria-pressed="false">RPGs</button> </div> <div id="libbody"></div> <script> "use strict"; let GAMES = []; +let KIND = ""; // "" = all; "boardgame" also covers expansions function gameCard(g) { const art = g.thumbnail || g.image; @@ -22,18 +26,25 @@ function gameCard(g) { ? `<img src="${esc(art)}" alt="" loading="lazy">` : `<div class="noart" aria-hidden="true">${esc((g.name || "?").charAt(0).toUpperCase())}</div>`} <div class="info"> - <div class="gname">${esc(g.name)}${g.year ? ` <span class="meta">(${esc(g.year)})</span>` : ""}</div> + <div class="gname">${esc(g.name)}${g.year ? ` <span class="meta">(${esc(g.year)})</span>` : ""} + ${g.type === "rpgitem" ? `<span class="chip open">RPG · local only</span>` : ""}</div> <div class="gmeta">${[players, time, weight, rank].filter(Boolean).map(esc).join(" · ")}</div> ${g.version ? `<div class="gmeta">${esc(g.version.name || "")}</div>` : ""} </div> </article>`; } +function matchesKind(g) { + if (!KIND) return true; + if (KIND === "boardgame") return g.type !== "rpgitem"; + return g.type === "rpgitem"; +} + function render() { const q = document.getElementById("libsearch").value.trim().toLowerCase(); - const rows = q - ? GAMES.filter(g => (g.name + " " + (g.designers || []).join(" ")).toLowerCase().includes(q)) - : GAMES; + let rows = GAMES.filter(matchesKind); + if (q) rows = rows.filter(g => + (g.name + " " + (g.designers || []).join(" ")).toLowerCase().includes(q)); document.getElementById("libcount").innerHTML = `<b>${rows.length}</b> of <b>${GAMES.length}</b> game(s)`; document.getElementById("libbody").innerHTML = rows.length @@ -56,6 +67,12 @@ async function refresh() { } document.getElementById("libsearch").addEventListener("input", render); +document.querySelectorAll("[data-kind]").forEach(b => b.addEventListener("click", () => { + KIND = b.dataset.kind; + document.querySelectorAll("[data-kind]").forEach(o => + o.setAttribute("aria-pressed", String(o === b))); + render(); +})); refresh().catch(err => errorBanner(err.message || err)); pollLoop(refresh, 10000, () => showBanner("")); </script> diff --git a/src/bggpipe/webreview.py b/src/bggpipe/webreview.py index 02657eb..66f5193 100644 --- a/src/bggpipe/webreview.py +++ b/src/bggpipe/webreview.py @@ -367,6 +367,7 @@ def create_app( "confidence": entry.confidence, "photos": list(entry.source_photos), "status": row["match_status"] if row else "awaiting_resolve", + "type": row["type"] if row else "", "bgg_id": row["bgg_id"] if row else "", "bgg_name": row["bgg_name"] if row else "", "version_name": row["version_name"] if row else "", diff --git a/tests/fixtures/bgg_cache/search_query=5-MINUTE-DUNGEON-type=rpgitem_58faea4564.xml b/tests/fixtures/bgg_cache/search_query=5-MINUTE-DUNGEON-type=rpgitem_58faea4564.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=5-MINUTE-DUNGEON-type=rpgitem_58faea4564.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-Game-Expansion-of-the-Heroic-Age---Featuring-New-Civ_7c5116c957.xml b/tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-Game-Expansion-of-the-Heroic-Age---Featuring-New-Civ_7c5116c957.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-Game-Expansion-of-the-Heroic-Age---Featuring-New-Civ_7c5116c957.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-Game-Expansion-of-the-Heroic-Age-type=rpgitem_8704f88656.xml b/tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-Game-Expansion-of-the-Heroic-Age-type=rpgitem_8704f88656.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-Game-Expansion-of-the-Heroic-Age-type=rpgitem_8704f88656.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-type=rpgitem_436f8cfac3.xml b/tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-type=rpgitem_436f8cfac3.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=ADVANCED-CIVILIZATION-type=rpgitem_436f8cfac3.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-A-SILENT-ROLE-PLAYING-GAME-type=rpgitem_8efe1bd6f0.xml b/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-A-SILENT-ROLE-PLAYING-GAME-type=rpgitem_8efe1bd6f0.xml new file mode 100644 index 0000000..3868c9e --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-A-SILENT-ROLE-PLAYING-GAME-type=rpgitem_8efe1bd6f0.xml @@ -0,0 +1 @@ +<items total="1"><item type="rpgitem" id="400001"><name type="primary" value="Alice Is Missing: A Silent Role Playing Game"/><yearpublished value="2020"/></item></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-A-SILENT-ROLE-PLAYING-type=rpgitem_daff8eceef.xml b/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-A-SILENT-ROLE-PLAYING-type=rpgitem_daff8eceef.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-A-SILENT-ROLE-PLAYING-type=rpgitem_daff8eceef.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-SILENT-FALLS-EXPANSION-type=rpgitem_82261134d9.xml b/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-SILENT-FALLS-EXPANSION-type=rpgitem_82261134d9.xml new file mode 100644 index 0000000..2988563 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-3A-SILENT-FALLS-EXPANSION-type=rpgitem_82261134d9.xml @@ -0,0 +1 @@ +<items total="1"><item type="rpgitem" id="400002"><name type="primary" value="Alice Is Missing: Silent Falls Expansion"/><yearpublished value="2023"/></item></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-type=rpgitem_088ab34fc2.xml b/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-type=rpgitem_088ab34fc2.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=ALICE-IS-MISSING-type=rpgitem_088ab34fc2.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=ALICE-IS-type=rpgitem_f2de3eea31.xml b/tests/fixtures/bgg_cache/search_query=ALICE-IS-type=rpgitem_f2de3eea31.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=ALICE-IS-type=rpgitem_f2de3eea31.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Age-of-Renaissance-type=rpgitem_83ec5d7426.xml b/tests/fixtures/bgg_cache/search_query=Age-of-Renaissance-type=rpgitem_83ec5d7426.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Age-of-Renaissance-type=rpgitem_83ec5d7426.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Blorvath-3A-Quest-of-the-Zzyzx-type=rpgitem_288971e44c.xml b/tests/fixtures/bgg_cache/search_query=Blorvath-3A-Quest-of-the-Zzyzx-type=rpgitem_288971e44c.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Blorvath-3A-Quest-of-the-Zzyzx-type=rpgitem_288971e44c.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Blorvath-type=rpgitem_ad250413dc.xml b/tests/fixtures/bgg_cache/search_query=Blorvath-type=rpgitem_ad250413dc.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Blorvath-type=rpgitem_ad250413dc.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Britannia-type=rpgitem_78177b4e85.xml b/tests/fixtures/bgg_cache/search_query=Britannia-type=rpgitem_78177b4e85.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Britannia-type=rpgitem_78177b4e85.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=CAT-CRIMES-type=rpgitem_3d56b6309b.xml b/tests/fixtures/bgg_cache/search_query=CAT-CRIMES-type=rpgitem_3d56b6309b.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=CAT-CRIMES-type=rpgitem_3d56b6309b.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=CIVILIZATION-Game-of-the-Heroic-Age---The-Dawn-of-History-8000-BC-to-250-B_4292e34c30.xml b/tests/fixtures/bgg_cache/search_query=CIVILIZATION-Game-of-the-Heroic-Age---The-Dawn-of-History-8000-BC-to-250-B_4292e34c30.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=CIVILIZATION-Game-of-the-Heroic-Age---The-Dawn-of-History-8000-BC-to-250-B_4292e34c30.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=CIVILIZATION-Game-of-the-Heroic-Age-type=rpgitem_05fdc9d88b.xml b/tests/fixtures/bgg_cache/search_query=CIVILIZATION-Game-of-the-Heroic-Age-type=rpgitem_05fdc9d88b.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=CIVILIZATION-Game-of-the-Heroic-Age-type=rpgitem_05fdc9d88b.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=CIVILIZATION-type=rpgitem_5e9e2b0726.xml b/tests/fixtures/bgg_cache/search_query=CIVILIZATION-type=rpgitem_5e9e2b0726.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=CIVILIZATION-type=rpgitem_5e9e2b0726.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=COSMIC-ENCOUNTER-type=rpgitem_14e74aa71f.xml b/tests/fixtures/bgg_cache/search_query=COSMIC-ENCOUNTER-type=rpgitem_14e74aa71f.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=COSMIC-ENCOUNTER-type=rpgitem_14e74aa71f.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Caf-C3-A9-International-type=rpgitem_957f2522ae.xml b/tests/fixtures/bgg_cache/search_query=Caf-C3-A9-International-type=rpgitem_957f2522ae.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Caf-C3-A9-International-type=rpgitem_957f2522ae.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Cards-Against-Humanity-type=rpgitem_04b2bcdf24.xml b/tests/fixtures/bgg_cache/search_query=Cards-Against-Humanity-type=rpgitem_04b2bcdf24.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Cards-Against-Humanity-type=rpgitem_04b2bcdf24.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Catan-type=rpgitem_63fb6e5035.xml b/tests/fixtures/bgg_cache/search_query=Catan-type=rpgitem_63fb6e5035.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Catan-type=rpgitem_63fb6e5035.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Citadels-type=rpgitem_22dea38263.xml b/tests/fixtures/bgg_cache/search_query=Citadels-type=rpgitem_22dea38263.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Citadels-type=rpgitem_22dea38263.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=DOCTOR-WHO-The-Game-of-Time-26-Space-type=rpgitem_48015c5934.xml b/tests/fixtures/bgg_cache/search_query=DOCTOR-WHO-The-Game-of-Time-26-Space-type=rpgitem_48015c5934.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=DOCTOR-WHO-The-Game-of-Time-26-Space-type=rpgitem_48015c5934.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=DUNGEON-21-type=rpgitem_95d15d03fd.xml b/tests/fixtures/bgg_cache/search_query=DUNGEON-21-type=rpgitem_95d15d03fd.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=DUNGEON-21-type=rpgitem_95d15d03fd.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Diplomacy-type=rpgitem_0747a2387d.xml b/tests/fixtures/bgg_cache/search_query=Diplomacy-type=rpgitem_0747a2387d.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Diplomacy-type=rpgitem_0747a2387d.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Dungeon-21-type=rpgitem_3bf546910a.xml b/tests/fixtures/bgg_cache/search_query=Dungeon-21-type=rpgitem_3bf546910a.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Dungeon-21-type=rpgitem_3bf546910a.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=EXPLODING-KITTENS-type=rpgitem_49ed26a640.xml b/tests/fixtures/bgg_cache/search_query=EXPLODING-KITTENS-type=rpgitem_49ed26a640.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=EXPLODING-KITTENS-type=rpgitem_49ed26a640.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=FLAT-TOP-type=rpgitem_c1e66fb254.xml b/tests/fixtures/bgg_cache/search_query=FLAT-TOP-type=rpgitem_c1e66fb254.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=FLAT-TOP-type=rpgitem_c1e66fb254.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Hebarceos-type=rpgitem_07740d9a32.xml b/tests/fixtures/bgg_cache/search_query=Hebarceos-type=rpgitem_07740d9a32.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Hebarceos-type=rpgitem_07740d9a32.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Herbaceous-type=rpgitem_d78832e3ed.xml b/tests/fixtures/bgg_cache/search_query=Herbaceous-type=rpgitem_d78832e3ed.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Herbaceous-type=rpgitem_d78832e3ed.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=History-of-the-World-type=rpgitem_7928f17f52.xml b/tests/fixtures/bgg_cache/search_query=History-of-the-World-type=rpgitem_7928f17f52.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=History-of-the-World-type=rpgitem_7928f17f52.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Joking-Hazard-type=rpgitem_3550758066.xml b/tests/fixtures/bgg_cache/search_query=Joking-Hazard-type=rpgitem_3550758066.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Joking-Hazard-type=rpgitem_3550758066.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=PATCH-WORK-type=rpgitem_afb691fd45.xml b/tests/fixtures/bgg_cache/search_query=PATCH-WORK-type=rpgitem_afb691fd45.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=PATCH-WORK-type=rpgitem_afb691fd45.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=ROYALS-type=rpgitem_740855ed66.xml b/tests/fixtures/bgg_cache/search_query=ROYALS-type=rpgitem_740855ed66.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=ROYALS-type=rpgitem_740855ed66.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=SANTORINI-type=rpgitem_b3713fc18f.xml b/tests/fixtures/bgg_cache/search_query=SANTORINI-type=rpgitem_b3713fc18f.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=SANTORINI-type=rpgitem_b3713fc18f.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=SCRAWL-type=rpgitem_28917ca749.xml b/tests/fixtures/bgg_cache/search_query=SCRAWL-type=rpgitem_28917ca749.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=SCRAWL-type=rpgitem_28917ca749.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=SHERIFF-OF-NOTTINGHAM-type=rpgitem_77cf1f8445.xml b/tests/fixtures/bgg_cache/search_query=SHERIFF-OF-NOTTINGHAM-type=rpgitem_77cf1f8445.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=SHERIFF-OF-NOTTINGHAM-type=rpgitem_77cf1f8445.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=SKIP-BO-type=rpgitem_3f9e6315a6.xml b/tests/fixtures/bgg_cache/search_query=SKIP-BO-type=rpgitem_3f9e6315a6.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=SKIP-BO-type=rpgitem_3f9e6315a6.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=SORCERER-The-Game-of-Magical-Conflict-type=rpgitem_2ba94ce51c.xml b/tests/fixtures/bgg_cache/search_query=SORCERER-The-Game-of-Magical-Conflict-type=rpgitem_2ba94ce51c.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=SORCERER-The-Game-of-Magical-Conflict-type=rpgitem_2ba94ce51c.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=SORCERER-type=rpgitem_0a90b76555.xml b/tests/fixtures/bgg_cache/search_query=SORCERER-type=rpgitem_0a90b76555.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=SORCERER-type=rpgitem_0a90b76555.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=STARFORCE-ALPHA-CENTAURI-Interstellar-Conflict-in-the-25th-Century-type=rp_d8afe77786.xml b/tests/fixtures/bgg_cache/search_query=STARFORCE-ALPHA-CENTAURI-Interstellar-Conflict-in-the-25th-Century-type=rp_d8afe77786.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=STARFORCE-ALPHA-CENTAURI-Interstellar-Conflict-in-the-25th-Century-type=rp_d8afe77786.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=STARFORCE-ALPHA-type=rpgitem_5fc9118ed9.xml b/tests/fixtures/bgg_cache/search_query=STARFORCE-ALPHA-type=rpgitem_5fc9118ed9.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=STARFORCE-ALPHA-type=rpgitem_5fc9118ed9.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Utter-Nonsense-21-type=rpgitem_f01cc4ca29.xml b/tests/fixtures/bgg_cache/search_query=Utter-Nonsense-21-type=rpgitem_f01cc4ca29.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Utter-Nonsense-21-type=rpgitem_f01cc4ca29.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Wingspan-3A-European-Expansion-type=rpgitem_38f0ad0fff.xml b/tests/fixtures/bgg_cache/search_query=Wingspan-3A-European-Expansion-type=rpgitem_38f0ad0fff.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Wingspan-3A-European-Expansion-type=rpgitem_38f0ad0fff.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Wingspan-Europe-type=rpgitem_4387722048.xml b/tests/fixtures/bgg_cache/search_query=Wingspan-Europe-type=rpgitem_4387722048.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Wingspan-Europe-type=rpgitem_4387722048.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Wingspan-type=rpgitem_f93bee11fc.xml b/tests/fixtures/bgg_cache/search_query=Wingspan-type=rpgitem_f93bee11fc.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Wingspan-type=rpgitem_f93bee11fc.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=Wiz-War-type=rpgitem_3adf0a77c6.xml b/tests/fixtures/bgg_cache/search_query=Wiz-War-type=rpgitem_3adf0a77c6.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=Wiz-War-type=rpgitem_3adf0a77c6.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/search_query=a-Gentle-Rain-type=rpgitem_a3470f199b.xml b/tests/fixtures/bgg_cache/search_query=a-Gentle-Rain-type=rpgitem_a3470f199b.xml new file mode 100644 index 0000000..ba791c3 --- /dev/null +++ b/tests/fixtures/bgg_cache/search_query=a-Gentle-Rain-type=rpgitem_a3470f199b.xml @@ -0,0 +1 @@ +<items total="0"></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/thing_id=400001-versions=1_6c87ca2e4a.xml b/tests/fixtures/bgg_cache/thing_id=400001-versions=1_6c87ca2e4a.xml new file mode 100644 index 0000000..ebee33f --- /dev/null +++ b/tests/fixtures/bgg_cache/thing_id=400001-versions=1_6c87ca2e4a.xml @@ -0,0 +1 @@ +<items><item type="boardgame" id="400001"><name type="primary" value="game"/><versions></versions></item></items> \ No newline at end of file diff --git a/tests/fixtures/bgg_cache/thing_id=400002-versions=1_a82fe6afff.xml b/tests/fixtures/bgg_cache/thing_id=400002-versions=1_a82fe6afff.xml new file mode 100644 index 0000000..24e2613 --- /dev/null +++ b/tests/fixtures/bgg_cache/thing_id=400002-versions=1_a82fe6afff.xml @@ -0,0 +1 @@ +<items><item type="boardgame" id="400002"><name type="primary" value="game"/><versions></versions></item></items> \ No newline at end of file diff --git a/tests/test_diff.py b/tests/test_diff.py index 9f708f8..acc3c1b 100644 --- a/tests/test_diff.py +++ b/tests/test_diff.py @@ -397,3 +397,11 @@ def test_second_copy_flag_travels_on_exhausted_adds(): result = compute_diff(rows, [_item(13, 900, version_id=123)]) (added,) = result.to_add assert added["second_copy"] == "1" + + +def test_rpgitem_rows_are_local_only_never_queued(): + rpg = {**_match("ALICE IS MISSING", "400001"), "type": "rpgitem"} + result = compute_diff([rpg, _match("Catan", "13")], []) + assert result.local_only == ["ALICE IS MISSING"] + assert [r["bgg_id"] for r in result.to_add] == ["13"] # RPG never queued + assert result.recognized == 1 # RPGs aren't counted against the queue diff --git a/tests/test_resolve.py b/tests/test_resolve.py index 8e0b90a..866ecb8 100644 --- a/tests/test_resolve.py +++ b/tests/test_resolve.py @@ -672,3 +672,16 @@ def test_merged_into_chains_resolve_to_terminal_survivor(): dedupe_matches([x, y, w], []) assert y["match_status"] == "merged" and y["merged_into"] == "Wingspan" assert x["merged_into"] == "Wingspan" # chain collapsed + + +def test_rpg_falls_back_to_rpgitem_search(client): + # Alice Is Missing: absent from the board-game search, present as an + # rpgitem — same geekdo API, resolved as a local library citizen + entry = TitleEntry( + title_raw="ALICE IS MISSING: A SILENT ROLE PLAYING GAME", + title_normalized="alice is missing silent role playing game", + ) + row = resolve_entry(client, entry) + assert row.match_status == "auto" + assert row.type == "rpgitem" + assert row.bgg_id == 400001