From 9663f297021c8fafd5964714d754dbfd24de71d7 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Wed, 5 Aug 2026 23:58:58 -0400 Subject: [PATCH] Review can search a chosen database: BGG or RPGGeek, by hand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The automatic cascade only reaches RPGGeek when BGG's board-game search comes up empty — so every D&D box, which BGG does list as board games, can never find its RPGGeek entry no matter how many times it is reopened. Eric hit exactly that and settled for keeping them local. Match cards now carry an editable query with two buttons, "search BGG" and "search RPGGeek", which replace the row's ballot with whatever the chosen database returns (owned counts and ranks attached when available; if that stats call fails the results still stand and the degradation is reported). The TUI's (f) re-search falls back to RPGGeek automatically when the board-game search is empty. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g --- data/matches.csv | 12 +++--- src/bggpipe/review.py | 49 +++++++++++++++++++++++++ src/bggpipe/static/app.css | 3 ++ src/bggpipe/templates/pages/review.html | 16 ++++++++ src/bggpipe/webreview.py | 22 +++++++++++ tests/test_review.py | 27 ++++++++++++++ 6 files changed, 123 insertions(+), 6 deletions(-) diff --git a/data/matches.csv b/data/matches.csv index a183c07..a3e586d 100644 --- a/data/matches.csv +++ b/data/matches.csv @@ -23,7 +23,7 @@ FLAT TOP,2529,Flat Top,1977,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2529 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,,,version_unknown,"[{""bgg_id"": 26, ""name"": ""Age of Renaissance"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4502.jpeg,, History of the World,224,History of the World,1991,boardgame,auto,20727,Avalon Hill/Hasbro edition,version_auto,"[{""bgg_id"": 224, ""name"": ""History of the World"", ""year"": 1991, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 5948, ""rank"": 1274}, {""bgg_id"": 235591, ""name"": ""History of the World"", ""year"": 2018, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1960, ""rank"": 3203}, {""bgg_id"": 426957, ""name"": ""History of the World"", ""year"": 2025, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 175, ""rank"": 19811}]","[{""version_id"": 20727, ""name"": ""Avalon Hill/Hasbro edition"", ""year"": 2001, ""publishers"": [""Avalon Hill""], ""languages"": [""English""], ""score"": 3}]",IMG_4502.jpeg,, -Dungeon!,,,,,ambiguous,,,,"[{""bgg_id"": 19503, ""name"": ""Dungeon (ICP)"", ""year"": 2001, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 77.8, ""owned"": 18, ""rank"": null}]",[],IMG_4502.jpeg,, +Dungeon!,1339,Dungeon!,1975,boardgame,approved,34390,English first edition,version_approved,"[{""bgg_id"": 19503, ""name"": ""Dungeon (ICP)"", ""year"": 2001, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 77.8, ""owned"": 18, ""rank"": null}]","[{""version_id"": 204893, ""name"": ""English (AUS) first edition"", ""year"": 1975, ""publishers"": [""Jedko Games""], ""languages"": [""English""], ""score"": 1}, {""version_id"": 34390, ""name"": ""English first edition"", ""year"": 1975, ""publishers"": [""Tactical Studies Rules (TSR)""], ""languages"": [""English""], ""score"": 1}, {""version_id"": 95838, ""name"": ""English second edition"", ""year"": 1975, ""publishers"": [""Tactical Studies Rules (TSR)""], ""languages"": [""English""], ""score"": 1}, {""version_id"": 25113, ""name"": ""English third edition"", ""year"": 1981, ""publishers"": [""Tactical Studies Rules (TSR)""], ""languages"": [""English""], ""score"": 1}, {""version_id"": 457691, ""name"": ""English edition 1989"", ""year"": 1989, ""publishers"": [""Tactical Studies Rules (TSR)""], ""languages"": [""English""], ""score"": 1}, {""version_id"": 164556, ""name"": ""English edition 2012"", ""year"": 2012, ""publishers"": [""Wizards of the Coast""], ""languages"": [""English""], ""score"": 1}, {""version_id"": 226404, ""name"": ""English edition 2014"", ""year"": 2014, ""publishers"": [""Wizards of the Coast""], ""languages"": [""English""], ""score"": 1}, {""version_id"": 70070, ""name"": ""German first edition"", ""year"": 1978, ""publishers"": [""Parker Brothers""], ""languages"": [""German""], ""score"": 0}, {""version_id"": 73074, ""name"": ""German second edition"", ""year"": 1986, ""publishers"": [""ASS Altenburger Spielkarten"", ""Tactical Studies Rules (TSR)""], ""languages"": [""German""], ""score"": 0}, {""version_id"": 230081, ""name"": ""Mexican edition"", ""year"": 1986, ""publishers"": [""Novedades Montecarlo""], ""languages"": [""Spanish""], ""score"": 0}]",IMG_4502.jpeg,, TICKET TO RIDE,9209,Ticket to Ride,2004,boardgame,auto,294188,English edition 2012 with Spiel des Jahres logo,version_approved,"[{""bgg_id"": 9209, ""name"": ""Ticket to Ride"", ""year"": 2004, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 468280, ""name"": ""15th anniversary special edition"", ""year"": 2019, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 469076, ""name"": ""Afrikaans/English edition"", ""year"": 2019, ""publishers"": [""Days of Wonder""], ""languages"": [""Afrikaans"", ""English""], ""score"": 3}, {""version_id"": 410646, ""name"": ""English edition 2007 with \""Over 1.1 million games sold\"""", ""year"": 2007, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 191794, ""name"": ""English edition 2007 with \""Over 2 million games sold\"""", ""year"": 2007, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 228833, ""name"": ""English edition 2012 with \""Over 3 million games sold\"""", ""year"": 2012, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 294188, ""name"": ""English edition 2012 with Spiel des Jahres logo"", ""year"": 2012, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 236459, ""name"": ""English edition 2013"", ""year"": 2013, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 447075, ""name"": ""English edition 2015"", ""year"": 2015, ""publishers"": [""Days of Wonder""], ""languages"": [""English""], ""score"": 3}]",IMG_4504.jpeg,, CATAN,13,Catan,1995,boardgame,auto,347121,English edition 2016,version_approved,"[{""bgg_id"": 13, ""name"": ""Catan"", ""year"": 1995, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 243104, ""rank"": 626}, {""bgg_id"": 66056, ""name"": ""Catan Двубоят"", ""year"": 2010, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 21829, ""rank"": 1187}]","[{""version_id"": 347121, ""name"": ""English edition 2016"", ""year"": 2016, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 495726, ""name"": ""English edition 2019"", ""year"": 2019, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 568647, ""name"": ""English edition 2020"", ""year"": 2020, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 733924, ""name"": ""English edition 2023"", ""year"": 2023, ""publishers"": [""Catan Studio""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 416798, ""name"": ""Afrikaans edition"", ""year"": 0, ""publishers"": [""Catan Studio""], ""languages"": [""Afrikaans""], ""score"": 2}]",IMG_4504.jpeg;IMG_4542.jpeg,, RAMPAGE,97903,Rampage,2013,boardgame,auto,,,version_unknown,"[{""bgg_id"": 97903, ""name"": ""Rampage"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4504.jpeg,, @@ -63,8 +63,8 @@ Walk the Plank!,35505,Walk the Plank!,2013,boardgame,auto,781669,English MDG-431 AGRICOLA,31260,Agricola,2007,boardgame,auto,297589,Z-Man Games English fifth edition,version_auto,"[{""bgg_id"": 31260, ""name"": ""Agricola"", ""year"": 2007, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 297589, ""name"": ""Z-Man Games English fifth edition"", ""year"": 2013, ""publishers"": [""Filosofia Éditions"", ""Lookout Games"", ""Z-Man Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 165308, ""name"": ""999 Games Dutch second edition 2011"", ""year"": 2011, ""publishers"": [""999 Games"", ""Lookout Games""], ""languages"": [""Dutch""], ""score"": 2}, {""version_id"": 266028, ""name"": ""Hobby World Russian first edition"", ""year"": 2013, ""publishers"": [""Hobby World"", ""Lookout Games""], ""languages"": [""Russian""], ""score"": 2}, {""version_id"": 22673, ""name"": ""Lookout Games German first edition"", ""year"": 2007, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 64715, ""name"": ""Lookout Games German second edition 2008 with animeeples"", ""year"": 2008, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 219203, ""name"": ""Lookout Games German seventh edition 2012 with DSP and SdJ"", ""year"": 2012, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 64718, ""name"": ""Lookout Games German third edition 2009 with DSP and SdJ"", ""year"": 2009, ""publishers"": [""Lookout Games""], ""languages"": [""German""], ""score"": 2}, {""version_id"": 113858, ""name"": ""Lookout Games Russian first edition 2011"", ""year"": 2011, ""publishers"": [""Lookout Games"", ""Smart Ltd""], ""languages"": [""Russian""], ""score"": 2}]",IMG_4518.jpeg,, METEOR,166109,Meteor,2014,boardgame,auto,,,version_unknown,"[{""bgg_id"": 25172, ""name"": ""Meteor"", ""year"": 1975, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 56, ""rank"": null}, {""bgg_id"": 166109, ""name"": ""Meteor"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 1396, ""rank"": 10371}, {""bgg_id"": 246740, ""name"": ""Meteors"", ""year"": 2018, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 92.3, ""owned"": 71, ""rank"": 25541}]",[],IMG_4518.jpeg,, DOCTOR WHO THE card GAME,125675,Doctor Who: The Card Game,2012,boardgame,auto,,,version_unknown,"[{""bgg_id"": 125675, ""name"": ""Doctor Who: The Card Game"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4518.jpeg,, -DUNGEONS & DRAGONS,,,,,local,,,,"[{""bgg_id"": 140509, ""name"": ""Dungeons & Dragons"", ""year"": 1985, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 65, ""rank"": null}]",[],IMG_4519.jpeg,, -DUNGEONS & DRAGONS FANTASY GAME RULES,,,,,local,,,,[],[],IMG_4519.jpeg,, +DUNGEONS & DRAGONS,,,,,ambiguous,,,,"[{""bgg_id"": 140509, ""name"": ""Dungeons & Dragons"", ""year"": 1985, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 65, ""rank"": null}]",[],IMG_4519.jpeg,, +DUNGEONS & DRAGONS FANTASY GAME RULES,,,,,ambiguous,,,,"[{""bgg_id"": 140509, ""name"": ""Dungeons & Dragons"", ""year"": 1985, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 67.8, ""owned"": 65, ""rank"": null}]",[],IMG_4519.jpeg,, "Before I Kill You, Mister Bond...",164,"Before I Kill You, Mister Bond",1996,boardgame,auto,28465,"Cheapass black and white ""Better Edition"" edition",version_auto,"[{""bgg_id"": 164, ""name"": ""Before I Kill You, Mister Bond"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 28465, ""name"": ""Cheapass black and white \""Better Edition\"" edition"", ""year"": 2001, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 291316, ""name"": ""English edition 2016"", ""year"": 2016, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 520361, ""name"": ""English second edition"", ""year"": 1997, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 28459, ""name"": ""First \""Renamed\"" edition, First color edition"", ""year"": 2004, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 180392, ""name"": ""First black and white envelope edition"", ""year"": 1996, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 28462, ""name"": ""Third black and white edition"", ""year"": 1998, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4519.jpeg;IMG_4554.jpeg,, MYSTERIUM,181304,Mysterium,2015,boardgame,auto,536289,English edition 2018-2,version_approved,"[{""bgg_id"": 181304, ""name"": ""Mysterium"", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 85034, ""rank"": 454}, {""bgg_id"": 113997, ""name"": ""Mysterium"", ""year"": 2013, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 5494, ""rank"": 1143}]","[{""version_id"": 781306, ""name"": ""English 2025 Refresh Edition"", ""year"": 2025, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 280251, ""name"": ""English edition 2015-1"", ""year"": 2015, ""publishers"": [""Esdevium"", ""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 493543, ""name"": ""English edition 2015-2"", ""year"": 2015, ""publishers"": [""Esdevium"", ""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 475162, ""name"": ""English edition 2016-1"", ""year"": 2016, ""publishers"": [""Esdevium"", ""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 351093, ""name"": ""English edition 2017-1"", ""year"": 2017, ""publishers"": [""Esdevium"", ""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 467608, ""name"": ""English edition 2017-2"", ""year"": 2017, ""publishers"": [""Esdevium"", ""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 404443, ""name"": ""English edition 2017-3"", ""year"": 2017, ""publishers"": [""Esdevium"", ""Libellud""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 536289, ""name"": ""English edition 2018-2"", ""year"": 2018, ""publishers"": [""Libellud""], ""languages"": [""English""], ""score"": 3}]",IMG_4520.jpeg,, Völuspá,128554,Völuspá,2012,boardgame,auto,,,version_unknown,"[{""bgg_id"": 128554, ""name"": ""Völuspá"", ""year"": 2012, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4520.jpeg,, @@ -114,10 +114,10 @@ MACHI KORO,143884,Machi Koro,2012,boardgame,auto,453051,"English fifth anniversa Dark Cults: The Horror Story Game,1784,Dark Cults,1983,boardgame,auto,110926,First edition,version_approved,"[{""bgg_id"": 1784, ""name"": ""Dark Cults"", ""year"": 1983, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 52.6, ""owned"": null, ""rank"": null}]","[{""version_id"": 110926, ""name"": ""First edition"", ""year"": 1983, ""publishers"": [""Dark House""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 110927, ""name"": ""Second edition"", ""year"": 1985, ""publishers"": [""Dark House""], ""languages"": [""English""], ""score"": 3}]",IMG_4552.jpeg,, Give Me The Brain,176,Give Me the Brain!,1996,boardgame,auto,27673,Czech edition,version_auto,"[{""bgg_id"": 176, ""name"": ""Give Me the Brain!"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 27673, ""name"": ""Czech edition"", ""year"": 1997, ""publishers"": [""Cheapass Games""], ""languages"": [""Czech""], ""score"": 4}, {""version_id"": 28103, ""name"": ""Cheapass English first paper envelope edition 1996"", ""year"": 1996, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 795469, ""name"": ""Cheapass English paper envelope edition 1998"", ""year"": 1998, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 24093, ""name"": ""Cheapass English special edition 2002"", ""year"": 2002, ""publishers"": [""Cheapass Games"", ""James Ernest Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 742802, ""name"": ""German zipper bag edition"", ""year"": 2000, ""publishers"": [""Cheapass Games"", ""Truant Spiele""], ""languages"": [""German""], ""score"": 2}]",IMG_4553.jpeg,, KILL DOCTOR LUCKY,257,Kill Doctor Lucky,1996,boardgame,auto,520364,"Cheapass envelope edition, second printing",version_auto,"[{""bgg_id"": 257, ""name"": ""Kill Doctor Lucky"", ""year"": 1996, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 520364, ""name"": ""Cheapass envelope edition, second printing"", ""year"": 1997, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 5}, {""version_id"": 28629, ""name"": ""Cheapass boxed edition, sixth printing"", ""year"": 2000, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 342532, ""name"": ""Cheapass English 19.5th Anniversary edition"", ""year"": 2016, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 302015, ""name"": ""Cheapass English/German 19.5th Anniversary edition"", ""year"": 2016, ""publishers"": [""Cheapass Games""], ""languages"": [""English"", ""German""], ""score"": 3}, {""version_id"": 24741, ""name"": ""Cheapass envelope edition, first printing"", ""year"": 1996, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 261344, ""name"": ""Cheapass envelope edition, fourth printing"", ""year"": 1999, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 133486, ""name"": ""Cheapass print & play edition 2011"", ""year"": 2011, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 24760, ""name"": ""Cheapass special edition, eighth printing"", ""year"": 2002, ""publishers"": [""Cheapass Games""], ""languages"": [""English""], ""score"": 3}]",IMG_4555.jpeg,, -ALICE IS MISSING SILENT FALLS EXPANSION,,,,,ambiguous,,,,"[{""bgg_id"": 380459, ""name"": ""Alice is Missing: Silent Falls"", ""year"": 2023, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 85.3, ""owned"": 226, ""rank"": null}]",[],IMG_4566.jpeg,, -ALICE IS MISSING A SILENT ROLE PLAYING GAME,,,,,ambiguous,,,,"[{""bgg_id"": 311654, ""name"": ""Alice is Missing"", ""year"": 2020, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 56.1, ""owned"": 2008, ""rank"": null}]",[],IMG_4566.jpeg,, +ALICE IS MISSING SILENT FALLS EXPANSION,380459,Alice is Missing: Silent Falls,2023,rpgitem,approved,743266,Alice ha desaparecido: Expansión Silent Falls,version_auto,"[{""bgg_id"": 380459, ""name"": ""Alice is Missing: Silent Falls"", ""year"": 2023, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 85.3, ""owned"": 226, ""rank"": null}]","[{""version_id"": 743266, ""name"": ""Alice ha desaparecido: Expansión Silent Falls"", ""year"": 2024, ""publishers"": [], ""languages"": [""Spanish""], ""score"": 2}]",IMG_4566.jpeg,, +ALICE IS MISSING A SILENT ROLE PLAYING GAME,311654,Alice is Missing,2020,rpgitem,approved,,,version_unknown,"[{""bgg_id"": 311654, ""name"": ""Alice is Missing"", ""year"": 2020, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 56.1, ""owned"": 2008, ""rank"": null}]","[{""version_id"": 559392, ""name"": ""Zaginięcie Alice PDF"", ""year"": 2021, ""publishers"": [], ""languages"": [""Polish""], ""score"": 2}, {""version_id"": 653634, ""name"": ""Zaginięcie Alice: Cicha gra fabularna"", ""year"": 2021, ""publishers"": [], ""languages"": [""Polish""], ""score"": 2}]",IMG_4566.jpeg,, ETHERFIELDS KITTENBURG EXPANSION,358880,Etherfields: Kittenburg Expansion,2022,boardgame,auto,603694,English edition,version_auto,"[{""bgg_id"": 358880, ""name"": ""Etherfields: Kittenburg Expansion"", ""year"": 2022, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 603694, ""name"": ""English edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 709658, ""name"": ""Polish edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""Polish""], ""score"": 2}, {""version_id"": 699892, ""name"": ""Spanish edition"", ""year"": 2022, ""publishers"": [""Awaken Realms""], ""languages"": [""Spanish""], ""score"": 2}]",IMG_4570.jpeg,, -Parsely,258849,Parsely,2018,rpgitem,auto,,,version_unknown,"[{""bgg_id"": 258849, ""name"": ""Parsely"", ""year"": 2018, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]",[],IMG_4573.jpeg,, +Parsely,258849,Parsely,2018,rpgitem,auto,449806,Hardcover version,version_approved,"[{""bgg_id"": 258849, ""name"": ""Parsely"", ""year"": 2018, ""type"": ""rpgitem"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 449806, ""name"": ""Hardcover version"", ""year"": 2018, ""publishers"": [], ""languages"": [""English""], ""score"": 1}, {""version_id"": 418367, ""name"": ""PDF Version"", ""year"": 2018, ""publishers"": [], ""languages"": [""English""], ""score"": 1}]",IMG_4573.jpeg,, "...and then, we held hands.",153999,"...and then, we held hands.",2015,boardgame,auto,280845,English edition,version_auto,"[{""bgg_id"": 153999, ""name"": ""...and then, we held hands."", ""year"": 2015, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 280845, ""name"": ""English edition"", ""year"": 2015, ""publishers"": [""LudiCreations""], ""languages"": [""English""], ""score"": 3}]",image.jpg,, Consentacle,166976,Consentacle,2014,boardgame,auto,410382,English Kickstarter edition,version_approved,"[{""bgg_id"": 166976, ""name"": ""Consentacle"", ""year"": 2014, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": null, ""rank"": null}]","[{""version_id"": 410382, ""name"": ""English Kickstarter edition"", ""year"": 2018, ""publishers"": [""(Self-Published)""], ""languages"": [""English""], ""score"": 3}, {""version_id"": 596756, ""name"": ""English Print-and-Play edition"", ""year"": 2018, ""publishers"": [""(Self-Published)""], ""languages"": [""English""], ""score"": 3}]",shelf-20260803-171316.jpg,, The Simpsons Trivia Game,2980,The Simpsons Trivia Game,2000,boardgame,auto,,,version_unknown,"[{""bgg_id"": 2980, ""name"": ""The Simpsons Trivia Game"", ""year"": 2000, ""type"": ""boardgame"", ""exact"": true, ""fuzzy"": 100.0, ""owned"": 786, ""rank"": 30823}, {""bgg_id"": 16343, ""name"": ""The Simpsons 2 Trivia Game"", ""year"": 2001, ""type"": ""boardgame"", ""exact"": false, ""fuzzy"": 95.2, ""owned"": 76, ""rank"": null}]",[],shelf-20260803-213735.jpg,, diff --git a/src/bggpipe/review.py b/src/bggpipe/review.py index 88e3ab3..0b983cf 100644 --- a/src/bggpipe/review.py +++ b/src/bggpipe/review.py @@ -30,6 +30,7 @@ from bggpipe.models import ( ) from bggpipe.normalize import normalize_title from bggpipe.resolve import ( + Candidate, MatchRow, TitleEntry, _score_version, @@ -369,6 +370,54 @@ class ReviewSession: row["version_name"] = "" self._save(row) + def research(self, row: dict, query: str, types: str | None = None) -> int: + """Re-search on the human's terms and put the results on the row's + ballot. `types` targets a specific database — "rpgitem" for + RPGGeek, whose entries the automatic cascade never reaches when BGG + has a same-named BOARD game (every D&D box hits this).""" + query = query.strip() + if not query: + raise ValueError("a search needs some text") + results = ( + self.client.search(query, types) if types else self.client.search(query) + ) + cands = [ + Candidate( + bgg_id=r.bgg_id, + name=r.name, + year=r.year, + type=r.type, + exact=normalize_title(r.name) == normalize_title(query), + fuzzy=0.0, + ) + for r in results + ][:12] + if cands: + try: + stats = { + t.bgg_id: t + for t in self.client.things([c.bgg_id for c in cands], stats=True) + } + except _BGG_ERRORS as err: + # owned counts and ranks only decorate the ballot — losing + # them must not lose the search the human just asked for + self._warn(f"couldn't fetch stats for these results ({err})") + stats = {} + for c in cands: + if c.bgg_id in stats: + c.owned = stats[c.bgg_id].owned + c.rank = stats[c.bgg_id].rank + c.publishers = list(stats[c.bgg_id].publishers) + row["match_status"] = "ambiguous" if cands else "unmatched" + row["candidates_json"] = json.dumps( + [c.as_json() for c in cands], ensure_ascii=False + ) + # a fresh ballot supersedes any earlier verdict on this row + row["bgg_id"] = "" + row["bgg_name"] = "" + self._save(row) + return len(cands) + def reopen_match(self, row: dict) -> None: """The human says a matched row is the WRONG game: clear the match and immediately re-search so the card comes back as a ballot — the diff --git a/src/bggpipe/static/app.css b/src/bggpipe/static/app.css index 7563722..34b1082 100644 --- a/src/bggpipe/static/app.css +++ b/src/bggpipe/static/app.css @@ -249,6 +249,9 @@ button.danger { color: var(--stop-ink); border-color: var(--stop); background: # .rowactions { margin-top: .7rem; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; font-size: .85rem; } .rowactions button { padding: .25rem .7rem; box-shadow: none; } .rowactions button.reject { color: var(--stop-ink); border-color: var(--stop); } +.research { display: flex; gap: .35rem; align-items: center; flex-wrap: wrap; } +.research input[type=text] { width: 12em; } +.research button { font-size: .78rem; } .rowactions input[type=text] { font: inherit; width: 8.5em; padding: .25rem .5rem; border: 2px solid var(--board-edge); border-radius: var(--radius); diff --git a/src/bggpipe/templates/pages/review.html b/src/bggpipe/templates/pages/review.html index c26f114..07a0db9 100644 --- a/src/bggpipe/templates/pages/review.html +++ b/src/bggpipe/templates/pages/review.html @@ -75,6 +75,14 @@ function matchCard(row, idx) {
m + + + + +
@@ -196,6 +204,14 @@ function render() { decide(b.closest(".card"), "reject")); m.querySelectorAll(".golocal").forEach(b => b.onclick = () => decide(b.closest(".card"), "local")); + m.querySelectorAll(".dosearch").forEach(b => b.onclick = () => { + const card = b.closest(".card"); + post("/api/research", { + title_raw: card.dataset.title, source_photos: card.dataset.photos, + row_ix: rowIx(card), query: card.querySelector(".rq").value, + types: b.dataset.types || null, + }); + }); m.querySelectorAll(".wronggame").forEach(b => b.onclick = () => post("/api/reopen-match", { title_raw: b.closest(".card").dataset.title, diff --git a/src/bggpipe/webreview.py b/src/bggpipe/webreview.py index 04ab85f..f2db1ad 100644 --- a/src/bggpipe/webreview.py +++ b/src/bggpipe/webreview.py @@ -201,6 +201,14 @@ class LocalGameBody(BaseModel): description: str | None = None +class ResearchBody(BaseModel): + title_raw: str + source_photos: str + row_ix: int | None = None + query: str + types: str | None = None # e.g. "rpgitem" to search RPGGeek + + class RemoveBody(BaseModel): title_raw: str source_photos: str = "" @@ -1344,6 +1352,20 @@ def create_app( replay_titles(cfg) return state() + @app.post("/api/research") + def api_research(body: ResearchBody) -> dict: + with lock: + revision["n"] += 1 + _refuse_if_rewriting() + row = find_row(body.title_raw, body.source_photos, body.row_ix) + try: + session.research(row, body.query, body.types) + except ValueError as err: + raise HTTPException(400, str(err)) from err + except _BGG_ERRORS as err: + raise HTTPException(502, f"search failed: {err}") from err + return state() + @app.post("/api/reopen-match") def api_reopen_match(body: RowRef) -> dict: with lock: diff --git a/tests/test_review.py b/tests/test_review.py index 2eca996..24997ad 100644 --- a/tests/test_review.py +++ b/tests/test_review.py @@ -671,3 +671,30 @@ def test_local_rows_can_be_looked_up_again_on_rpggeek(tmp_path): candidates = json.loads(row["candidates_json"]) assert any(c["type"] == "rpgitem" for c in candidates) assert any("Alice is Missing" in c["name"] for c in candidates) + + +def test_research_can_target_rpggeek_explicitly(tmp_path): + """BGG has board games called "Dungeons & Dragons", so the automatic + cascade never reaches RPGGeek for them — only an explicit search can.""" + cfg = _setup(tmp_path, [_row(title_raw="Wingspan", match_status="unmatched")]) + session = ReviewSession( + cfg, console=quiet_console(), input_fn=scripted(), client=fixture_client() + ) + row = session.rows[0] + # cached search, but the stats batch for these results was never + # recorded: losing the decoration must not lose the search + session.client = BGGClient( + cache_dir=FIXTURES, + transport=httpx.MockTransport( + lambda req: httpx.Response(401, text="Unauthorized") + ), + ) + found = session.research(row, "Citadels") + assert found and row["match_status"] == "ambiguous" + ids = {c["bgg_id"] for c in json.loads(row["candidates_json"])} + assert {478, 205398} <= ids + assert session.warnings # the missing stats are reported, not swallowed + # a fresh ballot supersedes the previous verdict + assert row["bgg_id"] == "" + with pytest.raises(ValueError, match="needs some text"): + session.research(row, " ")