110 lines · 3.5 KB
Raw Download
1
<?php
2
// JSON endpoint: proxy swatch searches to filamentcolors.xyz (their API has no
3
// CORS headers, so the browser can't call it directly) with a local cache.
4
require_once __DIR__ . '/inc/auth.php';
5
6
header('Content-Type: application/json');
7
header('Cache-Control: no-store');
8
9
if (!is_logged_in()) {
10
    http_response_code(401);
11
    echo '{"error":"unauthorized"}';
12
    exit;
13
}
14
15
function fc_fetch(string $url): ?array
16
{
17
    if (function_exists('curl_init')) {
18
        $ch = curl_init($url);
19
        curl_setopt_array($ch, [
20
            CURLOPT_RETURNTRANSFER => true,
21
            CURLOPT_CONNECTTIMEOUT => 4,
22
            CURLOPT_TIMEOUT        => 6,
23
            CURLOPT_FOLLOWLOCATION => true,
24
            CURLOPT_MAXREDIRS      => 2,
25
            CURLOPT_USERAGENT      => 'autofeed-spool-tracker/1.0',
26
        ]);
27
        $body = curl_exec($ch);
28
        $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
29
        curl_close($ch);
30
        if ($body === false || $code !== 200) {
31
            return null;
32
        }
33
    } else {
34
        $ctx = stream_context_create(['http' => [
35
            'timeout' => 6,
36
            'header'  => "User-Agent: autofeed-spool-tracker/1.0\r\n",
37
        ]]);
38
        $body = @file_get_contents($url, false, $ctx);
39
        if ($body === false) {
40
            return null;
41
        }
42
    }
43
    $data = json_decode($body, true);
44
    return is_array($data) ? $data : null;
45
}
46
47
// Search their swatch library; returns rows for the form or null when unreachable.
48
function fc_search(string $q, string $brand): ?array
49
{
50
    $params = ['page_size' => 10, 'color_name__icontains' => $q];
51
    if ($brand !== '') {
52
        $params['manufacturer__name__icontains'] = $brand;
53
    }
54
    $data = fc_fetch('https://filamentcolors.xyz/api/swatch/?' . http_build_query($params));
55
    if ($data === null) {
56
        return null;
57
    }
58
    $out = [];
59
    foreach ($data['results'] ?? [] as $s) {
60
        $hex = strtolower((string)($s['hex_color'] ?? ''));
61
        $out[] = [
62
            'type'       => (string)($s['filament_type']['name'] ?? ''),
63
            'brand'      => (string)($s['manufacturer']['name'] ?? ''),
64
            'color_name' => (string)($s['color_name'] ?? ''),
65
            'hex'        => preg_match('/^[0-9a-f]{6}$/', $hex) ? '#' . $hex : null,
66
        ];
67
    }
68
    return $out;
69
}
70
71
$q     = trim((string)($_GET['q'] ?? ''));
72
$brand = trim((string)($_GET['brand'] ?? ''));
73
74
if (strlen($q) < 2) {
75
    echo '[]';
76
    exit;
77
}
78
79
$key = strtolower($q) . '|' . strtolower($brand);
80
81
$st = db()->prepare("SELECT json FROM api_cache WHERE key = ? AND fetched_at > datetime('now', '-7 days')");
82
$st->execute([$key]);
83
$hit = $st->fetchColumn();
84
if ($hit !== false) {
85
    echo $hit;
86
    exit;
87
}
88
89
$results = fc_search($q, $brand);
90
if ($results !== null && !$results && $brand !== '') {
91
    // The locally entered brand may be spelled differently from theirs
92
    // (e.g. "Prusa" vs "Prusament"): retry on color name alone.
93
    $results = fc_search($q, '');
94
}
95
96
if ($results === null) {
97
    // Upstream unreachable: an expired cache entry beats nothing.
98
    $st = db()->prepare('SELECT json FROM api_cache WHERE key = ?');
99
    $st->execute([$key]);
100
    $old = $st->fetchColumn();
101
    echo $old !== false ? $old : '[]';
102
    exit;
103
}
104
105
$json = json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
106
// INSERT OR REPLACE re-inserts the row, so fetched_at picks up its default.
107
db()->prepare('INSERT OR REPLACE INTO api_cache (key, json) VALUES (?, ?)')->execute([$key, $json]);
108
db()->exec("DELETE FROM api_cache WHERE fetched_at < datetime('now', '-30 days')");
109
echo $json;
110