Skip to content

Integración con Scryfall

Pablo Martínez López edited this page Oct 20, 2026 · 1 revision

Integración con Scryfall

English · Español

Todo el contacto con Scryfall ocurre en Rust, en src-tauri/src/scryfall/. El módulo ofrece un único cliente HTTP con límite de ritmo y reintentos, una caché de búsquedas en memoria, una caché persistente de cartas en SQLite y el flujo que convierte una lista parseada en cartas con sus imágenes guardadas en el almacén de blobs. La vista web nunca habla con la red.

Límites: la sintaxis de la lista es Parseo de la lista del mazo; las fuentes que no son Scryfall (fichas de Cockatrice, nombres en otros idiomas) son Fuentes externas; vincular una imagen propia del usuario con una carta real es Identificación de cartas y OCR.

Ficheros

Fichero Responsabilidad
src-tauri/src/scryfall/client.rs Scryfall (la única puerta a api.scryfall.com), trait CardApi, Simbolo.
src-tauri/src/scryfall/rate.rs Limitador: separación mínima entre peticiones a la API.
src-tauri/src/scryfall/busquedas.rs Busquedas: caché en memoria de una hora para los resultados de búsqueda, acotada por la RAM.
src-tauri/src/scryfall/cache.rs guardar, leer, leer_impresion: caché persistente de cartas (tabla scryfall_cards).
src-tauri/src/scryfall/models.rs ScryCard, CardFace, ImageUris, Identifier, Collection, SearchPage, SetInfo...
src-tauri/src/scryfall/resolve.rs resolve(): el flujo lista -> cartas; trait Guardar; ResolveOptions, ImagePolicy.
src-tauri/src/scryfall/fichas_auto.rs Detección de fichas mediante all_parts (fichas_de, faltan_en_el_deck).
src-tauri/src/scryfall/fichas.rs Tabla de tipo de ficha en español -> nombre de ficha en inglés (la usa la identificación por OCR).
src-tauri/src/scryfall/nombres.rs Catálogo local de todos los nombres de carta y códigos de set (Indice, indice(), codigos_de_sets()).
src-tauri/src/commands/cards.rs Comandos: 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> (un cliente compartido, un limitador de ritmo compartido).

El cliente (client.rs)

Reglas documentadas en la cabecera del módulo e implementadas en Scryfall::enviar:

  • Límite de ritmo. Toda petición a api.scryfall.com espera primero a Limitador::turno(): una marca de tiempo protegida por un mutex; quien llama duerme hasta que pasen al menos 100 ms (Duration::from_millis(100) en Scryfall::new) desde la petición anterior. Un solo limitador para toda la app, así que los trabajos concurrentes comparten el presupuesto.
  • Las imágenes son distintas. Las descargas (CardApi::download) se saltan el limitador (con_ritmo = false) porque las imágenes de las cartas las sirve el CDN de Scryfall (*.scryfall.io), pero se limitan a 6 a la vez con un semáforo (DESCARGAS_A_LA_VEZ).
  • Reintentos. El HTTP 429 y cualquier 5xx se reintentan hasta MAX_REINTENTOS = 3 veces, esperando 0,5 s, 1 s, 2 s, o la cabecera Retry-After (en su forma de segundos) cuando está presente. Tras el último reintento: 429 -> ErrorCode::RateLimited; los demás fallos -> ErrorCode::Network con estado.
  • El 404 no es un error. enviar devuelve None; json() lo convierte en Ok(None) y quienes llaman deciden. Por eso una búsqueda sin resultados (Scryfall responde 404) da una lista vacía, y por eso card() devuelve Option<ScryCard>.
  • Tiempos de espera. 30 s en total, 10 s de conexión. User agent ProxyBuilder/<version>.
  • Registro de auditoría. Cada petición real se registra con tracing (metodo, host, ruta, estado, mensaje «red»), de modo que se puede comprobar que abrir un mazo guardado no descarga nada.
  • Punto de inyección para tests. El resolvedor depende del trait CardApi (collection, card, card_lang, search, download), así que los tests usan un falso (Falso en los tests de resolve.rs) en lugar de la red.

