Review can search a chosen database: BGG or RPGGeek, by hand
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
50a710ce99
commit
9663f29702
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -75,6 +75,14 @@ function matchCard(row, idx) {
|
||||
<div class="rowactions">
|
||||
<span><kbd>m</kbd> <input type="text" inputmode="numeric" placeholder="BGG id, then ⏎"
|
||||
aria-label="manual BGG id"></span>
|
||||
<span class="research">
|
||||
<input type="text" class="rq" value="${esc(row.title_raw)}"
|
||||
aria-label="search text for ${esc(row.title_raw)}">
|
||||
<button class="dosearch" data-types="">search BGG</button>
|
||||
<button class="dosearch" data-types="rpgitem"
|
||||
title="BGG's board-game entries hide same-named RPGs from the automatic search">
|
||||
search RPGGeek</button>
|
||||
</span>
|
||||
<button class="reject" title="press r"><kbd>r</kbd> reject — not a game / bad read</button>
|
||||
<button class="golocal" title="press l"><kbd>l</kbd> not on BGG — keep locally</button>
|
||||
</div>
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user