diff --git a/index3.php b/index3.php
new file mode 100644
index 0000000..88fe8c7
--- /dev/null
+++ b/index3.php
@@ -0,0 +1,1218 @@
+ [
+ '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)) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ⬇️ = htmlspecialchars($languageManager->get('download', $locale)) ?>
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($languageManager->get('calendar_title', $locale)) ?>
+
+
+
+
+
+ = htmlspecialchars($languageManager->get('guestbook', $locale)) ?>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index4.php b/index4.php
new file mode 100644
index 0000000..88fe8c7
--- /dev/null
+++ b/index4.php
@@ -0,0 +1,1218 @@
+ [
+ '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)) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ⬇️ = htmlspecialchars($languageManager->get('download', $locale)) ?>
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($languageManager->get('calendar_title', $locale)) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+