Endpoints usados

Método de Scryfall Endpoint de Scryfall Notas
collection(ids) POST /cards/collection Cuerpo {identifiers: [...]} donde Identifier es Id, SetNumber, NameSet o Name (serializado sin etiqueta, untagged). Máximo 75 por llamada (LOTE).
card(id) GET /cards/{id} None en el 404.
card_lang(set, number, lang) GET /cards/{set}/{number}/{lang} Segmentos de ruta codificados con porcentaje (urlencode; los números de coleccionista pueden contener ★ o -).
search(q, prints, limit) GET /cards/search prints = true añade unique=prints&include_multilingual=true. Sigue next_page hasta limit (limitado a TOPE_BUSQUEDA = 175).
search_multilingual(q, limit) GET /cards/search Solo include_multilingual=true (un resultado por carta, en cualquier idioma); sin tope de 175.
autocomplete(q) GET /cards/autocomplete?include_extras=true El comando ignora las consultas de menos de 2 caracteres.
card_names() GET /catalog/card-names Alimenta el índice local de nombres.
sets() GET /sets Explorador de sets y reconocimiento de códigos de set; se mapea a SetInfo.
symbology() GET /symbology SVG de los símbolos de maná para el diseñador.
download(url) cualquier URL de imagen Solo bytes; quien llama los almacena.

Sintaxis de búsqueda

La app pasa el texto del usuario tal cual a la búsqueda de Scryfall, así que toda la sintaxis de Scryfall funciona en «Añadir carta» (t:goblin, set:neo, o:"draw a card"...). commands/cards.rs le añade:

  • consulta_busqueda: <q> lang:<code> cuando se elige un idioma distinto de any, más is:hires para solo alta resolución.
  • consulta_impresiones (variantes / impresiones): !"<nombre exacto>" + los mismos filtros. Se quitan las comillas dobles del nombre para que no puedan cerrar la comilla antes de tiempo.
  • consulta_set: e:<code> más un t:<type> opcional; ambos se filtran a alfanuméricos/letras para que el explorador de sets no pueda inyectar sintaxis de consulta adicional.
  • El flujo identify añade include:extras -layout:art_series -layout:front_card (Scryfall oculta las fichas sin include:extras; las cartas de arte y las cartas frontales de Jumpstart son ruido).

card_search y card_prints también añaden resultados del catálogo de fichas de Cockatrice cuando el idioma es en/sin definir y la opción de solo alta resolución está desactivada (véase Fuentes externas): card_search las pone después de los resultados de Scryfall, o primero si una se llama exactamente como la consulta y Scryfall no tiene ninguna; card_prints las añade al final. card_prints se traga los errores de búsqueda (unwrap_or_default) porque una ficha que Scryfall no tiene no es un fallo.

Cachés

Caché Dónde Duración Propósito
Resultados de búsqueda memoria, Busquedas de busquedas.rs 1 hora (VIDA_BUSQUEDA) Reabrir un diálogo o buscar la misma carta en varias listas no repite la petición. Clave = consulta + parámetros extra + límite. Acotada a cartas_segun_memoria() cartas (el 0,25 % de la RAM total / 4 KB por carta, limitado a 5 000..50 000); las entradas más antiguas se expulsan primero, y un resultado mayor que el tope no se almacena. Vida corta a propósito: los resultados pueden acabar en la caché persistente con la marca «ahora», y una entrada longeva haría que precios viejos parecieran recientes.
Datos de carta tabla SQLite scryfall_cards (id, oracle_id, name, lang, set_code, collector_number, json, fetched_at; índices por nombre+idioma y por set+número+idioma), escrita por cache::guardar TTL_MS = 7 días para los datos de juego; los precios los renueva aparte a las 24 h commands/stats.rs Permite que las estadísticas, «vincular» y el diseñador funcionen sin conexión. La fila guarda el JSON completo de la carta.
Nombres / códigos de set ficheros en app_cache_dir 7 días Véase «Catálogo de nombres» más abajo.

