73d39b49ea
products.php reprefixava les URLs dels Sheets cap a kapvoe-portfoli.treblarella.org/assets/products, on 15 imatges no existeixen (404). Ara apunta a img.treblarella.org, on totes 35 imatges existeixen i es serveixen correctament. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
182 lines
6.0 KiB
PHP
182 lines
6.0 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
|
|
|
$googleScriptUrl = 'https://script.googleusercontent.com/macros/echo?user_content_key=AWDtjMVe9JQMYFUTitGGiHSzQsuEyr7VsarNnNpOluFcWHXa-CGnuKhrinmwBYVLw1otHQoBg5rnYNpGlIvCg_I8u1QhCKr-FQwCC2bG9LdttpST6nS_k8TxRcaT5LmmDjeZENcy8A0ujTU1yJwFLoxudMFW-OGjkYtywE5YT_CUJpKQWmqPN8IRV1drNysBtQFfQH1tXUS1JrOODghrxxjAA3T77kqRnz-aaMZJ23YfZntVm4C1KpaBBloFs4OO2wYIcD7Sf1iAYX1OMjIHXsCypvIgFRfIdhMqADWRmluv&lib=MLri0H8XjzrQY2XIy3BzwJ1YbTZr7i_Wa';
|
|
$cacheDir = __DIR__ . '/cache';
|
|
$cacheFile = $cacheDir . '/products.json';
|
|
$cacheTtl = 120; // segons
|
|
|
|
function respond(int $statusCode, array $payload): void
|
|
{
|
|
http_response_code($statusCode);
|
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
|
exit;
|
|
}
|
|
|
|
function fetchRemoteJson(string $url): array
|
|
{
|
|
if (!function_exists('curl_init')) {
|
|
throw new RuntimeException('cURL no està disponible al PHP de la Synology.');
|
|
}
|
|
|
|
$ch = curl_init($url);
|
|
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_FOLLOWLOCATION => true,
|
|
CURLOPT_TIMEOUT => 20,
|
|
CURLOPT_CONNECTTIMEOUT => 10,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Accept: application/json',
|
|
'User-Agent: KapvoePortfolioAPI/1.0'
|
|
],
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
CURLOPT_SSL_VERIFYHOST => 2,
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
|
|
if ($response === false) {
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
throw new RuntimeException("Error cURL: {$error}");
|
|
}
|
|
|
|
$statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($statusCode < 200 || $statusCode >= 300) {
|
|
throw new RuntimeException("Resposta remota no vàlida. HTTP {$statusCode}");
|
|
}
|
|
|
|
$json = json_decode($response, true);
|
|
|
|
if (!is_array($json)) {
|
|
throw new RuntimeException('La resposta remota no és JSON vàlid.');
|
|
}
|
|
|
|
return $json;
|
|
}
|
|
|
|
function normalizeBool($value): bool
|
|
{
|
|
$v = mb_strtolower(trim((string)($value ?? '')));
|
|
return in_array($v, ['1', 'true', 'sí', 'si', 'yes', 'y'], true);
|
|
}
|
|
|
|
function normalizeText($value): string
|
|
{
|
|
return trim((string)($value ?? ''));
|
|
}
|
|
|
|
function normalizePriceNumber($value): ?float
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
$raw = str_replace(['€', ' '], '', (string)$value);
|
|
$raw = str_replace(',', '.', $raw);
|
|
|
|
return is_numeric($raw) ? (float)$raw : null;
|
|
}
|
|
|
|
function normalizeStock($value): int
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return 0;
|
|
}
|
|
|
|
return max(0, (int)$value);
|
|
}
|
|
|
|
function normalizeProduct(array $p): array
|
|
{
|
|
$priceNumber = normalizePriceNumber($p['europe_price_number'] ?? $p['PREU'] ?? $p['PREU EUROPA'] ?? null);
|
|
$priceText = normalizeText($p['europe_price_text'] ?? '');
|
|
|
|
if ($priceText === '' && $priceNumber !== null) {
|
|
$priceText = number_format($priceNumber, 2, ',', '.') . ' €';
|
|
}
|
|
|
|
$image = normalizeText($p['image_url'] ?? $p['IMAGE_URL'] ?? '');
|
|
$filename = basename($image);
|
|
|
|
return [
|
|
'product_code' => normalizeText($p['product_code'] ?? $p['Product Code'] ?? $p['PRODUCT CODE'] ?? ''),
|
|
'model' => normalizeText($p['model'] ?? $p['MODEL'] ?? ''),
|
|
'category' => normalizeText($p['category'] ?? $p['CATEGORIA'] ?? $p['CATEGORY'] ?? ''),
|
|
'colors' => normalizeText($p['colors'] ?? $p['COLORS'] ?? $p['VIDRE'] ?? ''),
|
|
'description' => normalizeText($p['description'] ?? $p['DESCRIPCIO'] ?? $p['DESCRIPTION'] ?? ''),
|
|
'image_url' => $filename ? "https://img.treblarella.org/assets/products/$filename" : '',
|
|
'stock' => normalizeStock($p['stock'] ?? $p['STOCK'] ?? 0),
|
|
'top_vendes' => normalizeBool($p['top_vendes'] ?? $p['TOP_VENDES'] ?? false),
|
|
'europe_price_number' => $priceNumber,
|
|
'europe_price_text' => $priceText
|
|
];
|
|
}
|
|
|
|
try {
|
|
if (!is_dir($cacheDir)) {
|
|
mkdir($cacheDir, 0775, true);
|
|
}
|
|
|
|
$forceRefresh = isset($_GET['refresh']) && $_GET['refresh'] === '1';
|
|
$useCache = !$forceRefresh && file_exists($cacheFile) && (time() - filemtime($cacheFile) < $cacheTtl);
|
|
|
|
if ($useCache) {
|
|
$cached = json_decode((string)file_get_contents($cacheFile), true);
|
|
if (is_array($cached)) {
|
|
respond(200, $cached);
|
|
}
|
|
}
|
|
|
|
if ($googleScriptUrl === 'POSA_AQUI_LA_TEVA_URL_DE_GOOGLE_SCRIPT') {
|
|
throw new RuntimeException('Has de configurar la URL del Google Script a api/products.php');
|
|
}
|
|
|
|
$remote = fetchRemoteJson($googleScriptUrl);
|
|
|
|
if (($remote['ok'] ?? null) !== true || !isset($remote['products']) || !is_array($remote['products'])) {
|
|
throw new RuntimeException('El JSON remot no té el format esperat.');
|
|
}
|
|
|
|
$normalizedProducts = array_map('normalizeProduct', $remote['products']);
|
|
|
|
$normalizedProducts = array_values(array_filter($normalizedProducts, function (array $p): bool {
|
|
return $p['product_code'] !== '';
|
|
}));
|
|
|
|
$payload = [
|
|
'ok' => true,
|
|
'source' => 'php-proxy',
|
|
'cached' => false,
|
|
'updated_at' => $remote['updated_at'] ?? date('c'),
|
|
'count' => count($normalizedProducts),
|
|
'products' => $normalizedProducts
|
|
];
|
|
|
|
file_put_contents($cacheFile, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
|
|
|
respond(200, $payload);
|
|
|
|
} catch (Throwable $e) {
|
|
if (file_exists($cacheFile)) {
|
|
$cached = json_decode((string)file_get_contents($cacheFile), true);
|
|
if (is_array($cached)) {
|
|
$cached['cached'] = true;
|
|
$cached['warning'] = 'S\'està retornant cache perquè la font remota ha fallat.';
|
|
$cached['error_detail'] = $e->getMessage();
|
|
respond(200, $cached);
|
|
}
|
|
}
|
|
|
|
respond(500, [
|
|
'ok' => false,
|
|
'error' => 'No s\'ha pogut obtenir el catàleg.',
|
|
'detail' => $e->getMessage()
|
|
]);
|
|
} |