diff --git a/aurora-livecam/SettingsManager.php b/aurora-livecam/SettingsManager.php index bb3336d..01263cc 100644 --- a/aurora-livecam/SettingsManager.php +++ b/aurora-livecam/SettingsManager.php @@ -48,7 +48,20 @@ class SettingsManager { 'zoom_timelapse' => [ 'show_zoom_controls' => true, 'max_zoom_level' => 4.0, - 'timelapse_reverse_enabled' => true + 'timelapse_reverse_enabled' => true, + 'weekly_timelapse_enabled' => true // Wochenzeitraffer Button + ], + // Auto-Screenshot für Galerie + 'auto_screenshot' => [ + 'enabled' => false, + 'interval_minutes' => 10, + 'max_images' => 144, // 24h bei 10min Intervall + 'save_to_gallery' => true + ], + // Email-Sharing + 'sharing' => [ + 'email_enabled' => false, + 'share_link_expiry_hours' => 24 ], // Punkt 5: Content Management 'content' => [ @@ -268,6 +281,32 @@ class SettingsManager { return $this->get('zoom_timelapse.timelapse_reverse_enabled') === true; } + public function isWeeklyTimelapseEnabled() { + return $this->get('zoom_timelapse.weekly_timelapse_enabled') !== false; + } + + // Auto-Screenshot Helper + public function isAutoScreenshotEnabled() { + return $this->get('auto_screenshot.enabled') === true; + } + + public function getAutoScreenshotInterval() { + return $this->get('auto_screenshot.interval_minutes') ?? 10; + } + + public function getAutoScreenshotMaxImages() { + return $this->get('auto_screenshot.max_images') ?? 144; + } + + // Sharing Helper + public function isEmailSharingEnabled() { + return $this->get('sharing.email_enabled') === true; + } + + public function getShareLinkExpiryHours() { + return $this->get('sharing.share_link_expiry_hours') ?? 24; + } + // SEO Helper public function getCustomTitle() { $title = $this->get('seo.custom_title'); diff --git a/aurora-livecam/api/auto-screenshot.php b/aurora-livecam/api/auto-screenshot.php new file mode 100644 index 0000000..406a399 --- /dev/null +++ b/aurora-livecam/api/auto-screenshot.php @@ -0,0 +1,104 @@ +isAutoScreenshotEnabled()) { + echo json_encode(['success' => false, 'error' => 'Auto-Screenshot deaktiviert']); + exit; +} + +// Optionale API-Key Validierung +$configFile = dirname(__DIR__) . '/config.php'; +if (file_exists($configFile)) { + $config = require $configFile; + $apiKey = $config['auto_screenshot_key'] ?? ''; + + if (!empty($apiKey) && ($_GET['key'] ?? '') !== $apiKey) { + http_response_code(403); + echo json_encode(['success' => false, 'error' => 'Ungültiger API-Key']); + exit; + } +} + +// Galerie-Verzeichnis erstellen +$galleryDir = dirname(__DIR__) . '/gallery/auto/'; +if (!is_dir($galleryDir)) { + mkdir($galleryDir, 0755, true); +} + +// Screenshot-Dateiname +$filename = 'auto_' . date('Y-m-d_H-i-s') . '.jpg'; +$filepath = $galleryDir . $filename; + +// Video-Stream URL +$streamUrl = 'test_video.m3u8'; +$logoPath = dirname(__DIR__) . '/logo.png'; + +// FFmpeg-Befehl zum Erstellen des Screenshots +$command = sprintf( + 'ffmpeg -i %s -vframes 1 -q:v 2 %s 2>&1', + escapeshellarg($streamUrl), + escapeshellarg($filepath) +); + +exec($command, $output, $returnVar); + +if ($returnVar !== 0 || !file_exists($filepath)) { + echo json_encode([ + 'success' => false, + 'error' => 'Screenshot fehlgeschlagen', + 'command' => $command, + 'output' => implode("\n", $output) + ]); + exit; +} + +// Alte Screenshots aufräumen (max. Anzahl einhalten) +$maxImages = $settingsManager->getAutoScreenshotMaxImages(); +$existingFiles = glob($galleryDir . 'auto_*.jpg'); +rsort($existingFiles); // Neueste zuerst + +if (count($existingFiles) > $maxImages) { + $filesToDelete = array_slice($existingFiles, $maxImages); + foreach ($filesToDelete as $file) { + @unlink($file); + } +} + +// Metadaten speichern +$metaFile = $galleryDir . 'metadata.json'; +$metadata = []; +if (file_exists($metaFile)) { + $metadata = json_decode(file_get_contents($metaFile), true) ?? []; +} + +$metadata[$filename] = [ + 'created_at' => date('Y-m-d H:i:s'), + 'timestamp' => time(), + 'size' => filesize($filepath) +]; + +// Nur die letzten maxImages behalten +$metadata = array_slice($metadata, -$maxImages, null, true); +file_put_contents($metaFile, json_encode($metadata, JSON_PRETTY_PRINT)); + +echo json_encode([ + 'success' => true, + 'file' => $filename, + 'path' => '/gallery/auto/' . $filename, + 'total_images' => count(glob($galleryDir . 'auto_*.jpg')) +]); diff --git a/aurora-livecam/api/gallery.php b/aurora-livecam/api/gallery.php new file mode 100644 index 0000000..4ecee5f --- /dev/null +++ b/aurora-livecam/api/gallery.php @@ -0,0 +1,97 @@ + true, 'images' => [], 'total' => 0]); + exit; +} + +// Parameter +$date = $_GET['date'] ?? null; +$from = $_GET['from'] ?? null; +$to = $_GET['to'] ?? null; +$limit = min(100, (int)($_GET['limit'] ?? 50)); +$offset = max(0, (int)($_GET['offset'] ?? 0)); + +// Alle Bilder holen +$allFiles = glob($galleryDir . 'auto_*.jpg'); +rsort($allFiles); // Neueste zuerst + +$images = []; + +foreach ($allFiles as $file) { + $filename = basename($file); + // Extrahiere Datum aus Dateinamen: auto_2024-01-30_14-30-00.jpg + if (preg_match('/auto_(\d{4}-\d{2}-\d{2})_(\d{2}-\d{2}-\d{2})\.jpg/', $filename, $matches)) { + $fileDate = $matches[1]; + $fileTime = str_replace('-', ':', $matches[2]); + + // Datumsfilter + if ($date !== null && $fileDate !== $date) { + continue; + } + + if ($from !== null && $fileDate < $from) { + continue; + } + + if ($to !== null && $fileDate > $to) { + continue; + } + + $images[] = [ + 'filename' => $filename, + 'path' => '/gallery/auto/' . $filename, + 'date' => $fileDate, + 'time' => $fileTime, + 'datetime' => $fileDate . ' ' . $fileTime, + 'timestamp' => strtotime($fileDate . ' ' . $fileTime), + 'size' => filesize($file) + ]; + } +} + +$total = count($images); + +// Pagination +$images = array_slice($images, $offset, $limit); + +// Verfügbare Daten (für Kalender/Filter) +$availableDates = []; +foreach (glob($galleryDir . 'auto_*.jpg') as $file) { + if (preg_match('/auto_(\d{4}-\d{2}-\d{2})/', basename($file), $m)) { + $availableDates[$m[1]] = ($availableDates[$m[1]] ?? 0) + 1; + } +} +krsort($availableDates); + +echo json_encode([ + 'success' => true, + 'images' => $images, + 'total' => $total, + 'offset' => $offset, + 'limit' => $limit, + 'available_dates' => $availableDates, + 'filters' => [ + 'date' => $date, + 'from' => $from, + 'to' => $to + ] +]); diff --git a/aurora-livecam/api/share.php b/aurora-livecam/api/share.php new file mode 100644 index 0000000..8591642 --- /dev/null +++ b/aurora-livecam/api/share.php @@ -0,0 +1,315 @@ +isEmailSharingEnabled()) { + echo json_encode(['success' => false, 'error' => 'E-Mail-Sharing ist deaktiviert']); + exit; +} + +// Config laden +$configFile = dirname(__DIR__) . '/config.php'; +$config = file_exists($configFile) ? require $configFile : []; +$mailConfig = $config['mail'] ?? []; + +if (empty($mailConfig['host']) || empty($mailConfig['username'])) { + echo json_encode(['success' => false, 'error' => 'E-Mail-Server nicht konfiguriert']); + exit; +} + +// === GET: Share-Link generieren === +if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['generate'])) { + $path = $_GET['path'] ?? ''; + $type = $_GET['type'] ?? 'video'; + + if (empty($path)) { + echo json_encode(['success' => false, 'error' => 'Kein Pfad angegeben']); + exit; + } + + // Token generieren + $expiryHours = $settingsManager->getShareLinkExpiryHours(); + $expiry = time() + ($expiryHours * 3600); + $token = hash_hmac('sha256', $path . $expiry, session_id() . 'share_secret'); + + // Share-Link speichern + $shareDir = dirname(__DIR__) . '/data/shares/'; + if (!is_dir($shareDir)) { + mkdir($shareDir, 0755, true); + } + + $shareId = bin2hex(random_bytes(16)); + $shareData = [ + 'id' => $shareId, + 'path' => $path, + 'type' => $type, + 'token' => $token, + 'expiry' => $expiry, + 'created_at' => date('Y-m-d H:i:s') + ]; + + file_put_contents($shareDir . $shareId . '.json', json_encode($shareData)); + + // URL generieren + $baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') + . '://' . $_SERVER['HTTP_HOST']; + $shareUrl = $baseUrl . '/api/share.php?view=' . $shareId; + + echo json_encode([ + 'success' => true, + 'share_url' => $shareUrl, + 'share_id' => $shareId, + 'expires_at' => date('Y-m-d H:i:s', $expiry) + ]); + exit; +} + +// === GET: Share-Link anzeigen === +if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['view'])) { + $shareId = preg_replace('/[^a-f0-9]/', '', $_GET['view']); + $shareFile = dirname(__DIR__) . '/data/shares/' . $shareId . '.json'; + + if (!file_exists($shareFile)) { + header('Content-Type: text/html; charset=utf-8'); + echo 'Link ungültig

❌ Link nicht gefunden

Dieser Share-Link existiert nicht oder wurde bereits gelöscht.

'; + exit; + } + + $shareData = json_decode(file_get_contents($shareFile), true); + + // Ablauf prüfen + if (time() > $shareData['expiry']) { + @unlink($shareFile); + header('Content-Type: text/html; charset=utf-8'); + echo 'Link abgelaufen

⏰ Link abgelaufen

Dieser Share-Link ist abgelaufen. Bitte fordere einen neuen Link an.

'; + exit; + } + + // Datei existiert? + $filePath = dirname(__DIR__) . $shareData['path']; + if (!file_exists($filePath)) { + header('Content-Type: text/html; charset=utf-8'); + echo 'Datei nicht gefunden

📭 Datei nicht gefunden

Die geteilte Datei existiert nicht mehr.

'; + exit; + } + + // Redirect zur Datei oder HTML-Seite mit eingebettetem Player + $isVideo = in_array(pathinfo($filePath, PATHINFO_EXTENSION), ['mp4', 'webm', 'mov']); + $isImage = in_array(pathinfo($filePath, PATHINFO_EXTENSION), ['jpg', 'jpeg', 'png', 'gif', 'webp']); + + $siteName = $config['app']['name'] ?? 'Aurora Livecam'; + $baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') + . '://' . $_SERVER['HTTP_HOST']; + + header('Content-Type: text/html; charset=utf-8'); + echo ' + + + + + Geteilte ' . ($isVideo ? 'Video' : 'Bild') . ' - ' . htmlspecialchars($siteName) . ' + + + +
+

📤 Geteilte' . ($isVideo ? 's Video' : 's Bild') . '

'; + + if ($isVideo) { + echo ''; + } else { + echo 'Geteiltes Bild'; + } + + echo ' + ⬇️ Herunterladen +
+ + +'; + exit; +} + +// === POST: E-Mail senden === +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + echo json_encode(['success' => false, 'error' => 'Nur POST erlaubt']); + exit; +} + +// JSON-Body parsen +$input = json_decode(file_get_contents('php://input'), true); +if (!$input) { + $input = $_POST; +} + +$email = filter_var($input['email'] ?? '', FILTER_VALIDATE_EMAIL); +$path = $input['path'] ?? ''; +$type = $input['type'] ?? 'video'; +$message = htmlspecialchars($input['message'] ?? ''); +$senderName = htmlspecialchars($input['sender_name'] ?? 'Ein Freund'); + +if (!$email) { + echo json_encode(['success' => false, 'error' => 'Ungültige E-Mail-Adresse']); + exit; +} + +if (empty($path)) { + echo json_encode(['success' => false, 'error' => 'Kein Pfad angegeben']); + exit; +} + +// Share-Link generieren +$expiryHours = $settingsManager->getShareLinkExpiryHours(); +$expiry = time() + ($expiryHours * 3600); + +$shareDir = dirname(__DIR__) . '/data/shares/'; +if (!is_dir($shareDir)) { + mkdir($shareDir, 0755, true); +} + +$shareId = bin2hex(random_bytes(16)); +$shareData = [ + 'id' => $shareId, + 'path' => $path, + 'type' => $type, + 'expiry' => $expiry, + 'created_at' => date('Y-m-d H:i:s'), + 'shared_to' => $email +]; + +file_put_contents($shareDir . $shareId . '.json', json_encode($shareData)); + +$baseUrl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') + . '://' . $_SERVER['HTTP_HOST']; +$shareUrl = $baseUrl . '/api/share.php?view=' . $shareId; +$siteName = $config['app']['name'] ?? 'Aurora Livecam'; + +// E-Mail senden +try { + $mail = new PHPMailer(true); + + // SMTP Konfiguration + $mail->isSMTP(); + $mail->Host = $mailConfig['host']; + $mail->SMTPAuth = true; + $mail->Username = $mailConfig['username']; + $mail->Password = $mailConfig['password']; + $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; + $mail->Port = $mailConfig['port'] ?? 587; + $mail->CharSet = 'UTF-8'; + + // Absender/Empfänger + $mail->setFrom($mailConfig['from_address'], $mailConfig['from_name'] ?? $siteName); + $mail->addAddress($email); + + // Inhalt + $mail->isHTML(true); + $mail->Subject = $senderName . ' hat ' . ($type === 'video' ? 'ein Video' : 'ein Bild') . ' mit dir geteilt'; + + $mail->Body = ' +
+
+

📤 ' . htmlspecialchars($siteName) . '

+
+
+

+ ' . htmlspecialchars($senderName) . ' hat ' . ($type === 'video' ? 'ein Video' : 'ein Bild') . ' mit dir geteilt! +

+ ' . (!empty($message) ? '
"' . nl2br($message) . '"
' : '') . ' + + ▶️ Jetzt ansehen + +

+ Dieser Link ist ' . $expiryHours . ' Stunden gültig. +

+
+
'; + + $mail->AltBody = $senderName . ' hat ' . ($type === 'video' ? 'ein Video' : 'ein Bild') . ' mit dir geteilt: ' . $shareUrl; + + $mail->send(); + + echo json_encode([ + 'success' => true, + 'message' => 'E-Mail wurde gesendet', + 'share_url' => $shareUrl + ]); + +} catch (Exception $e) { + error_log('Share email error: ' . $e->getMessage()); + echo json_encode([ + 'success' => false, + 'error' => 'E-Mail konnte nicht gesendet werden', + 'share_url' => $shareUrl // URL trotzdem zurückgeben + ]); +} diff --git a/aurora-livecam/api/video-search.php b/aurora-livecam/api/video-search.php new file mode 100644 index 0000000..751cb81 --- /dev/null +++ b/aurora-livecam/api/video-search.php @@ -0,0 +1,192 @@ + [], + 'ai_videos' => [], + 'gallery_images' => [] +]; + +// AI-Kategorien +$aiCategories = ['sunny', 'rainy', 'snowy', 'planes', 'birds', 'sunset', 'sunrise', 'rainbow']; + +// === TAGESVIDEOS SUCHEN === +if ($type === 'all' || $type === 'daily') { + $pattern = $videoDir . 'daily_video_*.mp4'; + $dailyVideos = glob($pattern); + + foreach ($dailyVideos as $video) { + $filename = basename($video); + + // Extrahiere Datum aus Dateinamen: daily_video_YYYYMMDD_HHMMSS.mp4 + if (preg_match('/daily_video_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})\.mp4/', $filename, $matches)) { + $videoDate = $matches[1] . '-' . $matches[2] . '-' . $matches[3]; + $videoTime = $matches[4] . ':' . $matches[5]; + $videoDateTime = $videoDate . ' ' . $videoTime . ':' . $matches[6]; + + // Datumsfilter + if ($date !== null && $videoDate !== $date) { + continue; + } + + if ($fromDate !== null && $videoDate < $fromDate) { + continue; + } + + if ($toDate !== null && $videoDate > $toDate) { + continue; + } + + // Uhrzeitfilter + if ($timeFrom !== null && $videoTime < $timeFrom) { + continue; + } + + if ($timeTo !== null && $videoTime > $timeTo) { + continue; + } + + // Spezifische Uhrzeit (mit 30 Min Toleranz) + if ($time !== null) { + $searchMinutes = intval(substr($time, 0, 2)) * 60 + intval(substr($time, 3, 2)); + $videoMinutes = intval($matches[4]) * 60 + intval($matches[5]); + + if (abs($searchMinutes - $videoMinutes) > 30) { + continue; + } + } + + $results['daily_videos'][] = [ + 'type' => 'daily', + 'filename' => $filename, + 'path' => '/videos/' . $filename, + 'date' => $videoDate, + 'time' => $videoTime, + 'datetime' => $videoDateTime, + 'timestamp' => strtotime($videoDateTime), + 'size' => filesize($video), + 'size_mb' => round(filesize($video) / (1024 * 1024), 2) + ]; + } + } +} + +// === AI-VIDEOS SUCHEN === +if ($type === 'all' || $type === 'ai') { + $searchCategories = $aiCategory ? [$aiCategory] : $aiCategories; + + foreach ($searchCategories as $category) { + $categoryDir = $aiDir . $category . '/'; + if (!is_dir($categoryDir)) continue; + + $pattern = $categoryDir . $category . '_*.mp4'; + $aiVideos = glob($pattern); + + foreach ($aiVideos as $video) { + $filename = basename($video); + + // Extrahiere Datum aus Dateinamen: category_YYYYMMDD_HHMMSS.mp4 + if (preg_match('/' . $category . '_(\d{4})(\d{2})(\d{2})_?(\d{2})?(\d{2})?(\d{2})?\.mp4/', $filename, $matches)) { + $videoDate = $matches[1] . '-' . $matches[2] . '-' . $matches[3]; + $videoTime = isset($matches[4]) ? ($matches[4] . ':' . ($matches[5] ?? '00')) : '00:00'; + $videoDateTime = $videoDate . ' ' . $videoTime; + + // Datumsfilter + if ($date !== null && $videoDate !== $date) { + continue; + } + + if ($fromDate !== null && $videoDate < $fromDate) { + continue; + } + + if ($toDate !== null && $videoDate > $toDate) { + continue; + } + + // Uhrzeitfilter + if ($timeFrom !== null && $videoTime < $timeFrom) { + continue; + } + + if ($timeTo !== null && $videoTime > $timeTo) { + continue; + } + + $results['ai_videos'][] = [ + 'type' => 'ai', + 'category' => $category, + 'filename' => $filename, + 'path' => '/ai/' . $category . '/' . $filename, + 'date' => $videoDate, + 'time' => $videoTime, + 'datetime' => $videoDateTime, + 'timestamp' => strtotime($videoDateTime), + 'size' => filesize($video), + 'size_mb' => round(filesize($video) / (1024 * 1024), 2) + ]; + } + } + } +} + +// Sortieren nach Datum/Zeit (neueste zuerst) +usort($results['daily_videos'], fn($a, $b) => $b['timestamp'] - $a['timestamp']); +usort($results['ai_videos'], fn($a, $b) => $b['timestamp'] - $a['timestamp']); + +// Limit anwenden +$results['daily_videos'] = array_slice($results['daily_videos'], 0, $limit); +$results['ai_videos'] = array_slice($results['ai_videos'], 0, $limit); + +// Statistiken +$results['stats'] = [ + 'total_daily' => count($results['daily_videos']), + 'total_ai' => count($results['ai_videos']), + 'total' => count($results['daily_videos']) + count($results['ai_videos']) +]; + +$results['filters'] = [ + 'date' => $date, + 'time' => $time, + 'from' => $fromDate, + 'to' => $toDate, + 'time_from' => $timeFrom, + 'time_to' => $timeTo, + 'type' => $type, + 'ai_category' => $aiCategory +]; + +$results['success'] = true; + +echo json_encode($results, JSON_PRETTY_PRINT); diff --git a/aurora-livecam/index.php b/aurora-livecam/index.php index b6991a7..249b41a 100644 --- a/aurora-livecam/index.php +++ b/aurora-livecam/index.php @@ -2803,9 +2803,11 @@ body.theme-neo footer { Snapshot speichern +isWeeklyTimelapseEnabled()): ?> Wochenzeitraffer + Videoclip speichern @@ -2820,6 +2822,40 @@ body.theme-neo footer {

Videoarchiv Tagesvideos

+ + +
+

+ 🔍 Suche nach Datum/Uhrzeit +

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ displayVisualCalendar(); @@ -3382,6 +3418,234 @@ document.addEventListener('DOMContentLoaded', function() { }); + + + + +isEmailSharingEnabled()): ?> + + + + isAdmin()): ?>