cache::leer_impresion encuentra una impresión por set + número + idioma (la de fetched_at más reciente). Los campos añadidos a ScryCard con el tiempo son Options donde None significa «guardada en la caché antes de que la app leyera este campo» (keywords, full_art, oversized); ScryCard::completa_para_disenar() y la comprobación de frescura de las estadísticas los usan para volver a pedir las filas viejas.

Resolver una lista (resolve.rs)

resolve(api, guardar, lista, opciones, cancelado, progreso) es genérico sobre CardApi y Guardar. Guardar es el punto de inyección de todo lo que no es Scryfall: guardar (almacenar los bytes de la imagen en el almacén de blobs; EnLaApp en commands/cards.rs llama a blobs.put_bytes), apuntar (escribir la carta en scryfall_cards), nombre_ingles (nombre en otro idioma -> inglés, mediante Fuentes externas), externa_por_nombre y externas_creadas_por (fichas de Cockatrice). Los valores por defecto no hacen nada, de modo que los tests pueden implementar solo guardar.

ResolveOptions { lang, image_policy, add_tokens }:

  • lang: un código de idioma de Scryfall (en, es...) o any/vacío para «el mejor disponible».
  • ImagePolicy: BestAvailable (cualquier carta con una URI de imagen), WithImage (además rechaza image_status missing/placeholder; el valor por defecto de la interfaz), HighresOnly (además exige highres_image).
  • add_tokens: añade también las fichas que crean las cartas.
flowchart TD
    A["ParsedDecklist"] --> B["filtrar Card e included<br/>Peticion distintas (name, set, collector)"]
    B --> C["1. POST /cards/collection<br/>lotes de 75, coincidencia por casa()"]
    C --> D{"¿sin coincidencia y sin set?"}
    D -- sí --> E["1b. nombre_ingles() máx. 20<br/>luego collection por nombre en inglés"]
    D -- no --> F
    E --> F["2-3. por cada carta encontrada"]
    F --> G{"¿quiere otro idioma?"}
    G -- sí --> H["en_idioma(): card_lang y luego search lang:xx"]
    G -- no --> I
    H --> I["segun_politica(): si no, buscar otra impresión"]
    I -- ninguna --> U1["Unresolved NoImage"]
    I -- ok --> J["finales (card, lang_fallback)"]
    J --> K{"¿add_tokens?"}
    K -- sí --> L["4. fichas_de() sobre las impresiones en inglés"]
    K -- no --> M
    L --> M["5. apuntar() a la caché<br/>descargar PNG por carta distinta"]
    M --> N["6. construir copias de DeckCard en el orden de la lista"]
    N --> O{"¿carta ausente en Scryfall?"}
    O -- sí --> P["externa_por_nombre() Cockatrice"]
    P -- ninguna --> U2["Unresolved NotFound"]
    N --> Q["añadir fichas, luego fichas externas"]
    Q --> R["ResolveResult { cards, unresolved }"]
Loading

