From 102507b04060bedad27e5da96a39c08a59f2e86e Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 9 Aug 2026 11:18:48 -0400 Subject: [PATCH] Box dimensions: enrich learns shelf math, dims reports the Kallax truth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eric's brief, implemented to the letter. BGG keeps physical dimensions on VERSIONS, not games, so enrich runs a second cached pass over thing?versions=1 (same batching, token, rate limit, and cache as every call). A game with a chosen version takes that exact version's numbers (source "version", mirrored onto its version dict); a versionless game gets numbers only when every printing with data agrees within 0.5" per axis (source "unanimous", keeping the MAX per axis — the planning question is "will it fit"); disagreement stores nulls as "conflicting" — never a guess — and BGG's 0 parses as "never entered", not a real dimension. rpgitems and local games are "absent". Read-only: upload untouched. The new offline `bggpipe dims` reports coverage by source, the ten biggest footprints, and a Kallax fit check (13.2" square opening, 15.4" deep; a box fits if SOME orientation puts two axes through the opening within the depth) — naming every misfit and every game whose dimensions can't be verified, because can't-verify ≠ fits. Trusted numbers surface on the Library detail page as a "box" row. First real run: 54 version-exact, 16 unanimous, 39 conflicting, 27 absent; three genuine misfits (Bugs in the Kitchen's 17" box, History of the World and Risk LotR both over the 15.4" depth). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g --- CLAUDE.md | 1 + data/games.json | 1424 ++++++++++++++++-- docs/guide.md | 18 + src/bggpipe/cli.py | 9 + src/bggpipe/dims.py | 112 ++ src/bggpipe/enrich.py | 96 +- src/bggpipe/models.py | 22 + src/bggpipe/templates/pages/librarygame.html | 6 + tests/test_dims.py | 84 ++ tests/test_enrich.py | 126 +- 10 files changed, 1767 insertions(+), 131 deletions(-) create mode 100644 src/bggpipe/dims.py create mode 100644 tests/test_dims.py diff --git a/CLAUDE.md b/CLAUDE.md index a9462da..68dfcfa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | 5 | `bggpipe upload` | add games via a logged-in Playwright session | working — all browser flows verified live 2026-08-06 (62 adds + 36 version updates landed) | | 6 | `bggpipe enrich` | fetch full game/version metadata into `games.json` | working | | + | `bggpipe export` | render the library as self-contained static pages (covers downloaded, never hotlinked; shelf photos never included; Powered-by-BGG badge slot) | working | +| + | `bggpipe dims` | offline shelf-space report over box dimensions enrich collects from BGG *versions* (dims live per-version, not per-game; 0 = never entered; versionless games need a unanimous chorus within 0.5"/axis else `conflicting` — never guessed) | working | Full design lives in `docs/spec.md` (read it before changing pipeline semantics); the upload-stage walkthrough is in `docs/bgg-upload-flow.md`. diff --git a/data/games.json b/data/games.json index 0164141..c3937ad 100644 --- a/data/games.json +++ b/data/games.json @@ -42,7 +42,14 @@ "weight": 2.2411, "rank": 1534, "users_owned": 4114, - "version": null + "version": null, + "dims": { + "width_in": 11.75, + "length_in": 11.75, + "depth_in": 2.75591, + "weight_lb": 2.89908, + "source": "unanimous" + } }, "40529": { "bgg_id": 40529, @@ -89,7 +96,14 @@ "weight": 2.6406, "rank": 3075, "users_owned": 1728, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "157969": { "bgg_id": 157969, @@ -160,7 +174,14 @@ "weight": 1.6589, "rank": 622, "users_owned": 57406, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "207830": { "bgg_id": 207830, @@ -217,7 +238,14 @@ "weight": 1.1262, "rank": 1133, "users_owned": 23887, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "240:24621": { "bgg_id": 240, @@ -291,7 +319,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 8.4, + "length_in": 11.5, + "depth_in": 2.1, + "weight_lb": null + }, + "dims": { + "width_in": 8.4, + "length_in": 11.5, + "depth_in": 2.1, + "weight_lb": null, + "source": "version" } }, "71:24006": { @@ -367,7 +406,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 8.4, + "length_in": 11.5, + "depth_in": 2.1, + "weight_lb": 2.0 + }, + "dims": { + "width_in": 8.4, + "length_in": 11.5, + "depth_in": 2.1, + "weight_lb": 2.0, + "source": "version" } }, "177:24972": { @@ -435,7 +485,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 8.4, + "length_in": 11.5, + "depth_in": 2.1, + "weight_lb": 1.9 + }, + "dims": { + "width_in": 8.4, + "length_in": 11.5, + "depth_in": 2.1, + "weight_lb": 1.9, + "source": "version" } }, "3090:25782": { @@ -487,7 +548,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 8.5, + "length_in": 11.5, + "depth_in": 2.25, + "weight_lb": null + }, + "dims": { + "width_in": 8.5, + "length_in": 11.5, + "depth_in": 2.25, + "weight_lb": null, + "source": "version" } }, "3585:211154": { @@ -550,7 +622,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 8.4, + "length_in": 11.5, + "depth_in": 2.1, + "weight_lb": null + }, + "dims": { + "width_in": 8.4, + "length_in": 11.5, + "depth_in": 2.1, + "weight_lb": null, + "source": "version" } }, "2524:219137": { @@ -605,7 +688,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 9.0, + "length_in": 11.625, + "depth_in": 2.0, + "weight_lb": null + }, + "dims": { + "width_in": 9.0, + "length_in": 11.625, + "depth_in": 2.0, + "weight_lb": null, + "source": "version" } }, "334011:701315": { @@ -662,7 +756,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 6.34, + "length_in": 8.94, + "depth_in": 1.57, + "weight_lb": null + }, + "dims": { + "width_in": 6.34, + "length_in": 8.94, + "depth_in": 1.57, + "weight_lb": null, + "source": "version" } }, "194655": { @@ -715,7 +820,14 @@ "weight": 1.7186, "rank": 315, "users_owned": 66422, - "version": null + "version": null, + "dims": { + "width_in": 11.8, + "length_in": 11.8, + "depth_in": 2.5, + "weight_lb": null, + "source": "unanimous" + } }, "161928": { "bgg_id": 161928, @@ -757,7 +869,14 @@ "weight": 1.0, "rank": 29645, "users_owned": 1285, - "version": null + "version": null, + "dims": { + "width_in": 3.0, + "length_in": 6.88, + "depth_in": 4.0, + "weight_lb": 2.0, + "source": "unanimous" + } }, "50381": { "bgg_id": 50381, @@ -813,7 +932,14 @@ "weight": 1.1716, "rank": 17609, "users_owned": 57071, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "193621": { "bgg_id": 193621, @@ -868,7 +994,14 @@ "weight": 1.0291, "rank": 3574, "users_owned": 17682, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "1269": { "bgg_id": 1269, @@ -913,7 +1046,14 @@ "weight": 1.1911, "rank": 13616, "users_owned": 27947, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "172225": { "bgg_id": 172225, @@ -987,7 +1127,14 @@ "weight": 1.0792, "rank": 4015, "users_owned": 94829, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "589:27352": { "bgg_id": 589, @@ -1042,7 +1189,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 8.0, + "length_in": 10.0, + "depth_in": 1.0, + "weight_lb": 1.1905 + }, + "dims": { + "width_in": 8.0, + "length_in": 10.0, + "depth_in": 1.0, + "weight_lb": 1.1905, + "source": "version" } }, "202982": { @@ -1087,7 +1245,14 @@ "weight": 1.0, "rank": 3385, "users_owned": 2784, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "2529": { "bgg_id": 2529, @@ -1142,7 +1307,14 @@ "weight": 4.2286, "rank": 3295, "users_owned": 2527, - "version": null + "version": null, + "dims": { + "width_in": 11.25, + "length_in": 14.25, + "depth_in": 1.4, + "weight_lb": null, + "source": "unanimous" + } }, "483": { "bgg_id": 483, @@ -1223,7 +1395,14 @@ "weight": 3.3234, "rank": 888, "users_owned": 25856, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "26": { "bgg_id": 26, @@ -1272,7 +1451,14 @@ "weight": 3.85, "rank": 2125, "users_owned": 3042, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "224:20727": { "bgg_id": 224, @@ -1341,7 +1527,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 10.5, + "length_in": 15.8, + "depth_in": 3.5, + "weight_lb": 4.55034 + }, + "dims": { + "width_in": 10.5, + "length_in": 15.8, + "depth_in": 3.5, + "weight_lb": 4.55034, + "source": "version" } }, "9209:294188": { @@ -1419,7 +1616,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 11.7, + "length_in": 11.7, + "depth_in": 2.8, + "weight_lb": 2.86601 + }, + "dims": { + "width_in": 11.7, + "length_in": 11.7, + "depth_in": 2.8, + "weight_lb": 2.86601, + "source": "version" } }, "13:347121": { @@ -1548,7 +1756,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 11.6142, + "length_in": 9.44882, + "depth_in": 3.14961, + "weight_lb": 2.42508 + }, + "dims": { + "width_in": 11.6142, + "length_in": 9.44882, + "depth_in": 3.14961, + "weight_lb": 2.42508, + "source": "version" } }, "97903": { @@ -1595,7 +1814,14 @@ "weight": 1.4753, "rank": 1362, "users_owned": 12030, - "version": null + "version": null, + "dims": { + "width_in": 11.2205, + "length_in": 11.2205, + "depth_in": 3.94, + "weight_lb": 4.41, + "source": "unanimous" + } }, "160477": { "bgg_id": 160477, @@ -1649,7 +1875,14 @@ "weight": 1.6583, "rank": 362, "users_owned": 39587, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "195314": { "bgg_id": 195314, @@ -1702,7 +1935,14 @@ "weight": 1.2235, "rank": 1213, "users_owned": 12032, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "1219:488621": { "bgg_id": 1219, @@ -1781,7 +2021,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 10.7, + "length_in": 14.6, + "depth_in": 2.2, + "weight_lb": null + }, + "dims": { + "width_in": 10.7, + "length_in": 14.6, + "depth_in": 2.2, + "weight_lb": null, + "source": "version" } }, "320202": { @@ -1829,7 +2080,14 @@ "weight": 1.75, "rank": 22810, "users_owned": 347, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "137909:216474": { "bgg_id": 137909, @@ -1885,7 +2143,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 11.5, + "length_in": 17.0, + "depth_in": 2.25, + "weight_lb": 2.35233 + }, + "dims": { + "width_in": 11.5, + "length_in": 17.0, + "depth_in": 2.25, + "weight_lb": 2.35233, + "source": "version" } }, "27710:606657": { @@ -1960,7 +2229,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 6.93, + "length_in": 4.84, + "depth_in": 0.98, + "weight_lb": 0.28 + }, + "dims": { + "width_in": 6.93, + "length_in": 4.84, + "depth_in": 0.98, + "weight_lb": 0.28, + "source": "version" } }, "169784:260454": { @@ -2019,7 +2299,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 6.4, + "length_in": 5.4, + "depth_in": 1.7, + "weight_lb": 0.3 + }, + "dims": { + "width_in": 6.4, + "length_in": 5.4, + "depth_in": 1.7, + "weight_lb": 0.3, + "source": "version" } }, "2452": { @@ -2134,7 +2425,14 @@ "weight": 1.1, "rank": 10481, "users_owned": 35939, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "208428": { "bgg_id": 208428, @@ -2181,7 +2479,14 @@ "weight": 1.7, "rank": 5792, "users_owned": 1604, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "42063": { "bgg_id": 42063, @@ -2220,7 +2525,14 @@ "weight": 0.0, "rank": null, "users_owned": 57, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "147370:231664": { "bgg_id": 147370, @@ -2274,7 +2586,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 10.5, + "length_in": 10.5, + "depth_in": 2.0, + "weight_lb": 2.51327 + }, + "dims": { + "width_in": 10.5, + "length_in": 10.5, + "depth_in": 2.0, + "weight_lb": 2.51327, + "source": "version" } }, "57215": { @@ -2319,7 +2642,14 @@ "weight": 1.2, "rank": 30462, "users_owned": 638, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "340420": { "bgg_id": 340420, @@ -2371,7 +2701,14 @@ "weight": 1.0, "rank": 8130, "users_owned": 3149, - "version": null + "version": null, + "dims": { + "width_in": 9.0, + "length_in": 10.0, + "depth_in": 3.0, + "weight_lb": 1.47, + "source": "unanimous" + } }, "139443": { "bgg_id": 139443, @@ -2417,7 +2754,14 @@ "weight": 1.1791, "rank": 9918, "users_owned": 8964, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "1260": { "bgg_id": 1260, @@ -2468,7 +2812,14 @@ "weight": 1.8711, "rank": 3154, "users_owned": 10681, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "2197": { "bgg_id": 2197, @@ -2516,7 +2867,14 @@ "weight": 2.234, "rank": 29937, "users_owned": 1207, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "116": { "bgg_id": 116, @@ -2570,7 +2928,14 @@ "weight": 1.2685, "rank": 1630, "users_owned": 34783, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "124548": { "bgg_id": 124548, @@ -2617,7 +2982,14 @@ "weight": 1.84, "rank": 14181, "users_owned": 925, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "169124": { "bgg_id": 169124, @@ -2671,7 +3043,14 @@ "weight": 1.3462, "rank": 1217, "users_owned": 16934, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "43443:103068": { "bgg_id": 43443, @@ -2733,7 +3112,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 10.5, + "length_in": 10.5, + "depth_in": 2.5, + "weight_lb": 2.3 + }, + "dims": { + "width_in": 10.5, + "length_in": 10.5, + "depth_in": 2.5, + "weight_lb": 2.3, + "source": "version" } }, "24310:374258": { @@ -2804,7 +3194,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 10.4331, + "length_in": 10.4331, + "depth_in": 1.9685, + "weight_lb": null + }, + "dims": { + "width_in": 10.4331, + "length_in": 10.4331, + "depth_in": 1.9685, + "weight_lb": null, + "source": "version" } }, "39856:296529": { @@ -2881,7 +3282,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 10.75, + "length_in": 10.75, + "depth_in": 2.25, + "weight_lb": 1.7 + }, + "dims": { + "width_in": 10.75, + "length_in": 10.75, + "depth_in": 2.25, + "weight_lb": 1.7, + "source": "version" } }, "2387": { @@ -2926,7 +3338,14 @@ "weight": 1.6296, "rank": 30388, "users_owned": 1398, - "version": null + "version": null, + "dims": { + "width_in": 4.0, + "length_in": 7.0, + "depth_in": 1.5, + "weight_lb": null, + "source": "unanimous" + } }, "20866": { "bgg_id": 20866, @@ -2967,7 +3386,14 @@ "weight": 0.0, "rank": null, "users_owned": 51, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "196105": { "bgg_id": 196105, @@ -3006,7 +3432,14 @@ "weight": 1.25, "rank": 10370, "users_owned": 493, - "version": null + "version": null, + "dims": { + "width_in": 10.5, + "length_in": 10.5, + "depth_in": 2.5, + "weight_lb": null, + "source": "unanimous" + } }, "125921:476263": { "bgg_id": 125921, @@ -3061,7 +3494,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 10.5, + "length_in": 10.5, + "depth_in": 3.25, + "weight_lb": null + }, + "dims": { + "width_in": 10.5, + "length_in": 10.5, + "depth_in": 3.25, + "weight_lb": null, + "source": "version" } }, "156994": { @@ -3104,7 +3548,14 @@ "weight": 0.0, "rank": null, "users_owned": 32, - "version": null + "version": null, + "dims": { + "width_in": 10.63, + "length_in": 10.63, + "depth_in": 2.00787, + "weight_lb": null, + "source": "unanimous" + } }, "269564": { "bgg_id": 269564, @@ -3153,7 +3604,14 @@ "weight": 2.0, "rank": 20853, "users_owned": 760, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "18755": { "bgg_id": 18755, @@ -3196,7 +3654,14 @@ "weight": 1.0, "rank": 30933, "users_owned": 1404, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "35505:781669": { "bgg_id": 35505, @@ -3256,7 +3721,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 6.0, + "length_in": 6.0, + "depth_in": 2.5, + "weight_lb": null + }, + "dims": { + "width_in": 6.0, + "length_in": 6.0, + "depth_in": 2.5, + "weight_lb": null, + "source": "version" } }, "31260:297589": { @@ -3333,7 +3809,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 9.0, + "length_in": 12.5, + "depth_in": 2.75, + "weight_lb": null + }, + "dims": { + "width_in": 9.0, + "length_in": 12.5, + "depth_in": 2.75, + "weight_lb": null, + "source": "version" } }, "166109": { @@ -3380,7 +3867,14 @@ "weight": 1.8571, "rank": 10371, "users_owned": 1396, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "125675": { "bgg_id": 125675, @@ -3424,7 +3918,14 @@ "weight": 1.9706, "rank": 14593, "users_owned": 2331, - "version": null + "version": null, + "dims": { + "width_in": 9.0, + "length_in": 6.4, + "depth_in": 2.2, + "weight_lb": 1.2, + "source": "unanimous" + } }, "164:28465": { "bgg_id": 164, @@ -3477,6 +3978,13 @@ "languages": [ "English" ] + }, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" } }, "181304:536289": { @@ -3553,7 +4061,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 11.7, + "length_in": 11.7, + "depth_in": 2.8, + "weight_lb": null + }, + "dims": { + "width_in": 11.7, + "length_in": 11.7, + "depth_in": 2.8, + "weight_lb": null, + "source": "version" } }, "128554": { @@ -3598,7 +4117,14 @@ "weight": 1.9481, "rank": 3416, "users_owned": 2369, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "822": { "bgg_id": 822, @@ -3692,7 +4218,14 @@ "weight": 1.8844, "rank": 240, "users_owned": 225740, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "201248": { "bgg_id": 201248, @@ -3742,7 +4275,14 @@ "weight": 1.7959, "rank": 3113, "users_owned": 6090, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "362205": { "bgg_id": 362205, @@ -3791,7 +4331,14 @@ "weight": 2.6875, "rank": 2680, "users_owned": 6067, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "220199": { "bgg_id": 220199, @@ -3830,7 +4377,14 @@ "weight": 1.0, "rank": 23981, "users_owned": 879, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "453845": { "bgg_id": 453845, @@ -3866,7 +4420,14 @@ "weight": 0.0, "rank": null, "users_owned": 11, - "version": null + "version": null, + "dims": { + "width_in": 6.75, + "length_in": 6.75, + "depth_in": 1.25, + "weight_lb": null, + "source": "unanimous" + } }, "329230": { "bgg_id": 329230, @@ -3908,7 +4469,14 @@ "weight": 1.0, "rank": 28676, "users_owned": 1148, - "version": null + "version": null, + "dims": { + "width_in": 3.0, + "length_in": 6.0, + "depth_in": 1.0, + "weight_lb": 0.2, + "source": "unanimous" + } }, "358320": { "bgg_id": 358320, @@ -3967,7 +4535,14 @@ "weight": 2.9538, "rank": 826, "users_owned": 9812, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "389113": { "bgg_id": 389113, @@ -4021,7 +4596,14 @@ "weight": 3.0, "rank": 8928, "users_owned": 337, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "65244:31208": { "bgg_id": 65244, @@ -4103,7 +4685,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 6.5, + "length_in": 8.75, + "depth_in": 2.75, + "weight_lb": 1.35143 + }, + "dims": { + "width_in": 6.5, + "length_in": 8.75, + "depth_in": 2.75, + "weight_lb": 1.35143, + "source": "version" } }, "95386:290572": { @@ -4169,7 +4762,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 3.93701, + "length_in": 5.51181, + "depth_in": 1.5748, + "weight_lb": 0.5 + }, + "dims": { + "width_in": 3.93701, + "length_in": 5.51181, + "depth_in": 1.5748, + "weight_lb": 0.5, + "source": "version" } }, "258:20840": { @@ -4238,6 +4842,13 @@ "languages": [ "English" ] + }, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" } }, "312786:514050": { @@ -4299,7 +4910,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 4.0, + "length_in": 6.0, + "depth_in": 2.0, + "weight_lb": null + }, + "dims": { + "width_in": 4.0, + "length_in": 6.0, + "depth_in": 2.0, + "weight_lb": null, + "source": "version" } }, "188614:779792": { @@ -4360,6 +4982,13 @@ "languages": [ "English" ] + }, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" } }, "273240:446069": { @@ -4429,7 +5058,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 7.5, + "length_in": 10.5, + "depth_in": 4.0, + "weight_lb": null + }, + "dims": { + "width_in": 7.5, + "length_in": 10.5, + "depth_in": 4.0, + "weight_lb": null, + "source": "version" } }, "324937": { @@ -4474,7 +5114,14 @@ "weight": 2.0, "rank": 11443, "users_owned": 482, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "4324:26859": { "bgg_id": 4324, @@ -4535,7 +5182,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 10.5, + "length_in": 15.75, + "depth_in": 2.75, + "weight_lb": 2.72 + }, + "dims": { + "width_in": 10.5, + "length_in": 15.75, + "depth_in": 2.75, + "weight_lb": 2.72, + "source": "version" } }, "419687:711400": { @@ -4589,7 +5247,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 10.5, + "length_in": 10.5, + "depth_in": 4.875, + "weight_lb": null + }, + "dims": { + "width_in": 10.5, + "length_in": 10.5, + "depth_in": 4.875, + "weight_lb": null, + "source": "version" } }, "295770": { @@ -4661,7 +5330,14 @@ "weight": 4.4078, "rank": 21, "users_owned": 25003, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "174430": { "bgg_id": 174430, @@ -4741,7 +5417,14 @@ "weight": 3.9191, "rank": 4, "users_owned": 105074, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "442312": { "bgg_id": 442312, @@ -4780,7 +5463,14 @@ "weight": 0.0, "rank": null, "users_owned": 10, - "version": null + "version": null, + "dims": { + "width_in": 6.25, + "length_in": 9.25, + "depth_in": 1.25, + "weight_lb": 0.85, + "source": "unanimous" + } }, "252153:404702": { "bgg_id": 252153, @@ -4842,7 +5532,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 11.811, + "length_in": 11.811, + "depth_in": 3.93701, + "weight_lb": 4.40925 + }, + "dims": { + "width_in": 11.811, + "length_in": 11.811, + "depth_in": 3.93701, + "weight_lb": 4.40925, + "source": "version" } }, "332398": { @@ -4904,7 +5605,14 @@ "weight": 3.0988, "rank": null, "users_owned": 13760, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "334065": { "bgg_id": 334065, @@ -4970,7 +5678,14 @@ "weight": 2.0703, "rank": 761, "users_owned": 16194, - "version": null + "version": null, + "dims": { + "width_in": 9.48819, + "length_in": 9.48819, + "depth_in": 2.79528, + "weight_lb": null, + "source": "unanimous" + } }, "283155": { "bgg_id": 283155, @@ -5037,7 +5752,14 @@ "weight": 2.1972, "rank": 273, "users_owned": 42351, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "255984:701034": { "bgg_id": 255984, @@ -5112,7 +5834,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 11.69, + "length_in": 12.0, + "depth_in": 4.25, + "weight_lb": 8.0 + }, + "dims": { + "width_in": 11.69, + "length_in": 12.0, + "depth_in": 4.25, + "weight_lb": 8.0, + "source": "version" } }, "283766:463140": { @@ -5180,7 +5913,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 11.7, + "length_in": 11.7, + "depth_in": 1.4, + "weight_lb": 2.49122 + }, + "dims": { + "width_in": 11.7, + "length_in": 11.7, + "depth_in": 1.4, + "weight_lb": 2.49122, + "source": "version" } }, "280794:458300": { @@ -5247,7 +5991,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 11.811, + "length_in": 11.811, + "depth_in": 4.72441, + "weight_lb": null + }, + "dims": { + "width_in": 11.811, + "length_in": 11.811, + "depth_in": 4.72441, + "weight_lb": null, + "source": "version" } }, "380837:650545": { @@ -5308,7 +6063,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 12.0, + "length_in": 12.0, + "depth_in": 3.0, + "weight_lb": 3.0 + }, + "dims": { + "width_in": 12.0, + "length_in": 12.0, + "depth_in": 3.0, + "weight_lb": 3.0, + "source": "version" } }, "393672:669782": { @@ -5380,7 +6146,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 3.0315, + "length_in": 4.17323, + "depth_in": 2.87402, + "weight_lb": 0.66139 + }, + "dims": { + "width_in": 3.0315, + "length_in": 4.17323, + "depth_in": 2.87402, + "weight_lb": 0.66139, + "source": "version" } }, "1927:28144": { @@ -5471,7 +6248,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 6.25, + "length_in": 9.25, + "depth_in": 1.75, + "weight_lb": null + }, + "dims": { + "width_in": 6.25, + "length_in": 9.25, + "depth_in": 1.75, + "weight_lb": null, + "source": "version" } }, "14535:29075": { @@ -5530,7 +6318,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 6.25, + "length_in": 9.25, + "depth_in": 1.875, + "weight_lb": 1.0 + }, + "dims": { + "width_in": 6.25, + "length_in": 9.25, + "depth_in": 1.875, + "weight_lb": 1.0, + "source": "version" } }, "9666:215137": { @@ -5586,7 +6385,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 6.25, + "length_in": 9.25, + "depth_in": 2.0, + "weight_lb": null + }, + "dims": { + "width_in": 6.25, + "length_in": 9.25, + "depth_in": 2.0, + "weight_lb": null, + "source": "version" } }, "291457": { @@ -5665,7 +6475,14 @@ "weight": 3.6415, "rank": 12, "users_owned": 89911, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "40508:644326": { "bgg_id": 40508, @@ -5723,7 +6540,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 3.7, + "length_in": 4.7, + "depth_in": 0.8, + "weight_lb": 0.21 + }, + "dims": { + "width_in": 3.7, + "length_in": 4.7, + "depth_in": 0.8, + "weight_lb": 0.21, + "source": "version" } }, "3181:87931": { @@ -5793,6 +6621,13 @@ "languages": [ "English" ] + }, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" } }, "173057": { @@ -5829,7 +6664,14 @@ "weight": 1.0, "rank": 26208, "users_owned": 272, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "150925": { "bgg_id": 150925, @@ -5876,7 +6718,14 @@ "weight": 1.3636, "rank": 27659, "users_owned": 829, - "version": null + "version": null, + "dims": { + "width_in": 6.10236, + "length_in": 6.10236, + "depth_in": 2.55906, + "weight_lb": 1.5, + "source": "unanimous" + } }, "193485:300446": { "bgg_id": 193485, @@ -5928,7 +6777,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 5.5, + "length_in": 8.0, + "depth_in": 1.75, + "weight_lb": 0.74957 + }, + "dims": { + "width_in": 5.5, + "length_in": 8.0, + "depth_in": 1.75, + "weight_lb": 0.74957, + "source": "version" } }, "226065:353389": { @@ -5997,7 +6857,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 8.9, + "length_in": 12.4, + "depth_in": 2.2, + "weight_lb": 1.10231 + }, + "dims": { + "width_in": 8.9, + "length_in": 12.4, + "depth_in": 2.2, + "weight_lb": 1.10231, + "source": "version" } }, "143884:453051": { @@ -6071,7 +6942,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 9.25, + "length_in": 9.25, + "depth_in": 2.75, + "weight_lb": 1.32277 + }, + "dims": { + "width_in": 9.25, + "length_in": 9.25, + "depth_in": 2.75, + "weight_lb": 1.32277, + "source": "version" } }, "1784:110926": { @@ -6123,6 +7005,13 @@ "languages": [ "English" ] + }, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" } }, "176:27673": { @@ -6178,7 +7067,18 @@ ], "languages": [ "Czech" - ] + ], + "width_in": 6.5, + "length_in": 9.5, + "depth_in": 0.5, + "weight_lb": 0.257941 + }, + "dims": { + "width_in": 6.5, + "length_in": 9.5, + "depth_in": 0.5, + "weight_lb": 0.257941, + "source": "version" } }, "257:520364": { @@ -6242,7 +7142,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 6.5, + "length_in": 9.5, + "depth_in": 0.375, + "weight_lb": null + }, + "dims": { + "width_in": 6.5, + "length_in": 9.5, + "depth_in": 0.375, + "weight_lb": null, + "source": "version" } }, "358880:603694": { @@ -6296,6 +7207,13 @@ "languages": [ "English" ] + }, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" } }, "153999:280845": { @@ -6355,7 +7273,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 4.88189, + "length_in": 7.16535, + "depth_in": 0.984252, + "weight_lb": 0.66139 + }, + "dims": { + "width_in": 4.88189, + "length_in": 7.16535, + "depth_in": 0.984252, + "weight_lb": 0.66139, + "source": "version" } }, "166976:410382": { @@ -6410,7 +7339,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 7.87402, + "length_in": 7.87402, + "depth_in": 1.69291, + "weight_lb": 1.41096 + }, + "dims": { + "width_in": 7.87402, + "length_in": 7.87402, + "depth_in": 1.69291, + "weight_lb": 1.41096, + "source": "version" } }, "2980": { @@ -6450,7 +7390,14 @@ "weight": 1.3333, "rank": 30823, "users_owned": 786, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "231302:361880": { "bgg_id": 231302, @@ -6504,7 +7451,18 @@ "languages": [ "English", "French" - ] + ], + "width_in": 7.0, + "length_in": 9.0, + "depth_in": 2.0, + "weight_lb": null + }, + "dims": { + "width_in": 7.0, + "length_in": 9.0, + "depth_in": 2.0, + "weight_lb": null, + "source": "version" } }, "1197": { @@ -6562,7 +7520,14 @@ "weight": 1.0656, "rank": 6149, "users_owned": 6143, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "352819": { "bgg_id": 352819, @@ -6597,7 +7562,14 @@ "weight": 0.0, "rank": null, "users_owned": 159, - "version": null + "version": null, + "dims": { + "width_in": 6.0, + "length_in": 9.0, + "depth_in": 1.75, + "weight_lb": null, + "source": "unanimous" + } }, "163412": { "bgg_id": 163412, @@ -6680,7 +7652,14 @@ "weight": 1.5967, "rank": 148, "users_owned": 124114, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "291847": { "bgg_id": 291847, @@ -6733,7 +7712,14 @@ "weight": 2.7143, "rank": 2384, "users_owned": 5675, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "40692:294178": { "bgg_id": 40692, @@ -6805,7 +7791,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 11.7, + "length_in": 11.7, + "depth_in": 2.8, + "weight_lb": 4.23288 + }, + "dims": { + "width_in": 11.7, + "length_in": 11.7, + "depth_in": 2.8, + "weight_lb": 4.23288, + "source": "version" } }, "432858:734890": { @@ -6857,7 +7854,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 10.0, + "length_in": 10.0, + "depth_in": 2.5, + "weight_lb": 4.0 + }, + "dims": { + "width_in": 10.0, + "length_in": 10.0, + "depth_in": 2.5, + "weight_lb": 4.0, + "source": "version" } }, "172503:265107": { @@ -6914,7 +7922,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 8.5, + "length_in": 9.5, + "depth_in": 1.4, + "weight_lb": 2.0 + }, + "dims": { + "width_in": 8.5, + "length_in": 9.5, + "depth_in": 1.4, + "weight_lb": 2.0, + "source": "version" } }, "417060:706506": { @@ -6969,7 +7988,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 5.25, + "length_in": 7.0, + "depth_in": 1.25, + "weight_lb": 1.0 + }, + "dims": { + "width_in": 5.25, + "length_in": 7.0, + "depth_in": 1.25, + "weight_lb": 1.0, + "source": "version" } }, "417064:706510": { @@ -7025,7 +8055,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 5.25, + "length_in": 7.0, + "depth_in": 1.25, + "weight_lb": 1.0 + }, + "dims": { + "width_in": 5.25, + "length_in": 7.0, + "depth_in": 1.25, + "weight_lb": 1.0, + "source": "version" } }, "2058": { @@ -7079,7 +8120,14 @@ "weight": 3.2027, "rank": null, "users_owned": 1574, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "452833": { "bgg_id": 452833, @@ -7114,7 +8162,14 @@ "weight": 0.0, "rank": null, "users_owned": 335, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "104590": { "bgg_id": 104590, @@ -7169,7 +8224,14 @@ "weight": 2.0661, "rank": null, "users_owned": 10158, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "conflicting" + } }, "104710:117685": { "bgg_id": 104710, @@ -7247,7 +8309,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 11.75, + "length_in": 11.75, + "depth_in": 3.0, + "weight_lb": 3.1 + }, + "dims": { + "width_in": 11.75, + "length_in": 11.75, + "depth_in": 3.0, + "weight_lb": 3.1, + "source": "version" } }, "local:cat crimes:IMG_4499.jpeg;IMG_4505.jpeg;shelf-20260803-215723.jpg": { @@ -7262,7 +8335,14 @@ "IMG_4499.jpeg", "IMG_4505.jpeg", "shelf-20260803-215723.jpg" - ] + ], + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "local:super mario checkers collector s edition:IMG_4508.jpeg": { "bgg_id": null, @@ -7272,7 +8352,14 @@ "publishers": [], "source_photos": [ "IMG_4508.jpeg" - ] + ], + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "local:gravity maze:IMG_4510.jpeg": { "bgg_id": null, @@ -7284,7 +8371,14 @@ ], "source_photos": [ "IMG_4510.jpeg" - ] + ], + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "local:laser maze beam bending logic game:IMG_4516.jpeg": { "bgg_id": null, @@ -7296,7 +8390,14 @@ ], "source_photos": [ "IMG_4516.jpeg" - ] + ], + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "local:elite sportz equipment table top curling:IMG_4526.jpeg": { "bgg_id": null, @@ -7308,7 +8409,14 @@ ], "source_photos": [ "IMG_4526.jpeg" - ] + ], + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "local:ouija:IMG_4529.jpeg": { "bgg_id": null, @@ -7320,7 +8428,14 @@ ], "source_photos": [ "IMG_4529.jpeg" - ] + ], + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "local:cat stax:IMG_4544.jpeg": { "bgg_id": null, @@ -7330,7 +8445,14 @@ "publishers": [], "source_photos": [ "IMG_4544.jpeg" - ] + ], + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "1339:34390": { "bgg_id": 1339, @@ -7407,7 +8529,18 @@ ], "languages": [ "English" - ] + ], + "width_in": 9.25, + "length_in": 11.25, + "depth_in": 1.25, + "weight_lb": null + }, + "dims": { + "width_in": 9.25, + "length_in": 11.25, + "depth_in": 1.25, + "weight_lb": null, + "source": "version" } }, "380459:743266": { @@ -7463,6 +8596,13 @@ "languages": [ "Spanish" ] + }, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" } }, "311654": { @@ -7516,7 +8656,14 @@ "weight": 1.9565, "rank": null, "users_owned": 2008, - "version": null + "version": null, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "258849:449806": { "bgg_id": 258849, @@ -7589,6 +8736,13 @@ "languages": [ "English" ] + }, + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" } }, "local:dungeons and dragons:IMG_4519.jpeg": { @@ -7601,7 +8755,14 @@ ], "source_photos": [ "IMG_4519.jpeg" - ] + ], + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } }, "local:dungeons and dragons fantasy game rules:IMG_4519.jpeg": { "bgg_id": null, @@ -7613,6 +8774,13 @@ ], "source_photos": [ "IMG_4519.jpeg" - ] + ], + "dims": { + "width_in": null, + "length_in": null, + "depth_in": null, + "weight_lb": null, + "source": "absent" + } } } diff --git a/docs/guide.md b/docs/guide.md index 2a4e1f7..2e648d7 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -109,6 +109,24 @@ BGG application approval can take a week or more. Until then: `extract` works im - `https://boardgamegeek.com/xmlapi2/collection?username=YOU&own=1&version=1` - `https://boardgamegeek.com/xmlapi2/collection?username=YOU&own=1&version=1&subtype=boardgameexpansion` +## Shelf-space planning + +Physical box dimensions live on BGG *versions*, not games, so enrich collects +them in a second cached pass: your exact version's numbers where you chose +one, otherwise a value only when every listed printing agrees (within half an +inch per axis — disagreement is stored as `conflicting`, never guessed, and +BGG's `0` means "never entered", not zero inches). Then: + +```sh +bggpipe dims +``` + +reports coverage, the ten biggest footprints, and a fit check against an IKEA +Kallax cube (13.2" × 13.2" opening, 15.4" deep) — listing every box that fits +in **no** orientation, and every game whose dimensions can't be verified, +because can't-verify is not the same as fits. Trusted dimensions also show on +each game's Library detail page. + ## Publishing your library on your own site ```sh diff --git a/src/bggpipe/cli.py b/src/bggpipe/cli.py index cd1ea2c..c82a6f2 100644 --- a/src/bggpipe/cli.py +++ b/src/bggpipe/cli.py @@ -193,6 +193,15 @@ def enrich( run_enrich(cfg, refresh=refresh) +@app.command() +def dims(config: ConfigOpt = None) -> None: + """Shelf-space report: box sizes, biggest footprints, Kallax fit.""" + from bggpipe.dims import run_dims_report + + cfg = load_config(config) + run_dims_report(cfg) + + @app.command() def export( out: Annotated[ diff --git a/src/bggpipe/dims.py b/src/bggpipe/dims.py new file mode 100644 index 0000000..20ffed0 --- /dev/null +++ b/src/bggpipe/dims.py @@ -0,0 +1,112 @@ +"""Shelf-space report over the box dimensions enrich collected. + +Reads games.json only — fully offline. The fit target is an IKEA Kallax +cube: a box fits if SOME orientation puts two axes through the ~13.2" +square opening with the third axis within the ~15.4" depth. "Can't +verify" is reported as exactly that, never as a fit. +""" + +from __future__ import annotations + +import json +from itertools import permutations + +import typer + +from bggpipe.config import Config + +KALLAX_OPENING_IN = 13.2 +KALLAX_DEPTH_IN = 15.4 + +_AXES = ("width_in", "length_in", "depth_in") + + +def fits_kallax(width: float, length: float, depth: float) -> bool: + return any( + a <= KALLAX_OPENING_IN and b <= KALLAX_OPENING_IN and c <= KALLAX_DEPTH_IN + for a, b, c in permutations((width, length, depth)) + ) + + +def _footprint(dims: dict) -> float: + """The two largest axes multiplied — the shelf area a box claims + lying in its flattest orientation.""" + axes = sorted((dims[a] for a in _AXES), reverse=True) + return axes[0] * axes[1] + + +def run_dims_report(cfg: Config) -> dict: + if not cfg.games_path.exists(): + typer.echo(f"{cfg.games_path} not found — run `bggpipe enrich` first.") + raise typer.Exit(code=1) + games: dict[str, dict] = json.loads(cfg.games_path.read_text()) + + by_source: dict[str, list[tuple[str, dict]]] = {} + for entry in games.values(): + dims = entry.get("dims") or dict.fromkeys(_AXES, None) | {"source": "absent"} + by_source.setdefault(dims["source"], []).append( + (entry.get("name") or "?", dims) + ) + + typer.echo(f"Box dimensions across {len(games)} game(s):") + for source, label in ( + ("version", "from the exact owned version"), + ("unanimous", "every listed version agrees"), + ("conflicting", "versions disagree — not guessed"), + ("absent", "no data on BGG (or no BGG entry)"), + ): + if by_source.get(source): + typer.echo(f" {len(by_source[source]):>4} {source} — {label}") + + measured = sorted( + ( + (name, dims) + for source in ("version", "unanimous") + for name, dims in by_source.get(source, []) + ), + key=lambda item: _footprint(item[1]), + reverse=True, + ) + if measured: + typer.echo("\nBiggest footprints (two largest axes, flattest lie):") + for name, dims in measured[:10]: + w, length, d = (dims[a] for a in _AXES) + typer.echo( + f" {_footprint(dims):>6.1f} sq in {name}" + f" ({w:g} × {length:g} × {d:g} in)" + ) + + misfits = [ + (name, dims) + for name, dims in measured + if not fits_kallax(*(dims[a] for a in _AXES)) + ] + unknown = [ + name + for source in ("conflicting", "absent") + for name, _ in by_source.get(source, []) + ] + typer.echo( + f'\nKallax check ({KALLAX_OPENING_IN:g}" × {KALLAX_OPENING_IN:g}" opening, ' + f'{KALLAX_DEPTH_IN:g}" deep):' + ) + if misfits: + typer.echo(f" {len(misfits)} box(es) do NOT fit in any orientation:") + for name, dims in misfits: + w, length, d = (dims[a] for a in _AXES) + typer.echo(f" {name} ({w:g} × {length:g} × {d:g} in)") + else: + typer.echo(" every measured box fits.") + if unknown: + typer.echo( + f" {len(unknown)} game(s) can't be verified (no trusted " + "dimensions — not the same as fitting):" + ) + for name in sorted(unknown): + typer.echo(f" {name}") + + return { + "by_source": {s: len(v) for s, v in by_source.items()}, + "misfits": [name for name, _ in misfits], + "unknown": sorted(unknown), + } diff --git a/src/bggpipe/enrich.py b/src/bggpipe/enrich.py index e335f40..b5807fb 100644 --- a/src/bggpipe/enrich.py +++ b/src/bggpipe/enrich.py @@ -27,12 +27,99 @@ from bggpipe.bgg_client import ( ) from bggpipe.config import Config from bggpipe.fsio import atomic_write_text -from bggpipe.models import is_confident_version, is_recognized +from bggpipe.models import is_confident_version, is_recognized, parse_version_dims from bggpipe.normalize import normalize_title from bggpipe.resolve import load_titles, read_matches BATCH_SIZE = 20 +_AXES = ("width_in", "length_in", "depth_in") +_AGREE_IN = 0.5 # per-axis tolerance for calling versionless dims unanimous + + +def _entry_dims(entry: dict, records: list[dict]) -> dict: + """The box-dimensions verdict for one game, from its versions' data. + + Known version: that exact version's numbers or nothing. No chosen + version: only a chorus that AGREES (within 0.5" per axis, all axes + present) counts — the maximum per axis is kept, since the planning + question is "will it fit", and disagreement beyond tolerance stores + nulls rather than a guess.""" + version_id = (entry.get("version") or {}).get("version_id") + if version_id: + rec = next((r for r in records if r["version_id"] == version_id), None) + if rec and all(rec[a] for a in _AXES): + return { + **{a: rec[a] for a in _AXES}, + "weight_lb": rec["weight_lb"], + "source": "version", + } + return dict.fromkeys((*_AXES, "weight_lb"), None) | {"source": "absent"} + complete = [r for r in records if all(r[a] for a in _AXES)] + if not complete: + return dict.fromkeys((*_AXES, "weight_lb"), None) | {"source": "absent"} + dims: dict = {} + for axis in _AXES: + values = [r[axis] for r in complete] + if max(values) - min(values) > _AGREE_IN: + return dict.fromkeys((*_AXES, "weight_lb"), None) | { + "source": "conflicting" + } + dims[axis] = max(values) + weights = [r["weight_lb"] for r in complete if r["weight_lb"]] + dims["weight_lb"] = ( + max(weights) if weights and max(weights) - min(weights) <= 0.5 else None + ) + return dims | {"source": "unanimous"} + + +def _dims_pass(games: dict, client: BGGClient, *, refresh: bool) -> tuple[int, bool]: + """Fill entry["dims"] for every BGG-backed game from versions=1 data + (physical dimensions live on VERSIONS, not games). Cached XML makes + re-runs free; rpgitems and local games get source "absent".""" + need_ids = sorted( + { + e["bgg_id"] + for e in games.values() + if e.get("type") in ("boardgame", "boardgameexpansion") + and e.get("bgg_id") + and (refresh or "dims" not in e) + } + ) + by_game: dict[int, list[dict]] = {} + blocked = False + for start in range(0, len(need_ids), BATCH_SIZE): + batch = need_ids[start : start + BATCH_SIZE] + try: + xml = client.get_xml( + "thing", + {"id": ",".join(str(i) for i in batch), "versions": "1"}, + refresh=refresh, + ) + except (BGGAuthError, BGGQueueTimeout): + blocked = True + break + by_game.update(parse_version_dims(xml)) + filled = 0 + for entry in games.values(): + if not (refresh or "dims" not in entry): + continue + if entry.get("bgg_id") in by_game: + entry["dims"] = _entry_dims(entry, by_game[entry["bgg_id"]]) + if entry["dims"]["source"] == "version" and entry.get("version"): + entry["version"].update( + {k: entry["dims"][k] for k in (*_AXES, "weight_lb")} + ) + filled += 1 + elif entry.get("bgg_id") is None or entry.get("type") not in ( + "boardgame", + "boardgameexpansion", + ): + entry["dims"] = dict.fromkeys((*_AXES, "weight_lb"), None) | { + "source": "absent" + } + return filled, blocked + def _version_info(row: dict) -> dict | None: if not is_confident_version(row): @@ -184,6 +271,11 @@ def run_enrich( f" pruned {len(stale)} stale entr{'y' if len(stale) == 1 else 'ies'}" ) + # box dimensions live on VERSIONS, not games: a second cached pass + # (read-only; upload is untouched) fills entry["dims"] for shelf math + dims_filled, dims_blocked = _dims_pass(games, client, refresh=refresh) + blocked = blocked or dims_blocked + games_path.parent.mkdir(parents=True, exist_ok=True) atomic_write_text( games_path, json.dumps(games, indent=2, ensure_ascii=False) + "\n" @@ -194,6 +286,8 @@ def run_enrich( # tally would drive "already present or waiting" negative waiting = len(targets) - updated parts = [f"{updated} fetched from BGG"] + if dims_filled: + parts.append(f"box dims checked for {dims_filled}") if local_keys: parts.append(f"{len(local_keys)} local-only") if waiting: diff --git a/src/bggpipe/models.py b/src/bggpipe/models.py index 8943a6d..5324cb7 100644 --- a/src/bggpipe/models.py +++ b/src/bggpipe/models.py @@ -268,6 +268,28 @@ def parse_things_full(xml_text: str) -> list[dict]: return games +def parse_version_dims(xml_text: str) -> dict[int, list[dict]]: + """Physical box data per game id from a versions=1 response. BGG uses + 0 for "never entered" — a zero axis or weight parses as None, never + as a real dimension. Returns plain dicts (artifact data).""" + out: dict[int, list[dict]] = {} + for item in _root(xml_text).findall("item"): + game_id = int(_required_attr(item, "id")) + records = out.setdefault(game_id, []) + for v_item in item.findall("versions/item"): + record: dict = {"version_id": int(_required_attr(v_item, "id"))} + for xml_name, field_name in ( + ("width", "width_in"), + ("length", "length_in"), + ("depth", "depth_in"), + ("weight", "weight_lb"), + ): + value = _attr_float(v_item.find(xml_name)) + record[field_name] = value if value else None + records.append(record) + return out + + def _required_attr(item: ET.Element, name: str) -> str: value = item.get(name) if not value: diff --git a/src/bggpipe/templates/pages/librarygame.html b/src/bggpipe/templates/pages/librarygame.html index 74e8323..2b5f4a9 100644 --- a/src/bggpipe/templates/pages/librarygame.html +++ b/src/bggpipe/templates/pages/librarygame.html @@ -126,9 +126,15 @@ function render(g) { aria-label="cover photo for ${esc(g.name)}">` : ""; + const dims = g.dims && g.dims.width_in + ? `${g.dims.width_in} × ${g.dims.length_in} × ${g.dims.depth_in} in` + + (g.dims.weight_lb ? ` · ${g.dims.weight_lb} lb` : "") + + (g.dims.source === "unanimous" ? " (all printings agree)" : "") + : ""; const facts = [ fact("players", players(g)), fact("playing time", playtime(g)), + fact("box", dims), fact("ages", g.min_age ? `${g.min_age}+` : ""), fact("weight", g.weight ? `${g.weight.toFixed(2)} / 5` : ""), fact("BGG rank", g.rank || ""), diff --git a/tests/test_dims.py b/tests/test_dims.py new file mode 100644 index 0000000..8c682a0 --- /dev/null +++ b/tests/test_dims.py @@ -0,0 +1,84 @@ +"""The shelf-space report: fit math, coverage buckets, honest unknowns.""" + +from __future__ import annotations + +import json + +import pytest +import typer + +from bggpipe.config import Config +from bggpipe.dims import fits_kallax, run_dims_report + + +def test_fits_kallax_tries_every_orientation(): + assert fits_kallax(11.6, 11.6, 2.8) # ordinary big-box + # too tall to stand, but slides in lying down: depth axis takes 15.0 + assert fits_kallax(15.0, 12.0, 3.0) + # two axes over the opening: no orientation works + assert not fits_kallax(16.2, 16.2, 4.0) + # fits the opening but too deep to close the wall behind it + assert not fits_kallax(12.0, 12.0, 15.5) + # exact boundary counts as fitting + assert fits_kallax(13.2, 13.2, 15.4) + + +def test_report_buckets_misfits_and_unknowns(tmp_path, capsys): + cfg = Config(data_dir=tmp_path / "data") + cfg.data_dir.mkdir(parents=True) + cfg.games_path.write_text( + json.dumps( + { + "1": { + "name": "Fits Fine", + "dims": { + "width_in": 11.6, + "length_in": 11.6, + "depth_in": 2.8, + "weight_lb": 4, + "source": "version", + }, + }, + "2": { + "name": "Monster Box", + "dims": { + "width_in": 16.2, + "length_in": 16.2, + "depth_in": 4.0, + "weight_lb": 7, + "source": "unanimous", + }, + }, + "3": { + "name": "Argued About", + "dims": { + "width_in": None, + "length_in": None, + "depth_in": None, + "weight_lb": None, + "source": "conflicting", + }, + }, + "4": {"name": "Never Measured"}, # pre-dims entry: absent + } + ) + ) + summary = run_dims_report(cfg) + assert summary["by_source"] == { + "version": 1, + "unanimous": 1, + "conflicting": 1, + "absent": 1, + } + assert summary["misfits"] == ["Monster Box"] + # can't verify is NOT the same as fits: both unknowns are named + assert summary["unknown"] == ["Argued About", "Never Measured"] + out = capsys.readouterr().out + assert "do NOT fit" in out and "Monster Box" in out + assert "can't be verified" in out and "Never Measured" in out + + +def test_report_without_games_json_exits_with_guidance(tmp_path): + cfg = Config(data_dir=tmp_path / "data") + with pytest.raises(typer.Exit): + run_dims_report(cfg) diff --git a/tests/test_enrich.py b/tests/test_enrich.py index 68d6e64..78cc126 100644 --- a/tests/test_enrich.py +++ b/tests/test_enrich.py @@ -151,11 +151,25 @@ def _cfg_with_matches(tmp_path): def _seed_batch_fixture(cache_dir): - """The batch cache entry run_enrich will ask for: sorted ids 13,266192.""" + """The cache entries run_enrich will ask for: the stats batch and the + dims pass's versions batch (sorted ids 13,266192).""" cache_dir.mkdir(parents=True, exist_ok=True) combined = FULL_THING_XML.replace("", "" + CATAN_MINIMAL[7:-8], 1) key = cache_key("thing", {"id": "13,266192", "stats": "1"}) (cache_dir / key).write_text(combined) + versions_key = cache_key("thing", {"id": "13,266192", "versions": "1"}) + (cache_dir / versions_key).write_text( + """ + + + + + + + + + """ + ) def test_run_enrich_writes_games_json_with_versions(tmp_path): @@ -209,7 +223,9 @@ def test_refresh_bypasses_cache_read(tmp_path): client = BGGClient(cache_dir=cache, transport=httpx.MockTransport(handler)) games = run_enrich(cfg, refresh=True, client=client) - assert len(requests) == 1 # cache read skipped, live fetch happened + # cache reads skipped, live fetches happened: stats batch + dims pass + assert any("stats=1" in u for u in requests) + assert any("versions=1" in u for u in requests) assert games["266192:465063"]["rating"] == 7.5 @@ -338,3 +354,109 @@ def test_corrupt_local_games_store_fails_loud(tmp_path): ) with pytest.raises(ValueError, match="local_games.json is corrupt"): run_enrich(cfg, client=bare) + + +DIMS_XML = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +def test_parse_version_dims_treats_zero_as_absent(): + from bggpipe.models import parse_version_dims + + by_game = parse_version_dims(DIMS_XML) + exact = next(r for r in by_game[240] if r["version_id"] == 24621) + assert exact["width_in"] == 8.4 and exact["weight_lb"] == 2 + zeroed = next(r for r in by_game[240] if r["version_id"] == 99999) + assert all( + zeroed[f] is None for f in ("width_in", "length_in", "depth_in", "weight_lb") + ) + + +def test_entry_dims_verdicts(): + """The four sources: exact version, unanimous chorus (within 0.5"), + conflicting chorus (nulls, never a guess), and absent.""" + from bggpipe.enrich import _entry_dims + from bggpipe.models import parse_version_dims + + by_game = parse_version_dims(DIMS_XML) + + versioned = _entry_dims({"version": {"version_id": 24621}}, by_game[240]) + assert versioned["source"] == "version" and versioned["width_in"] == 8.4 + + # chosen version exists but carries only zeros: absent, not borrowed + zeroed = _entry_dims({"version": {"version_id": 99999}}, by_game[240]) + assert zeroed["source"] == "absent" and zeroed["width_in"] is None + + unanimous = _entry_dims({"version": None}, by_game[500]) + assert unanimous["source"] == "unanimous" + assert unanimous["width_in"] == 11.8 # max per axis: the fit question + assert unanimous["depth_in"] == 3.0 + + conflicting = _entry_dims({"version": None}, by_game[600]) + assert conflicting["source"] == "conflicting" + assert conflicting["width_in"] is None + + assert _entry_dims({"version": None}, [])["source"] == "absent" + + +def test_enrich_fills_dims_from_cached_versions(tmp_path): + from bggpipe.enrich import run_enrich + + cfg = _cfg_with_matches(tmp_path) + cache = tmp_path / "cache" + _seed_batch_fixture(cache) + (cache / cache_key("thing", {"id": "13,266192", "versions": "1"})).write_text( + """ + + + + + + + + + """ + ) + client = BGGClient(cache_dir=cache, transport=httpx.MockTransport(_no_network)) + games = run_enrich(cfg, client=client) + wingspan = games["266192:465063"] + assert wingspan["dims"]["source"] == "version" + assert wingspan["dims"]["depth_in"] == 2.8 + assert wingspan["version"]["width_in"] == 11.7 # mirrored onto the version + assert games["13"]["dims"]["source"] == "unanimous"