[ 'title' => 'Aurora Weather Livecam', 'welcome' => 'Willkommen bei der Aurora Live Webcam', 'subline' => 'Livebilder, Zeitraffer, Archiv & Community – alles in einem sonnigen Dashboard.', 'live' => 'Live', 'timelapse' => 'Zeitraffer', 'archive' => 'Archiv', 'gallery' => 'Galerie', 'community' => 'Community', 'contact' => 'Kontakt', 'guestbook' => 'Gästebuch', 'send' => 'Senden', 'name' => 'Name', 'email' => 'E-Mail', 'message' => 'Nachricht', 'screenshot' => 'Screenshot', 'clip' => 'Clip aufnehmen', 'download' => 'Neueste Aufnahme laden', 'calendar_title' => 'Visueller Wetterkalender', 'language' => 'Sprache', 'rating' => 'Bewertung', 'comment' => 'Kommentar', 'add_entry' => 'Eintrag hinzufügen', 'view_all' => 'Alle anzeigen', 'privacy' => 'Privatsphäre', 'share' => 'Teilen', 'pip' => 'Bild-in-Bild', 'stats' => 'Stream-Status', 'starlink' => 'Starlink-Verbindung', 'starlink_caption' => 'Scanne den QR-Code für das Starlink Satelliteninternet.', 'starlink_alt' => 'Starlink QR-Code', ], 'en' => [ 'title' => 'Aurora Weather Livecam', 'welcome' => 'Welcome to the Aurora Live Webcam', 'subline' => 'Live footage, timelapse, archive & community – all in one sunny dashboard.', 'live' => 'Live', 'timelapse' => 'Timelapse', 'archive' => 'Archive', 'gallery' => 'Gallery', 'community' => 'Community', 'contact' => 'Contact', 'guestbook' => 'Guestbook', 'send' => 'Send', 'name' => 'Name', 'email' => 'Email', 'message' => 'Message', 'screenshot' => 'Screenshot', 'clip' => 'Record clip', 'download' => 'Download latest capture', 'calendar_title' => 'Visual Weather Calendar', 'language' => 'Language', 'rating' => 'Rating', 'comment' => 'Comment', 'add_entry' => 'Add entry', 'view_all' => 'View all', 'privacy' => 'Privacy', 'share' => 'Share', 'pip' => 'Picture-in-Picture', 'stats' => 'Stream status', 'starlink' => 'Starlink Connection', 'starlink_caption' => 'Scan the QR code to reach Starlink satellite internet.', 'starlink_alt' => 'Starlink QR code', ], 'fr' => [ 'title' => 'Aurora Weather Livecam', 'welcome' => 'Bienvenue sur la webcam Aurora', 'subline' => 'Images en direct, time-lapse, archive et communauté – réunis dans un tableau de bord ensoleillé.', 'live' => 'Direct', 'timelapse' => 'Accéléré', 'archive' => 'Archive', 'gallery' => 'Galerie', 'community' => 'Communauté', 'contact' => 'Contact', 'guestbook' => 'Livre d’or', 'send' => 'Envoyer', 'name' => 'Nom', 'email' => 'E-mail', 'message' => 'Message', 'screenshot' => 'Capture', 'clip' => 'Enregistrer un clip', 'download' => 'Télécharger la dernière capture', 'calendar_title' => 'Calendrier météo visuel', 'language' => 'Langue', 'rating' => 'Évaluation', 'comment' => 'Commentaire', 'add_entry' => 'Ajouter une entrée', 'view_all' => 'Tout voir', 'privacy' => 'Confidentialité', 'share' => 'Partager', 'pip' => 'Picture-in-Picture', 'stats' => 'Statut du flux', 'starlink' => 'Connexion Starlink', 'starlink_caption' => 'Scannez le QR code pour accéder à l’internet satellite Starlink.', 'starlink_alt' => 'QR code Starlink', ], 'it' => [ 'title' => 'Aurora Weather Livecam', 'welcome' => 'Benvenuti alla webcam Aurora', 'subline' => 'Live, time-lapse, archivio e community – tutto in un dashboard soleggiato.', 'live' => 'Live', 'timelapse' => 'Time-lapse', 'archive' => 'Archivio', 'gallery' => 'Galleria', 'community' => 'Community', 'contact' => 'Contatto', 'guestbook' => 'Libro degli ospiti', 'send' => 'Invia', 'name' => 'Nome', 'email' => 'E-mail', 'message' => 'Messaggio', 'screenshot' => 'Screenshot', 'clip' => 'Registra clip', 'download' => 'Scarica l’ultima registrazione', 'calendar_title' => 'Calendario Meteo Visivo', 'language' => 'Lingua', 'rating' => 'Valutazione', 'comment' => 'Commento', 'add_entry' => 'Aggiungi', 'view_all' => 'Vedi tutto', 'privacy' => 'Privacy', 'share' => 'Condividi', 'pip' => 'Picture-in-Picture', 'stats' => 'Stato del flusso', 'starlink' => 'Connessione Starlink', 'starlink_caption' => 'Scansiona il QR code per accedere a Starlink Internet satellitare.', 'starlink_alt' => 'QR code Starlink', ], 'zh' => [ 'title' => '极光天气直播摄像头', 'welcome' => '欢迎来到极光直播摄像头', 'subline' => '实时画面、延时摄影、档案与社区——尽在阳光活力仪表盘。', 'live' => '直播', 'timelapse' => '延时摄影', 'archive' => '档案', 'gallery' => '图集', 'community' => '社区', 'contact' => '联系', 'guestbook' => '留言簿', 'send' => '发送', 'name' => '姓名', 'email' => '邮箱', 'message' => '留言', 'screenshot' => '截图', 'clip' => '录制剪辑', 'download' => '下载最新捕获', 'calendar_title' => '可视化天气日历', 'language' => '语言', 'rating' => '评分', 'comment' => '评论', 'add_entry' => '添加条目', 'view_all' => '查看全部', 'privacy' => '隐私', 'share' => '分享', 'pip' => '画中画', 'stats' => '流状态', 'starlink' => 'Starlink 连接', 'starlink_caption' => '扫描二维码访问 Starlink 高速卫星网络。', 'starlink_alt' => 'Starlink 二维码', ], ]; public function getCurrentLocale(): string { if (isset($_POST['language'])) { $_SESSION['lang'] = $_POST['language']; } return $_SESSION['lang'] ?? 'de'; } public function get(string $key, ?string $locale = null): string { $locale = $locale ?? $this->getCurrentLocale(); $locale = array_key_exists($locale, $this->translations) ? $locale : 'de'; return $this->translations[$locale][$key] ?? $this->translations['de'][$key] ?? $key; } public function getAllTranslations(): array { return $this->translations; } } class WebcamManager { private string $videoSrc; public function __construct(string $videoSrc = STREAM_SOURCE) { $this->videoSrc = $videoSrc; } public function getVideoSrc(): string { return $this->videoSrc; } public function getImageFiles(): array { $files = glob(IMAGE_DIR . '/screenshot_*.jpg') ?: []; usort($files, static fn(string $a, string $b) => filemtime($b) <=> filemtime($a)); return array_slice($files, 0, 10); } public function getLatestVideo(): ?string { $videos = glob(VIDEO_DIR . '/*.mp4'); if (!$videos) { return null; } usort($videos, static fn(string $a, string $b) => filemtime($b) <=> filemtime($a)); return $videos[0]; } public function captureSnapshot(): array { $outputFile = 'snapshot_' . date('YmdHis') . '.jpg'; $targetPath = UPLOAD_DIR . '/' . $outputFile; $command = sprintf( "ffmpeg -y -i %s -i %s -filter_complex 'overlay=main_w-overlay_w-10:10' -frames:v 1 -q:v 2 %s", escapeshellarg($this->videoSrc), escapeshellarg(LOGO_PATH), escapeshellarg($targetPath) ); exec($command, $output, $returnVar); if ($returnVar !== 0 || !file_exists($targetPath)) { return ['success' => false, 'message' => 'Snapshot konnte nicht erstellt werden.']; } return ['success' => true, 'file' => basename($targetPath)]; } public function captureClip(int $duration = 10): array { $outputFile = 'sequence_' . date('YmdHis') . '.mp4'; $targetPath = UPLOAD_DIR . '/' . $outputFile; $command = sprintf( "ffmpeg -y -i %s -i %s -filter_complex 'overlay=10:10' -t %d -c:v libx264 -preset fast -crf 23 %s", escapeshellarg($this->videoSrc), escapeshellarg(LOGO_PATH), $duration, escapeshellarg($targetPath) ); exec($command, $output, $returnVar); if ($returnVar !== 0 || !file_exists($targetPath)) { return ['success' => false, 'message' => 'Clip konnte nicht erstellt werden.']; } return ['success' => true, 'file' => basename($targetPath)]; } public function getStreamStats(): array { return [ 'bitrate' => rand(4200, 6200), 'latency' => rand(3, 9), 'updated' => date('H:i:s'), ]; } public function getGallery(): array { $images = []; foreach (glob(GALLERY_DIR . '/*.{jpg,jpeg,png,gif}', GLOB_BRACE) ?: [] as $file) { $timestamp = filemtime($file) ?: 0; $images[] = [ 'src' => str_replace(__DIR__ . '/', '', $file), 'date' => date('Y-m-d H:i', $timestamp), 'timestamp' => $timestamp, ]; } usort($images, static fn(array $a, array $b) => $b['timestamp'] <=> $a['timestamp']); $images = array_slice($images, 0, 10); return array_map(static fn(array $image) => [ 'src' => $image['src'], 'date' => $image['date'], ], $images); } } class VisualCalendarManager { private array $monthNames = [ 1 => ['de' => 'Januar', 'en' => 'January', 'it' => 'Gennaio', 'fr' => 'Janvier', 'zh' => '一月'], 2 => ['de' => 'Februar', 'en' => 'February', 'it' => 'Febbraio', 'fr' => 'Février', 'zh' => '二月'], 3 => ['de' => 'März', 'en' => 'March', 'it' => 'Marzo', 'fr' => 'Mars', 'zh' => '三月'], 4 => ['de' => 'April', 'en' => 'April', 'it' => 'Aprile', 'fr' => 'Avril', 'zh' => '四月'], 5 => ['de' => 'Mai', 'en' => 'May', 'it' => 'Maggio', 'fr' => 'Mai', 'zh' => '五月'], 6 => ['de' => 'Juni', 'en' => 'June', 'it' => 'Giugno', 'fr' => 'Juin', 'zh' => '六月'], 7 => ['de' => 'Juli', 'en' => 'July', 'it' => 'Luglio', 'fr' => 'Juillet', 'zh' => '七月'], 8 => ['de' => 'August', 'en' => 'August', 'it' => 'Agosto', 'fr' => 'Août', 'zh' => '八月'], 9 => ['de' => 'September', 'en' => 'September', 'it' => 'Settembre', 'fr' => 'Septembre', 'zh' => '九月'], 10 => ['de' => 'Oktober', 'en' => 'October', 'it' => 'Ottobre', 'fr' => 'Octobre', 'zh' => '十月'], 11 => ['de' => 'November', 'en' => 'November', 'it' => 'Novembre', 'fr' => 'Novembre', 'zh' => '十一月'], 12 => ['de' => 'Dezember', 'en' => 'December', 'it' => 'Dicembre', 'fr' => 'Décembre', 'zh' => '十二月'], ]; public function getMonthData(int $year, int $month): array { $firstDay = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month)); $daysInMonth = (int) $firstDay->format('t'); $days = []; for ($day = 1; $day <= $daysInMonth; $day++) { $date = sprintf('%04d%02d%02d', $year, $month, $day); $pattern = VIDEO_DIR . "/daily_video_{$date}_*.mp4"; $matches = glob($pattern) ?: []; $days[] = [ 'day' => $day, 'hasVideos' => !empty($matches), 'count' => count($matches) ]; } return [ 'year' => $year, 'month' => $month, 'monthName' => $this->monthNames[$month] ?? $this->monthNames[date('n')], 'days' => $days, ]; } public function getVideosForDate(int $year, int $month, int $day): array { $date = sprintf('%04d%02d%02d', $year, $month, $day); $videos = []; foreach (glob(VIDEO_DIR . "/daily_video_{$date}_*.mp4") as $file) { $videos[] = [ 'file' => basename($file), 'size' => filesize($file), 'time' => date('H:i', filemtime($file)), ]; } return $videos; } } class GuestbookManager { private array $entries = []; public function __construct() { $content = json_decode((string) file_get_contents(GUESTBOOK_FILE), true); $this->entries = is_array($content) ? $content : []; } public function addEntry(string $name, string $message, int $rating = 5): array { $entry = [ 'name' => htmlspecialchars($name, ENT_QUOTES, 'UTF-8'), 'message' => htmlspecialchars($message, ENT_QUOTES, 'UTF-8'), 'rating' => max(1, min(5, $rating)), 'created' => date('Y-m-d H:i:s') ]; $this->entries[] = $entry; file_put_contents(GUESTBOOK_FILE, json_encode($this->entries, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); return $entry; } public function getEntries(int $limit = 10): array { return array_slice(array_reverse($this->entries), 0, $limit); } } class ContactManager { private string $adminEmail = 'metacube@gmail.com'; private string $gmailUser = 'metacube@gmail.com'; private string $gmailAppPassword = 'qggk hsxz fdkq jgxa'; public function handle(string $name, string $email, string $message): array { if ($name === '' || $email === '' || $message === '') { return ['success' => false, 'message' => 'Bitte alle Felder ausfüllen.']; } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { return ['success' => false, 'message' => 'Bitte eine gültige E-Mail-Adresse verwenden.']; } if (mb_strlen($message) < 10) { return ['success' => false, 'message' => 'Die Nachricht ist zu kurz.']; } $payload = [ 'name' => htmlspecialchars(trim($name), ENT_QUOTES, 'UTF-8'), 'email' => filter_var(trim($email), FILTER_SANITIZE_EMAIL), 'message' => htmlspecialchars(trim($message), ENT_QUOTES, 'UTF-8'), 'date' => date('Y-m-d H:i:s'), 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown', ]; $existing = json_decode((string) file_get_contents(FEEDBACK_FILE), true); $existing = is_array($existing) ? $existing : []; $existing[] = $payload; file_put_contents(FEEDBACK_FILE, json_encode($existing, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $mail = new PHPMailer(true); try { $mail->isSMTP(); $mail->Host = 'smtp.gmail.com'; $mail->SMTPAuth = true; $mail->Username = $this->gmailUser; $mail->Password = $this->gmailAppPassword; $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = 587; $mail->setFrom($this->gmailUser, 'Aurora Livecam'); $mail->addAddress($this->adminEmail); $mail->addReplyTo($payload['email'], $payload['name']); $mail->isHTML(true); $mail->CharSet = 'UTF-8'; $mail->Subject = 'Neue Kontaktanfrage von ' . $payload['name']; $mail->Body = '

Aurora Kontakt

' . '

Name: ' . $payload['name'] . '

' . '

E-Mail: ' . $payload['email'] . '

' . '

Nachricht:
' . nl2br($payload['message']) . '

' . '
Gesendet am ' . $payload['date'] . ' | IP ' . $payload['ip'] . ''; $mail->send(); } catch (Exception $e) { error_log('Mail error: ' . $mail->ErrorInfo); return ['success' => false, 'message' => 'Nachricht gespeichert, E-Mail konnte nicht gesendet werden.']; } return ['success' => true, 'message' => 'Vielen Dank! Wir melden uns zeitnah.']; } } class AdminManager { public function isAdmin(): bool { return isset($_SESSION['admin']) && $_SESSION['admin'] === true; } public function login(string $username, string $password): bool { if ($username === 'admin' && $password === 'sonne4000$$$$Q') { $_SESSION['admin'] = true; return true; } return false; } } $languageManager = new LanguageManager(); $locale = $languageManager->getCurrentLocale(); $webcamManager = new WebcamManager(); $calendarManager = new VisualCalendarManager(); $guestbookManager = new GuestbookManager(); $contactManager = new ContactManager(); $adminManager = new AdminManager(); if (isset($_GET['download_video']) && $_GET['download_video'] === 'latest') { $latest = $webcamManager->getLatestVideo(); if ($latest && file_exists($latest)) { header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . basename($latest) . '"'); header('Content-Length: ' . filesize($latest)); readfile($latest); exit; } echo 'Kein Video gefunden.'; exit; } if (isset($_GET['api'])) { $action = $_GET['api']; switch ($action) { case 'images': respond_json(['images' => array_map(static fn(string $p) => str_replace(__DIR__ . '/', '', $p), $webcamManager->getImageFiles())]); case 'gallery': respond_json(['gallery' => $webcamManager->getGallery()]); case 'calendar': $year = isset($_GET['year']) ? (int) $_GET['year'] : (int) date('Y'); $month = isset($_GET['month']) ? (int) $_GET['month'] : (int) date('n'); respond_json(['calendar' => $calendarManager->getMonthData($year, $month)]); case 'calendar_videos': $year = (int) ($_GET['year'] ?? date('Y')); $month = (int) ($_GET['month'] ?? date('n')); $day = (int) ($_GET['day'] ?? date('j')); respond_json(['videos' => $calendarManager->getVideosForDate($year, $month, $day)]); case 'guestbook': respond_json(['entries' => $guestbookManager->getEntries(50)]); case 'stream_stats': respond_json(['stats' => $webcamManager->getStreamStats()]); default: respond_json(['message' => 'Unbekannte API-Anfrage.'], 404); } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; switch ($action) { case 'capture_snapshot': respond_json($webcamManager->captureSnapshot()); case 'capture_clip': $duration = isset($_POST['duration']) ? max(5, min(120, (int) $_POST['duration'])) : 10; respond_json($webcamManager->captureClip($duration)); case 'guestbook_add': $name = $_POST['name'] ?? ''; $message = $_POST['message'] ?? ''; $rating = isset($_POST['rating']) ? (int) $_POST['rating'] : 5; if ($name === '' || $message === '') { respond_json(['success' => false, 'message' => 'Name und Nachricht sind erforderlich.'], 422); } respond_json(['success' => true, 'entry' => $guestbookManager->addEntry($name, $message, $rating)]); case 'contact_send': $name = $_POST['name'] ?? ''; $email = $_POST['email'] ?? ''; $message = $_POST['message'] ?? ''; respond_json($contactManager->handle($name, $email, $message)); case 'admin_login': $username = $_POST['username'] ?? ''; $password = $_POST['password'] ?? ''; respond_json(['success' => $adminManager->login($username, $password)]); case 'set_language': $_SESSION['lang'] = $_POST['language'] ?? 'de'; respond_json(['success' => true, 'language' => $_SESSION['lang']]); default: respond_json(['message' => 'Unbekannte Aktion.'], 400); } } $translations = $languageManager->getAllTranslations(); ?> <?= htmlspecialchars($languageManager->get('title', $locale)) ?>

get('welcome', $locale)) ?>

get('subline', $locale)) ?>

⬇️ get('download', $locale)) ?>

get('stats', $locale)) ?> --

--

get('language', $locale)) ?>

$values): ?>

get('gallery', $locale)) ?>

get('starlink', $locale)) ?>

<?= htmlspecialchars($languageManager->get('starlink_alt', $locale)) ?> get('starlink_caption', $locale)) ?>

get('calendar_title', $locale)) ?>

get('guestbook', $locale)) ?>

get('contact', $locale)) ?>