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 '
+
+
+ Stream: 0.00 MBit/s
+
';
+ }
-
-
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 .= '
';
+ $output .= '🤖 AI-Erkennung:';
+ foreach ($this->aiCategories as $cat => $info) {
+ $output .= '' . $info['icon'] . '';
+ }
+ $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 .= '';
$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 .= '';
+
+ // Videos für dieses Event
+ $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 {
";
}
-
+
}
$output .= '';
return $output;
}
-
+
}
class ContactManager {
- //private $adminEmail = 'ingo.kohler.zh@gmail.com'; // ← Empfänger
- private $adminEmail = 'metacube@gmail.com'; // ← Empfänger
+ private $adminEmail = 'metacube@gmail.com';
private $feedbackFile = 'feedbacks.json';
- private $gmailUser = 'metacube@gmail.com'; // ← DEINE GMAIL-ADRESSE
- private $gmailAppPassword = 'qggk hsxz fdkq jgxa'; // ← APP-PASSWORT VON GMAIL
-
+ private $gmailUser = 'metacube@gmail.com';
+ private $gmailAppPassword = 'qggk hsxz fdkq jgxa';
+
public function displayForm() {
return '