├── README.md ├── example_output.png └── scryfall-google-sheets.js /README.md: -------------------------------------------------------------------------------- 1 | Retrieve Scryfall results from inside Google Sheets 2 | 3 | ![](example_output.png) 4 | 5 | # Installation 6 | 7 | 1. Copy the contents of [scryfall-google-sheets.js](https://raw.githubusercontent.com/scryfall/google-sheets/main/scryfall-google-sheets.js). 8 | 1. Open your sheet and go to **Extensions → Apps Scripts** 9 | 1. Paste the contents of your clipboard into the script editor, then hit Save. (Nothing special will happen. You do not need to hit Deploy.) 10 | 1. You're done! The `=SCRYFALL()` function should now be available on your spreadsheet. 11 | 1. Return to your spreadsheet. Pick any cell, focus the formula bar up to, and start typing `=SCRYFALL`. Its autocomplete should begin showing up. 12 | 13 | # Usage 14 | 15 | ``` 16 | =SCRYFALL(query, fields, num_results, order, dir, unique) 17 | ``` 18 | 19 | * `query`: Scryfall search query. 20 | * `fields`: A single string containing a space-separated list of fields from a card object to return, using `.` for nested items (e.g. prices.eur). 21 | *For example: `"name type oracle_text power toughness"`* 22 | * `num_results`: Number of results to return (maximum 700) 23 | * `order`: The order to sort cards by, `"name"` is the default. You can find other sort orders [in the API help for sorting cards](https://scryfall.com/docs/api/cards/search#sorting-cards). 24 | * `dir`: Direction to return the sorted cards: `"auto"`, `"asc"`, or `"desc"` 25 | * `unique`: Find unique `"cards"` (default), `"art"`, or `"prints"`. 26 | 27 | If you are unsure what fields can be in a [card object](https://scryfall.com/docs/api/cards), here is [an example](https://api.scryfall.com/cards/4dcdcad5-e4fb-480e-984f-1ac5cdc986b9?format=json&pretty=true). 28 | 29 | # Examples 30 | 31 | As it can be difficult to describe how to use a function, here are some examples: 32 | 33 | ### List of creatures with 10 or more power 34 | `=SCRYFALL("type:creature pow>=10")` 35 | 36 | ### The price of every card in Dominaria, sorted by price (USD) 37 | `=SCRYFALL("set:dom", "name prices.usd prices.eur", 750, "price")` 38 | 39 | ### Legacy legal cards in paper but not available on Magic Online, returning 700 results 40 | `=SCRYFALL("in:paper -in:mtgo legal:legacy", "name", 700)` 41 | 42 | ### List of cards with Jaya Ballard flavor text, returning card name, set name, mana cost, and flavor text 43 | `=SCRYFALL("flavor:'jaya ballard'", "name set_name mana flavor")` 44 | 45 | ### Commander cards not available in foil, with name, set name, release date, color identity, URL, and oracle text, sorted by EDHREC popularity 46 | `=SCRYFALL("-in:foil game:paper legal:commander -is:reprint -is:reserved", "name set_name released_at color url oracle", 150, "edhrec")` 47 | 48 | # Handling large searches 49 | 50 | Note that your search *must* return a result in 30 seconds or less. Asking for too many results can result in 51 | your spreadsheet showing an ERROR. Repeating a `=SCRYFALL()` function with the same query may work on a second 52 | attempt, as Scryfall caches results. 53 | 54 | If you want to have a spreadsheet with more than 700 results, your best bet is to shard your results. For example, 55 | if you wanted a list of all legal Commanders (which has over 1000 results), you can do: 56 | 57 | `=SCRYFALL("is:commander legal:commander name:/^[abcdefghijklm]/", "name", 700)` 58 | `=SCRYFALL("is:commander legal:commander name:/^[nopqrstuvwxyz]/", "name", 700)` 59 | -------------------------------------------------------------------------------- /example_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scryfall/google-sheets/4ac517eb0452b59f50bc697e79d415be1ddcd19b/example_output.png -------------------------------------------------------------------------------- /scryfall-google-sheets.js: -------------------------------------------------------------------------------- 1 | // this function is available here: 2 | // https://github.com/scryfall/google-sheets/blob/main/scryfall-google-sheets.js 3 | // and was last updated on 2021-01-08 (probably) 4 | 5 | const MAX_RESULTS_ = 700; // a safe max due to Google Sheets timeout system 6 | 7 | /** 8 | * Inserts the results of a search in Scryfall into your spreadsheet 9 | * 10 | * @param {"name:braids type:legendary"} query Scryfall search query 11 | * @param {"name power toughness"} fields List of fields to return from Scryfall, "name" is default 12 | * @param {150} num_results Number of results (default 150, maximum 700) 13 | * @param {name} order The order to sort cards by, "name" is default 14 | * @param {auto} dir Direction to return the sorted cards: auto, asc, or desc 15 | * @param {cards} unique Remove duplicate cards (default), art, or prints 16 | * @return List of Scryfall search results 17 | * @customfunction 18 | */ 19 | const SCRYFALL = (query, fields = "name", num_results = 150, 20 | order = "name", dir = "auto", unique = "cards") => { 21 | if (query === undefined) { 22 | throw new Error("Must include a query"); 23 | } 24 | 25 | // don't break scryfall 26 | if (num_results > MAX_RESULTS_) { 27 | num_results = MAX_RESULTS_; 28 | } 29 | 30 | // the docs say fields is space separated, but allow comma separated too 31 | fields = fields.split(/[\s,]+/); 32 | 33 | // most people won't know the JSON field names for cards, so let's do some mapping of 34 | // what they'll try to what it should be 35 | const field_mappings = { 36 | "color": "color_identity", 37 | "colors": "color_identity", 38 | "flavor": "flavor_text", 39 | "mana": "mana_cost", 40 | "o": "oracle_text", 41 | "oracle": "oracle_text", 42 | "price": "prices.usd", 43 | "type": "type_line", 44 | "uri": "scryfall_uri", 45 | "url": "scryfall_uri", 46 | } 47 | 48 | // do the same friendly thing, but for sorting options 49 | const order_mappings = { 50 | "price": "usd", 51 | "prices.eur": "eur", 52 | "prices.usd": "usd", 53 | }; 54 | 55 | fields = fields.map(field => field_mappings[field] === undefined ? field : field_mappings[field]); 56 | order = order_mappings[order] === undefined ? order : order_mappings[order]; 57 | 58 | // google script doesn't have URLSearchParams 59 | const scryfall_query = { 60 | q: query, 61 | order: order, 62 | dir: dir, 63 | unique: unique, 64 | }; 65 | 66 | // query scryfall 67 | const cards = scryfallSearch_(scryfall_query, num_results); 68 | 69 | // now, let's accumulate the results 70 | let output = []; 71 | 72 | cards.splice(0, num_results).forEach(card => { 73 | let row = []; 74 | 75 | // there is probably a better way to handle card faces, but this is 76 | // probably sufficient for the vast majority of use cases 77 | if ("card_faces" in card) { 78 | Object.assign(card, card["card_faces"][0]); 79 | } 80 | 81 | // a little hack to make images return an image function; note that Google 82 | // sheets doesn't currently execute it or anything 83 | card["image"] = `=IMAGE("${card["image_uris"]["normal"]}", 4, 340, 244)`; 84 | 85 | fields.forEach(field => { 86 | // grab the field from the card data 87 | let val = deepFind_(card, field) || ""; 88 | 89 | // then, let's do some nice data massaging for use inside Sheets 90 | if (typeof val === "string") { 91 | val = val.replace(/\n/g, "\n\n"); // double space for readability 92 | } else if (Array.isArray(val)) { 93 | val = field.includes("color") ? val.join("") : val.join(", "); 94 | } 95 | 96 | row.push(val); 97 | }); 98 | 99 | output.push(row); 100 | }); 101 | 102 | return output; 103 | }; 104 | 105 | const deepFind_ = (obj, path) => { 106 | return path.split(".").reduce((prev, curr) => prev && prev[curr], obj) 107 | }; 108 | 109 | 110 | // paginated query of scryfall 111 | const scryfallSearch_ = (params, num_results = MAX_RESULTS_) => { 112 | const query_string = Object.entries(params).map(([key, val]) => `${key}=${encodeURIComponent(val)}`).join('&'); 113 | const scryfall_url = `https://api.scryfall.com/cards/search?${query_string}`; 114 | 115 | let data = []; 116 | let page = 1; 117 | let response; 118 | 119 | // try to get the results from scryfall 120 | try { 121 | while (true) { 122 | response = JSON.parse(UrlFetchApp.fetch(`${scryfall_url}&page=${page}`).getContentText()); 123 | 124 | if (!response.data) { 125 | throw new Error("No results from Scryfall"); 126 | } 127 | 128 | data.push(...response.data); 129 | 130 | if (!response.has_more || data.length > num_results) { 131 | break; 132 | } 133 | 134 | page++; 135 | } 136 | } catch (error) { 137 | throw new Error(`Unable to retrieve results from Scryfall: ${error}`); 138 | } 139 | 140 | return data; 141 | }; 142 | --------------------------------------------------------------------------------