RPGs pull real RPGGeek data; off-BGG games get facts and a cover photo

Two gaps at the edges of the library, both closed.

RPGGeek items live in the same database but use their own link types —
rpgdesigner, rpgpublisher, rpggenre, rpgcategory, rpgmechanic — so a
board-game-only parser found none of them and both RPG entries showed
just a year and a description. parse_things_full now reads both
vocabularies (plus rpgproducer/rpgseries): .dungeon gains John Battle
and Project Nerves, Parsely gains Jared A. Sorensen and its genres.

An off-BGG game has no API to enrich it and no publisher art to fetch,
so its detail page now hosts the only source it will ever have: a form
for title, year, players, playing time, publishers, designers and
notes, plus a cover photo upload. Both persist in data/local_games.json
and data/local_art/ (committed, like every other curation store) and
enrich merges them over the photo reads, so a rebuild can't erase them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-05 23:45:09 -04:00
co-authored by Claude Fable 5
parent f5e949bd62
commit 7e95ed607d
10 changed files with 582 additions and 21 deletions
+56
View File
@@ -1288,3 +1288,59 @@ def test_library_detail_serves_one_game_with_provenance(tmp_path):
assert web.get("/api/library/nope").status_code == 404
page = web.get("/library/game/240:24621")
assert page.status_code == 200 and 'href="/library"' in page.text
def test_local_game_notes_and_art_round_trip(tmp_path):
"""BGG has nothing for an off-BGG game, so the owner's own words and
photo are its only metadata — and must survive enrich rebuilding
games.json from titles.json."""
from bggpipe.enrich import run_enrich
cfg = make_cfg(tmp_path)
rows = read_matches(cfg.matches_path)
rows.append(
_row(title_raw="Homebrew Game", match_status="local", source_photos="shelf.jpg")
)
write_matches(cfg.matches_path, rows)
web = TestClient(create_app(cfg, client=unauthorized_client(tmp_path)))
key = "local:homebrew game:shelf.jpg"
saved = web.post(
f"/api/local-game/{key}",
json={
"name": "Homebrew Game",
"year": "1998",
"min_players": "2",
"max_players": "6",
"publishers": "Basement Press, Friend's Garage",
"description": " Made by a friend. ",
},
).json()["saved"]
assert saved["year"] == 1998
assert saved["publishers"] == ["Basement Press", "Friend's Garage"]
assert saved["description"] == "Made by a friend."
art = web.post(
f"/api/local-art/{key}",
files={"file": ("box.jpg", b"\xff\xd8jpeg", "image/jpeg")},
).json()
assert art["image"].startswith("/local-art/")
assert web.get(art["image"]).status_code == 200
# enrich folds both into the library entry
games = run_enrich(cfg, client=unauthorized_client(tmp_path))
entry = games[key]
assert entry["year"] == 1998 and entry["max_players"] == 6
assert entry["image"] == art["image"]
assert entry["publishers"] == ["Basement Press", "Friend's Garage"]
# guards: BGG-matched games and non-photos are refused
assert web.post("/api/local-game/13", json={"name": "Catan"}).status_code == 400
assert (
web.post(
f"/api/local-art/{key}",
files={"file": ("notes.txt", b"hi", "text/plain")},
).status_code
== 400
)
assert web.post(f"/api/local-game/{key}", json={"year": "19x8"}).status_code == 400