I was working on Gallery Casale and was originally just pulling in the latest post featured images, which was nice enough, but I wanted a few more options. When using Bricks, I used the API query feature to retrieve images from my Flickr account, but I wanted to see if it was possible to use Etch instead. A couple of people had done work getting other JSON data into Etch, so I borrowed their ideas and the Flickr API documentation (a challenging resource) to build my prompt for ChatGPT. It only took a couple of reworks, and I now have a way to populate the latest images and a specific album.
I opted not to make this a plugin, since the API query functionality is considered a high priority.
The latest images
I use WPCodeBox for this. Once you add it, configure your API Key and user ID, then go to [your-website-url]/wp-admin/?fw_flickr_sync=1 while logged in as the admin. This will initiate the data and set a CRON job to run hourly.
<?php
/**
* Flickr → Etch Loop Library Sync (WPCodeBox-safe)
* Writes/updates a Loop Library entry in the `etch_loops` option.
*/
if (!defined('ABSPATH')) exit;
/** ====== CONFIG (edit API key and USER ID) ====== */
define('FW_FLICKR_API_KEY', 'PUT_YOUR_FLICKR_API_KEY_HERE');
define('FW_FLICKR_USER_ID', 'PUT YOUR UNDER ID HERE');
define('FW_LOOP_SLUG', 'fw-flickr-feed'); // internal array key in etch_loops
define('FW_LOOP_KEY', 'fw_flickr_feed'); // Etch loop key (underscores only)
define('FW_PER_PAGE', 48);
define('FW_CRON_HOOK', 'fw_flickr_to_etch_sync');
define('FW_LAST_UPLOAD_OPT', 'fw_flickr_last_upload_ts');
/**
* Ensure cron is scheduled (WPCodeBox doesn't have activation hooks).
*/
add_action('init', function () {
if (wp_next_scheduled(FW_CRON_HOOK)) return;
wp_schedule_event(time() + 120, 'hourly', FW_CRON_HOOK);
}, 5);
/**
* Manual run: /wp-admin/?fw_flickr_sync=1 (admins only)
*/
add_action('admin_init', function () {
if (!current_user_can('manage_options')) return;
if (!isset($_GET['fw_flickr_sync'])) return;
fw_flickr_sync_to_etch(true);
wp_safe_redirect(remove_query_arg('fw_flickr_sync'));
exit;
});
add_action(FW_CRON_HOOK, function () {
fw_flickr_sync_to_etch(false);
});
/**
* Sync Flickr → etch_loops.
*/
function fw_flickr_sync_to_etch($force_full = false) {
if (!FW_FLICKR_API_KEY || FW_FLICKR_API_KEY === 'PUT_YOUR_FLICKR_API_KEY_HERE') return;
$etch_loops = get_option('etch_loops');
if (!is_array($etch_loops)) {
// Etch not active or option not present yet.
return;
}
// Existing stored items (what's currently in the Etch loop entry)
$existing = array();
if (isset($etch_loops[FW_LOOP_SLUG]['config']['data']) && is_array($etch_loops[FW_LOOP_SLUG]['config']['data'])) {
$existing = $etch_loops[FW_LOOP_SLUG]['config']['data'];
}
$last_upload = (int) get_option(FW_LAST_UPLOAD_OPT, 0);
// You asked for: url_l,url_m,description
// Add date_upload so we can detect new data / sort.
$extras = 'url_l,url_m,description,date_upload';
$need_fill = $force_full || (count($existing) < (int) FW_PER_PAGE);
$new_items = array();
$max_upload = $last_upload;
if ($need_fill) {
/**
* FILL MODE:
* Fetch pages starting at page 1 until we have PER_PAGE items or run out.
* (Still merge with existing so we don't erase stored items while filling.)
*/
$page = 1;
$safety_max_pages = 10; // avoids runaway if something weird happens
while (count($new_items) < (int) FW_PER_PAGE && $page <= $safety_max_pages) {
$batch = fw_flickr_fetch_public_photos_page($page, (int) FW_PER_PAGE, $extras);
if (empty($batch)) break;
// append batch into new_items (dedupe within batch stream)
foreach ($batch as $it) {
if (empty($it['id'])) continue;
$new_items[$it['id']] = $it; // keyed by id to dedupe
$ts = isset($it['dateupload']) ? (int) $it['dateupload'] : 0;
if ($ts > $max_upload) $max_upload = $ts;
if (count($new_items) >= (int) FW_PER_PAGE) break;
}
$page++;
}
$new_items = array_values($new_items);
} else {
/**
* INCREMENTAL MODE:
* Stored already has >= PER_PAGE, so only fetch new uploads.
*/
if ($last_upload > 0) {
$args = array(
'method' => 'flickr.people.getPublicPhotos',
'api_key' => FW_FLICKR_API_KEY,
'user_id' => FW_FLICKR_USER_ID,
'format' => 'json',
'nojsoncallback' => '1',
'per_page' => (string) FW_PER_PAGE,
'page' => '1',
'extras' => $extras,
'min_upload_date'=> (string) ($last_upload + 1),
);
$url = add_query_arg($args, 'https://api.flickr.com/services/rest/');
$res = wp_remote_get($url, array(
'timeout' => 20,
'headers' => array(
'User-Agent' => 'WordPress; Flickr→Etch Sync; ' . home_url('/'),
'Accept' => 'application/json',
),
));
if (is_wp_error($res)) return;
$code = (int) wp_remote_retrieve_response_code($res);
if ($code < 200 || $code >= 300) return;
$data = json_decode(wp_remote_retrieve_body($res), true);
$photos = isset($data['photos']['photo']) ? $data['photos']['photo'] : null;
if (!is_array($photos) || empty($photos)) return; // no new uploads
foreach ($photos as $p) {
$it = fw_flickr_normalize_photo($p);
if (!$it) continue;
$new_items[] = $it;
$ts = isset($it['dateupload']) ? (int) $it['dateupload'] : 0;
if ($ts > $max_upload) $max_upload = $ts;
}
} else {
// No last upload stored yet; treat as fill.
return fw_flickr_sync_to_etch(true);
}
}
// Merge: new first, then existing; de-dupe by id
$merged = array();
$seen = array();
foreach (array_merge($new_items, $existing) as $it) {
if (!is_array($it)) continue;
$id = isset($it['id']) ? (string) $it['id'] : '';
if (!$id || isset($seen[$id])) continue;
$seen[$id] = true;
$merged[] = $it;
}
// Sort newest first
usort($merged, function ($a, $b) {
return ((int) ($b['dateupload'] ?? 0)) <=> ((int) ($a['dateupload'] ?? 0));
});
// Your rule:
// - if > PER_PAGE: trim to latest PER_PAGE
// - else: keep all (do not erase while still filling)
if (count($merged) > (int) FW_PER_PAGE) {
$merged = array_slice($merged, 0, (int) FW_PER_PAGE);
}
$entry = array(
'name' => 'Flickr Feed',
'key' => FW_LOOP_KEY,
'global' => true,
'config' => array(
'type' => 'json',
'data' => $merged,
),
);
// Only write if changed
$current = get_option('etch_loops');
$current_entry = (is_array($current) && isset($current[FW_LOOP_SLUG])) ? $current[FW_LOOP_SLUG] : null;
if (maybe_serialize($current_entry) !== maybe_serialize($entry)) {
$etch_loops[FW_LOOP_SLUG] = $entry;
update_option('etch_loops', $etch_loops);
}
update_option(FW_LAST_UPLOAD_OPT, $max_upload, false);
}
/**
* Fetch one page of public photos and normalize.
*/
function fw_flickr_fetch_public_photos_page($page, $per_page, $extras) {
$args = array(
'method' => 'flickr.people.getPublicPhotos',
'api_key' => FW_FLICKR_API_KEY,
'user_id' => FW_FLICKR_USER_ID,
'format' => 'json',
'nojsoncallback' => '1',
'per_page' => (string) $per_page,
'page' => (string) $page,
'extras' => (string) $extras,
);
$url = add_query_arg($args, 'https://api.flickr.com/services/rest/');
$res = wp_remote_get($url, array(
'timeout' => 20,
'headers' => array(
'User-Agent' => 'WordPress; Flickr→Etch Sync; ' . home_url('/'),
'Accept' => 'application/json',
),
));
if (is_wp_error($res)) return array();
$code = (int) wp_remote_retrieve_response_code($res);
if ($code < 200 || $code >= 300) return array();
$data = json_decode(wp_remote_retrieve_body($res), true);
$photos = isset($data['photos']['photo']) ? $data['photos']['photo'] : null;
if (!is_array($photos) || empty($photos)) return array();
$out = array();
foreach ($photos as $p) {
$it = fw_flickr_normalize_photo($p);
if ($it) $out[] = $it;
}
return $out;
}
/**
* Normalize one Flickr photo record to your Etch JSON item shape.
*/
function fw_flickr_normalize_photo($p) {
if (!is_array($p)) return null;
$id = isset($p['id']) ? (string) $p['id'] : '';
if (!$id) return null;
$owner = isset($p['owner']) ? (string) $p['owner'] : '';
$title = isset($p['title']) ? (string) $p['title'] : '';
$desc = '';
if (isset($p['description']['_content'])) {
$desc = (string) $p['description']['_content'];
}
$url_l = !empty($p['url_l']) ? (string) $p['url_l'] : '';
$url_m = !empty($p['url_m']) ? (string) $p['url_m'] : '';
$dateupload = isset($p['dateupload']) ? (int) $p['dateupload'] : 0;
return array(
'id' => $id,
'title' => $title,
'url_l' => $url_l,
'url_m' => $url_m,
'description' => $desc,
'link' => ($owner && $id) ? "https://www.flickr.com/photos/{$owner}/{$id}/" : '',
'dateupload' => $dateupload,
);
}A Specific Album
The trigger for this is /wp-admin/?fw_flickr_album_sync=1. You need to add your API key, user ID, and the Album D.
<?php
/**
* Flickr Album → Etch Loop Library Sync (WPCodeBox-safe)
* Creates a separate Loop Library JSON entry: "Flickr Album Feed"
*/
if (!defined('ABSPATH')) exit;
/** ====== CONFIG ====== */
/** ====== CONFIG (edit API key and USER ID) ====== */
define('FW_FLICKR_API_KEY', 'PUT_YOUR_FLICKR_API_KEY_HERE');
define('FW_FLICKR_USER_ID', 'PUT YOUR UNDER ID HERE');
define('FW_FLICKR_PHOTOSET_ID', 'PUT YOUR ALBUM ID HERE'); // album id (no trailing slash)
define('FW_ALBUM_LOOP_SLUG', 'fw-flickr-album-feed'); // internal array key in etch_loops
define('FW_ALBUM_LOOP_KEY', 'fw_flickr_album_feed'); // Etch loop key (underscores only)
define('FW_ALBUM_PER_PAGE', 40);
define('FW_ALBUM_CRON_HOOK', 'fw_flickr_album_to_etch_sync');
/**
* Ensure cron is scheduled.
*/
add_action('init', function () {
if (wp_next_scheduled(FW_ALBUM_CRON_HOOK)) return;
wp_schedule_event(time() + 120, 'hourly', FW_ALBUM_CRON_HOOK);
}, 5);
/**
* Manual run: /wp-admin/?fw_flickr_album_sync=1 (admins only)
*/
add_action('admin_init', function () {
if (!current_user_can('manage_options')) return;
if (!isset($_GET['fw_flickr_album_sync'])) return;
fw_flickr_album_sync_to_etch();
wp_safe_redirect(remove_query_arg('fw_flickr_album_sync'));
exit;
});
add_action(FW_ALBUM_CRON_HOOK, function () {
fw_flickr_album_sync_to_etch();
});
function fw_flickr_album_sync_to_etch(): void {
if (!FW_FLICKR_API_KEY || FW_FLICKR_API_KEY === 'PUT_YOUR_FLICKR_API_KEY_HERE') return;
$etch_loops = get_option('etch_loops');
if (!is_array($etch_loops)) return;
// Existing stored items
$existing = array();
if (isset($etch_loops[FW_ALBUM_LOOP_SLUG]['config']['data']) && is_array($etch_loops[FW_ALBUM_LOOP_SLUG]['config']['data'])) {
$existing = $etch_loops[FW_ALBUM_LOOP_SLUG]['config']['data'];
}
$target = (int) FW_ALBUM_PER_PAGE;
/**
* Always fetch the latest up-to-N items from the album (page 1..x).
* If the album contains < N total, we'll just fetch what's available.
*/
$fetched = array();
$page = 1;
$per_page = $target; // fetch in N-sized pages to minimize calls
$max_pages = 10; // safety limit
$extras = 'url_l,url_m,description,date_upload';
while (count($fetched) < $target && $page <= $max_pages) {
$args = array(
'method' => 'flickr.photosets.getPhotos',
'api_key' => FW_FLICKR_API_KEY,
'photoset_id' => FW_FLICKR_PHOTOSET_ID,
'user_id' => FW_FLICKR_USER_ID,
'format' => 'json',
'nojsoncallback' => '1',
'per_page' => (string) $per_page,
'page' => (string) $page,
'extras' => $extras,
);
$url = add_query_arg($args, 'https://api.flickr.com/services/rest/');
$res = wp_remote_get($url, array(
'timeout' => 20,
'headers' => array(
'User-Agent' => 'WordPress; Flickr Album→Etch Sync; ' . home_url('/'),
'Accept' => 'application/json',
),
));
if (is_wp_error($res)) break;
$code = (int) wp_remote_retrieve_response_code($res);
if ($code < 200 || $code >= 300) break;
$data = json_decode(wp_remote_retrieve_body($res), true);
$photos = $data['photoset']['photo'] ?? null;
if (!is_array($photos) || empty($photos)) break;
foreach ($photos as $p) {
$id = isset($p['id']) ? (string) $p['id'] : '';
if (!$id) continue;
$owner = isset($p['owner']) ? (string) $p['owner'] : FW_FLICKR_USER_ID;
$desc = '';
if (isset($p['description']['_content'])) {
$desc = (string) $p['description']['_content'];
}
$item = array(
'id' => $id,
'title' => isset($p['title']) ? (string) $p['title'] : '',
'url_l' => !empty($p['url_l']) ? (string) $p['url_l'] : '',
'url_m' => !empty($p['url_m']) ? (string) $p['url_m'] : '',
'description' => $desc,
'link' => ($owner && $id) ? "https://www.flickr.com/photos/{$owner}/{$id}/" : '',
'dateupload' => isset($p['dateupload']) ? (int) $p['dateupload'] : 0,
);
// Dedup fetched by id
$fetched[$id] = $item;
if (count($fetched) >= $target) break;
}
// If this page returned fewer than per_page, we likely hit the end
if (count($photos) < $per_page) break;
$page++;
}
$fetched = array_values($fetched);
/**
* Merge: fetched latest first, then existing.
* This is the “append until N” rule when album has < N total.
*/
$merged = array();
$seen = array();
foreach (array_merge($fetched, $existing) as $it) {
if (!is_array($it)) continue;
$id = isset($it['id']) ? (string) $it['id'] : '';
if (!$id || isset($seen[$id])) continue;
$seen[$id] = true;
$merged[] = $it;
}
// Sort newest first
usort($merged, function ($a, $b) {
return ((int) ($b['dateupload'] ?? 0)) <=> ((int) ($a['dateupload'] ?? 0));
});
// Your rule: only trim once we exceed N
if (count($merged) > $target) {
$merged = array_slice($merged, 0, $target);
}
$entry = array(
'name' => 'Flickr Album Feed',
'key' => FW_ALBUM_LOOP_KEY,
'global' => true,
'config' => array(
'type' => 'json',
'data' => $merged,
),
);
// Only write if changed (optional, but good)
$current = get_option('etch_loops');
$current_entry = (is_array($current) && isset($current[FW_ALBUM_LOOP_SLUG])) ? $current[FW_ALBUM_LOOP_SLUG] : null;
if (maybe_serialize($current_entry) !== maybe_serialize($entry)) {
$etch_loops[FW_ALBUM_LOOP_SLUG] = $entry;
update_option('etch_loops', $etch_loops);
}
}
Leave a Reply