true, CURLOPT_CONNECTTIMEOUT => 4, CURLOPT_TIMEOUT => 6, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 2, CURLOPT_USERAGENT => 'autofeed-spool-tracker/1.0', ]); $body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($body === false || $code !== 200) { return null; } } else { $ctx = stream_context_create(['http' => [ 'timeout' => 6, 'header' => "User-Agent: autofeed-spool-tracker/1.0\r\n", ]]); $body = @file_get_contents($url, false, $ctx); if ($body === false) { return null; } } $data = json_decode($body, true); return is_array($data) ? $data : null; } // Search their swatch library; returns rows for the form or null when unreachable. function fc_search(string $q, string $brand): ?array { $params = ['page_size' => 10, 'color_name__icontains' => $q]; if ($brand !== '') { $params['manufacturer__name__icontains'] = $brand; } $data = fc_fetch('https://filamentcolors.xyz/api/swatch/?' . http_build_query($params)); if ($data === null) { return null; } $out = []; foreach ($data['results'] ?? [] as $s) { $hex = strtolower((string)($s['hex_color'] ?? '')); $out[] = [ 'type' => (string)($s['filament_type']['name'] ?? ''), 'brand' => (string)($s['manufacturer']['name'] ?? ''), 'color_name' => (string)($s['color_name'] ?? ''), 'hex' => preg_match('/^[0-9a-f]{6}$/', $hex) ? '#' . $hex : null, ]; } return $out; } $q = trim((string)($_GET['q'] ?? '')); $brand = trim((string)($_GET['brand'] ?? '')); if (strlen($q) < 2) { echo '[]'; exit; } $key = strtolower($q) . '|' . strtolower($brand); $st = db()->prepare("SELECT json FROM api_cache WHERE key = ? AND fetched_at > datetime('now', '-7 days')"); $st->execute([$key]); $hit = $st->fetchColumn(); if ($hit !== false) { echo $hit; exit; } $results = fc_search($q, $brand); if ($results !== null && !$results && $brand !== '') { // The locally entered brand may be spelled differently from theirs // (e.g. "Prusa" vs "Prusament"): retry on color name alone. $results = fc_search($q, ''); } if ($results === null) { // Upstream unreachable: an expired cache entry beats nothing. $st = db()->prepare('SELECT json FROM api_cache WHERE key = ?'); $st->execute([$key]); $old = $st->fetchColumn(); echo $old !== false ? $old : '[]'; exit; } $json = json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); // INSERT OR REPLACE re-inserts the row, so fetched_at picks up its default. db()->prepare('INSERT OR REPLACE INTO api_cache (key, json) VALUES (?, ?)')->execute([$key, $json]); db()->exec("DELETE FROM api_cache WHERE fetched_at < datetime('now', '-30 days')"); echo $json;