Local supporter

Displaying photos from URL

by | Jun 19, 2025 | How To, Learn, Paradigm

When I downloaded my list of plants and animals from GBIF, they came in with scientific names and no pictures. I couldn’t tell you what most of those species were, even if you told me their common name. I have to learn them all.

I want to display temporary photos so I know what I’m looking for as I dig through my own photos. How do I know what photo is which species? I ask iNaturalist.

Correction. This is a business that needs to come out of the red. Time is money I don’t have. I want 500 pictures in five minutes. I asked ChatGPT how I can ask iNaturalist’s API. After about 20 failed attempts, I finally got a script that gave me a URL of a photo for every species on my list. The script is a bit complicated because it achieves three goals for all 500 species.

I won’t bore you with the frustration of failed attempts. Here is what FINALLY worked using Google Apps Script to run it:

function fetchINatPhotosAndAttributions() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const rows = sheet.getDataRange().getValues();

  for (let i = 1; i < rows.length; i++) { // Skip header
    const taxonUrl = rows[i][2];
    if (!taxonUrl || !taxonUrl.includes("/taxa/")) continue;

    const taxonId = taxonUrl.split("/taxa/")[1];
    const apiUrl = `https://api.inaturalist.org/v1/taxa/${taxonId}`;

    try {
      const response = UrlFetchApp.fetch(apiUrl);
      const json = JSON.parse(response.getContentText());
      const photo = json.results[0].default_photo;
      if (!photo) continue;

      const photoUrl = photo.medium_url || "";
      let rawAttribution = photo.attribution || "Photo © Unknown";
      const licenseCode = photo.license_code || "";
      const licenseUrl = getLicenseUrl(licenseCode);
      const licenseLink = licenseUrl
        ? `<a href="${licenseUrl}">${licenseCode.toUpperCase()}</a>`
        : licenseCode;

      // 🔹 Remove "uploaded by ..." if present
      rawAttribution = rawAttribution.replace(/,?\s*uploaded by .+$/i, "");

      // 🔹 Remove existing license mention (e.g., "(CC BY-NC)" or "CC BY-NC")
      rawAttribution = rawAttribution.replace(/\(CC [^)]+\)/i, "").replace(/CC [A-Z\-]+/i, "").trim();

      // 🔹 Remove trailing commas or extra spaces
      rawAttribution = rawAttribution.replace(/,\s*$/, "").trim();

      const cleanAttribution = `${rawAttribution}${licenseLink ? " (" + licenseLink + ")" : ""}`;

      // Write photo URL to column D and attribution to column E
      sheet.getRange(i + 1, 4).setValue(photoUrl);
      sheet.getRange(i + 1, 5).setValue(cleanAttribution);

    } catch (e) {
      Logger.log(`Error at row ${i + 1}: ${e.message}`);
    }

    Utilities.sleep(200); // avoid throttling
  }
}

function getLicenseUrl(code) {
  const base = "https://creativecommons.org/licenses/";
  const map = {
    "cc0": "zero/1.0/",
    "cc-by": "by/4.0/",
    "cc-by-sa": "by-sa/4.0/",
    "cc-by-nd": "by-nd/4.0/",
    "cc-by-nc": "by-nc/4.0/",
    "cc-by-nc-sa": "by-nc-sa/4.0/",
    "cc-by-nc-nd": "by-nc-nd/4.0/"
  };
  return code.toLowerCase() in map ? base + map[code.toLowerCase()] : "";
}

This code gave me two new columns on my spreadsheet. I now have the iNaturalist photo URL and photographer for every species on my list.

Questions? Suggestions? Send me a note.

8 + 14 =