Paso a paso

  1. Filtro de entrada. Solo se usan las líneas con kind == Card && included. Las líneas se agrupan en Peticion { name, set, collector } distintas; las líneas repetidas se resuelven una vez y se copian qty veces al final.
  2. Búsqueda. Cada Peticion se convierte en un Identifier: set + número -> SetNumber; solo set -> NameSet; si no, Name. Para los nombres A // B solo se envía la primera cara (nombre_para_buscar), porque /cards/collection no encuentra el nombre completo de una carta de doble cara. Las peticiones van en lotes de 75. Las respuestas se emparejan con Peticion::casa (set y número sin distinguir mayúsculas, o ScryCard::responde_a(name): nombre completo, nombre impreso o el nombre de cualquier cara) y nunca por posición, porque las entradas not_found desplazan el orden de la respuesta. Fase de progreso: Lookup.
  3. Nombres en otros idiomas (1b). Las peticiones que no encontraron nada y no tienen set pueden estar escritas en otro idioma («Muro de algas»). El idioma probado es options.lang, o el español cuando está vacío, es any o en. Para un máximo de 20 de esas líneas (MAX_TRADUCIDAS), Guardar::nombre_ingles se ejecuta de forma concurrente (el recurso en línea es lento); cada acierto se busca de nuevo por su nombre en inglés y se empareja con responde_a.
  4. Idioma y política de imagen, por carta encontrada (fase de progreso Language):
    • Si el usuario quiere un idioma y el lang de la carta es distinto, en_idioma prueba GET /cards/{set}/{number}/{lang} y, si falta o está en otro idioma, busca !"name" lang:xx entre las impresiones, prefiriendo el mismo set y luego cualquiera. Si no existe nada, la carta se queda como está.
    • segun_politica comprueba la política. Si la carta no la cumple, se buscan todas las impresiones de ese nombre y se elige la mejor que la cumpla: mismo idioma y set, luego mismo idioma, luego cualquiera. Si ninguna la cumple, la petición es NoImage.
    • El indicador lang_fallback es quiere_idioma && final.lang != options.lang; pasa a CardMeta.lang_fallback y se muestra como la insignia «vuelta a EN».
    • Para las fichas se recuerda la impresión original en inglés vista en el paso 1 (inglesas), porque las impresiones traducidas llevan all_parts incompleto.
  5. Fichas (add_tokens). A partir de la impresión en inglés de cada carta distinta, fichas_de devuelve las fichas que crea (véase más abajo); se añaden al final del mazo como CardSource::Token.
  6. Imágenes. Cada carta (y ficha) distinta se escribe en scryfall_cards (apuntar) y luego se descarga en grupos de 12 con join_all (el semáforo del cliente mantiene la concurrencia real en 6). ScryCard::print_url(face) elige el PNG (745x1040) cuando existe y, si no, large; el origen se registra como ImageOrigin::ScryfallPng o ScryfallLarge. Una carta de doble cara (is_dfc: sin image_uris de nivel superior y dos caras con imagen) descarga también la cara 1 y pasa a ser BackSpec::Dfc. Fase de progreso Images.
  7. Ensamblado, en el orden de la lista. Cada copia es una DeckCard nueva (UUID nuevo, CardSource::Scryfall { scryfall_id }, CardMeta de meta_de). La sección de la línea fija marca: Sideboard -> Banquillo, Maybeboard -> Opcional (véase Parseo de la lista del mazo). Una petición que Scryfall no encontró se prueba en Cockatrice por nombre (externa_por_nombre); si sigue sin aparecer queda como Unresolved { line_no, name, reason: NotFound }. Una petición descartada por la política queda como Unresolved ... NoImage. Siguen las fichas y después las fichas de otras fuentes que crean las cartas del mazo (externas_creadas_por, sin duplicados por nombre respecto a las fichas de Scryfall).

Cancelación, eventos, estado de ocupado

resolve_decklist devuelve un job_id de inmediato y se ejecuta en el runtime asíncrono de Tauri. El progreso sale por el evento resolve://progress (ResolveProgressEvent { jobId, progress: { phase, done, total } }, fases lookup, language, tokens, images) y el final por resolve://done (ResolveDoneEvent { jobId, result, error }). state.jobs (src-tauri/src/jobs.rs) reparte ids y un indicador de cancelación AtomicBool; job_cancel(jobId) lo activa y el resolvedor lo comprueba entre lotes (comprobar -> ErrorCode::Cancelled, en cuyo caso no se devuelve ninguna carta). El comando mantiene un guard de state.trabajo(), que hace que el actualizador automático se niegue a ejecutarse (Busy) mientras haya un trabajo en marcha.

