UNPKG

scryfall

Version:

A simple wrapper around Scryfall's public API.

416 lines (398 loc) 14.6 kB
import * as https from "https"; import * as qs from "querystring"; import * as url from "url"; import { ScryfallListResponse } from "./ScryfallResponses"; import { ScryfallSet } from "./ScryfallSet"; import { ScryfallCard } from "./ScryfallCard"; import { ScryfallError } from "./ScryfallError"; import { ScryfallRuling } from "./ScryfallRuling"; /** * Attempts to autocomplete the specified token, returning a list of possible matches. * @param token The token to search for. * @param cb An optional callback to pass names to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function autocomplete(token: string, cb?: (matches: string[]) => void): Promise<string[]> { const ret = (cb) => { APIRequest( `/cards/autocomplete?q=${token}`, (cards) => { cb(Array.isArray(cards) ? cards : [cards]); }, true ); }; if (cb) { ret(cb); } else { return new Promise<string[]>((resolve, reject) => ret(resolve)); } } /** * Fetches a card with the given name, if only one match if found. Fails on multiple matches. * @param name The card name to search for, case-insensitive. * @param fuzzy Whether to use a fuzzy or an exact search. * @param cb An optional callback to pass card data to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function getCardByName( name: string, fuzzy: boolean = false, cb?: (err: ScryfallError, card: ScryfallCard) => void ): Promise<ScryfallCard> { const ret = (res, rej) => { APIRequest(`/cards/named?${fuzzy ? "fuzzy" : "exact"}=${name}`, (card: ScryfallError | ScryfallCard) => { if (card.object === "error") { rej ? rej(card) : res(card); } else { rej ? res(card) : res(null, card); } }); }; if (cb) { ret(cb, null); } else { return new Promise<ScryfallCard>((resolve, reject) => ret(resolve, reject)); } } /** * Fetches a list of rulings for the specified card. * @param card The card object to retrieve rulings for. * @param cb An optional callback to pass names to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function getRulings(card: ScryfallCard, cb?: (rulings: ScryfallRuling[]) => void): Promise<ScryfallRuling[]>; /** * Fetches a list of rulings for a card with the specified set/code. * @param setCode The card set. * @param cardNumber The card number. * @param cb An optional callback to pass names to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function getRulings( setCode: string, cardNumber: string, cb?: (err: Error | ScryfallError, rulings: ScryfallRuling[]) => void ): Promise<ScryfallRuling[]>; export function getRulings( first: ScryfallCard | string, second?: any, cb?: (err: Error | ScryfallError, rulings: ScryfallRuling[]) => void ): Promise<ScryfallRuling[]> { const ret = (res, rej?) => { let identifier = ""; const err = new Error("Invalid parameters given."); if (cb) { identifier = `${first}/${second}`; } else if (typeof first === "string") { identifier = first; } else if (first.id) { identifier = first.id; } else if (rej && typeof rej === "function") { rej(err); } else { res(err); } APIRequest( `/cards/${identifier}/rulings`, (rulings: ScryfallError | ScryfallRuling[]) => { if (Array.isArray(rulings)) { if (rej) { res(rulings); } else { res(null, rulings); } } else { if (rej) { rej(rulings); } else { res(rulings); } } }, true ); }; if (cb) { ret(cb); } else if (typeof second === "function") { ret(second); } else { return new Promise<ScryfallRuling[]>((resolve, reject) => ret(resolve, reject)); } } /** * Fetches a specified page of cards from the list of all recorded cards. * @param page The page to retrieve. * @param cb An optional callback to pass card data to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function getAllCards(page: number, cb?: (cards: ScryfallCard[]) => void): Promise<ScryfallCard[]> { const ret = (cb) => { APIRequest(`/cards?page=${page}`, cb, true, [], true); }; if (cb) { ret(cb); } else { return new Promise<ScryfallCard[]>((resolve, reject) => ret(resolve)); } } /** * Gets a card by its set code and collector number. Only available for cards that have collector numbers. * @param code The set code for this card. * @param number The collector number for this card. * @param cb An optional callback to pass card data to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function getCard( code: string, number: number, cb?: (err: ScryfallError, card?: ScryfallCard) => void ): Promise<ScryfallCard>; /** * Gets a card by its multiverse id. Only available to cards that have multiverse ids. * @param multiverseId The multiverse id for this card. * @param type The type of this id. Must be the string literal "multiverse". * @param cb An optional callback to pass card data to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function getCard( multiverseId: number, type: "multiverse", cb?: (err: ScryfallError, card?: ScryfallCard) => void ): Promise<ScryfallCard>; /** * Gets a card by its Magic Online id. Only available to cards that exist on Magic Online. * @param multiverseId The Magic Online id for this card. * @param type The type of this id. Must be the string literal "mtgo". * @param cb An optional callback to pass card data to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function getCard( mtgoId: number, type: "mtgo", cb?: (err: ScryfallError, card?: ScryfallCard) => void ): Promise<ScryfallCard>; /** * Gets a card by its Scryfall id. Available to every card fetchable through this API. It'd be kind of weird if it wasn't. * @param scryfallId The Scryfall id for this card. * @param cb An optional callback to pass card data to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function getCard( scryfallId: string, cb?: (err: ScryfallError, card?: ScryfallCard) => void ): Promise<ScryfallCard>; export function getCard( first?: number | string, second?: any, cb?: (err: ScryfallError, card?: ScryfallCard) => void ): Promise<ScryfallCard> { const ret = (res, rej) => { let firstType = typeof first; let secondType = isNaN(parseInt(second && second.replace ? second.replace(/[^0-9]/g, "") : second)) ? typeof second : "number"; let url = "/cards/"; let err = new Error(); switch (secondType) { case "undefined": case "function": // This will be a scryfall id lookup. if (firstType !== "string") { err.message = "The given Scryfall id is invalid"; } else { url += first; if (typeof second === "function") { res = second; } } break; case "string": // This will be a lookup by a multiverse or mtgo id. if (second !== "mtgo" && second !== "multiverse") { err.message = "Unable to determine the type of id being used"; } else { url += `${second}/${first}`; } break; case "number": // This will be a lookup by a set/collector pair. if (firstType !== "string") { err.message = "Unable to determine set code/collector number being used."; } else { url += `${first}/${encodeURIComponent(second)}`; } break; default: err.message = `Unable to parse arguments: ${secondType}`; rej ? rej(err) : res(err); } if (err && err.message) { rej ? rej(err) : res(err); } else { APIRequest(url, (cardData) => { if (cardData.object === "error") { rej ? rej(cardData as ScryfallError) : res(cardData as ScryfallError); } else if (cardData.object === "list") { console.warn("Scryfall card request returned more than one result - check your parameters."); rej ? res(cardData) : res(null, cardData); } else { rej ? res(cardData) : res(null, cardData); } }); } }; if (cb || typeof second === "function") { ret(cb, undefined); } else { return new Promise<ScryfallCard>(ret); } } /** * Fetches all versions of a card with the specified name. * @param name The card name to search for. * @param cb An optional callback to pass card data to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function cardVersions(name: string, cb?: (cards: ScryfallCard[]) => void): Promise<ScryfallCard[]> { const ret = (cb) => { APIRequest( `/cards/search?q=%2b%2b!%22${name}%22`, (cardData) => { cb(cardData); }, true ); }; if (cb) { ret(cb); } else { return new Promise<ScryfallCard[]>((resolve, reject) => ret(resolve)); } } /** * Fetches a list of all sets available on scryfall. * @param cb An optional callback to pass set data to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function allSets(cb?: (sets: ScryfallSet[]) => void): Promise<ScryfallSet[]> { const ret = (cb) => { APIRequest( "/sets", (resp) => { for (let i = 0; i < resp.length; i++) { resp[i].released_at = new Date(resp[i].released_at); } cb(resp); }, true ); }; if (cb) { ret(cb); } else { return new Promise<ScryfallSet[]>((resolve, reject) => ret(resolve)); } } /** * Fetches all the cards printed in a set with the specified code. * @param code The code of the set to search for. * @param cb An optional callback to pass card data to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function fromSet(code: string, cb?: (cards: ScryfallCard[]) => void): Promise<ScryfallCard[]> { const ret = (cb) => { APIRequest( `/cards/search?order=set&q=%2B%2Be%3A${code}`, (resp) => { cb(resp); }, true ); }; if (cb) { ret(cb); } else { return new Promise<ScryfallCard[]>((resolve, reject) => ret(resolve)); } } /** * Fetches a random card. * @param format The format to retrieve this card as. Defaults to json. * @param cb An optional callback to pass card data to. * @returns A promise, if no callback is specified. Otherwise nothing. */ export function randomCard( format: "json" | "image" | "text" = "json", cb?: (card: ScryfallCard) => void ): Promise<ScryfallCard> { const ret = (cb) => { APIRequest("/cards/random", (resp) => { cb(resp); }); }; if (cb) { ret(cb); } else { return new Promise<ScryfallCard>((resolve, reject) => ret(resolve)); } } const scryfallMethods = { fromSet: fromSet, allSets: allSets, autocomplete: autocomplete, cardVersions: cardVersions, getCard: getCard, getRulings: getRulings, randomCard: randomCard }; export { scryfallMethods as Scryfall }; /** * Makes a request to the Scryfall API. * @param uri The path to request, including any query parameters. * @param cb The callback to invoke when the request has completed. * @param preserve Whether or not to preserve the original response structure from this request. * @param page Whether or not to return data as pages. */ function APIRequest( uri: string, cb: (res: any) => void, preserve: boolean = false, _partialData = [], page: boolean = false ) { let parsedUrl = url.parse(uri); let query = qs.parse(parsedUrl.query); if (!query.format) { query.format = "json"; } let reqOps = { host: parsedUrl.host || "api.scryfall.com", path: (parsedUrl.pathname || "") + "?" + qs.stringify(query), headers: {} }; let req = https.get(reqOps, (resp) => { if (resp.statusCode === 429) { throw new Error("Too many requests have been made. Please wait a moment before making a new request."); } let responseData = ""; resp.on("data", (chunk) => { responseData += chunk; }); resp.on("end", () => { try { let jsonResp = JSON.parse(responseData); _partialData = _partialData.concat(jsonResp.data || jsonResp); if (!page && jsonResp.has_more && jsonResp.data.length > 0) { APIRequest(jsonResp.next_page, cb, preserve, _partialData); } else { if (!preserve && Array.isArray(_partialData) && _partialData.length) { _partialData = _partialData[0]; } cb(_partialData); } } catch (e) { console.error(e); } }); }); req.end(); }