PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ] ); function fetchRemoteJson(string $url): array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => 20, CURLOPT_TIMEOUT => 300, CURLOPT_USERAGENT => 'AstroMINT MPC Comet Importer/1.0', CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'Accept-Encoding: gzip', ], ]); $response = curl_exec($ch); if ($response === false) { throw new RuntimeException('Download fehlgeschlagen: ' . curl_error($ch)); } $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $contentType = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE); if ($httpCode !== 200) { throw new RuntimeException('HTTP-Fehler beim Abruf: ' . $httpCode); } $isGzip = str_ends_with(strtolower($url), '.gz') || str_contains(strtolower($contentType), 'gzip') || str_starts_with($response, "\x1f\x8b"); if ($isGzip) { $decoded = gzdecode($response); if ($decoded === false) { throw new RuntimeException('GZip-Datei konnte nicht entpackt werden'); } $response = $decoded; } $data = json_decode($response, true); if (!is_array($data)) { throw new RuntimeException('Ungültige JSON-Antwort'); } return $data; } function toNullableString(mixed $value): ?string { if ($value === null) { return null; } $value = trim((string) $value); return $value === '' ? null : $value; } function toNullableInt(mixed $value): ?int { if ($value === null || $value === '') { return null; } if (!is_numeric($value)) { return null; } return (int) $value; } function toNullableFloat(mixed $value): ?float { if ($value === null || $value === '') { return null; } if (!is_numeric($value)) { return null; } return (float) $value; } function requireIntField(array $row, string $fieldName): int { $value = toNullableInt($row[$fieldName] ?? null); if ($value === null) { throw new RuntimeException("Pflichtfeld fehlt oder ist ungültig: {$fieldName}"); } return $value; } function requireFloatField(array $row, string $fieldName): float { $value = toNullableFloat($row[$fieldName] ?? null); if ($value === null) { throw new RuntimeException("Pflichtfeld fehlt oder ist ungültig: {$fieldName}"); } return $value; } function requireStringField(array $row, string $fieldName): string { $value = toNullableString($row[$fieldName] ?? null); if ($value === null) { throw new RuntimeException("Pflichtfeld fehlt oder ist ungültig: {$fieldName}"); } return $value; } $recordsReceived = 0; $recordsInserted = 0; $recordsUpdated = 0; $recordsSkipped = 0; try { $rows = fetchRemoteJson($sourceUrl); if ($rows === [] || array_is_list($rows) === false) { throw new RuntimeException('Es wurde keine gültige Kometenliste empfangen'); } $stmtExists = $pdo->prepare(" SELECT id FROM comets_mpc WHERE designation_and_name = :designation_and_name LIMIT 1 "); $stmtUpsert = $pdo->prepare(" INSERT INTO comets_mpc ( orbit_type, provisional_packed_desig, year_of_perihelion, month_of_perihelion, day_of_perihelion, perihelion_dist, eccentricity, arg_perihelion_deg, ascending_node_deg, inclination_deg, absolute_magnitude_h, slope_parameter_g, designation_and_name, reference_text, raw_json, source_url, imported_at, created_at, updated_at ) VALUES ( :orbit_type, :provisional_packed_desig, :year_of_perihelion, :month_of_perihelion, :day_of_perihelion, :perihelion_dist, :eccentricity, :arg_perihelion_deg, :ascending_node_deg, :inclination_deg, :absolute_magnitude_h, :slope_parameter_g, :designation_and_name, :reference_text, :raw_json, :source_url, NOW(), NOW(), NOW() ) ON DUPLICATE KEY UPDATE orbit_type = VALUES(orbit_type), provisional_packed_desig = VALUES(provisional_packed_desig), year_of_perihelion = VALUES(year_of_perihelion), month_of_perihelion = VALUES(month_of_perihelion), day_of_perihelion = VALUES(day_of_perihelion), perihelion_dist = VALUES(perihelion_dist), eccentricity = VALUES(eccentricity), arg_perihelion_deg = VALUES(arg_perihelion_deg), ascending_node_deg = VALUES(ascending_node_deg), inclination_deg = VALUES(inclination_deg), absolute_magnitude_h = VALUES(absolute_magnitude_h), slope_parameter_g = VALUES(slope_parameter_g), reference_text = VALUES(reference_text), raw_json = VALUES(raw_json), source_url = VALUES(source_url), imported_at = NOW(), updated_at = NOW() "); $pdo->beginTransaction(); foreach ($rows as $row) { try { $designationAndName = requireStringField($row, 'Designation_and_name'); $recordsReceived++; $stmtExists->execute([ ':designation_and_name' => $designationAndName, ]); $exists = (bool) $stmtExists->fetchColumn(); $stmtUpsert->execute([ ':orbit_type' => substr(requireStringField($row, 'Orbit_type'), 0, 1), ':provisional_packed_desig' => toNullableString($row['Provisional_packed_desig'] ?? null), ':year_of_perihelion' => requireIntField($row, 'Year_of_perihelion'), ':month_of_perihelion' => requireIntField($row, 'Month_of_perihelion'), ':day_of_perihelion' => requireFloatField($row, 'Day_of_perihelion'), ':perihelion_dist' => requireFloatField($row, 'Perihelion_dist'), ':eccentricity' => requireFloatField($row, 'e'), ':arg_perihelion_deg' => requireFloatField($row, 'Peri'), ':ascending_node_deg' => requireFloatField($row, 'Node'), ':inclination_deg' => requireFloatField($row, 'i'), ':absolute_magnitude_h' => toNullableFloat($row['H'] ?? null), ':slope_parameter_g' => toNullableFloat($row['G'] ?? null), ':designation_and_name' => $designationAndName, ':reference_text' => toNullableString($row['Ref'] ?? null), ':raw_json' => json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ':source_url' => $sourceUrl, ]); if ($exists) { $recordsUpdated++; } else { $recordsInserted++; } } catch (Throwable $rowError) { $recordsSkipped++; echo "Übersprungen: " . $rowError->getMessage() . "\n"; } } $pdo->commit(); echo "Import erfolgreich\n"; echo "Empfangen: {$recordsReceived}\n"; echo "Neu: {$recordsInserted}\n"; echo "Aktualisiert: {$recordsUpdated}\n"; echo "Übersprungen: {$recordsSkipped}\n"; } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } echo "Fehler: " . $e->getMessage() . "\n"; exit(1); }