El frontend usa lanzarTrabajo (src/lib/trabajos.ts), que escucha antes de lanzar (un trabajo corto puede terminar antes de que invoke devuelva su id) y almacena en un búfer los eventos tempranos. resolver() en src/features/decklist/acciones.ts construye las opciones a partir del store de la interfaz (lang, imagePolicy, addTokens), muestra las etiquetas de fase a partir de los eventos de progreso y trata Cancelled como «no ha cambiado nada». «Reaplicar idioma» (reaplicarIdioma) construye un ParsedDecklist sintético con una línea por cada carta de Scryfall no bloqueada (nombre + su set y número actuales) y llama al mismo flujo con addTokens: false; después cambia imágenes y metadatos conservando el id, las ediciones, el bloqueo y lo propio de cada carta (aplicarReaplicado en src/lib/cartas.ts).

Variantes e impresiones

  • card_prints(name, lang, highres_only) -> Vec<CardSummary>: todas las impresiones de un nombre exacto (hasta 175), usado por el selector de variantes (variant-dialog.tsx) y por «Añadir carta» en su modo de nombre exacto (buscar(texto, exacto = true) en add-card-dialog.tsx; en otro caso llama a card_search). lang: null significa cualquier idioma.
  • card_fetch(scryfall_id) -> DeckCard (traer_carta): obtiene una impresión, la guarda en la caché, descarga su PNG (y la cara trasera) y devuelve una carta con origen Token si su layout es token/double_faced_token, y Scryfall en otro caso. Quien llama la inserta o la usa para sustituir la imagen de otra carta.
  • CardSummary es la fila ligera para las rejillas: ids, nombres, set, número, una huella de miniatura (no una URL), highres, image_status, is_dfc, type_line y fuente para las cartas que no son de Scryfall. Miniaturas: resumir() registra cada URL de imagen bajo un SHA-256 de la URL en AppState.miniaturas (se vacía al pasar de 5 000 entradas) y la vista web pide pbimg://.../t/<huella>; el manejador del protocolo (src-tauri/src/protocol.rs) resuelve la huella, descarga (con el cliente de Scryfall para https://cards.scryfall.io/, y en otro caso con Externo, que tiene lista de permitidos) y guarda en caché el derivado. Véase Vista previa en vivo.
  • sets_list y set_cards(code, type_filter) alimentan el Explorador de sets.

Detección de fichas (fichas_auto.rs)

Se usa dos veces: «añadir fichas» al resolver, y «Detectar fichas» sobre un mazo existente (deck_tokens_detect, interfaz en src/features/decklist/fichas-dialog.tsx).

  • Las fichas figuran en las entradas all_parts de una carta con component == "token" (excluyendo la propia carta).
  • Por qué solo impresiones en inglés: Scryfall ofrece all_parts completo solo en la impresión en inglés; las impresiones traducidas no lo tienen o lo tienen parcial. Así que, para cada carta que no esté en inglés, fichas_de obtiene primero su carta en inglés por nombre (de la cara frontal) mediante /cards/collection y lee allí las partes. Si no se encuentra la carta en inglés, se siguen usando las partes de la propia carta traducida.
  • Después se piden los ids de las fichas en lotes de 75 por id. Dos fichas cuentan como la misma si su clave_de_ficha coincide: el oracle id, o name|type_line|power/toughness cuando no hay, de modo que la misma ficha de distintos sets de fichas aparece una sola vez. Cada resultado recuerda qué carta la creó (creada_por).
  • faltan_en_el_deck(api, deck, de_la_cache, cancelado) quita además las fichas que el mazo ya tiene: el mismo id, la misma ficha de otro set o una ficha con el mismo nombre (también una carta propia con ese nombre). Las cartas salen de la caché local cuando es posible y de Scryfall en otro caso. Después deck_tokens_detect añade también las fichas de Cockatrice que crean las cartas del mazo. Solo propone (FichaPropuesta { carta, creada_por }); el diálogo añade las elegidas con card_fetch.

Catálogo de nombres (nombres.rs)

