= htmlspecialchars($languageManager->get('timelapse_title', $locale)) ?>
+= htmlspecialchars($languageManager->get('timelapse_caption', $locale)) ?>
+= htmlspecialchars($languageManager->get('archive_title', $locale)) ?>
++
diff --git a/indexnew.php b/indexnew.php new file mode 100644 index 0000000..a1de8eb --- /dev/null +++ b/indexnew.php @@ -0,0 +1,1720 @@ + [ + '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', + 'timelapse_title' => 'Zeitraffer-Magie', + 'timelapse_caption' => 'Die letzten Keyframes werden automatisch abgespielt.', + 'archive_title' => 'Video-Archiv', + 'archive_hint' => 'Wähle Jahr und Monat um gespeicherte Clips zu laden.', + 'no_videos' => 'Keine Videos verfügbar.', + 'download_video' => 'Video herunterladen', + ], + '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', + 'timelapse_title' => 'Timelapse magic', + 'timelapse_caption' => 'Latest keyframes will autoplay.', + 'archive_title' => 'Video archive', + 'archive_hint' => 'Pick a year and month to browse stored clips.', + 'no_videos' => 'No videos available.', + 'download_video' => 'Download clip', + ], + 'it' => [ + 'title' => 'Aurora Weather Livecam', + 'welcome' => 'Benvenuto alla webcam Aurora', + 'subline' => 'Immagini live, time-lapse, archivio e community – tutto in un unico cruscotto soleggiato.', + 'live' => 'Live', + 'timelapse' => 'Time-lapse', + 'archive' => 'Archivio', + 'gallery' => 'Galleria', + 'community' => 'Community', + 'contact' => 'Contatto', + 'guestbook' => 'Guestbook', + 'send' => 'Invia', + 'name' => 'Nome', + 'email' => 'Email', + 'message' => 'Messaggio', + 'screenshot' => 'Screenshot', + 'clip' => 'Registra clip', + 'download' => 'Scarica ultima cattura', + 'calendar_title' => 'Calendario Meteo Visivo', + 'language' => 'Lingua', + 'rating' => 'Valutazione', + 'comment' => 'Commento', + 'add_entry' => 'Aggiungi voce', + 'view_all' => 'Vedi tutto', + 'privacy' => 'Privacy', + 'share' => 'Condividi', + 'pip' => 'Picture-in-Picture', + 'stats' => 'Stato stream', + 'starlink' => 'Connessione Starlink', + 'starlink_caption' => 'Scansiona il QR code per internet satellitare Starlink.', + 'starlink_alt' => 'QR code Starlink', + 'timelapse_title' => 'Magia time-lapse', + 'timelapse_caption' => 'Gli ultimi keyframe vengono riprodotti automaticamente.', + 'archive_title' => 'Archivio video', + 'archive_hint' => 'Scegli anno e mese per sfogliare le clip.', + 'no_videos' => 'Nessun video disponibile.', + 'download_video' => 'Scarica clip', + ], + 'fr' => [ + 'title' => 'Aurora Weather Livecam', + 'welcome' => 'Bienvenue sur la webcam Aurora', + 'subline' => 'Images en direct, time-lapse, archives et communauté – dans un tableau de bord ensoleillé.', + 'live' => 'Direct', + 'timelapse' => 'Time-lapse', + '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' => 'Image dans l’image', + 'stats' => 'Statut du flux', + 'starlink' => 'Connexion Starlink', + 'starlink_caption' => 'Scannez le QR code pour internet par satellite Starlink.', + 'starlink_alt' => 'QR code Starlink', + 'timelapse_title' => 'Magie time-lapse', + 'timelapse_caption' => 'Les derniers keyframes se jouent automatiquement.', + 'archive_title' => 'Archive vidéo', + 'archive_hint' => 'Choisissez année et mois pour parcourir les clips.', + 'no_videos' => 'Aucune vidéo disponible.', + 'download_video' => 'Télécharger la vidéo', + ], + '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 二维码', + 'timelapse_title' => '延时魔法', + 'timelapse_caption' => '自动播放最新关键帧。', + 'archive_title' => '视频档案', + 'archive_hint' => '选择年份和月份来浏览存档剪辑。', + 'no_videos' => '暂无可用视频。', + 'download_video' => '下载视频', + ], + ]; + + 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 string $videoDir; + 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 __construct(string $videoDir = VIDEO_DIR) + { + $this->videoDir = rtrim($videoDir, '/'); + } + + 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 = $this->videoDir . "/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($this->videoDir . "/daily_video_{$date}_*.mp4") as $file) { + $fileName = basename($file); + $token = generate_download_token($fileName); + $videos[] = [ + 'file' => $fileName, + 'size' => filesize($file), + 'time' => date('H:i', filemtime($file)), + 'sizeFormatted' => round(filesize($file) / (1024 * 1024), 2), + 'token' => $token, + 'download' => '?download_specific_video=' . rawurlencode($fileName) . '&token=' . $token, + 'day' => $day, + ]; + } + return $videos; + } +} + +class VideoArchiveManager +{ + private string $videoDir; + private array $monthNames = [ + '01' => 'Januar', + '02' => 'Februar', + '03' => 'März', + '04' => 'April', + '05' => 'Mai', + '06' => 'Juni', + '07' => 'Juli', + '08' => 'August', + '09' => 'September', + '10' => 'Oktober', + '11' => 'November', + '12' => 'Dezember', + ]; + + public function __construct(string $videoDir = VIDEO_DIR) + { + $this->videoDir = rtrim($videoDir, '/'); + } + + public function getVideosGroupedByDate(): array + { + $videos = []; + foreach (glob($this->videoDir . '/daily_video_*.mp4') ?: [] as $video) { + if (!preg_match('/daily_video_(\d{8})_\d{6}\.mp4$/', basename($video), $matches)) { + continue; + } + $dateStr = $matches[1]; + $year = substr($dateStr, 0, 4); + $month = substr($dateStr, 4, 2); + $videos[$year][$month][] = $this->formatVideoRecord($video); + } + foreach ($videos as $year => $months) { + foreach ($months as $month => $items) { + usort($videos[$year][$month], static fn(array $a, array $b) => $b['timestamp'] <=> $a['timestamp']); + } + } + krsort($videos); + return $videos; + } + + public function getAvailableYearsAndMonths(): array + { + $grouped = $this->getVideosGroupedByDate(); + $result = []; + foreach ($grouped as $year => $months) { + $result[$year] = array_values(array_keys($months)); + rsort($result[$year]); + } + return $result; + } + + public function getVideosForYearAndMonth(int $year, string $month): array + { + $month = str_pad($month, 2, '0', STR_PAD_LEFT); + $grouped = $this->getVideosGroupedByDate(); + return $grouped[$year][$month] ?? []; + } + + public function getMonthName(string $month): string + { + $month = str_pad($month, 2, '0', STR_PAD_LEFT); + return $this->monthNames[$month] ?? $month; + } + + public function handleDownloadRequest(?string $fileName, ?string $token): void + { + if ($fileName === null || $token === null) { + return; + } + $cleanFile = basename($fileName); + $expected = generate_download_token($cleanFile); + if (!hash_equals($expected, $token)) { + respond_json(['message' => 'Ungültiger Download-Token.'], 403); + } + $videoDir = realpath($this->videoDir); + $fullPath = realpath($this->videoDir . '/' . $cleanFile); + if (!$videoDir || !$fullPath || strpos($fullPath, $videoDir) !== 0 || !file_exists($fullPath)) { + respond_json(['message' => 'Datei nicht gefunden.'], 404); + } + header('Content-Description: File Transfer'); + header('Content-Type: video/mp4'); + header('Content-Disposition: attachment; filename="' . basename($fullPath) . '"'); + header('Content-Length: ' . filesize($fullPath)); + readfile($fullPath); + exit; + } + + private function formatVideoRecord(string $path): array + { + $fileName = basename($path); + $timestamp = filemtime($path) ?: time(); + $size = filesize($path) ?: 0; + $date = DateTimeImmutable::createFromFormat('YmdHis', preg_replace('/[^0-9]/', '', $fileName)) ?: new DateTimeImmutable('@' . $timestamp); + $token = generate_download_token($fileName); + return [ + 'file' => $fileName, + 'path' => $path, + 'day' => (int) $date->format('d'), + 'time' => $date->format('H:i'), + 'size' => $size, + 'sizeFormatted' => round($size / (1024 * 1024), 2), + 'timestamp' => $timestamp, + 'download' => '?download_specific_video=' . rawurlencode($fileName) . '&token=' . $token, + 'token' => $token, + ]; + } +} + +class GuestbookManager +{ + private array $entries = []; + + public function __construct() + { + $content = json_decode((string) file_get_contents(GUESTBOOK_FILE), true); + $this->entries = is_array($content) ? $content : []; + } + + private function persist(): void + { + file_put_contents(GUESTBOOK_FILE, json_encode($this->entries, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } + + 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; + $this->persist(); + return $entry; + } + + public function getEntries(int $limit = 10): array + { + return array_slice(array_reverse($this->entries), 0, $limit); + } + + public function deleteEntry(int $index): bool + { + if (!isset($this->entries[$index])) { + return false; + } + unset($this->entries[$index]); + $this->entries = array_values($this->entries); + $this->persist(); + return true; + } +} + +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 = '
Name: ' . $payload['name'] . '
' . + 'E-Mail: ' . $payload['email'] . '
' . + 'Nachricht:
' . nl2br($payload['message']) . '
= htmlspecialchars($languageManager->get('subline', $locale)) ?>
+= htmlspecialchars($languageManager->get('timelapse_caption', $locale)) ?>
++