Add card USD pricing and a deck maybeboard

Prices come from Scryfall's prices.usd on printings, shown as a hover badge
on card tiles. The price is picked independently of whichever printing
supplies the display image (the cheapest known price across all printings),
since the display printing is often a promo/variant with no listed price.

The maybeboard lets a deck track cards under consideration without counting
them in deck analysis stats or library assignment totals. Cards move between
mainboard and maybeboard via a per-card toggle; maybeboarding the commander
clears commander status.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 21:32:25 -05:00
parent e3f8a299de
commit cf9c930b02
14 changed files with 327 additions and 48 deletions

View File

@@ -612,6 +612,8 @@ struct ScryPrinting {
image_uris: Option<ImageUris>,
#[serde(default)]
card_faces: Option<Vec<PrintFace>>,
#[serde(default)]
prices: Option<ScryPrices>,
}
#[derive(Deserialize)]
@@ -620,6 +622,12 @@ struct ImageUris {
normal: Option<String>,
}
#[derive(Deserialize)]
struct ScryPrices {
#[serde(default)]
usd: Option<String>,
}
#[derive(Deserialize)]
struct PrintFace {
#[serde(default)]
@@ -632,8 +640,8 @@ fn load_printings(conn: &Connection, reader: impl Read, cancel: &CancelToken) ->
{
let mut ins = conn.prepare(
"INSERT OR IGNORE INTO printings
(scryfall_id,oracle_id,set_code,collector_number,finishes,rarity,image_uri,back_image_uri)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
(scryfall_id,oracle_id,set_code,collector_number,finishes,rarity,image_uri,back_image_uri,price_usd)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
)?;
stream_json_array::<_, ScryPrinting, _>(reader, |p| {
if cancel.is_cancelled() {
@@ -664,6 +672,12 @@ fn load_printings(conn: &Connection, reader: impl Read, cancel: &CancelToken) ->
.and_then(|f| f.image_uris.as_ref())
.and_then(|i| i.normal.clone())
.unwrap_or_default();
let price_usd = p
.prices
.as_ref()
.and_then(|pr| pr.usd.as_ref())
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
ins.execute(params![
p.id,
oracle_id,
@@ -673,6 +687,7 @@ fn load_printings(conn: &Connection, reader: impl Read, cancel: &CancelToken) ->
p.rarity,
image,
back_image,
price_usd,
])?;
count += 1;
Ok(())
@@ -953,4 +968,34 @@ mod tests {
assert_eq!(front, "http://img/split-combined");
assert_eq!(back, "", "split cards have no distinct back face");
}
#[test]
fn captures_usd_price_defaulting_to_zero_when_absent() {
let printings = r#"[
{"id":"p-priced","oracle_id":"oid-priced","set":"m19","collector_number":"1",
"rarity":"common","prices":{"usd":"12.34","usd_foil":"20.00"}},
{"id":"p-null","oracle_id":"oid-null","set":"m19","collector_number":"2",
"rarity":"common","prices":{"usd":null}},
{"id":"p-noprices","oracle_id":"oid-noprices","set":"m19","collector_number":"3",
"rarity":"common"}
]"#;
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("reference.db");
let conn = open_reference_for_build(&dest).unwrap();
load_printings(&conn, printings.as_bytes(), &CancelToken::new()).unwrap();
let fetch = |id: &str| -> f64 {
conn.query_row(
"SELECT price_usd FROM printings WHERE scryfall_id=?1",
params![id],
|r| r.get(0),
)
.unwrap()
};
assert_eq!(fetch("p-priced"), 12.34);
assert_eq!(fetch("p-null"), 0.0);
assert_eq!(fetch("p-noprices"), 0.0);
}
}