Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 59 additions & 14 deletions scryfall-google-sheets.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// this function is available here:
// https://github.com/scryfall/google-sheets/blob/main/scryfall-google-sheets.js
// and was last updated on 2021-01-08 (probably)
// and was updated with bug fixes for API rate limits and User-Agent policies (2026-04-03).

const MAX_RESULTS_ = 700; // a safe max due to Google Sheets timeout system

// ---- Configure these ----
const SCRYFALL_USER_AGENT_ = "TLASheet-Scryfall-Integration/1.1";
const SCRYFALL_ACCEPT_ = "application/json;q=0.9,*/*;q=0.8";
// -------------------------

/**
* Inserts the results of a search in Scryfall into your spreadsheet
*
Expand All @@ -17,8 +22,8 @@ const MAX_RESULTS_ = 700; // a safe max due to Google Sheets timeout system
* @customfunction
*/
const SCRYFALL = (query, fields = "name", num_results = 150,
order = "name", dir = "auto", unique = "cards") => {
if (query === undefined) {
order = "name", dir = "auto", unique = "cards") => {
if (query === undefined) {
throw new Error("Must include a query");
}

Expand All @@ -28,7 +33,9 @@ const SCRYFALL = (query, fields = "name", num_results = 150,
}

// the docs say fields is space separated, but allow comma separated too
fields = fields.split(/[\s,]+/);
if (typeof fields === "string") {
fields = fields.split(/[\s,]+/);
}

// most people won't know the JSON field names for cards, so let's do some mapping of
// what they'll try to what it should be
Expand Down Expand Up @@ -80,7 +87,9 @@ const SCRYFALL = (query, fields = "name", num_results = 150,

// a little hack to make images return an image function; note that Google
// sheets doesn't currently execute it or anything
card["image"] = `=IMAGE("${card["image_uris"]["normal"]}", 4, 340, 244)`;
if (card["image_uris"] && card["image_uris"]["normal"]) {
card["image"] = `=IMAGE("${card["image_uris"]["normal"]}", 4, 340, 244)`;
}

fields.forEach(field => {
// grab the field from the card data
Expand All @@ -99,43 +108,79 @@ const SCRYFALL = (query, fields = "name", num_results = 150,
output.push(row);
});

// If no cards were found, return an error message to display in the cell gracefully
if (output.length === 0) {
return [Array(fields.length).fill("Card not found")];
}

return output;
};

const deepFind_ = (obj, path) => {
return path.split(".").reduce((prev, curr) => prev && prev[curr], obj)
};


// paginated query of scryfall
const scryfallSearch_ = (params, num_results = MAX_RESULTS_) => {
const query_string = Object.entries(params).map(([key, val]) => `${key}=${encodeURIComponent(val)}`).join('&');
const scryfall_url = `https://api.scryfall.com/cards/search?${query_string}`;

let data = [];
let page = 1;
let response;
let responseText;

// Set up headers to comply with new Scryfall API requirements
const options = {
'method': 'get',
'headers': {
'User-Agent': SCRYFALL_USER_AGENT_,
'Accept': SCRYFALL_ACCEPT_
},
'muteHttpExceptions': true
};

// try to get the results from scryfall
try {
while (true) {
response = JSON.parse(UrlFetchApp.fetch(`${scryfall_url}&page=${page}`).getContentText());
Utilities.sleep(150); // Respect Scryfall's rate limit of 10 requests/sec

if (!response.data) {
throw new Error("No results from Scryfall");
let response = UrlFetchApp.fetch(`${scryfall_url}&page=${page}`, options);
let responseCode = response.getResponseCode();
responseText = response.getContentText();

// Handle 404 (no results found for the query) Gracefully
if (responseCode === 404) {
break; // Return empty data array
}

data.push(...response.data);
// Handle Rate Limiting
if (responseCode === 429) {
Utilities.sleep(1000); // Sleep longer if we hit rate limits and retry
continue;
}

if (!response.has_more || data.length > num_results) {
// Throw error ONLY on other actual HTTP errors
if (responseCode !== 200) {
throw new Error(`Scryfall API returned ${responseCode}: ${responseText}`);
}

let parsedResponse = JSON.parse(responseText);

if (!parsedResponse.data) {
break;
}

data.push(...parsedResponse.data);

if (!parsedResponse.has_more || data.length >= num_results) {
break;
}

page++;
}
} catch (error) {
throw new Error(`Unable to retrieve results from Scryfall: ${error}`);
throw new Error(`Unable to retrieve results from Scryfall: ${error.message}`);
}

return data;
};
};