Moxfield import
This commit is contained in:
@@ -47,7 +47,15 @@ async def _fetch_bytes(client: httpx.AsyncClient, url: str, slug: str) -> bytes:
|
||||
return r.content
|
||||
|
||||
|
||||
async def resolve_card(client: httpx.AsyncClient, slug: str, name: str, scry_url: str | None) -> dict:
|
||||
async def resolve_card(
|
||||
client: httpx.AsyncClient, slug: str, name: str, scry_url: str | None, scry_id: str | None = None
|
||||
) -> dict:
|
||||
if scry_id:
|
||||
url = f"https://api.scryfall.com/cards/{quote(scry_id)}"
|
||||
r = await _scryfall_get(client, url, slug)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
if scry_url:
|
||||
parsed = parse_scry_url(scry_url)
|
||||
if not parsed:
|
||||
@@ -85,7 +93,7 @@ def extract_images(data: dict) -> tuple[str, str | None]:
|
||||
def build_tts(slug: str) -> dict:
|
||||
deck = db.get_deck(slug)
|
||||
cards = db.get_cards(slug)
|
||||
commander_name = (deck["commander"] or "").strip()
|
||||
commander_names = [n for n in (deck["commander"] or "").splitlines() if n.strip()]
|
||||
|
||||
done = [c for c in cards if c["fetch_status"] == "done" and c["filename"]]
|
||||
if not done:
|
||||
@@ -133,34 +141,53 @@ def build_tts(slug: str) -> dict:
|
||||
"CustomDeck": {str(n): entry(c)},
|
||||
}
|
||||
|
||||
commander = next((c for c in done if c["name"] == commander_name), None) if commander_name else None
|
||||
rest = [c for c in done if commander is None or c["id"] != commander["id"]]
|
||||
def build_pile(pile_cards: list, nickname: str, x: float) -> dict | None:
|
||||
if len(pile_cards) >= 2:
|
||||
deck_ids, custom_deck, contained = [], {}, []
|
||||
for c in pile_cards:
|
||||
n = next_k()
|
||||
e = entry(c)
|
||||
custom_deck[str(n)] = e
|
||||
deck_ids.append(n * 100)
|
||||
contained.append({
|
||||
"Name": "Card", "Transform": transform(), "Nickname": c["name"],
|
||||
**base, "CardID": n * 100, "CustomDeck": {str(n): e},
|
||||
})
|
||||
return {
|
||||
"Name": "Deck", "Transform": transform(x=x), "Nickname": nickname,
|
||||
"Description": "", "Locked": False, "Grid": True, "Snap": True,
|
||||
"Autoraise": True, "Sticky": True, "Tooltip": True, "GridProjection": False,
|
||||
"HideWhenFaceDown": True, "Hands": False, "SidewaysCard": False,
|
||||
"DeckIDs": deck_ids, "CustomDeck": custom_deck, "ContainedObjects": contained,
|
||||
"LuaScript": "", "LuaScriptState": "", "XmlUI": "",
|
||||
}
|
||||
if pile_cards:
|
||||
return card_object(pile_cards[0], x=x, rz=0.0)
|
||||
return None
|
||||
|
||||
token_cards = [c for c in done if c["is_token"]]
|
||||
playable = [c for c in done if not c["is_token"]]
|
||||
|
||||
commanders = []
|
||||
used_ids = set()
|
||||
for cname in commander_names:
|
||||
match = next((c for c in playable if c["name"] == cname and c["id"] not in used_ids), None)
|
||||
if match:
|
||||
commanders.append(match)
|
||||
used_ids.add(match["id"])
|
||||
rest = [c for c in playable if c["id"] not in used_ids]
|
||||
|
||||
objects = []
|
||||
if commander:
|
||||
objects.append(card_object(commander, x=-5.0, rz=0.0))
|
||||
for i, cmd in enumerate(commanders):
|
||||
objects.append(card_object(cmd, x=-5.0 - i * 2.5, rz=0.0))
|
||||
|
||||
if len(rest) >= 2:
|
||||
deck_ids, custom_deck, contained = [], {}, []
|
||||
for c in rest:
|
||||
n = next_k()
|
||||
e = entry(c)
|
||||
custom_deck[str(n)] = e
|
||||
deck_ids.append(n * 100)
|
||||
contained.append({
|
||||
"Name": "Card", "Transform": transform(), "Nickname": c["name"],
|
||||
**base, "CardID": n * 100, "CustomDeck": {str(n): e},
|
||||
})
|
||||
objects.append({
|
||||
"Name": "Deck", "Transform": transform(), "Nickname": deck["deck_name"],
|
||||
"Description": "", "Locked": False, "Grid": True, "Snap": True,
|
||||
"Autoraise": True, "Sticky": True, "Tooltip": True, "GridProjection": False,
|
||||
"HideWhenFaceDown": True, "Hands": False, "SidewaysCard": False,
|
||||
"DeckIDs": deck_ids, "CustomDeck": custom_deck, "ContainedObjects": contained,
|
||||
"LuaScript": "", "LuaScriptState": "", "XmlUI": "",
|
||||
})
|
||||
elif rest:
|
||||
objects.append(card_object(rest[0], rz=0.0))
|
||||
main_pile = build_pile(rest, deck["deck_name"], x=0.0)
|
||||
if main_pile:
|
||||
objects.append(main_pile)
|
||||
|
||||
token_pile = build_pile(token_cards, f"{deck['deck_name']} — Tokens", x=6.0)
|
||||
if token_pile:
|
||||
objects.append(token_pile)
|
||||
|
||||
return {
|
||||
"SaveName": deck["deck_name"], "Date": "", "VersionNumber": "",
|
||||
@@ -200,19 +227,31 @@ async def _process(client: httpx.AsyncClient, slug: str):
|
||||
name = card["name"]
|
||||
card_id = card["id"]
|
||||
scry_url = card["scry_url"]
|
||||
scry_id = card["scry_id"]
|
||||
base = safe_filename(name) if name else f"card_{card_id}"
|
||||
filename = f"{base}_{card_id}.png"
|
||||
back_filename = f"{base}_{card_id}_back.png"
|
||||
dest = deck_dir / filename
|
||||
label = f"[{card['position']}] {name or scry_url}"
|
||||
label = f"[{card['position']}] {name or scry_url or scry_id}"
|
||||
|
||||
# Dedup key: an exact scryfall id is the most precise match (safe to
|
||||
# alias across cards even without a name yet); a scry_url is a precise
|
||||
# print and must never be aliased; otherwise fall back to name.
|
||||
dedup_col, dedup_val = (None, None)
|
||||
if scry_id:
|
||||
dedup_col, dedup_val = "scry_id", scry_id
|
||||
elif not scry_url:
|
||||
dedup_col, dedup_val = "name", name
|
||||
|
||||
if dest.exists():
|
||||
bf = back_filename if (deck_dir / back_filename).exists() else None
|
||||
with db.conn() as c:
|
||||
price_row = c.execute(
|
||||
"SELECT price_usd FROM cards WHERE deck_slug=? AND name=? AND fetch_status='done' AND id!=? LIMIT 1",
|
||||
(slug, name, card_id)
|
||||
).fetchone()
|
||||
price_row = None
|
||||
if dedup_col:
|
||||
price_row = c.execute(
|
||||
f"SELECT price_usd FROM cards WHERE deck_slug=? AND {dedup_col}=? AND fetch_status='done' AND id!=? LIMIT 1",
|
||||
(slug, dedup_val, card_id)
|
||||
).fetchone()
|
||||
price = price_row["price_usd"] if price_row else None
|
||||
if price is not None:
|
||||
c.execute("UPDATE cards SET fetch_status='done', filename=?, back_filename=?, price_usd=? WHERE id=?",
|
||||
@@ -224,13 +263,12 @@ async def _process(client: httpx.AsyncClient, slug: str):
|
||||
db.recalc_done(slug)
|
||||
continue
|
||||
|
||||
# Name-based dedup only — URL adds are precise prints and must never be aliased
|
||||
if not scry_url:
|
||||
if dedup_col:
|
||||
with db.conn() as c:
|
||||
row = c.execute(
|
||||
"SELECT filename, back_filename, price_usd FROM cards "
|
||||
"WHERE deck_slug=? AND name=? AND fetch_status='done' AND id!=? LIMIT 1",
|
||||
(slug, name, card_id)
|
||||
f"SELECT filename, back_filename, price_usd FROM cards "
|
||||
f"WHERE deck_slug=? AND {dedup_col}=? AND fetch_status='done' AND id!=? LIMIT 1",
|
||||
(slug, dedup_val, card_id)
|
||||
).fetchone()
|
||||
if row and row["filename"] and (deck_dir / row["filename"]).exists():
|
||||
shutil.copy(deck_dir / row["filename"], dest)
|
||||
@@ -246,7 +284,7 @@ async def _process(client: httpx.AsyncClient, slug: str):
|
||||
continue
|
||||
|
||||
try:
|
||||
data = await resolve_card(client, slug, name, scry_url)
|
||||
data = await resolve_card(client, slug, name, scry_url, scry_id)
|
||||
front_url, back_url = extract_images(data)
|
||||
price = float(data.get("prices", {}).get("usd") or 0)
|
||||
resolved_name = name or data.get("name", "")
|
||||
|
||||
Reference in New Issue
Block a user