From 936a72fd8c3862871009ea52a1a31ec4c734b1ee Mon Sep 17 00:00:00 2001 From: Metacube Date: Mon, 30 Mar 2026 11:27:52 +0200 Subject: [PATCH] Update print statement from 'Hello' to 'Goodbye' --- index.php | 6585 +++++++++++++++++++++++++++++------------------------ 1 file changed, 3620 insertions(+), 2965 deletions(-) diff --git a/index.php b/index.php index 6f2cad1..bc55a9c 100644 --- a/index.php +++ b/index.php @@ -3,14 +3,113 @@ use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\Exception; require __DIR__ . '/vendor/autoload.php'; +require_once 'SettingsManager.php'; +require_once 'WeatherManager.php'; + +// ============================================================ +// REMOTE CACHE SYSTEM - verhindert SMB-Blocking bei Seitenaufruf +// ============================================================ +class RemoteCache { + private static $instance = null; + private $cache = null; + private $cacheFile; + private $lockFile; + private $maxAge; + + private function __construct() { + $this->cacheFile = __DIR__ . '/remote_cache.json'; + $this->lockFile = __DIR__ . '/remote_cache.lock'; + $this->maxAge = 300; + $this->loadCache(); + } + + public static function getInstance() { + if (self::$instance === null) self::$instance = new self(); + return self::$instance; + } + + private function loadCache() { + if (file_exists($this->cacheFile)) { + $raw = @file_get_contents($this->cacheFile); + $this->cache = $raw ? json_decode($raw, true) : null; + } + if ($this->cache === null || (time() - ($this->cache['updated_at'] ?? 0)) > $this->maxAge) { + $this->triggerBackgroundUpdate(); + } + } + + private function triggerBackgroundUpdate() { + if (file_exists($this->lockFile) && (time() - filemtime($this->lockFile)) < 120) return; + $script = __DIR__ . '/remote_cache_update.php'; + if (file_exists($script)) exec('php ' . escapeshellarg($script) . ' > /dev/null 2>&1 &'); + } + + public function isAvailable() { return ($this->cache !== null && !empty($this->cache['available'])); } + + public function getVideos() { + if (!$this->isAvailable()) return []; + return $this->cache['videos'] ?? []; + } + + public function getImageWebPaths() { + if (!$this->isAvailable()) return []; + $paths = []; + foreach (($this->cache['images'] ?? []) as $img) $paths[] = $img['web_path']; + return $paths; + } + + public function getVideosForDate($dateStr) { + $result = []; + foreach ($this->getVideos() as $v) { + if (strpos($v['filename'], "daily_video_{$dateStr}_") === 0) $result[] = $v; + } + return $result; + } + + public function hasVideosForDate($dateStr) { + foreach ($this->getVideos() as $v) { + if (strpos($v['filename'], "daily_video_{$dateStr}_") === 0) return true; + } + return false; + } + + public function getLatestVideo() { + $latest = null; $latestTime = 0; + foreach ($this->getVideos() as $v) { + if (($v['mtime'] ?? 0) > $latestTime) { $latestTime = $v['mtime']; $latest = $v; } + } + return $latest; + } + + public function getCacheAge() { + if ($this->cache === null) return PHP_INT_MAX; + return time() - ($this->cache['updated_at'] ?? 0); + } +} + +$remoteCache = RemoteCache::getInstance(); + +// Multi-Tenant Bootstrap laden (falls vorhanden) +if (file_exists(__DIR__ . '/src/bootstrap.php')) { + require_once __DIR__ . '/src/bootstrap.php'; +} + +// SettingsManager initialisieren +$settingsManager = new SettingsManager(); + +// WeatherManager initialisieren +$weatherManager = new WeatherManager($settingsManager); + +// AJAX-Handler für Settings und Weather (VOR anderen Ausgaben!) +$settingsManager->handleAjax(); +$weatherManager->handleAjax(); if (isset($_GET['download_video'])) { - $videoDir = './videos/'; $latestVideo = null; $latestTime = 0; - // Finde das neueste Video - foreach (glob($videoDir . '*.mp4') as $video) { + // Lokale Videos direkt (schnell) + foreach (glob('./videos/*.mp4') as $video) { $mtime = filemtime($video); if ($mtime > $latestTime) { $latestTime = $mtime; @@ -18,6 +117,14 @@ if (isset($_GET['download_video'])) { } } + // Remote-Videos aus Cache (kein glob auf SMB!) + $remoteLatest = $remoteCache->getLatestVideo(); + if ($remoteLatest && ($remoteLatest['mtime'] ?? 0) > $latestTime) { + if (file_exists($remoteLatest['path'])) { + $latestVideo = $remoteLatest['path']; + } + } + if ($latestVideo) { header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); @@ -48,88 +155,215 @@ function safeRedirect($url) { exit(); } -// Hauptlogik +// Hauptlogik - Domain Redirects werden jetzt in bootstrap.php behandelt +// (Legacy-Redirect bleibt als Fallback falls Bootstrap nicht geladen) $oldDomains = [ 'www.aurora-wetter-lifecam.ch', 'www.aurora-wetter-livecam.ch' ]; $newDomain = 'www.aurora-weather-livecam.com'; -if (in_array($_SERVER['HTTP_HOST'], $oldDomains)) { +if (in_array($_SERVER['HTTP_HOST'] ?? '', $oldDomains)) { $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http'; $newUrl = $protocol . '://' . $newDomain . $_SERVER['REQUEST_URI']; - - // Logging für Debugging - error_log("Umleitung von {$_SERVER['HTTP_HOST']} nach $newUrl"); - if (!headers_sent()) { header("HTTP/1.1 301 Moved Permanently"); header("Location: " . $newUrl); - } else { - echo ''; + exit(); + } +} + +// Site-Konfiguration: Nutze Multi-Tenant System falls verfügbar, sonst Legacy +if (function_exists('getSiteConfig')) { + // Multi-Tenant Modus (aus bootstrap.php) + $tenantConfig = getSiteConfig(); + $isSeecam = ($tenantConfig['tenant_slug'] === 'seecam'); + + $siteConfig = [ + 'domain' => $_SERVER['HTTP_HOST'] ?? 'localhost', + 'domainUrl' => (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost'), + 'logo' => $tenantConfig['logo_path'] ?? ($isSeecam ? 'seecam.jpg' : 'logo.png'), + 'siteName' => $tenantConfig['site_name'], + 'siteNameFull' => $tenantConfig['site_name_full'], + 'siteNameFullEn' => $tenantConfig['site_name_full'], + 'siteTitle' => $tenantConfig['site_name_full'] . ' - Live Webcam', + 'author' => $tenantConfig['site_name_full'], + 'alternateName' => $tenantConfig['site_name'] . ' Webcam Schweiz', + 'welcomeDe' => $tenantConfig['welcome_de'] ?: ('Willkommen bei ' . $tenantConfig['site_name_full']), + 'welcomeEn' => $tenantConfig['welcome_en'] ?: ('Welcome to ' . $tenantConfig['site_name_full']), + 'aboutDe' => $tenantConfig['site_name_full'] . ' ist ein Herzensprojekt von Wetterbegeisterten.', + 'aboutEn' => $tenantConfig['site_name_full'] . ' is a passion project by weather enthusiasts.', + 'blogTitle' => $tenantConfig['site_name'] . ' Wetter Blog', + 'footerName' => $tenantConfig['site_name_full'], + 'copyright' => '© ' . date('Y') . ' ' . $tenantConfig['site_name_full'], + // Zusätzliche Multi-Tenant Felder + 'tenant_id' => $tenantConfig['tenant_id'] ?? 0, + 'primary_color' => $tenantConfig['primary_color'] ?? '#667eea', + 'secondary_color' => $tenantConfig['secondary_color'] ?? '#764ba2', + 'custom_css' => $tenantConfig['custom_css'] ?? '', + ]; +} else { + // Legacy-Modus (hardcoded) + $isSeecam = ($_SERVER['HTTP_HOST'] === 'www.seecam.ch' || $_SERVER['HTTP_HOST'] === 'seecam.ch'); + + if ($isSeecam) { + $siteConfig = [ + 'domain' => 'www.seecam.ch', + 'domainUrl' => 'https://www.seecam.ch', + 'logo' => 'seecam.jpg', + 'siteName' => 'Seecam', + 'siteNameFull' => 'Seecam Wetter Livecam', + 'siteNameFullEn' => 'Seecam Weather Livecam', + 'siteTitle' => 'Zürich Oberland Webcam Live - Zürichsee & Patrouille Suisse | Seecam 24/7', + 'author' => 'Seecam Wetter Livecam', + 'alternateName' => 'Seecam Webcam Schweiz', + 'welcomeDe' => 'Willkommen bei Seecam Wetter Livecam', + 'welcomeEn' => 'Welcome to Seecam Weather Livecam', + 'aboutDe' => 'Seecam Wetter Livecam ist ein Herzensprojekt von Wetterbegeisterten.', + 'aboutEn' => 'Seecam Weather Livecam is a passion project.', + 'blogTitle' => 'Seecam Wetter Blog', + 'footerName' => 'Seecam Wetter Livecam', + 'copyright' => '© 2024 Seecam Wetter Livecam - Webcam Zürich Oberland', + 'tenant_id' => 0, + 'primary_color' => '#667eea', + 'secondary_color' => '#764ba2', + 'custom_css' => '', + ]; + } else { + $siteConfig = [ + 'domain' => 'www.aurora-weather-livecam.com', + 'domainUrl' => 'https://www.aurora-weather-livecam.com', + 'logo' => 'logo.png', + 'siteName' => 'Aurora', + 'siteNameFull' => 'Aurora Wetter Livecam', + 'siteNameFullEn' => 'Aurora Weather Livecam', + 'siteTitle' => 'Zürich Oberland Webcam Live - Zürichsee & Patrouille Suisse | Aurora Livecam 24/7', + 'author' => 'Aurora Wetter Livecam', + 'alternateName' => 'Aurora Webcam Schweiz', + 'welcomeDe' => 'Willkommen bei Aurora Wetter Livecam', + 'welcomeEn' => 'Welcome to Aurora Weather Livecam', + 'aboutDe' => 'Aurora Wetter Livecam ist ein Herzensprojekt von Wetterbegeisterten.', + 'aboutEn' => 'Aurora Weather Livecam is a passion project.', + 'blogTitle' => 'Aurora Wetter Blog', + 'footerName' => 'Aurora Wetter Livecam', + 'copyright' => '© 2024 Aurora Wetter Lifecam - Webcam Zürich Oberland', + 'tenant_id' => 0, + 'primary_color' => '#667eea', + 'secondary_color' => '#764ba2', + 'custom_css' => '', + ]; } - exit(); } - - session_start(); error_reporting(E_ALL); ini_set('display_errors', 1); $imageDir = "./image"; // Angepasst an das Ausgabeverzeichnis des Bash-Skripts -$imageFiles = glob("$imageDir/screenshot_*.jpg"); +$localImages = glob("$imageDir/screenshot_*.jpg"); +$remoteImagesWeb = $remoteCache->getImageWebPaths(); // Aus Cache statt glob auf SMB +$imageFiles = array_merge($localImages, $remoteImagesWeb); rsort($imageFiles); // Sortiert die Dateien in umgekehrter Reihenfolge (neueste zuerst) $imageFilesJson = json_encode($imageFiles); - -class WebcamManager { + +class ViewerCounter { + private $file = 'active_viewers.json'; + private $timeout = 30; // Zeit in Sekunden, bis ein User als "offline" gilt + + public function handleHeartbeat() { + $ip = md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']); // Anonymisierte ID + $now = time(); + + $viewers = []; + + // 1. Datei lesen (mit Lock für Sicherheit bei vielen Zugriffen) + if (file_exists($this->file)) { + $content = file_get_contents($this->file); + $viewers = json_decode($content, true) ?? []; + } + + // 2. Aktuellen User updaten + $viewers[$ip] = $now; + + // 3. Alte User entfernen & Zählen + $activeCount = 0; + $newViewers = []; + + foreach ($viewers as $userIp => $lastSeen) { + if ($now - $lastSeen < $this->timeout) { + $newViewers[$userIp] = $lastSeen; + $activeCount++; + } + } + + // 4. Speichern + file_put_contents($this->file, json_encode($newViewers)); + + // 5. Ergebnis zurückgeben + header('Content-Type: application/json'); + echo json_encode(['count' => $activeCount]); + exit; + } + + public function getInitialCount() { + if (file_exists($this->file)) { + $viewers = json_decode(file_get_contents($this->file), true) ?? []; + // Nur grob zählen, genaues Update macht das JS sofort nach Laden + return count($viewers); + } + return 1; // Zumindest man selbst ist da + } +} + +// Instanz erstellen +$viewerCounter = new ViewerCounter(); + + + +class WebcamManager { private $videoSrc = 'test_video.m3u8'; private $logoPath = 'logo.png'; - public function displayWebcam() { - return ''; -} + // Zeigt NUR das Video ohne Schnickschnack + public function displayWebcam() { + return ' +
+ +
'; + } + + // Das ist die neue Anzeige für unten links + public function displayStreamStats() { + return ' + '; + } - - public function captureSnapshot() { - $outputFile = 'snapshot_' . date('YmdHis') . '.jpg'; - $command = "ffmpeg -i {$this->videoSrc} -i {$this->logoPath} -filter_complex 'overlay=main_w-overlay_w-10:10' -vframes 1 -q:v 2 {$outputFile}"; - - + $src = escapeshellarg($this->videoSrc); + $logo = escapeshellarg($this->logoPath); + $command = "ffmpeg -i {$src} -i {$logo} -filter_complex 'overlay=main_w-overlay_w-10:10' -vframes 1 -q:v 2 {$outputFile}"; exec($command, $output, $returnVar); + if ($returnVar !== 0) return "Fehler"; - if ($returnVar !== 0) { - return "Fehler beim Erstellen des Snapshots."; - } - - // Kopieren des Snapshots in den Uploads-Ordner $uploadDir = "uploads/"; - if (!file_exists($uploadDir)) { - mkdir($uploadDir, 0777, true); - - } - + if (!file_exists($uploadDir)) mkdir($uploadDir, 0777, true); $uploadFile = $uploadDir . $outputFile; - if (copy($outputFile, $uploadFile)) { - - } else { - - } - - + copy($outputFile, $uploadFile); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . $outputFile . '"'); @@ -137,56 +371,29 @@ class WebcamManager { unlink($outputFile); exit; } -// public function getImageFiles() { -// // Nur JPG-Dateien aus uploads/, KEINE MP4-Dateien -// $imageFiles = glob("uploads/*.{jpg,jpeg,png,gif}", GLOB_BRACE); - -// // Filtere unerwünschte Dateien aus -// $imageFiles = array_filter($imageFiles, function($file) { -// $basename = basename($file); -// // Blockiere sequence_*.mp4 und andere unerwünschte Dateien -// return pathinfo($file, PATHINFO_EXTENSION) !== 'mp4' && -// strpos($basename, 'sequence_') !== 0; -// }); - -// return json_encode(array_values($imageFiles)); -// } -public function getImageFiles() { - // Screenshots aus dem image/ Ordner holen - $imageFiles = glob("image/screenshot_*.jpg"); - sort($imageFiles); // Neueste zuerst - return json_encode($imageFiles); -} + public function getImageFiles() { + global $remoteCache; + $localFiles = glob("image/screenshot_*.jpg") ?: []; + $remoteWeb = $remoteCache->getImageWebPaths(); // Aus Cache statt glob auf SMB + $imageFiles = array_merge($localFiles, $remoteWeb); + if ($imageFiles) rsort($imageFiles); else $imageFiles = []; + return json_encode($imageFiles); + } - - - public function captureVideoSequence($duration = 10) { $outputFile = 'sequence_' . date('YmdHis') . '.mp4'; - $command = "ffmpeg -i {$this->videoSrc} -i {$this->logoPath} -filter_complex 'overlay=10:10' -t {$duration} -c:v libx264 -preset fast -crf 23 {$outputFile}"; - + $src = escapeshellarg($this->videoSrc); + $logo = escapeshellarg($this->logoPath); + $dur = intval($duration); + $command = "ffmpeg -i {$src} -i {$logo} -filter_complex 'overlay=10:10' -t {$dur} -c:v libx264 -preset fast -crf 23 {$outputFile}"; exec($command, $output, $returnVar); + if ($returnVar !== 0) return "Fehler"; - if ($returnVar !== 0) { - return "Fehler beim Erstellen der Video-Sequenz."; - } - - // Kopieren des Videoclips in den Uploads-Ordner - $uploadDir = "uploads/"; - if (!file_exists($uploadDir)) { - mkdir($uploadDir, 0777, true); - echo "Uploads-Ordner erstellt.
"; - } - - $uploadFile = $uploadDir . $outputFile; - if (copy($outputFile, $uploadFile)) { - - } else { - echo "Fehler beim Kopieren des Videoclips in den Uploads-Ordner.
"; - } - - + $uploadDir = "uploads/"; + if (!file_exists($uploadDir)) mkdir($uploadDir, 0777, true); + $uploadFile = $uploadDir . $outputFile; + copy($outputFile, $uploadFile); header('Content-Type: video/mp4'); header('Content-Disposition: attachment; filename="' . $outputFile . '"'); @@ -194,142 +401,77 @@ public function getImageFiles() { unlink($outputFile); exit; } -public function getJavaScript() { - return " - document.addEventListener('DOMContentLoaded', function () { - var video = document.getElementById('webcam-player'); - var videoSrc = '{$this->videoSrc}'; - - // Mobile Detection - var isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); - var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); - - // Controls NUR auf Desktop verstecken - video.controls = false; - - if (isIOS) { - // iOS native HLS - video.src = videoSrc; - video.setAttribute('playsinline', ''); - video.setAttribute('webkit-playsinline', ''); - video.muted = true; - - video.addEventListener('loadedmetadata', function() { - video.play().catch(function(e) { - console.log('iOS Autoplay blockiert'); - }); - }); - - } else if (Hls.isSupported()) { - var hls = new Hls({ - // Mobile-optimierte Einstellungen - liveSyncDurationCount: isMobile ? 2 : 3, - liveMaxLatencyDurationCount: isMobile ? 5 : 10, - liveDurationInfinity: true, - enableWorker: !isMobile, - lowLatencyMode: false, - backBufferLength: isMobile ? 30 : 90, - maxBufferLength: isMobile ? 30 : 60, - maxMaxBufferLength: isMobile ? 60 : 120, - maxBufferSize: isMobile ? 60*1000*1000 : 120*1000*1000, - - // Mobile-spezifische Timeouts - manifestLoadingTimeOut: isMobile ? 20000 : 10000, - manifestLoadingMaxRetry: 8, - levelLoadingTimeOut: isMobile ? 20000 : 10000, - levelLoadingMaxRetry: 8, - fragLoadingTimeOut: isMobile ? 20000 : 10000, - fragLoadingMaxRetry: 8, - - // Qualität für Mobile - startLevel: isMobile ? 0 : -1, - abrEwmaDefaultEstimate: isMobile ? 500000 : 1000000 - }); - - hls.loadSource(videoSrc); - hls.attachMedia(video); - - hls.on(Hls.Events.MANIFEST_PARSED, function () { - console.log('Stream geladen'); - - if (isMobile) { + + public function getJavaScript() { + return " + document.addEventListener('DOMContentLoaded', function () { + var video = document.getElementById('webcam-player'); + var videoSrc = '{$this->videoSrc}'; + var bitrateBadge = document.getElementById('bitrate-display'); + var bitrateValue = document.getElementById('bitrate-value'); + var isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); + var isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); + + if(video) { + video.controls = false; + if (isIOS) { + video.src = videoSrc; + video.setAttribute('playsinline', ''); + video.setAttribute('webkit-playsinline', ''); video.muted = true; + if(bitrateBadge) bitrateBadge.style.display = 'none'; + video.addEventListener('loadedmetadata', function() { video.play().catch(console.log); }); + } else if (Hls.isSupported()) { + var hls = new Hls({ enableWorker: !isMobile, lowLatencyMode: false }); + hls.loadSource(videoSrc); + hls.attachMedia(video); + hls.on(Hls.Events.MANIFEST_PARSED, function () { + if (isMobile) video.muted = true; + video.play().catch(console.log); + if(bitrateBadge) bitrateBadge.style.display = 'inline-flex'; + }); + hls.on(Hls.Events.FRAG_LOADED, function(event, data) { + var bandwidth = hls.bandwidthEstimate; + if (bandwidth && !isNaN(bandwidth) && bitrateValue) { + var mbs = bandwidth / 8 / 1024 / 1024; + if (mbs > 0) bitrateValue.textContent = mbs.toFixed(2); + } + }); } - - // Live-Position anpassen - if (hls.liveSyncPosition !== null) { - var targetPosition = hls.liveSyncPosition - 60; - console.log('Setze Position auf: ' + targetPosition); - video.currentTime = targetPosition; - } - - video.play().catch(function(e) { - console.log('Autoplay blockiert'); - }); - }); - - // Fehlerbehandlung - hls.on(Hls.Events.ERROR, function(event, data) { - if (data.fatal) { - switch(data.type) { - case Hls.ErrorTypes.NETWORK_ERROR: - console.log('Netzwerkfehler - versuche erneut...'); - setTimeout(function() { - hls.startLoad(); - }, 3000); - break; - case Hls.ErrorTypes.MEDIA_ERROR: - console.log('Media-Fehler - Recovery...'); - hls.recoverMediaError(); - break; - default: - console.log('Kritischer Fehler - Neustart...'); - hls.destroy(); - location.reload(); - break; - } - } - }); - - } else if (video.canPlayType('application/vnd.apple.mpegurl')) { - // Fallback für andere Browser - video.src = videoSrc; - video.muted = true; - video.addEventListener('loadedmetadata', function () { - video.currentTime = Math.max(0, video.duration - 60); - video.play(); - }); - } - }); - "; -} - - - - - - - - public function setVideoSrc($src) { - $this->videoSrc = $src; + } + }); + "; } + + public function setVideoSrc($src) { $this->videoSrc = $src; } } - - - - - - - class VisualCalendarManager { private $videoDir; + private $videoDirs; + private $aiDir; private $monthNames; - - public function __construct($videoDir = './videos/') { - $this->videoDir = $videoDir; + private $settingsManager; + + // AI-Kategorien mit Icons und Farben + private $aiCategories = [ + 'sunny' => ['icon' => '☀️', 'name' => 'Sonnig', 'color' => '#FFD700'], + 'rainy' => ['icon' => '🌧️', 'name' => 'Regen', 'color' => '#4682B4'], + 'snowy' => ['icon' => '❄️', 'name' => 'Schnee', 'color' => '#E0FFFF'], + 'planes' => ['icon' => '✈️', 'name' => 'Flugzeuge', 'color' => '#87CEEB'], + 'birds' => ['icon' => '🐦', 'name' => 'Vögel', 'color' => '#98FB98'], + 'sunset' => ['icon' => '🌅', 'name' => 'Sonnenuntergang', 'color' => '#FF6347'], + 'sunrise' => ['icon' => '🌄', 'name' => 'Sonnenaufgang', 'color' => '#FFA07A'], + 'rainbow' => ['icon' => '🌈', 'name' => 'Regenbogen', 'color' => '#FF69B4'], + ]; + + public function __construct($videoDir = './videos/', $aiDir = './ai/', $settingsManager = null) { + $this->videoDirs = is_array($videoDir) ? $videoDir : [$videoDir]; + $this->videoDir = $this->videoDirs[0]; + $this->aiDir = $aiDir; + $this->settingsManager = $settingsManager; $this->monthNames = [ 1 => ['de' => 'Januar', 'en' => 'January', 'it' => 'Gennaio', 'fr' => 'Janvier', 'zh' => '一月'], 2 => ['de' => 'Februar', 'en' => 'February', 'it' => 'Febbraio', 'fr' => 'Février', 'zh' => '二月'], @@ -345,12 +487,90 @@ class VisualCalendarManager { 12 => ['de' => 'Dezember', 'en' => 'December', 'it' => 'Dicembre', 'fr' => 'Décembre', 'zh' => '十二月'] ]; } + + /** + * Holt AI-Events für ein bestimmtes Datum + */ + public function getAiEventsForDate($year, $month, $day) { + $events = []; + $dateStr = sprintf('%04d%02d%02d', $year, $month, $day); + + foreach ($this->aiCategories as $category => $info) { + $categoryDir = $this->aiDir . $category . '/'; + if (!is_dir($categoryDir)) continue; + + // Suche nach Videos für dieses Datum + $pattern = $categoryDir . "{$category}_{$dateStr}*.mp4"; + $videos = glob($pattern); + + if (!empty($videos)) { + $events[$category] = [ + 'icon' => $info['icon'], + 'name' => $info['name'], + 'color' => $info['color'], + 'videos' => $videos, + 'count' => count($videos) + ]; + } + } + + return $events; + } + + /** + * Prüft ob AI-Events für ein Datum existieren + */ + public function hasAiEventsForDate($year, $month, $day) { + $dateStr = sprintf('%04d%02d%02d', $year, $month, $day); + + foreach (array_keys($this->aiCategories) as $category) { + $pattern = $this->aiDir . $category . "/{$category}_{$dateStr}*.mp4"; + if (count(glob($pattern)) > 0) { + return true; + } + } + return false; + } + + /** + * Holt kurze Icon-Liste für Kalender-Anzeige + */ + public function getAiIconsForDate($year, $month, $day) { + $icons = []; + $events = $this->getAiEventsForDate($year, $month, $day); + + foreach ($events as $category => $info) { + $icons[] = $info['icon']; + } + + return $icons; + } + /** + * Wandelt Dateisystem-Pfad in Web-URL um + */ +private function videoPathToUrl($path) { + // Remote-Videos: HLS-Stream wenn verfügbar, sonst Fallback auf direkten Pfad + if (strpos($path, '/mnt/aurora-remote/videos/') === 0) { + $basename = basename($path, '.mp4'); + $videoId = str_replace('daily_video_', '', $basename); + $hlsMaster = '/mnt/aurora-remote/hls/' . $videoId . '/master.m3u8'; + if (file_exists($hlsMaster)) { + return '/remote-hls/' . $videoId . '/master.m3u8'; + } + // Fallback: direkter Apache-Alias + return str_replace('/mnt/aurora-remote/videos/', '/remote-videos/', $path); + } + return $path; +} + public function getVideosForDate($year, $month, $day) { + global $remoteCache; $videos = []; $dateStr = sprintf('%04d%02d%02d', $year, $month, $day); - - foreach (glob($this->videoDir . "daily_video_{$dateStr}_*.mp4") as $video) { + + // Lokale Videos direkt (schnell) + foreach (glob($this->videoDirs[0] . "daily_video_{$dateStr}_*.mp4") as $video) { $videos[] = [ 'path' => $video, 'filename' => basename($video), @@ -358,101 +578,262 @@ class VisualCalendarManager { 'time' => date('H:i', filemtime($video)) ]; } - + + // Remote-Videos aus Cache (kein glob auf SMB!) + foreach ($remoteCache->getVideosForDate($dateStr) as $rv) { + $videos[] = [ + 'path' => $rv['path'], + 'filename' => $rv['filename'], + 'filesize' => $rv['filesize'], + 'time' => date('H:i', $rv['mtime']) + ]; + } + return $videos; } - + public function hasVideosForDate($year, $month, $day) { + global $remoteCache; $dateStr = sprintf('%04d%02d%02d', $year, $month, $day); - $pattern = $this->videoDir . "daily_video_{$dateStr}_*.mp4"; - return count(glob($pattern)) > 0; + // Lokal prüfen (schnell) + if (count(glob($this->videoDirs[0] . "daily_video_{$dateStr}_*.mp4")) > 0) return true; + // Remote aus Cache + return $remoteCache->hasVideosForDate($dateStr); } - + public function displayVisualCalendar() { $currentYear = isset($_GET['cal_year']) ? intval($_GET['cal_year']) : date('Y'); $currentMonth = isset($_GET['cal_month']) ? intval($_GET['cal_month']) : date('n'); $selectedDay = isset($_GET['cal_day']) ? intval($_GET['cal_day']) : null; - + + // Settings für Video-Modus holen + $playInPlayer = $this->settingsManager ? $this->settingsManager->get('video_mode.play_in_player') : true; + $allowDownload = $this->settingsManager ? $this->settingsManager->get('video_mode.allow_download') : true; + $output = '
'; - + // Navigation $output .= '
'; $output .= ''; $output .= '

' . $this->monthNames[$currentMonth]['de'] . ' ' . $currentYear . '

'; $output .= ''; $output .= '
'; + + // AI-Legende + //$output .= '
'; ai wieder einblende das unten rausnehmendas rein + $output .= ''; + // Kalender-Grid $output .= '
'; - + // Wochentage Header $weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; foreach ($weekdays as $day) { $output .= '
' . $day . '
'; } - + // Tage des Monats $firstDay = mktime(0, 0, 0, $currentMonth, 1, $currentYear); $daysInMonth = date('t', $firstDay); - $dayOfWeek = date('N', $firstDay) - 1; // 0 = Montag - + $dayOfWeek = date('N', $firstDay) - 1; + // Leere Zellen vor dem ersten Tag for ($i = 0; $i < $dayOfWeek; $i++) { $output .= '
'; } - - // Tage mit Videos markieren + + // Tage mit Videos/AI-Events markieren for ($day = 1; $day <= $daysInMonth; $day++) { $hasVideos = $this->hasVideosForDate($currentYear, $currentMonth, $day); + $hasAiEvents = $this->hasAiEventsForDate($currentYear, $currentMonth, $day); + $aiIcons = $this->getAiIconsForDate($currentYear, $currentMonth, $day); $isSelected = ($selectedDay == $day); $isToday = ($currentYear == date('Y') && $currentMonth == date('n') && $day == date('j')); - + $classes = 'calendar-day'; if ($hasVideos) $classes .= ' has-video'; + if ($hasAiEvents) $classes .= ' has-ai-events'; if ($isSelected) $classes .= ' selected'; if ($isToday) $classes .= ' today'; - + $output .= '
'; $output .= '' . $day . ''; + + // Video-Indikator if ($hasVideos) { $output .= '📹'; } + + // AI-Event-Icons (max 3 anzeigen) + if (!empty($aiIcons)) { + $output .= '
'; + $displayIcons = array_slice($aiIcons, 0, 3); + foreach ($displayIcons as $icon) { + $output .= '' . $icon . ''; + } + if (count($aiIcons) > 3) { + $output .= '+' . (count($aiIcons) - 3) . ''; + } + $output .= '
'; + } + $output .= '
'; } - + $output .= '
'; // calendar-grid - - // Video-Liste für ausgewählten Tag + + // Video-Liste + AI-Events für ausgewählten Tag if ($selectedDay) { $videos = $this->getVideosForDate($currentYear, $currentMonth, $selectedDay); + $aiEvents = $this->getAiEventsForDate($currentYear, $currentMonth, $selectedDay); + + $output .= '
'; + $output .= '

📅 ' . sprintf('%02d.%02d.%04d', $selectedDay, $currentMonth, $currentYear) . '

'; + + // === TAGESVIDEOS === if (!empty($videos)) { $output .= '
'; - $output .= '

Videos vom ' . sprintf('%02d.%02d.%04d', $selectedDay, $currentMonth, $currentYear) . '

'; + $output .= '
📹 Tagesvideos
'; $output .= '
    '; - + foreach ($videos as $video) { $sizeInMb = round($video['filesize'] / (1024 * 1024), 2); $token = hash_hmac('sha256', $video['path'], session_id()); - + $videoUrl = '?download_specific_video=' . urlencode($video['path']) . '&token=' . $token; + $videoWebUrl = $this->videoPathToUrl($video['path']); + $output .= '
  • '; $output .= '🕐 ' . $video['time'] . ' Uhr'; $output .= '' . $sizeInMb . ' MB'; - $output .= ''; - $output .= '⬇️ Download'; - $output .= ''; + $output .= '
    '; + + // Play Button (wenn aktiviert) + if ($playInPlayer) { + $output .= ''; + + $output .= '▶️ Abspielen'; + $output .= ''; + } + + // Download Button (wenn aktiviert) + if ($allowDownload) { + $output .= ''; + $output .= '⬇️ Download'; + $output .= ''; + } + + $output .= '
    '; $output .= '
  • '; } - + $output .= '
'; $output .= '
'; + } + + // === AI-EREIGNISSE === + if (!empty($aiEvents) && (!$this->settingsManager || $this->settingsManager->isAIEventsEnabled())) { + $output .= '
'; + $output .= '
🤖 AI-erkannte Ereignisse
'; + $output .= '
'; + + foreach ($aiEvents as $category => $event) { + $output .= '
'; + $output .= '
'; + $output .= '' . $event['icon'] . ''; + $output .= '' . $event['name'] . ''; + $output .= '
'; + + // Videos für dieses Event + $output .= '
'; + foreach ($event['videos'] as $video) { + $filename = basename($video); + $sizeInMb = round(filesize($video) / (1024 * 1024), 2); + $token = hash_hmac('sha256', $video, session_id()); + + // Play Button + if ($playInPlayer) { + $output .= ''; + $output .= '▶️ Abspielen (' . $sizeInMb . ' MB)'; + $output .= ''; + } + + // Download Button + if ($allowDownload) { + $output .= ''; + $output .= '⬇️ Download'; + $output .= ''; + } + } + $output .= '
'; + + $output .= '
'; // ai-event-card + } + + $output .= '
'; // ai-events-grid + $output .= '
'; // ai-events-section + } + + // Keine Inhalte + if (empty($videos) && empty($aiEvents)) { + $output .= '
'; + $output .= '

📭 Keine Videos oder AI-Ereignisse für diesen Tag verfügbar.

'; + $output .= '
'; + } + + $output .= '
'; // day-details + } + + $output .= '
'; // visual-calendar-container + + return $output; + } + + /** + * Handler für AI-Video Downloads + */ + public function handleAiVideoDownload() { + if (isset($_GET['download_ai_video']) && isset($_GET['token'])) { + $videoPath = $_GET['download_ai_video']; + $token = $_GET['token']; + + // Token-Validierung + $expectedToken = hash_hmac('sha256', $videoPath, session_id()); + if (!hash_equals($expectedToken, $token)) { + echo "Ungültiger Token. Zugriff verweigert."; + exit; + } + + // Sicherheitsüberprüfung + $aiDir = realpath($this->aiDir); + $requestedPath = realpath($videoPath); + + if ($requestedPath && strpos($requestedPath, $aiDir) === 0 && file_exists($requestedPath)) { + $extension = pathinfo($requestedPath, PATHINFO_EXTENSION); + if (strtolower($extension) !== 'mp4') { + echo "Nur MP4-Dateien können heruntergeladen werden."; + exit; + } + + header('Content-Description: File Transfer'); + header('Content-Type: video/mp4'); + header('Content-Disposition: attachment; filename="'.basename($requestedPath).'"'); + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + header('Content-Length: ' . filesize($requestedPath)); + readfile($requestedPath); + exit; } else { - $output .= '
Keine Videos für diesen Tag verfügbar.
'; + echo "Datei nicht gefunden oder ungültiger Dateipfad."; + exit; } } - - $output .= '
'; // visual-calendar-container - - return $output; } } @@ -516,7 +897,7 @@ class GuestbookManager { } return false; } - + private function saveEntries() { @@ -528,27 +909,30 @@ class GuestbookManager {
-