diff --git a/altcoins/index.php b/altcoins/index.php
new file mode 100644
index 0000000..58ad4ea
--- /dev/null
+++ b/altcoins/index.php
@@ -0,0 +1,342 @@
+ 'Ethereum (ETH)',
+ 'symbol' => 'ETH',
+ 'price' => 3725.42,
+ 'ma200' => 3450.15,
+ 'last_update' => '2024-04-21 14:00 UTC',
+ ],
+ [
+ 'name' => 'BNB',
+ 'symbol' => 'BNB',
+ 'price' => 598.12,
+ 'ma200' => 612.77,
+ 'last_update' => '2024-04-21 14:00 UTC',
+ ],
+ [
+ 'name' => 'Solana (SOL)',
+ 'symbol' => 'SOL',
+ 'price' => 158.34,
+ 'ma200' => 143.05,
+ 'last_update' => '2024-04-21 14:00 UTC',
+ ],
+ [
+ 'name' => 'XRP',
+ 'symbol' => 'XRP',
+ 'price' => 0.57,
+ 'ma200' => 0.63,
+ 'last_update' => '2024-04-21 14:00 UTC',
+ ],
+ [
+ 'name' => 'Dogecoin (DOGE)',
+ 'symbol' => 'DOGE',
+ 'price' => 0.19,
+ 'ma200' => 0.15,
+ 'last_update' => '2024-04-21 14:00 UTC',
+ ],
+ [
+ 'name' => 'Cardano (ADA)',
+ 'symbol' => 'ADA',
+ 'price' => 0.48,
+ 'ma200' => 0.62,
+ 'last_update' => '2024-04-21 14:00 UTC',
+ ],
+ [
+ 'name' => 'Avalanche (AVAX)',
+ 'symbol' => 'AVAX',
+ 'price' => 47.22,
+ 'ma200' => 44.61,
+ 'last_update' => '2024-04-21 14:00 UTC',
+ ],
+ [
+ 'name' => 'Polkadot (DOT)',
+ 'symbol' => 'DOT',
+ 'price' => 8.81,
+ 'ma200' => 7.29,
+ 'last_update' => '2024-04-21 14:00 UTC',
+ ],
+ [
+ 'name' => 'Chainlink (LINK)',
+ 'symbol' => 'LINK',
+ 'price' => 17.02,
+ 'ma200' => 18.40,
+ 'last_update' => '2024-04-21 14:00 UTC',
+ ],
+ [
+ 'name' => 'Polygon (MATIC)',
+ 'symbol' => 'MATIC',
+ 'price' => 0.92,
+ 'ma200' => 0.98,
+ 'last_update' => '2024-04-21 14:00 UTC',
+ ],
+];
+
+function determineSignal(float $price, float $ma200): array
+{
+ if ($price >= $ma200) {
+ return ['LONG', 'Preis notiert über dem 200-Tage-Durchschnitt.'];
+ }
+
+ return ['SHORT', 'Preis notiert unter dem 200-Tage-Durchschnitt.'];
+}
+
+function formatNumber(float $value, int $decimals = 2): string
+{
+ return number_format($value, $decimals, ',', '.');
+}
+?>
+
+
+
+
+
+ Top 10 Altcoins – MA200 Signale
+
+
+
+
+
+
+
+
+
+ Asset
+ Preis (USD)
+ MA200 (USD)
+ Abweichung
+ Signal
+ Zuletzt aktualisiert
+
+
+
+ 0)
+ ? ($diff / $coin['ma200']) * 100
+ : 0;
+ ?>
+
+
+
+
+
+ $
+ $
+
+ = 0 ? '+' : '-') . formatNumber(abs($diff)); ?>
+ (= 0 ? '+' : '-') . formatNumber(abs($diffPercent)); ?> %)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Telegram Broadcast
+ Kopiere die Zusammenfassung, um sie in deinem Signal-Channel zu posten, sobald eine MA200-Überschreitung
+ stattfindet.
+
+
+
+
+
diff --git a/anfrage.php b/anfrage.php
index f8557c0..bac248c 100644
--- a/anfrage.php
+++ b/anfrage.php
@@ -30,20 +30,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = 'Bitte Datum und Ort ausfüllen.';
} else {
createRequest($data);
- $recipient = $band['contact_email'] ?: SUPPORT_EMAIL;
- $emailBody = sprintf(
- 'Neue Anfrage für %s
Datum: %s Ort: %s Budget: %s CHF Event: %s Nachricht:
%s
',
- htmlspecialchars($band['name']),
- htmlspecialchars($data['event_date']),
- htmlspecialchars($data['location']),
- number_format((int) $data['budget'], 0, ',', '.'),
- htmlspecialchars($data['event_type']),
- nl2br(htmlspecialchars($data['message']))
- );
- $sent = sendEmail($recipient, 'Neue Anfrage – ' . $band['name'], $emailBody);
- $message = $sent
- ? 'Anfrage gespeichert und an die Band gemeldet.'
- : 'Anfrage gespeichert – E-Mail an die Band konnte nicht gesendet werden.';
+ $message = 'Anfrage gespeichert und an die Band gemeldet.';
+ sendEmail('info@' . preg_replace('/\s+/', '', strtolower($band['name'])) . '.ch', 'Neue Anfrage', 'Neue Anfrage für ' . $band['name']);
}
}
diff --git a/aurora.php b/aurora.php
new file mode 100644
index 0000000..a1de8eb
--- /dev/null
+++ b/aurora.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 = 'Aurora Kontakt ' .
+ 'Name: ' . $payload['name'] . '
' .
+ 'E-Mail: ' . $payload['email'] . '
' .
+ 'Nachricht: ' . nl2br($payload['message']) . '
' .
+ 'Gesendet am ' . $payload['date'] . ' | IP ' . $payload['ip'] . ' ';
+ $mail->send();
+ } catch (Exception $e) {
+ error_log('Mail error: ' . $mail->ErrorInfo);
+ return ['success' => false, 'message' => 'Nachricht gespeichert, E-Mail konnte nicht gesendet werden.'];
+ }
+
+ return ['success' => true, 'message' => 'Vielen Dank! Wir melden uns zeitnah.'];
+ }
+}
+
+class AdminManager
+{
+ public function isAdmin(): bool
+ {
+ return isset($_SESSION['admin']) && $_SESSION['admin'] === true;
+ }
+
+ public function login(string $username, string $password): bool
+ {
+ if ($username === 'admin' && $password === 'sonne4000$$$$Q') {
+ $_SESSION['admin'] = true;
+ return true;
+ }
+ return false;
+ }
+
+ public function logout(): void
+ {
+ unset($_SESSION['admin']);
+ }
+
+ public function handleImageUpload(array $file): array
+ {
+ if (!$this->isAdmin()) {
+ return ['success' => false, 'message' => 'Nicht autorisiert.'];
+ }
+ if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
+ return ['success' => false, 'message' => 'Keine Datei empfangen.'];
+ }
+ $allowed = ['jpg', 'jpeg', 'png', 'gif'];
+ $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+ if (!in_array($extension, $allowed, true)) {
+ return ['success' => false, 'message' => 'Nur JPG, PNG oder GIF sind erlaubt.'];
+ }
+ if ($file['size'] > 5 * 1024 * 1024) {
+ return ['success' => false, 'message' => 'Datei ist größer als 5 MB.'];
+ }
+ $target = UPLOAD_DIR . '/' . uniqid('admin_', true) . '.' . $extension;
+ if (!move_uploaded_file($file['tmp_name'], $target)) {
+ return ['success' => false, 'message' => 'Upload fehlgeschlagen.'];
+ }
+ return ['success' => true, 'file' => basename($target)];
+ }
+
+ public function updateSocialLink(string $platform, string $url): array
+ {
+ if (!$this->isAdmin()) {
+ return ['success' => false, 'message' => 'Nicht autorisiert.'];
+ }
+ $links = json_decode((string) file_get_contents(SOCIAL_LINKS_FILE), true);
+ $links = is_array($links) ? $links : [];
+ $links[$platform] = $url;
+ file_put_contents(SOCIAL_LINKS_FILE, json_encode($links, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
+ return ['success' => true, 'links' => $links];
+ }
+
+ public function getSocialLinks(): array
+ {
+ $links = json_decode((string) file_get_contents(SOCIAL_LINKS_FILE), true);
+ return is_array($links) ? $links : [];
+ }
+}
+
+$languageManager = new LanguageManager();
+$locale = $languageManager->getCurrentLocale();
+$webcamManager = new WebcamManager();
+$calendarManager = new VisualCalendarManager();
+$guestbookManager = new GuestbookManager();
+$contactManager = new ContactManager();
+$adminManager = new AdminManager();
+$archiveManager = new VideoArchiveManager();
+
+if (isset($_GET['download_specific_video'])) {
+ $archiveManager->handleDownloadRequest($_GET['download_specific_video'], $_GET['token'] ?? null);
+}
+
+if (isset($_GET['download_video']) && $_GET['download_video'] === 'latest') {
+ $latest = $webcamManager->getLatestVideo();
+ if ($latest && file_exists($latest)) {
+ header('Content-Description: File Transfer');
+ header('Content-Type: application/octet-stream');
+ header('Content-Disposition: attachment; filename="' . basename($latest) . '"');
+ header('Content-Length: ' . filesize($latest));
+ readfile($latest);
+ exit;
+ }
+ echo 'Kein Video gefunden.';
+ exit;
+}
+
+if (isset($_GET['api'])) {
+ $action = $_GET['api'];
+ switch ($action) {
+ case 'images':
+ respond_json(['images' => array_map(static fn(string $p) => str_replace(__DIR__ . '/', '', $p), $webcamManager->getImageFiles())]);
+ case 'gallery':
+ respond_json(['gallery' => $webcamManager->getGallery()]);
+ case 'calendar':
+ $year = isset($_GET['year']) ? (int) $_GET['year'] : (int) date('Y');
+ $month = isset($_GET['month']) ? (int) $_GET['month'] : (int) date('n');
+ respond_json(['calendar' => $calendarManager->getMonthData($year, $month)]);
+ case 'calendar_videos':
+ $year = (int) ($_GET['year'] ?? date('Y'));
+ $month = (int) ($_GET['month'] ?? date('n'));
+ $day = (int) ($_GET['day'] ?? date('j'));
+ respond_json(['videos' => $calendarManager->getVideosForDate($year, $month, $day)]);
+ case 'archive':
+ $year = (int) ($_GET['year'] ?? date('Y'));
+ $month = str_pad((string) ($_GET['month'] ?? date('n')), 2, '0', STR_PAD_LEFT);
+ respond_json([
+ 'available' => $archiveManager->getAvailableYearsAndMonths(),
+ 'monthName' => $archiveManager->getMonthName($month),
+ 'videos' => $archiveManager->getVideosForYearAndMonth($year, $month)
+ ]);
+ case 'guestbook':
+ respond_json(['entries' => $guestbookManager->getEntries(50)]);
+ case 'stream_stats':
+ respond_json(['stats' => $webcamManager->getStreamStats()]);
+ default:
+ respond_json(['message' => 'Unbekannte API-Anfrage.'], 404);
+ }
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $action = $_POST['action'] ?? '';
+ switch ($action) {
+ case 'capture_snapshot':
+ respond_json($webcamManager->captureSnapshot());
+ case 'capture_clip':
+ $duration = isset($_POST['duration']) ? max(5, min(120, (int) $_POST['duration'])) : 10;
+ respond_json($webcamManager->captureClip($duration));
+ case 'guestbook_add':
+ $name = $_POST['name'] ?? '';
+ $message = $_POST['message'] ?? '';
+ $rating = isset($_POST['rating']) ? (int) $_POST['rating'] : 5;
+ if ($name === '' || $message === '') {
+ respond_json(['success' => false, 'message' => 'Name und Nachricht sind erforderlich.'], 422);
+ }
+ respond_json(['success' => true, 'entry' => $guestbookManager->addEntry($name, $message, $rating)]);
+ case 'guestbook_delete':
+ if (!$adminManager->isAdmin()) {
+ respond_json(['success' => false, 'message' => 'Nicht autorisiert.'], 403);
+ }
+ $index = isset($_POST['entry']) ? (int) $_POST['entry'] : -1;
+ respond_json(['success' => $guestbookManager->deleteEntry($index)]);
+ case 'contact_send':
+ $name = $_POST['name'] ?? '';
+ $email = $_POST['email'] ?? '';
+ $message = $_POST['message'] ?? '';
+ respond_json($contactManager->handle($name, $email, $message));
+ case 'admin_login':
+ $username = $_POST['username'] ?? '';
+ $password = $_POST['password'] ?? '';
+ respond_json(['success' => $adminManager->login($username, $password)]);
+ case 'admin_logout':
+ $adminManager->logout();
+ respond_json(['success' => true]);
+ case 'admin_upload':
+ respond_json($adminManager->handleImageUpload($_FILES['file'] ?? []));
+ case 'social_update':
+ $platform = $_POST['platform'] ?? '';
+ $url = $_POST['url'] ?? '';
+ respond_json($adminManager->updateSocialLink($platform, $url));
+ case 'set_language':
+ $_SESSION['lang'] = $_POST['language'] ?? 'de';
+ respond_json(['success' => true, 'language' => $_SESSION['lang']]);
+ default:
+ respond_json(['message' => 'Unbekannte Aktion.'], 400);
+ }
+}
+
+$translations = $languageManager->getAllTranslations();
+?>
+
+
+
+
+
+ = htmlspecialchars($languageManager->get('title', $locale)) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 📸 = htmlspecialchars($languageManager->get('screenshot', $locale)) ?>
+
+
+ 🎬 = htmlspecialchars($languageManager->get('clip', $locale)) ?>
+
+
+ ⬇️ = htmlspecialchars($languageManager->get('download', $locale)) ?>
+
+
+ 📺 = htmlspecialchars($languageManager->get('pip', $locale)) ?>
+
+
+ ☀️ = htmlspecialchars($languageManager->get('share', $locale)) ?>
+
+
+
+
+
+
+
+ = htmlspecialchars($languageManager->get('timelapse_title', $locale)) ?>
+ = htmlspecialchars($languageManager->get('timelapse_caption', $locale)) ?>
+
+
+
+
+
+
+
+ = htmlspecialchars($languageManager->get('archive_title', $locale)) ?>
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($languageManager->get('calendar_title', $locale)) ?>
+
+
+
+
+
+ = htmlspecialchars($languageManager->get('guestbook', $locale)) ?>
+
+
+
+
+
+
+
+
+ © = date('Y') ?> Aurora Weather Livecam · = htmlspecialchars($languageManager->get('privacy', $locale)) ?> · tiny view
+
+
+
+
+
diff --git a/auroraalt.php b/auroraalt.php
index f18648e..3fe33f0 100644
--- a/auroraalt.php
+++ b/auroraalt.php
@@ -1,47 +1,4045 @@
",
- 'Reply-To: ' . $replyTo,
- ];
+require __DIR__ . '/vendor/autoload.php';
- if (!empty($options['cc'])) {
- $headers[] = 'Cc: ' . $options['cc'];
+if (isset($_GET['download_video'])) {
+ $videoDir = './videos/';
+ $latestVideo = null;
+ $latestTime = 0;
+
+ // Finde das neueste Video
+ foreach (glob($videoDir . '*.mp4') as $video) {
+ $mtime = filemtime($video);
+ if ($mtime > $latestTime) {
+ $latestTime = $mtime;
+ $latestVideo = $video;
}
+ }
- if (!empty($options['bcc'])) {
- $headers[] = 'Bcc: ' . $options['bcc'];
- }
-
- $sendmailPath = ini_get('sendmail_path');
- $transportAvailable = true;
- if (stripos(PHP_OS, 'WIN') !== 0) {
- $binary = trim(strtok((string) $sendmailPath, ' '));
- if ($binary && !is_executable($binary)) {
- $transportAvailable = false;
- }
- }
-
- if (!$transportAvailable) {
- return false;
- }
-
- $additionalHeaders = implode("\r\n", $headers);
- $encodedSubject = function_exists('mb_encode_mimeheader')
- ? mb_encode_mimeheader($subject, 'UTF-8')
- : $subject;
-
- return @mail($to, $encodedSubject, $htmlBody, $additionalHeaders);
+ if ($latestVideo) {
+ header('Content-Description: File Transfer');
+ header('Content-Type: application/octet-stream');
+ header('Content-Disposition: attachment; filename="'.basename($latestVideo).'"');
+ header('Expires: 0');
+ header('Cache-Control: must-revalidate');
+ header('Pragma: public');
+ header('Content-Length: ' . filesize($latestVideo));
+ readfile($latestVideo);
+ exit;
+ } else {
+ echo "Kein Video zum Herunterladen gefunden.";
+ exit;
}
}
+
+
+
+
+// Funktion zur sicheren Umleitung
+function safeRedirect($url) {
+ if (!headers_sent()) {
+ header("HTTP/1.1 301 Moved Permanently");
+ header("Location: " . $url);
+ } else {
+ echo '';
+ }
+ exit();
+}
+
+// Hauptlogik
+$oldDomains = [
+ 'www.aurora-wetter-lifecam.ch',
+ 'www.aurora-wetter-livecam.ch'
+];
+$newDomain = 'www.aurora-weather-livecam.com';
+
+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();
+}
+
+
+
+
+
+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");
+rsort($imageFiles); // Sortiert die Dateien in umgekehrter Reihenfolge (neueste zuerst)
+$imageFilesJson = json_encode($imageFiles);
+
+class WebcamManager {
+
+ private $videoSrc = 'test_video.m3u8';
+ private $logoPath = 'logo.png';
+
+ public function displayWebcam() {
+ 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}";
+
+
+ exec($command, $output, $returnVar);
+
+ 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);
+
+ }
+
+ $uploadFile = $uploadDir . $outputFile;
+ if (copy($outputFile, $uploadFile)) {
+
+ } else {
+
+ }
+
+
+
+ header('Content-Type: application/octet-stream');
+ header('Content-Disposition: attachment; filename="' . $outputFile . '"');
+ readfile($outputFile);
+ 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 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}";
+
+ exec($command, $output, $returnVar);
+
+ 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. ";
+ }
+
+
+
+ header('Content-Type: video/mp4');
+ header('Content-Disposition: attachment; filename="' . $outputFile . '"');
+ readfile($outputFile);
+ 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) {
+ video.muted = true;
+ }
+
+ // 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;
+ }
+}
+
+
+
+
+
+
+
+
+
+
+class VisualCalendarManager {
+ private $videoDir;
+ private $monthNames;
+
+ public function __construct($videoDir = './videos/') {
+ $this->videoDir = $videoDir;
+ $this->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 getVideosForDate($year, $month, $day) {
+ $videos = [];
+ $dateStr = sprintf('%04d%02d%02d', $year, $month, $day);
+
+ foreach (glob($this->videoDir . "daily_video_{$dateStr}_*.mp4") as $video) {
+ $videos[] = [
+ 'path' => $video,
+ 'filename' => basename($video),
+ 'filesize' => filesize($video),
+ 'time' => date('H:i', filemtime($video))
+ ];
+ }
+
+ return $videos;
+ }
+
+ public function hasVideosForDate($year, $month, $day) {
+ $dateStr = sprintf('%04d%02d%02d', $year, $month, $day);
+ $pattern = $this->videoDir . "daily_video_{$dateStr}_*.mp4";
+ return count(glob($pattern)) > 0;
+ }
+
+ 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;
+
+ $output = '';
+
+ // Navigation
+ $output .= '
';
+ $output .= '◀ ';
+ $output .= '
' . $this->monthNames[$currentMonth]['de'] . ' ' . $currentYear . ' ';
+ $output .= '▶ ';
+ $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
+
+ // Leere Zellen vor dem ersten Tag
+ for ($i = 0; $i < $dayOfWeek; $i++) {
+ $output .= '
';
+ }
+
+ // Tage mit Videos markieren
+ for ($day = 1; $day <= $daysInMonth; $day++) {
+ $hasVideos = $this->hasVideosForDate($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 ($isSelected) $classes .= ' selected';
+ if ($isToday) $classes .= ' today';
+
+ $output .= '
';
+ $output .= '' . $day . ' ';
+ if ($hasVideos) {
+ $output .= '📹 ';
+ }
+ $output .= '
';
+ }
+
+ $output .= '
'; // calendar-grid
+
+ // Video-Liste für ausgewählten Tag
+ if ($selectedDay) {
+ $videos = $this->getVideosForDate($currentYear, $currentMonth, $selectedDay);
+ if (!empty($videos)) {
+ $output .= '
';
+ $output .= '
Videos vom ' . sprintf('%02d.%02d.%04d', $selectedDay, $currentMonth, $currentYear) . ' ';
+ $output .= '
';
+
+ foreach ($videos as $video) {
+ $sizeInMb = round($video['filesize'] / (1024 * 1024), 2);
+ $token = hash_hmac('sha256', $video['path'], session_id());
+
+ $output .= '';
+ $output .= '🕐 ' . $video['time'] . ' Uhr ';
+ $output .= '' . $sizeInMb . ' MB ';
+ $output .= '';
+ $output .= '⬇️ Download';
+ $output .= ' ';
+ $output .= ' ';
+ }
+
+ $output .= ' ';
+ $output .= '
';
+ } else {
+ $output .= '
Keine Videos für diesen Tag verfügbar.
';
+ }
+ }
+
+ $output .= '
'; // visual-calendar-container
+
+ return $output;
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+class GuestbookManager {
+ private $entries = [];
+ private $dbFile = 'guestbook.json';
+
+ public function __construct() {
+ if (file_exists($this->dbFile)) {
+ $this->entries = json_decode(file_get_contents($this->dbFile), true);
+ }
+ }
+
+ public function handleFormSubmission() {
+ if (isset($_POST['guestbook'], $_POST['guest-name'], $_POST['guest-message'])) {
+ $this->addEntry($_POST['guest-name'], $_POST['guest-message']);
+ $this->saveEntries();
+ }
+ }
+
+ private function addEntry($name, $message) {
+ $this->entries[] = [
+ 'name' => $name,
+ 'message' => $message,
+ 'date' => date('Y-m-d H:i:s')
+ ];
+ }
+
+ public function deleteEntry($index) {
+ if (isset($this->entries[$index])) {
+ unset($this->entries[$index]);
+ $this->entries = array_values($this->entries); // Re-indizieren des Arrays
+ $this->saveEntries();
+ return true;
+ }
+ return false;
+ }
+
+
+
+ private function saveEntries() {
+ file_put_contents($this->dbFile, json_encode($this->entries));
+ }
+
+ public function displayForm() {
+ return '
+
+
+
+
+ Name:
+
+
+
+ Nachricht:
+
+
+
+ Eintrag hinzufügen
+
+
+ ';
+ }
+public function displayEntries($isAdmin = false) {
+ $output = '';
+ foreach ($this->entries as $index => $entry) {
+ $output .= "
+
+
{$entry['name']}
+
{$entry['message']}
+
{$entry['date']}";
+ if ($isAdmin) {
+ $output .= "
+
+
+ Löschen
+ ";
+ }
+
+ }
+ $output .= '
';
+ return $output;
+}
+
+
+}
+
+
+class ContactManager {
+ //private $adminEmail = 'ingo.kohler.zh@gmail.com'; // ← Empfänger
+ private $adminEmail = 'metacube@gmail.com'; // ← Empfänger
+ private $feedbackFile = 'feedbacks.json';
+ private $gmailUser = 'metacube@gmail.com'; // ← DEINE GMAIL-ADRESSE
+ private $gmailAppPassword = 'qggk hsxz fdkq jgxa'; // ← APP-PASSWORT VON GMAIL
+
+ public function displayForm() {
+ return '
+
+
+
+ Name:
+
+
+
+
+ E-Mail:
+
+
+
+
+ Nachricht:
+
+
+
+
+ Nachricht senden
+
+
+
';
+ }
+
+ public function handleSubmission($name, $email, $message) {
+ // Validierung
+ if (empty($name) || empty($email) || empty($message)) {
+ return [
+ 'success' => false,
+ 'message' => 'Alle Felder sind erforderlich'
+ ];
+ }
+
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ return [
+ 'success' => false,
+ 'message' => 'Ungültige E-Mail-Adresse'
+ ];
+ }
+
+ if (strlen($message) < 10) {
+ return [
+ 'success' => false,
+ 'message' => 'Nachricht zu kurz (mindestens 10 Zeichen)'
+ ];
+ }
+
+ // Sanitize
+ $name = htmlspecialchars(trim($name), ENT_QUOTES, 'UTF-8');
+ $email = filter_var(trim($email), FILTER_SANITIZE_EMAIL);
+ $message = htmlspecialchars(trim($message), ENT_QUOTES, 'UTF-8');
+
+ // Feedback speichern
+ $feedback = [
+ 'name' => $name,
+ 'email' => $email,
+ 'message' => $message,
+ 'date' => date('Y-m-d H:i:s'),
+ 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
+ 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
+ ];
+
+ $feedbacks = file_exists($this->feedbackFile)
+ ? json_decode(file_get_contents($this->feedbackFile), true)
+ : [];
+
+ if (!is_array($feedbacks)) {
+ $feedbacks = [];
+ }
+
+ $feedbacks[] = $feedback;
+ file_put_contents($this->feedbackFile, json_encode($feedbacks, JSON_PRETTY_PRINT));
+
+ // E-MAIL SENDEN MIT GMAIL SMTP
+ $mailSent = $this->sendEmailViaGmail($name, $email, $message, $feedback['date'], $feedback['ip']);
+
+ if ($mailSent) {
+ return [
+ 'success' => true,
+ 'message' => 'Vielen Dank! Ihre Nachricht wurde gesendet.'
+ ];
+ } else {
+ error_log("Mail-Fehler: Nachricht von {$email} konnte nicht gesendet werden");
+ return [
+ 'success' => false,
+ 'message' => 'Nachricht wurde gespeichert, aber E-Mail konnte nicht gesendet werden.'
+ ];
+ }
+ }
+
+ private function sendEmailViaGmail($name, $email, $message, $date, $ip) {
+ $mail = new PHPMailer(true);
+
+
+ try {
+ // DEBUG-MODUS AKTIVIEREN
+ $mail->SMTPDebug = 2;
+ $mail->Debugoutput = function($str, $level) {
+ error_log("PHPMailer Debug: $str");
+ };
+
+ // SMTP Konfiguration
+ $mail->isSMTP();
+ $mail->Host = 'smtp.gmail.com';
+ $mail->SMTPAuth = true;
+ $mail->Username = $this->gmailUser; // metacube@gmail.com
+ $mail->Password = $this->gmailAppPassword; // qggk hsxz fdkq jgxa
+ $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
+ $mail->Port = 587;
+
+
+
+
+ // Absender & Empfänger
+ $mail->setFrom($this->gmailUser, 'Aurora Livecam');
+ $mail->addAddress($this->adminEmail); // admin@aurora-live-weathercam.com
+ $mail->addReplyTo($email, $name);
+
+ // Inhalt
+ $mail->isHTML(true);
+ $mail->CharSet = 'UTF-8';
+ $mail->Subject = '🔔 Neue Kontaktanfrage von ' . $name;
+ $mail->Body = $this->getEmailTemplate($name, $email, $message, $date, $ip);
+
+ $mail->send();
+ return true;
+ } catch (Exception $e) {
+ error_log("PHPMailer Error: {$mail->ErrorInfo}");
+ return false;
+ }
+ }
+
+ private function getEmailTemplate($name, $email, $message, $date, $ip) {
+ return "
+
+
+
+
+
+
+
+
+
+
+
+
+ 👤 Name:
+ {$name}
+
+
+
+
+
+ 💬 Nachricht:
+
+
+
+ " . nl2br($message) . "
+
+
+
+
+
+
+
+
+
+ ";
+ }
+}
+
+class AdminManager {
+ public function isAdmin() {
+ return isset($_SESSION['admin']) && $_SESSION['admin'] === true;
+ }
+ public function handleLogin($username, $password) {
+ echo "Login-Versuch: Username = $username, Passwort = $password"; // Debugging
+ if ($username === 'admin' && $password === 'sonne4000$$$$Q') {
+ $_SESSION['admin'] = true;
+ return true;
+ }
+ return false;
+ }
+
+ public function handleImageUpload($file) {
+ if (!$this->isAdmin()) {
+ return false; // Nur Admins dürfen Bilder hochladen
+ }
+
+ if (!isset($file['tmp_name']) || empty($file['tmp_name'])) {
+ echo "Keine Datei hochgeladen.";
+ return false;
+ }
+
+ $target_dir = "uploads/";
+ if (!file_exists($target_dir)) {
+ mkdir($target_dir, 0777, true);
+ }
+
+ $target_file = $target_dir . basename($file["name"]);
+ $uploadOk = 1;
+ $imageFileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));
+
+
+ $check = @getimagesize($file["tmp_name"]);
+ if($check === false) {
+ echo "Die Datei ist kein Bild.";
+ return false;
+ }
+
+
+ if ($file["size"] > 5000000) { // 5MB Limit
+ echo "Die Datei ist zu groß.";
+ return false;
+ }
+
+ // Erlauben Sie nur bestimmte Dateiformate
+ if($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg"
+ && $imageFileType != "gif" ) {
+ echo "Nur JPG, JPEG, PNG & GIF Dateien sind erlaubt.";
+ return false;
+ }
+
+ // Wenn alles in Ordnung ist, versuchen Sie, die Datei hochzuladen
+ if (move_uploaded_file($file["tmp_name"], $target_file)) {
+ echo "Die Datei ". basename( $file["name"]). " wurde hochgeladen.";
+ return true;
+ } else {
+ echo "Es gab einen Fehler beim Hochladen der Datei.";
+ return false;
+ }
+ }
+
+
+ public function displayLoginForm() {
+ return '
+
+
+ Benutzername:
+
+ Passwort:
+
+ Einloggen
+ ';
+ }
+
+ public function displayAdminContent() {
+ $feedbacks = json_decode(file_get_contents('feedbacks.json') ?: '[]', true);
+ $output = '
Admin-Bereich ';
+ foreach ($feedbacks as $feedback) {
+ $output .= "
";
+ $output .= "
{$feedback['name']} ({$feedback['email']}) ";
+ $output .= "
{$feedback['message']}
";
+ $output .= "
{$feedback['date']} ";
+ $output .= "
";
+ }
+ $output .= '
';
+
+ $output .= '
+
Social Media Links verwalten
+
+
+
+ Facebook
+ Instagram
+ TikTok
+
+
+ Aktualisieren
+ ';
+ $output .= '
+
Bild hochladen
+
+
+
+ ';
+ return $output;
+ }
+
+ public function displayGalleryImages() {
+ $output = '
';
+ $files = glob("uploads/*.*");
+ foreach($files as $file) {
+ $filename = basename($file);
+ $extension = pathinfo($file, PATHINFO_EXTENSION);
+
+ // NUR Bilddateien anzeigen, KEINE Videos
+ if (in_array(strtolower($extension), ['jpg', 'jpeg', 'png', 'gif'])) {
+ $output .= '
';
+ }
+ }
+ $output .= '
';
+ return $output;
+}
+
+
+
+
+ public function handleSocialMediaUpdate($platform, $url) {
+ $socialLinks = json_decode(file_get_contents('social_links.json') ?: '{}', true);
+ $socialLinks[$platform] = $url;
+ file_put_contents('social_links.json', json_encode($socialLinks));
+ }
+}
+
+
+
+// Weather Bingo Manager
+//require_once 'weather_bingo.php';
+//$weatherBingo = new WeatherBingo();
+
+
+
+
+
+
+// Neue VideoArchiveManager Klasse
+
+
+class VideoArchiveManager {
+ private $videoDir;
+ private $monthNames;
+
+ public function __construct($videoDir = './videos/') {
+ $this->videoDir = $videoDir;
+ $this->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 getVideosGroupedByDate() {
+ $videos = [];
+
+ foreach (glob($this->videoDir . 'daily_video_*.mp4') as $video) {
+ // Dateinamenformat: daily_video_YYYYMMDD_HHMMSS.mp4
+ if (preg_match('/daily_video_(\d{8})_\d{6}\.mp4/', basename($video), $matches)) {
+ $dateStr = $matches[1]; // YYYYMMDD
+ $year = substr($dateStr, 0, 4);
+ $month = substr($dateStr, 4, 2);
+ $day = substr($dateStr, 6, 2);
+
+ // Gruppiere nach Jahr und Monat
+ $videos[$year][$month][] = [
+ 'path' => $video,
+ 'filename' => basename($video),
+ 'day' => $day,
+ 'filesize' => filesize($video),
+ 'modified' => filemtime($video)
+ ];
+ }
+ }
+
+ // Nach Tag sortieren
+ foreach ($videos as $year => $months) {
+ foreach ($months as $month => $days) {
+ usort($videos[$year][$month], function($a, $b) {
+ return $b['day'] - $a['day']; // Absteigend sortieren (neueste zuerst)
+ });
+ }
+ }
+
+ return $videos;
+ }
+
+ public function getAvailableYearsAndMonths() {
+ $videos = $this->getVideosGroupedByDate();
+ $result = [];
+
+ foreach ($videos as $year => $months) {
+ $result[$year] = array_keys($months);
+ }
+
+ return $result;
+ }
+
+ public function getVideosForYearAndMonth($year, $month) {
+ $videos = $this->getVideosGroupedByDate();
+ return isset($videos[$year][$month]) ? $videos[$year][$month] : [];
+ }
+
+ public function displayCalendarInterface() {
+ $yearsAndMonths = $this->getAvailableYearsAndMonths();
+
+ $output = '
';
+ $output .= '
Video-Archiv ';
+
+ if (empty($yearsAndMonths)) {
+ $output .= '
Keine Videos verfügbar.
';
+ } else {
+ $output .= '
';
+ $output .= '
';
+
+ // Jahr-Auswahl
+ $output .= 'Jahr: ';
+ $output .= '';
+
+ foreach ($yearsAndMonths as $year => $months) {
+ $selected = (isset($_GET['calendar_year']) && $_GET['calendar_year'] == $year) ? 'selected' : '';
+ $output .= "$year ";
+ }
+
+ $output .= ' ';
+
+ // Monats-Auswahl
+ $output .= 'Monat: ';
+ $output .= '';
+
+ // Wenn ein Jahr ausgewählt wurde, zeige die verfügbaren Monate
+ if (isset($_GET['calendar_year']) && isset($yearsAndMonths[$_GET['calendar_year']])) {
+ foreach ($yearsAndMonths[$_GET['calendar_year']] as $month) {
+ $selected = (isset($_GET['calendar_month']) && $_GET['calendar_month'] == $month) ? 'selected' : '';
+ $output .= "{$this->monthNames[$month]} ";
+ }
+ }
+
+ $output .= ' ';
+ $output .= 'Anzeigen ';
+ $output .= ' ';
+ $output .= '';
+
+ // Wenn Jahr und Monat ausgewählt wurden, zeige die Videos
+ if (isset($_GET['calendar_year']) && isset($_GET['calendar_month'])) {
+ $year = $_GET['calendar_year'];
+ $month = $_GET['calendar_month'];
+ $videos = $this->getVideosForYearAndMonth($year, $month);
+
+ if (!empty($videos)) {
+ $output .= '
';
+ $output .= "
Videos für {$this->monthNames[$month]} $year ";
+ $output .= '
';
+ $output .= '
';
+ } else {
+ $output .= "
Keine Videos für {$this->monthNames[$month]} $year gefunden.
";
+ }
+ }
+ }
+
+ $output .= '
';
+ return $output;
+ }
+
+ public function handleSpecificVideoDownload() {
+ if (isset($_GET['download_specific_video']) && isset($_GET['token'])) {
+ $videoPath = $_GET['download_specific_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: Stelle sicher, dass das Video im erlaubten Verzeichnis liegt
+ $videoDir = realpath($this->videoDir);
+ $requestedPath = realpath($videoPath);
+
+ if ($requestedPath && strpos($requestedPath, $videoDir) === 0 && file_exists($requestedPath)) {
+ // Nur MP4-Dateien erlauben
+ $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 {
+ echo "Datei nicht gefunden oder ungültiger Dateipfad.";
+ exit;
+ }
+ }
+ }
+}
+
+
+
+
+
+
+
+$webcamManager = new WebcamManager();
+$imageFilesJson = $webcamManager->getImageFiles();
+$guestbookManager = new GuestbookManager();
+$contactManager = new ContactManager();
+$adminManager = new AdminManager();
+
+// Nach den anderen Manager-Instanzen hinzufügen
+$videoArchiveManager = new VideoArchiveManager('./videos/');
+
+// Video-Download-Handler nach dem existierenden Download-Handler hinzufügen
+$videoArchiveManager->handleSpecificVideoDownload();
+
+
+if (isset($_GET['action'])) {
+ switch ($_GET['action']) {
+ case 'snapshot':
+ $webcamManager->captureSnapshot();
+
+ break;
+ case 'sequence':
+ $webcamManager->captureVideoSequence();
+
+ break;
+
+ }
+
+}
+
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_guestbook') {
+ if ($adminManager->isAdmin() && isset($_POST['delete_entry'])) {
+ $index = $_POST['delete_entry'];
+ if ($guestbookManager->deleteEntry($index)) {
+ $_SESSION['message'] = "Eintrag erfolgreich gelöscht.";
+ } else {
+ $_SESSION['error'] = "Fehler beim Löschen des Eintrags.";
+ }
+ // Umleitung zur gleichen Seite, um Neuladen des Formulars zu verhindern
+ header("Location: " . $_SERVER['PHP_SELF'] . "#guestbook");
+ exit();
+ }
+}
+
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (isset($_POST['guestbook'])) {
+ $guestbookManager->handleFormSubmission();
+ } elseif (isset($_POST['contact'])) {
+ $result = $contactManager->handleSubmission($_POST['name'], $_POST['email'], $_POST['message']);
+
+ // JSON-Response für AJAX
+ if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
+ strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
+ header('Content-Type: application/json');
+ echo json_encode($result);
+ exit;
+ }
+
+ // Normale Formular-Submission
+ $_SESSION['contact_result'] = $result;
+ header('Location: ' . $_SERVER['PHP_SELF'] . '#kontakt');
+ exit;
+
+ } elseif (isset($_POST['admin-login'])) {
+ $adminManager->handleLogin($_POST['username'], $_POST['password']);
+ } elseif (isset($_POST['update-social-media'])) {
+ $adminManager->handleSocialMediaUpdate($_POST['social-platform'], $_POST['social-url']);
+
+
+ } elseif (isset($_FILES["fileToUpload"]) && $adminManager->isAdmin()) {
+ $adminManager->handleImageUpload($_FILES["fileToUpload"]);
+}}
+?>
+
+
+
+
+
+
+
+
+
+
Aurora Livecam - Einzigartige Live-Webcam und Wetter>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Willkommen bei Aurora Wetter Livecam
+
+
+
+
+
+
+ Erleben Sie faszinierende Ausblicke der Züricher Region - in Echtzeit!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ displayWebcam(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Videoarchiv Tagesvideos
+
+ displayVisualCalendar();
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Folge uns und kopiere den Code und sende es deinen Freunden in Tiktok, Facebook, Instagram usw.
+
+
+
+
+
+ Klicke auf den QR-Code, um die URL zu kopieren
+
+
+
+
+
+
+
+
📋 Nutzungsbedingungen & Chat-Regeln
+
+
+
⚠️ Wichtige Verhaltensregeln:
+
+ Keine sexuellen, pornografischen oder anzüglichen Inhalte
+ Keine Gewaltdarstellungen oder -androhungen
+ Kein Rassismus, Diskriminierung oder Hassrede
+ Keine persönlichen Daten (Telefonnummern, Adressen) teilen
+ Keine Werbung oder Spam
+ Respektvoller Umgang miteinander
+ Keine illegalen Aktivitäten oder Inhalte
+
+
+
+
+
📸 Webcam-Nutzung:
+
Die Webcam zeigt öffentlichen Raum. Es werden keine Personen gezielt aufgenommen. Die Nutzung erfolgt auf eigene Verantwortung.
+
+
+
+
💬 Chat-Nutzung:
+
Mit der Nutzung des Chats akzeptieren Sie diese Regeln. Verstöße führen zur sofortigen Sperrung. Chat-Nachrichten werden 24 Stunden gespeichert.
+
+
+
+
⚖️ Rechtliches:
+
Haftungsausschluss: Der Betreiber übernimmt keine Haftung für Inhalte von Nutzern. Jeder Nutzer ist für seine Beiträge selbst verantwortlich.
+
Datenschutz: Es werden nur technisch notwendige Daten gespeichert (IP-Adresse für 24h zur Missbrauchsprävention).
+
+
+
+
+
+ Ich akzeptiere die Nutzungsbedingungen
+
+
+
+
+
+ Akzeptieren & Chat nutzen
+
+
+ Ablehnen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Gästebuch
+ {$_SESSION['message'][$currentLang]}";
+ unset($_SESSION['message']);
+ }
+ if (isset($_SESSION['error'])) {
+ echo "
{$_SESSION['error'][$currentLang]}
";
+ unset($_SESSION['error']);
+ }
+
+ echo $guestbookManager->displayForm();
+ echo $guestbookManager->displayEntries($adminManager->isAdmin());
+ ?>
+
+
+
+
+
+
+
+
+
+
+
Kontakt
+
Haben Sie Fragen, Anregungen oder möchten uns unterstützen? Wir freuen uns auf Ihre Nachricht!
+
+
+
+
+
+ displayForm(); ?>
+
+
+
+
+
+
+
+
+ displayGalleryImages(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ Aurora Wetter Livecam ist ein Herzensprojekt von Wetterbegeisterten. Wir möchten Ihnen die Schönheit der Natur und Faszination des Wetters näher bringen.
+
+
+ Dazu betreiben wir seit 2010 rund um die Uhr hochauflösende Webcams. Besonders stolz sind wir auf einzigartige Einblicke, wie z.B. die Trainingsflüge der Patrouille Suisse jeden Montagmorgen.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ isAdmin()): ?>
+
+
+
Admin-Bereich
+ displayAdminContent(); ?>
+
+
+
+
+
+
Admin Login
+ displayLoginForm(); ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Impressum
+
+
Aurora Wetter Livecam
+
M. Kessler
+
Dürnten
+
Schweiz
+
+ Anfragen per Kontaktformular
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/band-detail.php b/band-detail.php
index 9947543..62ca9ba 100644
--- a/band-detail.php
+++ b/band-detail.php
@@ -1,7 +1,6 @@
'',
- 'email' => '',
- 'message' => '',
-];
+$message = '';
+$error = '';
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- if (isset($_POST['review']) && $user) {
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && $user) {
+ if (isset($_POST['review'])) {
if (!eligibleForReview($bandId, (int) $user['id'])) {
- $reviewError = 'Für Bewertungen ist eine bestätigte Buchung nötig.';
+ $error = 'Für Bewertungen ist eine bestätigte Buchung nötig.';
} else {
$comment = trim((string) ($_POST['comment'] ?? ''));
if (mb_strlen($comment) > 200) {
- $reviewError = 'Maximal 200 Zeichen erlaubt.';
+ $error = 'Maximal 200 Zeichen erlaubt.';
} else {
storeReview([
'band_id' => $bandId,
@@ -41,35 +33,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'rating' => (int) $_POST['rating'],
'comment' => $comment,
]);
- $reviewMessage = 'Danke! Deine Bewertung wartet auf Freigabe.';
- }
- }
- }
-
- if (isset($_POST['contact'])) {
- $contactForm['name'] = trim((string) ($_POST['contact_name'] ?? ''));
- $contactForm['email'] = trim((string) ($_POST['contact_email'] ?? ''));
- $contactForm['message'] = trim((string) ($_POST['contact_message'] ?? ''));
-
- if ($contactForm['name'] === '' || $contactForm['message'] === '') {
- $contactError = 'Bitte Name und Nachricht ausfüllen.';
- } elseif (!filter_var($contactForm['email'], FILTER_VALIDATE_EMAIL)) {
- $contactError = 'Bitte eine gültige E-Mail-Adresse angeben.';
- } else {
- $recipient = $band['contact_email'] ?: SUPPORT_EMAIL;
- $body = sprintf(
- 'Neue Nachricht über die Bandseite %s.
Von: %s (%s)
Nachricht: %s
',
- htmlspecialchars($band['name']),
- htmlspecialchars($contactForm['name']),
- htmlspecialchars($contactForm['email']),
- nl2br(htmlspecialchars($contactForm['message']))
- );
- $sent = sendEmail($recipient, 'Kontaktformular – ' . $band['name'], $body);
- if ($sent) {
- $contactMessage = 'Nachricht an die Band wurde verschickt.';
- $contactForm = ['name' => '', 'email' => '', 'message' => ''];
- } else {
- $contactError = 'E-Mail-Versand zur Band nicht möglich. Bitte später erneut versuchen.';
+ $message = 'Danke! Deine Bewertung wartet auf Freigabe.';
}
}
}
@@ -139,30 +103,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-
-
Bewertungen
- = htmlspecialchars($reviewMessage) ?>
- = htmlspecialchars($reviewError) ?>
+ = htmlspecialchars($message) ?>
+ = htmlspecialchars($error) ?>
= htmlspecialchars($review['author']) ?> – = (int) $review['rating'] ?> ★
diff --git a/database.sql b/database.sql
index d342a9f..c36c71c 100644
--- a/database.sql
+++ b/database.sql
@@ -23,7 +23,6 @@ CREATE TABLE IF NOT EXISTS bands (
status TEXT NOT NULL DEFAULT 'prüfung',
style_tags TEXT,
video_url TEXT,
- contact_email TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(user_id) REFERENCES users(id)
);
diff --git a/functional_test.php b/functional_test.php
index 713bf4e..6f38f79 100644
--- a/functional_test.php
+++ b/functional_test.php
@@ -3,15 +3,17 @@
declare(strict_types=1);
require_once __DIR__ . '/includes/config.php';
-require_once __DIR__ . '/includes/functions.php';
-require_once __DIR__ . '/includes/auth.php';
-require_once __DIR__ . '/includes/email.php';
$options = array_slice($argv, 1);
if (in_array('--fresh', $options, true) && file_exists(DB_PATH)) {
unlink(DB_PATH);
+ echo "Bestehende Datenbank entfernt – wird neu initialisiert.\n";
}
+require_once __DIR__ . '/includes/functions.php';
+require_once __DIR__ . '/includes/auth.php';
+require_once __DIR__ . '/includes/email.php';
+
if (!isset($_SERVER['REQUEST_METHOD'])) {
$_SERVER['REQUEST_METHOD'] = 'GET';
}
@@ -254,15 +256,12 @@ $runner->run('Registrierung legt Band an', function (PDO $pdo) {
if (!$userRow || $userRow['role'] !== 'band') {
throw new RuntimeException('User wurde nicht gespeichert.');
}
- $band = $pdo->prepare('SELECT status, contact_email FROM bands WHERE user_id = :id');
+ $band = $pdo->prepare('SELECT status FROM bands WHERE user_id = :id');
$band->execute([':id' => $userRow['id']]);
$bandRow = $band->fetch(PDO::FETCH_ASSOC);
if (!$bandRow || $bandRow['status'] !== 'prüfung') {
throw new RuntimeException('Bandprofil wurde nicht angelegt.');
}
- if (($bandRow['contact_email'] ?? '') !== $email) {
- throw new RuntimeException('Kontakt-E-Mail der Band fehlt.');
- }
return 'Token erstellt und Bandstatus "prüfung" bestätigt.';
}, true);
@@ -282,30 +281,6 @@ $runner->run('Band-Detailseite rendert', function () {
return 'HTML-Länge: ' . strlen($html) . ' Zeichen';
});
-$runner->run('Kontaktformular der Bandseite', function () {
- $logFile = __DIR__ . '/storage/logs/mail.log';
- if (!is_dir(dirname($logFile))) {
- mkdir(dirname($logFile), 0775, true);
- }
- $before = file_exists($logFile) ? filesize($logFile) : 0;
- $html = renderPage('band-detail.php', ['id' => 1], [
- 'contact' => '1',
- 'contact_name' => 'QA Bot',
- 'contact_email' => 'qa@example.com',
- 'contact_message' => 'Testnachricht an die Band.',
- ]);
- $success = strpos($html, 'Nachricht an die Band wurde verschickt.') !== false;
- $fallback = strpos($html, 'E-Mail-Versand zur Band nicht möglich.') !== false;
- if (!$success && !$fallback) {
- throw new RuntimeException('Kontaktformular meldete keinen Versandstatus.');
- }
- $after = filesize($logFile);
- if ($after <= $before) {
- throw new RuntimeException('Kein Mail-Logeintrag für Kontaktformular.');
- }
- return 'Kontaktformular meldete Erfolg und schrieb ins Log.';
-});
-
$runner->run('Anfrageformular rendert', function () {
$html = renderPage('anfrage.php', ['band_id' => 1]);
if (strpos($html, 'Anfrage an') === false) {
diff --git a/includes/auth.php b/includes/auth.php
index 66f099d..045c413 100644
--- a/includes/auth.php
+++ b/includes/auth.php
@@ -56,21 +56,18 @@ function register(array $data): array
':token' => $token,
':created' => (new DateTimeImmutable())->format('c'),
]);
- $userId = (int) db()->lastInsertId();
if ($data['role'] === 'band') {
- $band = db()->prepare('INSERT INTO bands (user_id, name, city, genre, price, description, status, contact_email)
- VALUES (:user_id, :name, :city, :genre, :price, :description, :status, :contact_email)');
- $bandEmail = $data['band_email'] ?? $data['email'];
+ $band = db()->prepare('INSERT INTO bands (user_id, name, city, genre, price, description, status)
+ VALUES (:user_id, :name, :city, :genre, :price, :description, :status)');
$band->execute([
- ':user_id' => $userId,
+ ':user_id' => (int) db()->lastInsertId(),
':name' => $data['band_name'] ?? 'Neue Band',
':city' => $data['city'] ?? '',
':genre' => $data['genre'] ?? '',
':price' => 0,
':description' => 'Bitte Profil ergänzen.',
':status' => 'prüfung',
- ':contact_email' => $bandEmail,
]);
}
diff --git a/includes/database.php b/includes/database.php
index 3882e57..392130a 100644
--- a/includes/database.php
+++ b/includes/database.php
@@ -21,22 +21,9 @@ function initializeDatabase(PDO $pdo): void
$schema = file_get_contents(__DIR__ . '/../database.sql');
$pdo->exec($schema);
- ensureBandContactEmailColumn($pdo);
-
seedData($pdo);
}
-function ensureBandContactEmailColumn(PDO $pdo): void
-{
- $columns = $pdo->query('PRAGMA table_info(bands)')->fetchAll(PDO::FETCH_ASSOC);
- foreach ($columns as $column) {
- if (($column['name'] ?? '') === 'contact_email') {
- return;
- }
- }
- $pdo->exec('ALTER TABLE bands ADD COLUMN contact_email TEXT');
-}
-
function seedData(PDO $pdo): void
{
$count = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
@@ -91,7 +78,6 @@ function seedData(PDO $pdo): void
'status' => 'aktiv',
'style_tags' => 'Funk,Retro,Showband',
'video_url' => 'https://www.youtube.com/embed/dQw4w9WgXcQ',
- 'contact_email' => 'booking@neongroove.ch',
],
[
'user_id' => null,
@@ -103,12 +89,11 @@ function seedData(PDO $pdo): void
'status' => 'aktiv',
'style_tags' => 'Pop,Party,LED',
'video_url' => 'https://www.youtube.com/embed/5NV6Rdv1a3I',
- 'contact_email' => 'hello@sonnenblitz.ch',
],
];
- $bandStmt = $pdo->prepare('INSERT INTO bands (user_id, name, city, genre, price, description, status, style_tags, video_url, contact_email)
- VALUES (:user_id, :name, :city, :genre, :price, :description, :status, :style_tags, :video_url, :contact_email)');
+ $bandStmt = $pdo->prepare('INSERT INTO bands (user_id, name, city, genre, price, description, status, style_tags, video_url)
+ VALUES (:user_id, :name, :city, :genre, :price, :description, :status, :style_tags, :video_url)');
foreach ($bands as $band) {
$bandStmt->execute([
@@ -121,7 +106,6 @@ function seedData(PDO $pdo): void
':status' => $band['status'],
':style_tags' => $band['style_tags'],
':video_url' => $band['video_url'],
- ':contact_email' => $band['contact_email'],
]);
$bandId = (int) $pdo->lastInsertId();
diff --git a/includes/email.php b/includes/email.php
index ebb3ac5..f4d88f2 100644
--- a/includes/email.php
+++ b/includes/email.php
@@ -1,27 +1,10 @@
SUPPORT_EMAIL,
- 'from_name' => SITE_NAME,
- ]);
-
$logDir = __DIR__ . '/../storage/logs';
if (!is_dir($logDir)) {
mkdir($logDir, 0775, true);
}
- $entry = sprintf(
- "%s\nTo: %s\nSubject: %s\nStatus: %s\n%s\n---\n",
- date('c'),
- $to,
- $subject,
- $sent ? 'SENT' : 'FAILED',
- $message
- );
+ $entry = sprintf("%s\nTo: %s\nSubject: %s\n%s\n---\n", date('c'), $to, $subject, $message);
file_put_contents($logDir . '/mail.log', $entry, FILE_APPEND);
-
- return $sent;
}
diff --git a/login.php b/login.php
index 18ea670..377ee10 100644
--- a/login.php
+++ b/login.php
@@ -37,7 +37,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'city' => trim((string) $_POST['city']),
'band_name' => $_POST['band_name'] ?? null,
'genre' => $_POST['genre'] ?? null,
- 'band_email' => $_POST['band_email'] ?? null,
]);
$verificationLink = BASE_URL . '/verify-email.php?token=' . urlencode($result['token']);
sendEmail($_POST['email'], 'E-Mail bestätigen', 'Bitte bestätige dein Konto: ' . $verificationLink);
@@ -98,9 +97,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
Bandname (falls Band)
- Band Kontakt-E-Mail
-
-
Genre
diff --git a/profil.php b/profil.php
index 9ce5771..7724dcd 100644
--- a/profil.php
+++ b/profil.php
@@ -13,7 +13,7 @@ if ($user['role'] === 'band') {
$band = $stmt->fetch(PDO::FETCH_ASSOC);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- $stmt = db()->prepare('UPDATE bands SET name = :name, city = :city, genre = :genre, price = :price, description = :description, style_tags = :tags, contact_email = :contact_email WHERE id = :id');
+ $stmt = db()->prepare('UPDATE bands SET name = :name, city = :city, genre = :genre, price = :price, description = :description, style_tags = :tags WHERE id = :id');
$stmt->execute([
':name' => $_POST['name'],
':city' => $_POST['city'],
@@ -21,7 +21,6 @@ if ($user['role'] === 'band') {
':price' => (int) $_POST['price'],
':description' => $_POST['description'],
':tags' => $_POST['style_tags'],
- ':contact_email' => $_POST['contact_email'] ?? '',
':id' => $band['id'],
]);
$message = 'Bandprofil aktualisiert (wartet ggf. auf Freigabe).';
@@ -56,9 +55,6 @@ if ($user['role'] === 'band') {
Genre
- Kontakt-E-Mail
-
-
Tags
diff --git a/synth/index.php b/synth/index.php
new file mode 100644
index 0000000..3814da6
--- /dev/null
+++ b/synth/index.php
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+ Mouse Synth Lab
+
+
+
+
+
+
+
+
Mouse Synth Lab
+
+ Zieh deine Maus durch den Pad und verwandle Bewegungen in Klang. Drei LFOs,
+ FM-Experimente und ein Delay/Distortion-Hybrid werden live verschaltet.
+ Der aktuelle Bitcoin-Kurs (= htmlspecialchars($btcLabel) ?> )
+ steuert, wie aggressiv der Mix moduliert und rückgekoppelt wird.
+
+
+
Bitcoin Quelle: = htmlspecialchars($btcSource) ?>
+
Letzter Wert: = htmlspecialchars($btcLabel) ?>
+
+
+
+ FM-Intensität
+
+
+
+ LFO-Speed
+
+
+
+ Texture Morph
+
+
+
+ Synth starten
+ Chaos Patch
+
+
+
+
+ Tipp: Halte die Maus gedrückt, damit der AudioContext aktiv bleibt, und lass den Cursor
+ Kreise fahren. Je nach Bitcoin-Laune schalten sich neue Rückkopplungen zu.
+
+
+
+
+
diff --git a/synth/synth.js b/synth/synth.js
new file mode 100644
index 0000000..1dc0ebe
--- /dev/null
+++ b/synth/synth.js
@@ -0,0 +1,262 @@
+const btcPrice = parseFloat(document.body.dataset.btcPrice || 'NaN');
+const normalizedCoin = Number.isFinite(btcPrice)
+ ? Math.min(Math.max((btcPrice - 15000) / 25000, 0), 1)
+ : 0.5;
+
+const pad = document.getElementById('synth-pad');
+const indicator = document.getElementById('pad-indicator');
+const fmDepthInput = document.getElementById('fm-depth');
+const lfoSpeedInput = document.getElementById('lfo-speed');
+const textureInput = document.getElementById('texture');
+const startBtn = document.getElementById('start-btn');
+const randomizeBtn = document.getElementById('randomize-btn');
+
+class MouseSynth {
+ constructor(options = {}) {
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
+ this.ctx = new AudioContext();
+ this.started = false;
+
+ this.coinBlend = options.coinBlend ?? 0.5;
+
+ this.setupNodes();
+ this.#bindEvents();
+ }
+
+ setupNodes() {
+ const ctx = this.ctx;
+
+ this.masterGain = ctx.createGain();
+ this.masterGain.gain.value = 0.0;
+
+ this.carrier = ctx.createOscillator();
+ this.carrier.type = 'sawtooth';
+
+ this.harmonic = ctx.createOscillator();
+ this.harmonic.type = 'triangle';
+ this.harmonic.detune.value = 702; // perfect fifth
+
+ this.fmOsc = ctx.createOscillator();
+ this.fmGain = ctx.createGain();
+ this.fmGain.gain.value = 320;
+
+ this.ampLfo = ctx.createOscillator();
+ this.ampLfo.type = 'sine';
+ this.ampLfo.frequency.value = 6;
+ this.ampLfoGain = ctx.createGain();
+ this.ampLfoGain.gain.value = 0.5;
+
+ this.filterLfo = ctx.createOscillator();
+ this.filterLfo.type = 'triangle';
+ this.filterLfo.frequency.value = 0.5;
+ this.filterLfoGain = ctx.createGain();
+ this.filterLfoGain.gain.value = 800;
+
+ this.sampleHold = ctx.createOscillator();
+ this.sampleHold.type = 'square';
+ this.sampleHold.frequency.value = 8;
+ this.sampleHoldGain = ctx.createGain();
+ this.sampleHoldGain.gain.value = 0.0025;
+
+ this.filter = ctx.createBiquadFilter();
+ this.filter.type = 'bandpass';
+ this.filter.frequency.value = 600;
+ this.filter.Q.value = 8;
+
+ this.delay = ctx.createDelay(1.2);
+ this.delay.delayTime.value = 0.45;
+ this.feedback = ctx.createGain();
+ this.feedback.gain.value = 0.32;
+
+ this.noise = this.#createNoise();
+ this.noiseGain = ctx.createGain();
+ this.noiseGain.gain.value = 0.0;
+
+ this.distortion = ctx.createWaveShaper();
+ this.#setDrive(400);
+
+ this.coinMorph = ctx.createGain();
+ this.coinMorph.gain.value = this.coinBlend;
+
+ this.reverb = ctx.createConvolver();
+ this.reverb.buffer = this.#makeImpulse(2.5);
+ this.reverbGain = ctx.createGain();
+ this.reverbGain.gain.value = 0.25;
+
+ // Connections
+ this.fmOsc.connect(this.fmGain).connect(this.carrier.frequency);
+ this.harmonic.connect(this.filter);
+ this.carrier.connect(this.filter);
+ this.ampLfo.connect(this.ampLfoGain).connect(this.masterGain.gain);
+ this.filterLfo.connect(this.filterLfoGain).connect(this.filter.frequency);
+ this.sampleHold.connect(this.sampleHoldGain).connect(this.filter.detune);
+
+ this.filter.connect(this.distortion);
+ this.noise.connect(this.noiseGain).connect(this.filter);
+
+ const wet = ctx.createGain();
+ const dry = ctx.createGain();
+ this.distortion.connect(dry).connect(this.masterGain);
+ this.distortion.connect(this.delay);
+ this.delay.connect(this.feedback).connect(this.delay);
+ this.delay.connect(this.coinMorph);
+ this.coinMorph.connect(wet);
+ wet.connect(this.reverb);
+ this.reverb.connect(this.reverbGain).connect(this.masterGain);
+
+ this.masterGain.connect(ctx.destination);
+
+ this.carrier.start();
+ this.harmonic.start();
+ this.fmOsc.start();
+ this.ampLfo.start();
+ this.filterLfo.start();
+ this.sampleHold.start();
+ this.noise.start();
+ }
+
+ async start() {
+ if (this.started) return;
+ await this.ctx.resume();
+ this.masterGain.gain.linearRampToValueAtTime(0.8, this.ctx.currentTime + 0.5);
+ this.started = true;
+ }
+
+ #bindEvents() {
+ fmDepthInput.addEventListener('input', () => {
+ this.fmGain.gain.setTargetAtTime(parseFloat(fmDepthInput.value), this.ctx.currentTime, 0.05);
+ });
+
+ lfoSpeedInput.addEventListener('input', () => {
+ const rate = parseFloat(lfoSpeedInput.value);
+ this.ampLfo.frequency.setTargetAtTime(rate, this.ctx.currentTime, 0.1);
+ this.filterLfo.frequency.setTargetAtTime(rate * 0.25, this.ctx.currentTime, 0.1);
+ });
+
+ textureInput.addEventListener('input', () => {
+ this.#updateTexture(parseFloat(textureInput.value));
+ });
+
+ randomizeBtn.addEventListener('click', () => this.randomize());
+ }
+
+ handlePointer(event) {
+ if (!this.started) return;
+ const rect = pad.getBoundingClientRect();
+ const x = (event.clientX - rect.left) / rect.width;
+ const y = (event.clientY - rect.top) / rect.height;
+ const freq = 120 + (1 - y) * 1080;
+
+ this.carrier.frequency.setTargetAtTime(freq, this.ctx.currentTime, 0.05);
+ this.harmonic.frequency.setTargetAtTime(freq * 1.5, this.ctx.currentTime, 0.05);
+ this.filter.frequency.setTargetAtTime(200 + x * 5200, this.ctx.currentTime, 0.08);
+ this.filter.Q.setTargetAtTime(4 + y * 18, this.ctx.currentTime, 0.1);
+ this.noiseGain.gain.setTargetAtTime(x * 0.3, this.ctx.currentTime, 0.2);
+
+ this.sampleHold.frequency.setTargetAtTime(4 + x * 20, this.ctx.currentTime, 0.1);
+ this.delay.delayTime.setTargetAtTime(0.15 + y * 0.6, this.ctx.currentTime, 0.2);
+ this.feedback.gain.setTargetAtTime(0.2 + x * 0.7 * this.coinBlend, this.ctx.currentTime, 0.2);
+ this.coinMorph.gain.setTargetAtTime(this.coinBlend * (0.4 + y * 0.6), this.ctx.currentTime, 0.3);
+ this.fmGain.gain.setTargetAtTime(parseFloat(fmDepthInput.value) + x * 200, this.ctx.currentTime, 0.05);
+ this.#updateTexture(textureInput.value, x, y);
+ }
+
+ handlePointerLeave() {
+ if (!this.started) return;
+ this.masterGain.gain.cancelScheduledValues(this.ctx.currentTime);
+ this.masterGain.gain.setTargetAtTime(0.15, this.ctx.currentTime, 0.5);
+ }
+
+ randomize() {
+ const fm = 80 + Math.random() * 720;
+ fmDepthInput.value = fm.toFixed(0);
+ this.fmGain.gain.setTargetAtTime(fm, this.ctx.currentTime, 0.1);
+
+ const lfo = 0.1 + Math.random() * 16;
+ lfoSpeedInput.value = lfo.toFixed(2);
+ this.ampLfo.frequency.setTargetAtTime(lfo, this.ctx.currentTime, 0.2);
+ this.filterLfo.frequency.setTargetAtTime(lfo * 0.3, this.ctx.currentTime, 0.2);
+
+ const texture = Math.random();
+ textureInput.value = texture.toFixed(2);
+ this.#updateTexture(texture);
+ }
+
+ #updateTexture(value, x = 0.5, y = 0.5) {
+ const amount = parseFloat(value);
+ const drive = 150 + amount * 850 + this.coinBlend * 400;
+ this.#setDrive(drive);
+ const morph = amount * (0.6 + this.coinBlend * 0.8);
+ this.distortion.oversample = morph > 0.5 ? '4x' : '2x';
+ this.reverbGain.gain.setTargetAtTime(0.15 + morph * 0.6, this.ctx.currentTime, 0.3);
+ const filterType = morph > 0.7 ? 'notch' : morph > 0.35 ? 'bandpass' : 'lowpass';
+ this.filter.type = filterType;
+ this.coinMorph.gain.setTargetAtTime(this.coinBlend * (0.4 + morph), this.ctx.currentTime, 0.3);
+ this.delay.delayTime.setTargetAtTime(0.2 + morph * 0.4 + (x * y) * 0.2, this.ctx.currentTime, 0.2);
+ }
+
+ #setDrive(amount) {
+ const curve = new Float32Array(1024);
+ for (let i = 0; i < curve.length; i++) {
+ const x = (i / curve.length) * 2 - 1;
+ curve[i] = Math.tanh(x * amount * 0.01);
+ }
+ this.distortion.curve = curve;
+ }
+
+ #createNoise() {
+ const buffer = this.ctx.createBuffer(1, this.ctx.sampleRate * 4, this.ctx.sampleRate);
+ const data = buffer.getChannelData(0);
+ for (let i = 0; i < data.length; i++) {
+ data[i] = Math.random() * 2 - 1;
+ }
+ const noise = this.ctx.createBufferSource();
+ noise.buffer = buffer;
+ noise.loop = true;
+ return noise;
+ }
+
+ #makeImpulse(seconds) {
+ const rate = this.ctx.sampleRate;
+ const length = rate * seconds;
+ const impulse = this.ctx.createBuffer(2, length, rate);
+ for (let ch = 0; ch < 2; ch++) {
+ const data = impulse.getChannelData(ch);
+ for (let i = 0; i < length; i++) {
+ data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2);
+ }
+ }
+ return impulse;
+ }
+}
+
+const synth = new MouseSynth({ coinBlend: normalizedCoin });
+
+startBtn.addEventListener('click', () => synth.start());
+
+pad.addEventListener('pointerdown', async (event) => {
+ await synth.start();
+ pad.setPointerCapture(event.pointerId);
+ indicator.style.opacity = '1';
+});
+
+pad.addEventListener('pointermove', (event) => {
+ indicator.style.left = `${event.offsetX}px`;
+ indicator.style.top = `${event.offsetY}px`;
+ synth.handlePointer(event);
+});
+
+pad.addEventListener('pointerup', (event) => {
+ pad.releasePointerCapture(event.pointerId);
+ synth.handlePointerLeave();
+});
+
+pad.addEventListener('pointerleave', () => synth.handlePointerLeave());
+
+if (!Number.isFinite(btcPrice)) {
+ const status = document.getElementById('btc-status');
+ if (status) {
+ status.insertAdjacentHTML('beforeend', 'BTC Feed nicht erreichbar – Synth läuft im Fantasy-Modus.
');
+ }
+}
+