first commit
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CelesTrak JSON Import
|
||||
* Quelle:
|
||||
* https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=json
|
||||
*
|
||||
* Tabellen:
|
||||
* - sat_satellites
|
||||
* - sat_tle_current
|
||||
* - sat_import_runs
|
||||
*/
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Konfiguration laden
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
$config = require __DIR__ . '/../../config/database.php';
|
||||
|
||||
$sourceUrl = 'https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=json';
|
||||
$sourceName = 'celestrak';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Datenbankverbindung
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;dbname=%s;charset=%s',
|
||||
$config['host'],
|
||||
$config['dbname'],
|
||||
$config['charset'] ?? 'utf8mb4'
|
||||
);
|
||||
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$config['user'],
|
||||
$config['pass'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Hilfsfunktionen
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
function fetchRemoteJson(string $url): array
|
||||
{
|
||||
$ch = curl_init($url);
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 20,
|
||||
CURLOPT_TIMEOUT => 180,
|
||||
CURLOPT_USERAGENT => 'Skyview CelesTrak 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);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
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 normalizeDateTime(?string $value): ?string
|
||||
{
|
||||
if ($value === null || trim($value) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$dt = new DateTimeImmutable($value, new DateTimeZone('UTC'));
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDate(?string $value): ?string
|
||||
{
|
||||
if ($value === null || trim($value) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$dt = new DateTimeImmutable($value, new DateTimeZone('UTC'));
|
||||
return $dt->format('Y-m-d');
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function toNullableString(mixed $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim((string)$value);
|
||||
return $value === '' ? null : $value;
|
||||
}
|
||||
|
||||
function toNullableFloat(mixed $value): ?float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (float)$value;
|
||||
}
|
||||
|
||||
function toNullableInt(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Import
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
$importRunId = null;
|
||||
$recordsReceived = 0;
|
||||
$recordsInserted = 0;
|
||||
$recordsUpdated = 0;
|
||||
|
||||
try {
|
||||
$stmtImportStart = $pdo->prepare("
|
||||
INSERT INTO sat_import_runs (
|
||||
source,
|
||||
started_at,
|
||||
status,
|
||||
message
|
||||
) VALUES (
|
||||
:source,
|
||||
NOW(),
|
||||
'running',
|
||||
NULL
|
||||
)
|
||||
");
|
||||
$stmtImportStart->execute([
|
||||
':source' => $sourceName,
|
||||
]);
|
||||
|
||||
$importRunId = (int)$pdo->lastInsertId();
|
||||
|
||||
$rows = fetchRemoteJson($sourceUrl);
|
||||
|
||||
if (empty($rows)) {
|
||||
throw new RuntimeException('Keine Datensätze empfangen');
|
||||
}
|
||||
|
||||
$stmtFindSatellite = $pdo->prepare("
|
||||
SELECT id
|
||||
FROM sat_satellites
|
||||
WHERE norad_cat_id = :norad_cat_id
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$stmtUpsertSatellite = $pdo->prepare("
|
||||
INSERT INTO sat_satellites (
|
||||
norad_cat_id,
|
||||
object_name,
|
||||
object_id,
|
||||
object_type,
|
||||
classification_type,
|
||||
country_code,
|
||||
launch_date,
|
||||
decay_date,
|
||||
rcs_size,
|
||||
active,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
:norad_cat_id,
|
||||
:object_name,
|
||||
:object_id,
|
||||
:object_type,
|
||||
:classification_type,
|
||||
:country_code,
|
||||
:launch_date,
|
||||
:decay_date,
|
||||
:rcs_size,
|
||||
1,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
object_name = VALUES(object_name),
|
||||
object_id = VALUES(object_id),
|
||||
object_type = VALUES(object_type),
|
||||
classification_type = VALUES(classification_type),
|
||||
country_code = VALUES(country_code),
|
||||
launch_date = VALUES(launch_date),
|
||||
decay_date = VALUES(decay_date),
|
||||
rcs_size = VALUES(rcs_size),
|
||||
active = 1,
|
||||
updated_at = NOW()
|
||||
");
|
||||
|
||||
$stmtCurrentExists = $pdo->prepare("
|
||||
SELECT id
|
||||
FROM sat_tle_current
|
||||
WHERE norad_cat_id = :norad_cat_id
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$stmtUpsertCurrent = $pdo->prepare("
|
||||
INSERT INTO sat_tle_current (
|
||||
satellite_id,
|
||||
norad_cat_id,
|
||||
epoch,
|
||||
creation_date,
|
||||
mean_motion,
|
||||
eccentricity,
|
||||
inclination,
|
||||
ra_of_asc_node,
|
||||
arg_of_pericenter,
|
||||
mean_anomaly,
|
||||
ephemeris_type,
|
||||
element_set_no,
|
||||
rev_at_epoch,
|
||||
bstar,
|
||||
mean_motion_dot,
|
||||
mean_motion_ddot,
|
||||
center_name,
|
||||
ref_frame,
|
||||
time_system,
|
||||
mean_element_theory,
|
||||
raw_json,
|
||||
source,
|
||||
imported_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
:satellite_id,
|
||||
:norad_cat_id,
|
||||
:epoch,
|
||||
:creation_date,
|
||||
:mean_motion,
|
||||
:eccentricity,
|
||||
:inclination,
|
||||
:ra_of_asc_node,
|
||||
:arg_of_pericenter,
|
||||
:mean_anomaly,
|
||||
:ephemeris_type,
|
||||
:element_set_no,
|
||||
:rev_at_epoch,
|
||||
:bstar,
|
||||
:mean_motion_dot,
|
||||
:mean_motion_ddot,
|
||||
:center_name,
|
||||
:ref_frame,
|
||||
:time_system,
|
||||
:mean_element_theory,
|
||||
:raw_json,
|
||||
:source,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
satellite_id = VALUES(satellite_id),
|
||||
epoch = VALUES(epoch),
|
||||
creation_date = VALUES(creation_date),
|
||||
mean_motion = VALUES(mean_motion),
|
||||
eccentricity = VALUES(eccentricity),
|
||||
inclination = VALUES(inclination),
|
||||
ra_of_asc_node = VALUES(ra_of_asc_node),
|
||||
arg_of_pericenter = VALUES(arg_of_pericenter),
|
||||
mean_anomaly = VALUES(mean_anomaly),
|
||||
ephemeris_type = VALUES(ephemeris_type),
|
||||
element_set_no = VALUES(element_set_no),
|
||||
rev_at_epoch = VALUES(rev_at_epoch),
|
||||
bstar = VALUES(bstar),
|
||||
mean_motion_dot = VALUES(mean_motion_dot),
|
||||
mean_motion_ddot = VALUES(mean_motion_ddot),
|
||||
center_name = VALUES(center_name),
|
||||
ref_frame = VALUES(ref_frame),
|
||||
time_system = VALUES(time_system),
|
||||
mean_element_theory = VALUES(mean_element_theory),
|
||||
raw_json = VALUES(raw_json),
|
||||
source = VALUES(source),
|
||||
imported_at = NOW(),
|
||||
updated_at = NOW()
|
||||
");
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$noradCatId = toNullableInt($row['NORAD_CAT_ID'] ?? null);
|
||||
|
||||
if ($noradCatId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$recordsReceived++;
|
||||
|
||||
$stmtCurrentExists->execute([
|
||||
':norad_cat_id' => $noradCatId,
|
||||
]);
|
||||
$currentExists = (bool)$stmtCurrentExists->fetchColumn();
|
||||
|
||||
$stmtUpsertSatellite->execute([
|
||||
':norad_cat_id' => $noradCatId,
|
||||
':object_name' => toNullableString($row['OBJECT_NAME'] ?? null),
|
||||
':object_id' => toNullableString($row['OBJECT_ID'] ?? null),
|
||||
':object_type' => toNullableString($row['OBJECT_TYPE'] ?? null),
|
||||
':classification_type' => toNullableString($row['CLASSIFICATION_TYPE'] ?? null),
|
||||
':country_code' => toNullableString($row['COUNTRY_CODE'] ?? null),
|
||||
':launch_date' => normalizeDate($row['LAUNCH_DATE'] ?? null),
|
||||
':decay_date' => normalizeDate($row['DECAY_DATE'] ?? null),
|
||||
':rcs_size' => toNullableString($row['RCS_SIZE'] ?? null),
|
||||
]);
|
||||
|
||||
$stmtFindSatellite->execute([
|
||||
':norad_cat_id' => $noradCatId,
|
||||
]);
|
||||
$satelliteId = $stmtFindSatellite->fetchColumn();
|
||||
|
||||
if (!$satelliteId) {
|
||||
throw new RuntimeException('satellite_id konnte nicht ermittelt werden für NORAD ' . $noradCatId);
|
||||
}
|
||||
|
||||
$stmtUpsertCurrent->execute([
|
||||
':satellite_id' => (int)$satelliteId,
|
||||
':norad_cat_id' => $noradCatId,
|
||||
':epoch' => normalizeDateTime($row['EPOCH'] ?? null),
|
||||
':creation_date' => normalizeDateTime($row['CREATION_DATE'] ?? null),
|
||||
':mean_motion' => toNullableFloat($row['MEAN_MOTION'] ?? null),
|
||||
':eccentricity' => toNullableFloat($row['ECCENTRICITY'] ?? null),
|
||||
':inclination' => toNullableFloat($row['INCLINATION'] ?? null),
|
||||
':ra_of_asc_node' => toNullableFloat($row['RA_OF_ASC_NODE'] ?? null),
|
||||
':arg_of_pericenter' => toNullableFloat($row['ARG_OF_PERICENTER'] ?? null),
|
||||
':mean_anomaly' => toNullableFloat($row['MEAN_ANOMALY'] ?? null),
|
||||
':ephemeris_type' => toNullableInt($row['EPHEMERIS_TYPE'] ?? null),
|
||||
':element_set_no' => toNullableInt($row['ELEMENT_SET_NO'] ?? null),
|
||||
':rev_at_epoch' => toNullableInt($row['REV_AT_EPOCH'] ?? null),
|
||||
':bstar' => toNullableFloat($row['BSTAR'] ?? null),
|
||||
':mean_motion_dot' => toNullableFloat($row['MEAN_MOTION_DOT'] ?? null),
|
||||
':mean_motion_ddot' => toNullableFloat($row['MEAN_MOTION_DDOT'] ?? null),
|
||||
':center_name' => toNullableString($row['CENTER_NAME'] ?? null),
|
||||
':ref_frame' => toNullableString($row['REF_FRAME'] ?? null),
|
||||
':time_system' => toNullableString($row['TIME_SYSTEM'] ?? null),
|
||||
':mean_element_theory' => toNullableString($row['MEAN_ELEMENT_THEORY'] ?? null),
|
||||
':raw_json' => json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
':source' => $sourceName,
|
||||
]);
|
||||
|
||||
if ($currentExists) {
|
||||
$recordsUpdated++;
|
||||
} else {
|
||||
$recordsInserted++;
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$stmtImportFinish = $pdo->prepare("
|
||||
UPDATE sat_import_runs
|
||||
SET
|
||||
finished_at = NOW(),
|
||||
records_received = :records_received,
|
||||
records_inserted = :records_inserted,
|
||||
records_updated = :records_updated,
|
||||
status = 'success',
|
||||
message = NULL
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmtImportFinish->execute([
|
||||
':records_received' => $recordsReceived,
|
||||
':records_inserted' => $recordsInserted,
|
||||
':records_updated' => $recordsUpdated,
|
||||
':id' => $importRunId,
|
||||
]);
|
||||
|
||||
echo "Import erfolgreich\n";
|
||||
echo "Empfangen: {$recordsReceived}\n";
|
||||
echo "Neu: {$recordsInserted}\n";
|
||||
echo "Aktualisiert: {$recordsUpdated}\n";
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
if ($importRunId !== null) {
|
||||
$stmtImportError = $pdo->prepare("
|
||||
UPDATE sat_import_runs
|
||||
SET
|
||||
finished_at = NOW(),
|
||||
records_received = :records_received,
|
||||
records_inserted = :records_inserted,
|
||||
records_updated = :records_updated,
|
||||
status = 'error',
|
||||
message = :message
|
||||
WHERE id = :id
|
||||
");
|
||||
$stmtImportError->execute([
|
||||
':records_received' => $recordsReceived,
|
||||
':records_inserted' => $recordsInserted,
|
||||
':records_updated' => $recordsUpdated,
|
||||
':message' => mb_substr($e->getMessage(), 0, 65000),
|
||||
':id' => $importRunId,
|
||||
]);
|
||||
}
|
||||
|
||||
echo "Fehler: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user