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:
@@ -16,6 +16,7 @@ const LIBRARY_MIGRATIONS: &[&str] = &[
|
||||
include_str!("sql/library/0001_init.sql"),
|
||||
include_str!("sql/library/0002_location.sql"),
|
||||
include_str!("sql/library/0003_deck_format.sql"),
|
||||
include_str!("sql/library/0004_deck_maybeboard.sql"),
|
||||
];
|
||||
|
||||
/// The attached alias under which the reference db is available at runtime.
|
||||
@@ -34,6 +35,19 @@ pub fn image_subquery(column: &str) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// Correlated subquery selecting the cheapest known USD price across *all*
|
||||
/// printings of the oracle card aliased `oc` — deliberately independent of
|
||||
/// [`image_subquery`]'s printing choice, since the printing that happens to
|
||||
/// supply the display image (first by rowid) is often a promo/variant with no
|
||||
/// listed price even when other printings of the same card do have one.
|
||||
/// Returns NULL (read as 0.0) when no printing has a known price.
|
||||
pub fn price_subquery() -> String {
|
||||
format!(
|
||||
"(SELECT MIN(p.price_usd) FROM {REF_DB}.printings p
|
||||
WHERE p.oracle_id = oc.oracle_id AND p.price_usd > 0)"
|
||||
)
|
||||
}
|
||||
|
||||
/// Create (if needed) and open a reference.db, applying the reference schema.
|
||||
/// Used by the ingester and by test fixtures.
|
||||
pub fn open_reference_for_build(path: &Path) -> Result<Connection> {
|
||||
|
||||
@@ -70,8 +70,8 @@ impl Catalog {
|
||||
)?;
|
||||
let new_id = self.conn.last_insert_rowid();
|
||||
self.conn.execute(
|
||||
"INSERT INTO deck_cards(deck_id, oracle_id, quantity, is_commander)
|
||||
SELECT ?1, oracle_id, quantity, is_commander FROM deck_cards WHERE deck_id = ?2",
|
||||
"INSERT INTO deck_cards(deck_id, oracle_id, quantity, is_commander, is_maybeboard)
|
||||
SELECT ?1, oracle_id, quantity, is_commander, is_maybeboard FROM deck_cards WHERE deck_id = ?2",
|
||||
params![new_id, id],
|
||||
)?;
|
||||
self.get_deck(new_id)
|
||||
@@ -86,6 +86,7 @@ impl Catalog {
|
||||
oracle_id: &str,
|
||||
quantity: i64,
|
||||
is_commander: bool,
|
||||
is_maybeboard: bool,
|
||||
) -> Result<()> {
|
||||
if quantity <= 0 {
|
||||
self.conn.execute(
|
||||
@@ -95,12 +96,13 @@ impl Catalog {
|
||||
return Ok(());
|
||||
}
|
||||
self.conn.execute(
|
||||
"INSERT INTO deck_cards(deck_id, oracle_id, quantity, is_commander)
|
||||
VALUES (?1, ?2, ?3, ?4)
|
||||
"INSERT INTO deck_cards(deck_id, oracle_id, quantity, is_commander, is_maybeboard)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
ON CONFLICT(deck_id, oracle_id) DO UPDATE SET
|
||||
quantity = excluded.quantity,
|
||||
is_commander = excluded.is_commander",
|
||||
params![deck_id, oracle_id, quantity, is_commander as i64],
|
||||
is_commander = excluded.is_commander,
|
||||
is_maybeboard = excluded.is_maybeboard",
|
||||
params![deck_id, oracle_id, quantity, is_commander as i64, is_maybeboard as i64],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -112,14 +114,16 @@ impl Catalog {
|
||||
let mut stmt = self.conn.prepare(&format!(
|
||||
"SELECT dc.quantity, dc.is_commander, COALESCE(lc.quantity_owned, 0),
|
||||
oc.oracle_id, oc.name, oc.mana_value, oc.colors, oc.color_identity,
|
||||
oc.type_line, {image}, {back}, oc.produced_mana, oc.produces_colorless
|
||||
oc.type_line, {image}, {back}, oc.produced_mana, oc.produces_colorless, {price},
|
||||
dc.is_maybeboard
|
||||
FROM deck_cards dc
|
||||
JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = dc.oracle_id
|
||||
LEFT JOIN library_cards lc ON lc.oracle_id = dc.oracle_id
|
||||
WHERE dc.deck_id = ?1
|
||||
ORDER BY oc.name COLLATE NOCASE",
|
||||
image = crate::db::image_subquery("image_uri"),
|
||||
back = crate::db::image_subquery("back_image_uri")
|
||||
back = crate::db::image_subquery("back_image_uri"),
|
||||
price = crate::db::price_subquery()
|
||||
))?;
|
||||
let rows = stmt
|
||||
.query_map(params![deck_id], |r| {
|
||||
@@ -138,7 +142,9 @@ impl Catalog {
|
||||
back_image_uri: r.get::<_, Option<String>>(10)?.unwrap_or_default(),
|
||||
produced_mana: ColorSet(r.get::<_, i64>(11)? as u8),
|
||||
produces_colorless: r.get::<_, i64>(12)? != 0,
|
||||
price_usd: r.get::<_, Option<f64>>(13)?.unwrap_or_default(),
|
||||
},
|
||||
is_maybeboard: r.get::<_, i64>(14)? != 0,
|
||||
})
|
||||
})?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
@@ -211,7 +217,7 @@ mod tests {
|
||||
|
||||
cat.add_owned("llanowar-elves", 4).unwrap();
|
||||
let elves = cat.create_deck("Elfball", Some("llanowar-elves")).unwrap();
|
||||
cat.set_deck_card(elves.id, "llanowar-elves", 4, false)
|
||||
cat.set_deck_card(elves.id, "llanowar-elves", 4, false, false)
|
||||
.unwrap();
|
||||
|
||||
// card_decks reports membership.
|
||||
@@ -238,8 +244,8 @@ mod tests {
|
||||
|
||||
let a = cat.create_deck("Deck A", None).unwrap();
|
||||
let b = cat.create_deck("Deck B", None).unwrap();
|
||||
cat.set_deck_card(a.id, "sol-ring", 1, false).unwrap();
|
||||
cat.set_deck_card(b.id, "sol-ring", 1, false).unwrap();
|
||||
cat.set_deck_card(a.id, "sol-ring", 1, false, false).unwrap();
|
||||
cat.set_deck_card(b.id, "sol-ring", 1, false, false).unwrap();
|
||||
|
||||
let lib = cat.list_library().unwrap();
|
||||
let sol = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap();
|
||||
@@ -257,7 +263,7 @@ mod tests {
|
||||
assert_eq!(d.format, "");
|
||||
|
||||
cat.set_deck_format(d.id, "Commander").unwrap();
|
||||
cat.set_deck_card(d.id, "llanowar-elves", 1, false).unwrap();
|
||||
cat.set_deck_card(d.id, "llanowar-elves", 1, false, false).unwrap();
|
||||
cat.set_deck_commander(d.id, Some("llanowar-elves")).unwrap();
|
||||
|
||||
let d = cat.get_deck(d.id).unwrap().unwrap();
|
||||
@@ -289,8 +295,8 @@ mod tests {
|
||||
// serra-angel intentionally NOT owned.
|
||||
|
||||
let d = cat.create_deck("Mixed", None).unwrap();
|
||||
cat.set_deck_card(d.id, "llanowar-elves", 4, false).unwrap();
|
||||
cat.set_deck_card(d.id, "serra-angel", 1, true).unwrap();
|
||||
cat.set_deck_card(d.id, "llanowar-elves", 4, false, false).unwrap();
|
||||
cat.set_deck_card(d.id, "serra-angel", 1, true, false).unwrap();
|
||||
|
||||
let contents = cat.deck_contents(d.id).unwrap();
|
||||
assert_eq!(contents.len(), 2);
|
||||
@@ -305,16 +311,49 @@ mod tests {
|
||||
assert!(unowned[0].is_commander);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maybeboard_cards_are_flagged_and_excluded_from_assignment() {
|
||||
let fx = test_support::make();
|
||||
let cat = &fx.catalog;
|
||||
cat.add_owned("sol-ring", 1).unwrap();
|
||||
|
||||
let d = cat.create_deck("Maybe Deck", None).unwrap();
|
||||
cat.set_deck_card(d.id, "llanowar-elves", 1, false, false).unwrap(); // mainboard
|
||||
cat.set_deck_card(d.id, "sol-ring", 1, false, true).unwrap(); // maybeboard
|
||||
|
||||
let contents = cat.deck_contents(d.id).unwrap();
|
||||
let elves = contents.iter().find(|e| e.card.oracle_id == "llanowar-elves").unwrap();
|
||||
let sol = contents.iter().find(|e| e.card.oracle_id == "sol-ring").unwrap();
|
||||
assert!(!elves.is_maybeboard);
|
||||
assert!(sol.is_maybeboard);
|
||||
|
||||
// Maybeboard membership doesn't count as an assigned copy, so owning
|
||||
// exactly 1 Sol Ring and maybeboarding it isn't over-assignment.
|
||||
let lib = cat.list_library().unwrap();
|
||||
let sol_lib = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap();
|
||||
assert_eq!(sol_lib.quantity_assigned, 0);
|
||||
assert!(!sol_lib.is_over_assigned());
|
||||
|
||||
// Flipping a card from maybeboard back to mainboard updates in place.
|
||||
cat.set_deck_card(d.id, "sol-ring", 1, false, false).unwrap();
|
||||
let contents = cat.deck_contents(d.id).unwrap();
|
||||
let sol = contents.iter().find(|e| e.card.oracle_id == "sol-ring").unwrap();
|
||||
assert!(!sol.is_maybeboard);
|
||||
let lib = cat.list_library().unwrap();
|
||||
let sol_lib = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap();
|
||||
assert_eq!(sol_lib.quantity_assigned, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_zero_removes_and_cascade_on_delete() {
|
||||
let fx = test_support::make();
|
||||
let cat = &fx.catalog;
|
||||
let d = cat.create_deck("Temp", None).unwrap();
|
||||
cat.set_deck_card(d.id, "lightning-bolt", 2, false).unwrap();
|
||||
cat.set_deck_card(d.id, "lightning-bolt", 0, false).unwrap();
|
||||
cat.set_deck_card(d.id, "lightning-bolt", 2, false, false).unwrap();
|
||||
cat.set_deck_card(d.id, "lightning-bolt", 0, false, false).unwrap();
|
||||
assert!(cat.card_decks("lightning-bolt").unwrap().is_empty());
|
||||
|
||||
cat.set_deck_card(d.id, "lightning-bolt", 1, false).unwrap();
|
||||
cat.set_deck_card(d.id, "lightning-bolt", 1, false, false).unwrap();
|
||||
cat.delete_deck(d.id).unwrap();
|
||||
assert!(cat.card_decks("lightning-bolt").unwrap().is_empty());
|
||||
}
|
||||
@@ -341,8 +380,8 @@ mod tests {
|
||||
|
||||
let src = cat.create_deck("Original", Some("llanowar-elves")).unwrap();
|
||||
cat.set_deck_format(src.id, "Commander").unwrap();
|
||||
cat.set_deck_card(src.id, "llanowar-elves", 2, true).unwrap();
|
||||
cat.set_deck_card(src.id, "lightning-bolt", 1, false).unwrap();
|
||||
cat.set_deck_card(src.id, "llanowar-elves", 2, true, false).unwrap();
|
||||
cat.set_deck_card(src.id, "lightning-bolt", 1, false, false).unwrap();
|
||||
|
||||
let copy = cat.duplicate_deck(src.id, "Original (copy)").unwrap();
|
||||
assert_eq!(copy.name, "Original (copy)");
|
||||
|
||||
@@ -19,12 +19,13 @@ impl Catalog {
|
||||
let mut sql = format!(
|
||||
"SELECT lc.oracle_id, lc.quantity_owned, lc.location,
|
||||
oc.name, oc.mana_value, oc.colors, oc.color_identity, oc.type_line, {image}, {back},
|
||||
oc.produced_mana, oc.produces_colorless
|
||||
oc.produced_mana, oc.produces_colorless, {price}
|
||||
FROM library_cards lc
|
||||
JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = lc.oracle_id
|
||||
WHERE lc.quantity_owned > 0",
|
||||
image = crate::db::image_subquery("image_uri"),
|
||||
back = crate::db::image_subquery("back_image_uri")
|
||||
back = crate::db::image_subquery("back_image_uri"),
|
||||
price = crate::db::price_subquery()
|
||||
);
|
||||
let mut vals: Vec<Value> = Vec::new();
|
||||
|
||||
@@ -228,7 +229,7 @@ mod tests {
|
||||
assert_eq!(bolt, vec!["Lightning Bolt"]);
|
||||
|
||||
let deck = cat.create_deck("Mono-R", None).unwrap();
|
||||
cat.set_deck_card(deck.id, "lightning-bolt", 1, false).unwrap();
|
||||
cat.set_deck_card(deck.id, "lightning-bolt", 1, false, false).unwrap();
|
||||
let in_deck = names(&cat.search_library(&LibraryFilter {
|
||||
in_deck: Some(deck.id),
|
||||
..Default::default()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ use crate::Catalog;
|
||||
|
||||
/// Map a row shaped `(oracle_id, quantity_owned, location, name, mana_value,
|
||||
/// colors, color_identity, type_line, image_uri, back_image_uri, produced_mana,
|
||||
/// produces_colorless)` into its library parts. Shared by [`Catalog::list_library`]
|
||||
/// and the criteria search.
|
||||
/// produces_colorless, price_usd)` into its library parts. Shared by
|
||||
/// [`Catalog::list_library`] and the criteria search.
|
||||
pub(crate) fn row_to_library_parts(
|
||||
r: &rusqlite::Row,
|
||||
) -> rusqlite::Result<(String, i64, String, CardRef)> {
|
||||
@@ -32,6 +32,7 @@ pub(crate) fn row_to_library_parts(
|
||||
back_image_uri: r.get::<_, Option<String>>(9)?.unwrap_or_default(),
|
||||
produced_mana: ColorSet(r.get::<_, i64>(10)? as u8),
|
||||
produces_colorless: r.get::<_, i64>(11)? != 0,
|
||||
price_usd: r.get::<_, Option<f64>>(12)?.unwrap_or_default(),
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -74,13 +75,14 @@ impl Catalog {
|
||||
let mut stmt = self.conn.prepare(&format!(
|
||||
"SELECT lc.oracle_id, lc.quantity_owned, lc.location,
|
||||
oc.name, oc.mana_value, oc.colors, oc.color_identity, oc.type_line, {image}, {back},
|
||||
oc.produced_mana, oc.produces_colorless
|
||||
oc.produced_mana, oc.produces_colorless, {price}
|
||||
FROM library_cards lc
|
||||
JOIN {REF_DB}.oracle_cards oc ON oc.oracle_id = lc.oracle_id
|
||||
WHERE lc.quantity_owned > 0
|
||||
ORDER BY oc.name COLLATE NOCASE",
|
||||
image = crate::db::image_subquery("image_uri"),
|
||||
back = crate::db::image_subquery("back_image_uri")
|
||||
back = crate::db::image_subquery("back_image_uri"),
|
||||
price = crate::db::price_subquery()
|
||||
))?;
|
||||
let rows = stmt
|
||||
.query_map([], row_to_library_parts)?
|
||||
@@ -199,9 +201,12 @@ impl Catalog {
|
||||
}
|
||||
|
||||
pub(crate) fn assigned_totals_by_oracle(&self) -> Result<HashMap<String, i64>> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT oracle_id, SUM(quantity) FROM deck_cards GROUP BY oracle_id")?;
|
||||
// Maybeboard cards aren't actually committed to a deck, so they
|
||||
// shouldn't count against a card's owned copies.
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT oracle_id, SUM(quantity) FROM deck_cards
|
||||
WHERE is_maybeboard = 0 GROUP BY oracle_id",
|
||||
)?;
|
||||
let mut map = HashMap::new();
|
||||
let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?)))?;
|
||||
for row in rows {
|
||||
@@ -292,6 +297,54 @@ mod tests {
|
||||
assert_eq!(elves.card.back_image_uri, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn price_usd_is_plumbed_through() {
|
||||
let fx = test_support::make();
|
||||
let cat = &fx.catalog;
|
||||
cat.connection()
|
||||
.execute(
|
||||
"UPDATE ref.printings SET price_usd = 12.34 WHERE oracle_id = 'sol-ring'",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
cat.add_owned("sol-ring", 1).unwrap();
|
||||
cat.add_owned("llanowar-elves", 1).unwrap();
|
||||
|
||||
let lib = cat.list_library().unwrap();
|
||||
let sol = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap();
|
||||
assert_eq!(sol.card.price_usd, 12.34);
|
||||
|
||||
// Fixture cards default to no known price.
|
||||
let elves = lib.iter().find(|c| c.card.oracle_id == "llanowar-elves").unwrap();
|
||||
assert_eq!(elves.card.price_usd, 0.0);
|
||||
}
|
||||
|
||||
/// Regression test: a card whose *displayed* printing (the one supplying
|
||||
/// `image_uri`) has no known price shouldn't show as priceless if a
|
||||
/// different printing does — e.g. a promo/showcase variant with no
|
||||
/// listed USD price, while the regular printing has one.
|
||||
#[test]
|
||||
fn price_uses_cheapest_available_printing_even_if_display_printing_has_none() {
|
||||
let fx = test_support::make();
|
||||
let cat = &fx.catalog;
|
||||
// The fixture's only printing ("print-sol-ring") supplies the image
|
||||
// and has no price. Add a second, imageless printing with a price.
|
||||
cat.connection()
|
||||
.execute(
|
||||
"INSERT INTO ref.printings (scryfall_id, oracle_id, price_usd)
|
||||
VALUES ('print-sol-ring-promo', 'sol-ring', 5.67)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
cat.add_owned("sol-ring", 1).unwrap();
|
||||
|
||||
let lib = cat.list_library().unwrap();
|
||||
let sol = lib.iter().find(|c| c.card.oracle_id == "sol-ring").unwrap();
|
||||
assert_eq!(sol.card.price_usd, 5.67);
|
||||
// Image still comes from the original (imaged) printing.
|
||||
assert_eq!(sol.card.image_uri, "http://img/sol-ring");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn location_is_stored_and_listed() {
|
||||
let fx = test_support::make();
|
||||
|
||||
@@ -99,6 +99,9 @@ pub struct CardRef {
|
||||
/// `produced_mana` including "C"), tracked separately since colorless
|
||||
/// isn't a WUBRG bit.
|
||||
pub produces_colorless: bool,
|
||||
/// USD market price (Scryfall's `prices.usd`) for the same printing that
|
||||
/// supplied `image_uri`, or 0 if unknown.
|
||||
pub price_usd: f64,
|
||||
}
|
||||
|
||||
/// A library row: a card the user owns, with quantity and its deck memberships.
|
||||
@@ -125,6 +128,10 @@ pub struct DeckEntry {
|
||||
/// Copies of this card in the deck.
|
||||
pub quantity: i64,
|
||||
pub is_commander: bool,
|
||||
/// On the maybeboard: tracked against the deck for consideration, but
|
||||
/// excluded from deck analysis stats and library assignment totals —
|
||||
/// it isn't actually part of the deck.
|
||||
pub is_maybeboard: bool,
|
||||
/// Copies owned in the library (0 if the card isn't in the library).
|
||||
pub quantity_owned: i64,
|
||||
}
|
||||
|
||||
@@ -34,10 +34,11 @@ impl NameIndex {
|
||||
pub fn build(conn: &Connection) -> Result<Self> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT oc.oracle_id, oc.name, oc.mana_value, oc.colors, oc.color_identity,
|
||||
oc.type_line, {image}, {back}, oc.produced_mana, oc.produces_colorless
|
||||
oc.type_line, {image}, {back}, oc.produced_mana, oc.produces_colorless, {price}
|
||||
FROM {REF_DB}.oracle_cards oc",
|
||||
image = crate::db::image_subquery("image_uri"),
|
||||
back = crate::db::image_subquery("back_image_uri")
|
||||
back = crate::db::image_subquery("back_image_uri"),
|
||||
price = crate::db::price_subquery()
|
||||
))?;
|
||||
let entries = stmt
|
||||
.query_map([], |r| {
|
||||
@@ -53,6 +54,7 @@ impl NameIndex {
|
||||
back_image_uri: r.get::<_, Option<String>>(7)?.unwrap_or_default(),
|
||||
produced_mana: ColorSet(r.get::<_, i64>(8)? as u8),
|
||||
produces_colorless: r.get::<_, i64>(9)? != 0,
|
||||
price_usd: r.get::<_, Option<f64>>(10)?.unwrap_or_default(),
|
||||
},
|
||||
})
|
||||
})?
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- v4 — maybeboard: cards tracked against a deck for consideration but not
|
||||
-- counted as part of it (excluded from deck analysis stats and from
|
||||
-- library assignment totals).
|
||||
ALTER TABLE deck_cards ADD COLUMN is_maybeboard INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -49,7 +49,8 @@ CREATE TABLE IF NOT EXISTS printings (
|
||||
finishes TEXT NOT NULL DEFAULT '', -- comma-joined, e.g. "nonfoil,foil"
|
||||
rarity TEXT NOT NULL DEFAULT '',
|
||||
image_uri TEXT NOT NULL DEFAULT '',
|
||||
back_image_uri TEXT NOT NULL DEFAULT '' -- transform/MDFC/reversible back face
|
||||
back_image_uri TEXT NOT NULL DEFAULT '', -- transform/MDFC/reversible back face
|
||||
price_usd REAL NOT NULL DEFAULT 0 -- Scryfall "prices.usd", 0 if unknown
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_printings_oid ON printings(oracle_id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user