Un índice local de todos los nombres de carta (unos 30 000 nombres en inglés de /catalog/card-names) y de todos los códigos de set (/sets), usado para reconocer el texto leído por OCR sin llamadas de red.

  • Ficheros en app_cache_dir: scryfall-card-names.json, scryfall-set-codes.json. Se renuevan cuando tienen más de 7 días; si la descarga falla se usa la copia vieja; la caché puede borrarse sin perder nada. Se cargan una vez por sesión en OnceCells (un fallo no se recuerda, así que se reintenta la próxima vez).
  • Indice mantiene dos mapas con clave normalizar(name) (minúsculas, sin acentos ni puntuación): exactos y plegados, donde se pliegan las letras que el OCR confunde (l, 1, | -> i; 0 -> o; plegar). Los nombres de cartas de doble cara se indexan enteros y por cada cara.
  • Búsquedas: exacto(line), al_principio(line) (el nombre más largo con el que empieza la línea, de dos palabras o más para que «Island of the dead» no sea «Island») y parecido(line) (la mejor coincidencia aproximada con similitud >= 0,85, recorriendo todo el catálogo y saltándose los candidatos cuya longitud difiere en más de un 15 %; es lenta, así que se usa para un puñado de líneas).
  • Consumidores: commands/identify.rs y originales.rs (véase Identificación de cartas y OCR).

Modos de fallo y códigos de error

Condición Código Dónde
Sin conexión, DNS, TLS, tiempo de espera Network (detalle) cualquier llamada
429 tras 3 reintentos RateLimited enviar
Otro no 2xx que no sea 404 (p. ej. 400 por una consulta incorrecta) Network (estado) enviar
Id de carta no encontrado NotFound (carta) card_fetch, card_link
Trabajo cancelado Cancelled resolve
Falla la escritura en la base de datos Db cache::guardar
Fallos por línea durante la resolución no es un error: entradas Unresolved en ResolveResult resolve

Todo resolve devuelve Err (y ninguna carta) si falla una petición de colección o una descarga de imagen; solo hay resultados parciales para los fallos a nivel de nombre.

Tests

  • src-tauri/src/scryfall/resolve.rs (mod tests, con la API falsa Falso): lista de cuatro cartas, marcas de banquillo, doble cara frontal/trasera, idioma de reserva, la política de alta resolución cambiando de impresión, fichas desde la impresión en inglés, fichas añadidas una sola vez, cancelación, más de 75 líneas en lotes, el progreso se completa, nombre en otro idioma mediante fuentes de reserva, reservas de Cockatrice.
  • src-tauri/src/scryfall/{rate,busquedas,cache,fichas,nombres,fichas_auto,models}.rs: tiempos del limitador (reloj en pausa), expulsión y caducidad de la caché, ida y vuelta de la caché, tabla de tipos de ficha, índice de nombres aproximado/exacto, deduplicación de fichas, parseo de modelos (fixture fixtures/dusk-dawn-es.json).
  • src-tauri/src/commands/cards.rs (mod tests): constructores de consultas, registro de miniaturas, traer_carta para normal/DFC/ficha/sin imagen/ausente.
  • Los tests en vivo contra la API real están marcados #[ignore] (por ejemplo scryfall_en_vivo en client.rs).

Por dónde empezar para ampliarlo

  • Un endpoint nuevo de Scryfall: añádelo a Scryfall (usa self.json(...) para que se apliquen el límite de ritmo y los reintentos); si el resolvedor o los tests lo necesitan, añádelo a CardApi y a los falsos (Falso en resolve.rs, Api en los tests de commands/cards.rs, SinRed en commands/stats.rs).
  • Un paso nuevo del flujo: respeta la regla «emparejar por identidad, nunca por posición», informa del progreso con una ResolvePhase y llama a comprobar(cancelado) entre lotes de red.
  • Campos de carta nuevos: añádelos a ScryCard como Option (para que el JSON antiguo de la caché siga deserializándose) y decide si hay que volver a pedir las filas viejas.

Véase también

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Frontend · Frontend

Reference · Referencia

Clone this wiki locally