Repository navigation
Scryfall Integration
English · Español
All contact with Scryfall happens in Rust, in src-tauri/src/scryfall/. The module provides one HTTP client with rate limiting and retries, an in-memory search cache, a persistent card cache in SQLite, and the pipeline that turns a parsed decklist into cards with their images stored in the blob store. The web view never talks to the network.
Boundaries: the list syntax is Decklist-Parsing; non-Scryfall sources (Cockatrice tokens, other-language names) are External-Sources; linking a user's own image to a real card is Card-Identification-and-OCR.
| File | Responsibility |
|---|---|
src-tauri/src/scryfall/client.rs |
Scryfall (the only door to api.scryfall.com), trait CardApi, Simbolo. |
src-tauri/src/scryfall/rate.rs |
Limitador: minimum spacing between API requests. |
src-tauri/src/scryfall/busquedas.rs |
Busquedas: one-hour in-memory cache of search results, bounded by RAM. |
src-tauri/src/scryfall/cache.rs |
guardar, leer, leer_impresion: persistent card cache (table scryfall_cards). |
src-tauri/src/scryfall/models.rs |
ScryCard, CardFace, ImageUris, Identifier, Collection, SearchPage, SetInfo... |
src-tauri/src/scryfall/resolve.rs |
resolve(): the decklist -> cards pipeline; trait Guardar; ResolveOptions, ImagePolicy. |
src-tauri/src/scryfall/fichas_auto.rs |
Token detection through all_parts (fichas_de, faltan_en_el_deck). |
src-tauri/src/scryfall/fichas.rs |
Spanish token type -> English token name table (used by OCR identification). |
src-tauri/src/scryfall/nombres.rs |
Local catalogue of all card names and set codes (Indice, indice(), codigos_de_sets()). |
src-tauri/src/commands/cards.rs |
Commands: resolve_decklist, job_cancel, card_autocomplete, card_search, card_prints, card_fetch, card_fetch_externa, deck_tokens_detect, sets_list, set_cards, import_archidekt. |
src-tauri/src/state.rs |
AppState.scryfall: Arc<Scryfall> (one shared client, one shared rate limiter). |
Rules documented in the module header and implemented in Scryfall::enviar:
-
Rate limit. Every request to
api.scryfall.comfirst awaitsLimitador::turno(): a mutex-guarded timestamp; the caller sleeps until at least 100 ms (Duration::from_millis(100)inScryfall::new) after the previous request. One limiter for the whole app, so concurrent jobs share the budget. -
Images are different. Downloads (
CardApi::download) skip the limiter (con_ritmo = false) because card images are served by Scryfall's CDN (*.scryfall.io), but they are bounded to 6 at a time by a semaphore (DESCARGAS_A_LA_VEZ). -
Retries. HTTP 429 and any 5xx are retried up to
MAX_REINTENTOS = 3times, waiting 0.5 s, 1 s, 2 s, or theRetry-Afterheader (seconds form) when present. After the last retry: 429 ->ErrorCode::RateLimited; other failures ->ErrorCode::Networkwithestado. -
404 is not an error.
enviarreturnsNone;json()turns that intoOk(None)and the callers decide. This is why a search with zero results (Scryfall answers 404) yields an empty list, and whycard()returnsOption<ScryCard>. -
Timeouts. 30 s total, 10 s connect. User agent
ProxyBuilder/<version>. -
Audit log. Every real request is logged with
tracing(metodo,host,ruta,estado, message "red"), so one can check that opening a saved deck downloads nothing. -
Testing seam. The resolver depends on the trait
CardApi(collection,card,card_lang,search,download), so tests use a fake (Falsoinresolve.rstests) instead of the network.
Method on Scryfall
|
Scryfall endpoint | Notes |
|---|---|---|
collection(ids) |
POST /cards/collection |
Body {identifiers: [...]} where Identifier is Id, SetNumber, NameSet or Name (serialised untagged). Max 75 per call (LOTE). |
card(id) |
GET /cards/{id} |
None on 404. |
card_lang(set, number, lang) |
GET /cards/{set}/{number}/{lang} |
Path segments percent-encoded (urlencode; collector numbers can contain ★ or -). |
search(q, prints, limit) |
GET /cards/search |
prints = true adds unique=prints&include_multilingual=true. Follows next_page until limit (capped to TOPE_BUSQUEDA = 175). |
search_multilingual(q, limit) |
GET /cards/search |
include_multilingual=true only (one result per card, any language); no 175 cap. |
autocomplete(q) |
GET /cards/autocomplete?include_extras=true |
The command ignores queries shorter than 2 characters. |
card_names() |
GET /catalog/card-names |
Feeds the local name index. |
sets() |
GET /sets |
Set browser and set-code recognition; mapped to SetInfo. |
symbology() |
GET /symbology |
Mana symbol SVGs for the designer. |
download(url) |
any image URL | Bytes only; the caller stores them. |
The app passes the user's text straight to Scryfall's search, so all Scryfall syntax works in "Add card" (t:goblin, set:neo, o:"draw a card"...). commands/cards.rs adds to it:
-
consulta_busqueda:<q> lang:<code>when a language other thananyis chosen, plusis:hiresfor high-resolution only. -
consulta_impresiones(variants / prints):!"<exact name>"+ the same filters. Double quotes inside the name are removed so they cannot close the quote early. -
consulta_set:e:<code>plus optionalt:<type>; both are filtered to alphanumerics/letters so the set browser cannot inject extra query syntax. - The
identifyflow appendsinclude:extras -layout:art_series -layout:front_card(tokens are hidden by Scryfall withoutinclude:extras; art cards and Jumpstart front cards are noise).
card_search and card_prints also append results from the Cockatrice token catalogue when the language is en/unset and high-res-only is off (see External-Sources): card_search puts them after Scryfall's results, or first if one is named exactly like the query and Scryfall has none; card_prints appends them. card_prints swallows search errors (unwrap_or_default) because a token Scryfall does not have is not a failure.
| Cache | Where | Lifetime | Purpose |
|---|---|---|---|
| Search results | memory, busquedas.rs Busquedas
|
1 hour (VIDA_BUSQUEDA) |
Reopening a dialog or searching the same card in several lists does not repeat the request. Key = query + extra params + limit. Capped to cartas_segun_memoria() cards (0.25 % of total RAM / 4 KB per card, clamped 5 000..50 000); oldest entries are evicted first, and a result larger than the cap is not stored. Short life on purpose: results may end up in the persistent cache stamped "now", and a long-lived entry would make old prices look fresh. |
| Card data | SQLite table scryfall_cards (id, oracle_id, name, lang, set_code, collector_number, json, fetched_at; indexes by name+lang and set+number+lang), written by cache::guardar
|
TTL_MS = 7 days for gameplay data; prices are refreshed separately after 24 h by commands/stats.rs
|
Lets statistics, "link" and the designer work offline. The row stores the whole card JSON. |
| Names / set codes | files in app_cache_dir
|
7 days | See "Names catalogue" below. |
cache::leer_impresion finds a printing by set + number + language (latest fetched_at). Fields added to ScryCard over time are Options where None means "cached before the app read this field" (keywords, full_art, oversized); ScryCard::completa_para_disenar() and the stats freshness check use them to refetch old rows.
resolve(api, guardar, lista, opciones, cancelado, progreso) is generic over CardApi and Guardar. Guardar is the injection point for everything that is not Scryfall: guardar (store image bytes in the blob store, EnLaApp in commands/cards.rs calls blobs.put_bytes), apuntar (write the card into scryfall_cards), nombre_ingles (other-language name -> English, via External-Sources), externa_por_nombre and externas_creadas_por (Cockatrice tokens). The defaults are no-ops, so tests can implement only guardar.
ResolveOptions { lang, image_policy, add_tokens }:
-
lang: a Scryfall language code (en,es...) orany/empty for "best available". -
ImagePolicy:BestAvailable(any card with an image URI),WithImage(also rejectsimage_statusmissing/placeholder; the UI default),HighresOnly(also requireshighres_image). -
add_tokens: also add tokens the cards create.
flowchart TD
A["ParsedDecklist"] --> B["filter Card and included<br/>distinct Peticion (name, set, collector)"]
B --> C["1. POST /cards/collection<br/>batches of 75, match by casa()"]
C --> D{"unmatched and no set?"}
D -- yes --> E["1b. nombre_ingles() max 20<br/>then collection by English name"]
D -- no --> F
E --> F["2-3. per found card"]
F --> G{"wants other language?"}
G -- yes --> H["en_idioma(): card_lang then search lang:xx"]
G -- no --> I
H --> I["segun_politica(): else search other printing"]
I -- none --> U1["Unresolved NoImage"]
I -- ok --> J["finales (card, lang_fallback)"]
J --> K{"add_tokens?"}
K -- yes --> L["4. fichas_de() on English printings"]
K -- no --> M
L --> M["5. apuntar() to cache<br/>download PNG per distinct card"]
M --> N["6. build DeckCard copies in list order"]
N --> O{"card missing on Scryfall?"}
O -- yes --> P["externa_por_nombre() Cockatrice"]
P -- none --> U2["Unresolved NotFound"]
N --> Q["append tokens, then external tokens"]
Q --> R["ResolveResult { cards, unresolved }"]
-
Input filter. Only lines with
kind == Card && includedare used. Lines are grouped into distinctPeticion { name, set, collector }; repeated lines resolve once and are copiedqtytimes at the end. -
Lookup. Each
Peticionbecomes anIdentifier: set + number ->SetNumber; set only ->NameSet; otherwiseName. ForA // Bnames only the first face is sent (nombre_para_buscar), because/cards/collectiondoes not find the full double-faced name. Requests go in batches of 75. Responses are matched withPeticion::casa(set and number case-insensitive, orScryCard::responde_a(name): full name, printed name, or any face name) and never by position, becausenot_foundentries shift the response order. Progress phase:Lookup. -
Other-language names (1b). Requests that found nothing and have no set may be written in another language ("Muro de algas"). The language tried is
options.lang, or Spanish when that is empty,anyoren. For at most 20 such lines (MAX_TRADUCIDAS),Guardar::nombre_inglesruns concurrently (the online fallback is slow); each hit is looked up again by English name and matched withresponde_a. -
Language and image policy, per found card (progress phase
Language):- If the user wants a language and the card's
langdiffers,en_idiomatriesGET /cards/{set}/{number}/{lang}and, if that is missing or in another language, searches!"name" lang:xxover printings preferring the same set, then any set. If nothing exists the card stays as is. -
segun_politicachecks the policy. If the card fails it, all printings of that name are searched and the best passing one is chosen: same language and set, then same language, then any. If none passes, the request isNoImage. - The flag
lang_fallbackisquiere_idioma && final.lang != options.lang; it becomesCardMeta.lang_fallback, shown as the badge "back to EN". - For tokens, the original English printing seen in step 1 is remembered (
inglesas) because translated printings carry incompleteall_parts.
- If the user wants a language and the card's
-
Tokens (
add_tokens). From the English printing of each distinct card,fichas_dereturns the tokens it creates (see below); they are appended at the end of the deck asCardSource::Token. -
Images. Every distinct card (and token) is written to
scryfall_cards(apuntar), then downloaded in groups of 12 withjoin_all(the client semaphore keeps real concurrency at 6).ScryCard::print_url(face)picks the PNG (745x1040) when available, elselarge; the origin is recorded asImageOrigin::ScryfallPngorScryfallLarge. A double-faced card (is_dfc: no top-levelimage_urisand two faces with images) downloads face 1 too and becomesBackSpec::Dfc. Progress phaseImages. -
Assembly, in list order. Each copy is a new
DeckCard(fresh UUID,CardSource::Scryfall { scryfall_id },CardMetafrommeta_de). The line's section setsmarca:Sideboard->Banquillo,Maybeboard->Opcional(see Decklist-Parsing). A request that Scryfall did not find is tried against Cockatrice by name (externa_por_nombre); if still nothing it isUnresolved { line_no, name, reason: NotFound }. A request filtered out by the policy isUnresolved ... NoImage. Tokens follow, then tokens from other sources created by the deck's cards (externas_creadas_por, de-duplicated by name against Scryfall's tokens).
resolve_decklist returns a job_id immediately and runs on the Tauri async runtime. Progress goes out on the event resolve://progress (ResolveProgressEvent { jobId, progress: { phase, done, total } }, phases lookup, language, tokens, images) and the end on resolve://done (ResolveDoneEvent { jobId, result, error }). state.jobs (src-tauri/src/jobs.rs) hands out ids and an AtomicBool cancel flag; job_cancel(jobId) sets it and the resolver checks it between batches (comprobar -> ErrorCode::Cancelled, in which case no cards are returned). The command holds a state.trabajo() guard, which makes the auto-updater refuse to run (Busy) while a job is running.
The front end uses lanzarTrabajo (src/lib/trabajos.ts), which listens before launching (a short job can finish before invoke returns its id) and buffers early events. src/features/decklist/acciones.ts resolver() builds the options from the UI store (lang, imagePolicy, addTokens), shows phase labels from the progress events, and treats Cancelled as "nothing changed". "Reapply language" (reaplicarIdioma) builds a synthetic ParsedDecklist with one line per non-locked Scryfall card (name + its current set and number) and calls the same pipeline with addTokens: false, then swaps images and metadata while keeping each card's id, edits, lock and custom back (aplicarReaplicado in src/lib/cartas.ts).
-
card_prints(name, lang, highres_only)->Vec<CardSummary>: all printings of an exact name (up to 175), used by the variant picker (variant-dialog.tsx) and by "Add card" in its exact-name mode (buscar(texto, exacto = true)inadd-card-dialog.tsx; otherwise it callscard_search).lang: nullmeans any language. -
card_fetch(scryfall_id)->DeckCard(traer_carta): fetch one printing, store it in the cache, download its PNG (and back face), and return a card with sourceTokenif itslayoutistoken/double_faced_token, elseScryfall. The caller inserts it or uses it to replace another card's image. -
CardSummaryis the light row for grids: ids, names, set, number, a thumbnail fingerprint (not a URL),highres,image_status,is_dfc,type_line, andfuentefor non-Scryfall cards. Thumbnails:resumir()registers each image URL under a SHA-256 of the URL inAppState.miniaturas(cleared when it passes 5 000 entries), and the web view requestspbimg://.../t/<fingerprint>; the protocol handler (src-tauri/src/protocol.rs) resolves the fingerprint, downloads (Scryfall client forhttps://cards.scryfall.io/, otherwise the allow-listedExterno) and caches the derivative. See Live-Preview. -
sets_listandset_cards(code, type_filter)feed Set-Browser.
Used twice: "add tokens" while resolving, and "Detect tokens" on an existing deck (deck_tokens_detect, UI in src/features/decklist/fichas-dialog.tsx).
- Tokens are listed in a card's
all_partsentries withcomponent == "token"(excluding the card itself). -
Why English printings only: Scryfall provides complete
all_partsonly on the English printing; translated printings have none or a partial list. So for every card not in English,fichas_defirst fetches its English card by (front-face) name via/cards/collectionand reads the parts there. If the English card cannot be found, the translated card's own parts are still used. - Token ids are then fetched in batches of 75 by id. Two tokens count as the same if their
clave_de_fichamatches: the oracle id, orname|type_line|power/toughnesswhen there is none, so the same token from different token sets appears once. Each result remembers which card created it (creada_por). -
faltan_en_el_deck(api, deck, de_la_cache, cancelado)additionally removes tokens the deck already has: same id, same token from another set, or a token with the same name (also a custom card named like it). Cards come from the local cache when possible and from Scryfall otherwise.deck_tokens_detectthen also adds Cockatrice tokens created by the deck's cards. It only proposes (FichaPropuesta { carta, creada_por }); the dialog adds the chosen ones withcard_fetch.
A local index of every card name (about 30 000 English names from /catalog/card-names) and every set code (/sets), used to recognise text read by OCR without network calls.
- Files in
app_cache_dir:scryfall-card-names.json,scryfall-set-codes.json. Refreshed when older than 7 days; if the download fails the stale copy is used; the cache can be deleted without losing anything. Loaded once per session intoOnceCells (a failure is not remembered, so it retries next time). -
Indicekeeps two maps keyed bynormalizar(name)(lower case, no accents or punctuation):exactosandplegados, where the OCR-confusable letters are folded (l,1,|->i;0->o;plegar). Double-faced names are indexed whole and by each face. - Lookups:
exacto(line),al_principio(line)(longest name the line starts with, two words or more so "Island of the dead" is not "Island"), andparecido(line)(best fuzzy match with similarity >= 0.85, scanning the whole catalogue, skipping candidates whose length differs by more than 15 %; slow, so it is used for a handful of lines). - Consumers:
commands/identify.rsandoriginales.rs(see Card-Identification-and-OCR).
| Condition | Code | Where |
|---|---|---|
| Offline, DNS, TLS, timeout |
Network (detalle) |
any call |
| 429 after 3 retries | RateLimited |
enviar |
| Other non-2xx, non-404 (e.g. 400 for a bad query) |
Network (estado) |
enviar |
| Card id not found |
NotFound (carta) |
card_fetch, card_link
|
| Job cancelled | Cancelled |
resolve |
| DB write fails | Db |
cache::guardar |
| Per-line failures during resolve | not an error: Unresolved entries in ResolveResult
|
resolve |
The whole resolve returns Err (and no cards) if a collection request or an image download fails; partial results exist only for name-level failures.
-
src-tauri/src/scryfall/resolve.rs(mod tests, with fakeFalsoAPI): four-card list, sideboard marks, DFC front/back, language fallback, high-res policy switching printing, tokens from the English printing, tokens appended once, cancellation, more than 75 lines in batches, progress completes, other-language name via fallback sources, Cockatrice fallbacks. -
src-tauri/src/scryfall/{rate,busquedas,cache,fichas,nombres,fichas_auto,models}.rs: limiter timing (paused clock), cache eviction and expiry, cache round trip, token type table, fuzzy/exact name index, token de-duplication, model parsing (fixturefixtures/dusk-dawn-es.json). -
src-tauri/src/commands/cards.rs(mod tests): query builders, thumbnail registry,traer_cartafor normal/DFC/token/no-image/missing. - Live tests against the real API are
#[ignore]d (for examplescryfall_en_vivoinclient.rs).
- A new Scryfall endpoint: add it to
Scryfall(useself.json(...)so rate limiting and retries apply); if the resolver or tests need it, add it toCardApiand to the fakes (Falsoinresolve.rs,Apiincommands/cards.rstests,SinRedincommands/stats.rs). - A new pipeline step: keep the "match by identity, never by position" rule, report progress with a
ResolvePhase, and callcomprobar(cancelado)between network batches. - New card fields: add them to
ScryCardasOption(so older cached JSON still deserialises) and decide whether old rows must be refetched.
- Decklist-Parsing, External-Sources, Card-Identification-and-OCR
- Add-Card-and-Variants - the dialogs that use search, prints and autocomplete.
-
Storage-and-Blobs - where downloaded images and
scryfall_cardslive. - Statistics - consumer of the card cache and the 24 h price refresh.
- IPC-and-Error-Handling, Rust-App-Layer
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia