From 6c1b185fc630e57f6f01a68e1c0892793a0931ff Mon Sep 17 00:00:00 2001 From: simonpipe Date: Thu, 18 Dec 2025 17:17:16 +0100 Subject: [PATCH] Upload files to "/" Neu aufgeteilter Code --- meta-handler.js | 79 +++++++++++++++++++++++++++++++++++++++++++++++ metadata.php | 69 +++++++++++++++++++++++++++++++++++++++++ preset-manager.js | 76 +++++++++++++++++++++++++++++++++++++++++++++ sleeptimer.js | 57 ++++++++++++++++++++++++++++++++++ ui-helper.js | 44 ++++++++++++++++++++++++++ 5 files changed, 325 insertions(+) create mode 100644 meta-handler.js create mode 100644 metadata.php create mode 100644 preset-manager.js create mode 100644 sleeptimer.js create mode 100644 ui-helper.js diff --git a/meta-handler.js b/meta-handler.js new file mode 100644 index 0000000..bb5548f --- /dev/null +++ b/meta-handler.js @@ -0,0 +1,79 @@ +const MetaHandler = { + proxyPath: 'optional/metadata.php', + updateInterval: 20000, + intervalId: null, + proxyActive: false, + + async init() { + try { + const response = await fetch(this.proxyPath, { method: 'HEAD' }); + this.proxyActive = response.ok; + } catch (e) { + this.proxyActive = false; + } + }, + + async fetchTitle(streamUrl) { + if (!this.proxyActive || !streamUrl) return; + try { + const res = await fetch(`${this.proxyPath}?url=${encodeURIComponent(streamUrl)}`); + const data = await res.json(); + if (data && data.title) { + this.updateUI(data.title); + } + } catch (e) { + console.error("Metadaten-Fehler:", e); + } + }, + + updateUI(title) { + const stationDisplay = document.getElementById('current-station-name'); + if (!stationDisplay) return; + + let currentFullText = stationDisplay.textContent; + let stationNameOnly = currentFullText.replace('Es läuft: ', '').split(' – ')[0].trim(); + + const cleanTitle = title.toLowerCase(); + const cleanStation = stationNameOnly.toLowerCase(); + + const isDuplicate = cleanTitle === cleanStation || cleanTitle.includes(cleanStation) || cleanStation.includes(cleanTitle); + const isSpam = title.includes('0848') || title.includes('.ch') || title.includes('@') || title.includes('Im Auftrag der SRG'); + + if (isDuplicate || isSpam) { + if (currentFullText.includes(' – ')) { + stationDisplay.textContent = currentFullText.split(' – ')[0]; + } + return; + } + + if (currentFullText.includes(' – ')) { + currentFullText = currentFullText.split(' – ')[0]; + } + stationDisplay.textContent = `${currentFullText} – ${title}`; + }, + + start(streamUrl) { + this.stop(); + // Wir rufen fetchTitle direkt auf, um die PHP-Verbindung zu testen + MetaHandler.fetchTitle(streamUrl); + + this.intervalId = setInterval(() => { + MetaHandler.fetchTitle(streamUrl); + }, this.updateInterval); + }, + + stop() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + const stationDisplay = document.getElementById('current-station-name'); + if (stationDisplay && stationDisplay.textContent.includes(' – ')) { + stationDisplay.textContent = stationDisplay.textContent.split(' – ')[0]; + } + } +}; + +// Global verfügbar machen +window.MetaHandler = MetaHandler; +MetaHandler.init(); \ No newline at end of file diff --git a/metadata.php b/metadata.php new file mode 100644 index 0000000..9057a43 --- /dev/null +++ b/metadata.php @@ -0,0 +1,69 @@ + 'Ungültige oder fehlende URL']); + exit; +} + +// WICHTIG: Die Header müssen Icy-MetaData enthalten, sonst senden viele Server gar nichts! +$opts = [ + "http" => [ + "method" => "GET", + "header" => "Icy-MetaData: 1\r\n", + "timeout" => 5, + "user_agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) RadioMetadataProxy/1.0" + ] +]; + +$context = stream_context_create($opts); +$fp = @fopen($url, 'r', false, $context); + +if (!$fp) { + echo json_encode(['error' => 'Stream konnte nicht geöffnet werden']); + exit; +} + +// Puffer erhöhen: Wir lesen bis zu 192 KB, um auch langsame Metadaten zu finden +$max_read = 196608; +$data = ''; +$read_size = 0; + +while (!feof($fp) && $read_size < $max_read) { + $chunk = fread($fp, 8192); + if ($chunk === false) break; + $data .= $chunk; + $read_size += strlen($chunk); + + // Vorzeitig abbrechen, wenn der Titel gefunden wurde + if (strpos($data, 'StreamTitle=') !== false) { + break; + } +} +fclose($fp); + +// Suche nach dem StreamTitle +if (preg_match("/StreamTitle='(.*?)';/", $data, $matches)) { + $title = trim($matches[1]); + + // UTF-8 Korrektur (falls der Sender ISO-8859-1 schickt) + if (!mb_check_encoding($title, 'UTF-8')) { + $title = utf8_encode($title); + } + + if (empty($title)) { + echo json_encode(['title' => null, 'status' => 'empty']); + } else { + echo json_encode(['title' => $title]); + } +} else { + echo json_encode(['title' => null, 'error' => 'Keine Metadaten gefunden']); +} \ No newline at end of file diff --git a/preset-manager.js b/preset-manager.js new file mode 100644 index 0000000..847a3f5 --- /dev/null +++ b/preset-manager.js @@ -0,0 +1,76 @@ +// preset-manager.js - Verwaltung der Radio-Sender +window.PresetManager = { + presets: [], + + // Lädt Presets aus LocalStorage oder der JSON-Datei + async load(callback) { + this.presets = JSON.parse(localStorage.getItem('presets')) || []; + if (this.presets.length === 0) { + try { + const response = await fetch('webradio_presets.json'); + if (!response.ok) throw new Error('Datei konnte nicht geladen werden'); + this.presets = await response.json(); + this.save(); + } catch (error) { + if (window.UI) window.UI.showAlert("Fehler", 'Fehler beim Laden der Presets: ' + error.message); + } + } + if (callback) callback(this.presets); + }, + + save() { + localStorage.setItem('presets', JSON.stringify(this.presets)); + }, + + // --- EXPORT/IMPORT (Gzip/Base64) --- + async generateExportCode() { + try { + if (!this.presets || this.presets.length === 0) return; + const compactData = this.presets.map(p => [p.name, p.url]); + const jsonString = JSON.stringify(compactData); + + const stream = new Blob([jsonString]).stream(); + const compressedStream = stream.pipeThrough(new CompressionStream('gzip')); + const response = new Response(compressedStream); + const buffer = await response.arrayBuffer(); + const base64String = btoa(String.fromCharCode(...new Uint8Array(buffer))); + + const exportField = document.getElementById('export-string-field'); + if (exportField) exportField.value = base64String; + } catch (e) { + console.error("Export-Fehler:", e); + } + }, + + async importFromString(code) { + try { + const binary = atob(code); + const bytes = Uint8Array.from(binary, c => c.charCodeAt(0)); + const stream = new Blob([bytes]).stream(); + const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip')); + const response = new Response(decompressedStream); + const resultText = await response.text(); + + const importedArray = JSON.parse(resultText); + this.presets = importedArray.map(item => ({ name: item[0], url: item[1] })); + this.save(); + location.reload(); + } catch (e) { + console.error("Import-Fehler:", e); + if (window.UI) window.UI.showAlert("Fehler", "Code ungültig oder beschädigt!"); + } + }, + + exportToFile() { + const presetsJSON = JSON.stringify(this.presets, null, 2); + const blob = new Blob([presetsJSON], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'radio_presets.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } +}; \ No newline at end of file diff --git a/sleeptimer.js b/sleeptimer.js new file mode 100644 index 0000000..e6793df --- /dev/null +++ b/sleeptimer.js @@ -0,0 +1,57 @@ +// sleeptimer.js - Verwaltet die automatische Abschaltung +window.SleepTimer = { + timeoutId: null, + endTime: null, + onExpiry: null, // Callback-Funktion, die bei Ablauf gerufen wird + + init(onExpiryCallback) { + this.onExpiry = onExpiryCallback; + }, + + start(minutes) { + this.clear(); + const durationMs = minutes * 60000; + this.endTime = Date.now() + durationMs; + + this.timeoutId = setTimeout(() => { + if (this.onExpiry) this.onExpiry(); + this.clear(); + }, durationMs); + + this.updateButtonUI(); + }, + + clear() { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + this.endTime = null; + this.updateButtonUI(); + }, + + updateButtonUI() { + const btn = document.getElementById('open-sleep-timer-modal'); + if (!btn) return; + + if (this.endTime) { + const remaining = Math.max(0, Math.ceil((this.endTime - Date.now()) / 60000)); + btn.innerHTML = ` ${remaining} Min`; + + // Alle 10 Sekunden die Anzeige aktualisieren + setTimeout(() => this.updateButtonUI(), 10000); + } else { + btn.textContent = 'Sleep Timer'; + } + + // Den "Aus"-Button im Modal steuern + const offBtn = document.getElementById('off-sleep-timer-modal'); + if (offBtn) { + offBtn.style.display = this.timeoutId ? 'inline-block' : 'none'; + } + }, + + isActive() { + return this.timeoutId !== null; + } +}; \ No newline at end of file diff --git a/ui-helper.js b/ui-helper.js new file mode 100644 index 0000000..b16ad76 --- /dev/null +++ b/ui-helper.js @@ -0,0 +1,44 @@ +// ui-helper.js - Verwaltet allgemeine UI-Elemente und Modals +window.UI = { + // Zeigt ein einfaches Informations-Fenster + showAlert(title, message) { + const modal = document.getElementById("custom-modal"); + document.getElementById("custom-modal-title").textContent = title; + document.getElementById("custom-modal-message").textContent = message; + document.getElementById("custom-modal-cancel").classList.add("hidden"); + modal.classList.remove("hidden"); + document.getElementById("custom-modal-ok").onclick = () => modal.classList.add("hidden"); + }, + + // Zeigt ein Bestätigungs-Fenster mit Ja/Nein + showConfirm(title, message, callback) { + const modal = document.getElementById("custom-modal"); + document.getElementById("custom-modal-title").textContent = title; + document.getElementById("custom-modal-message").textContent = message; + const cancelBtn = document.getElementById("custom-modal-cancel"); + cancelBtn.classList.remove("hidden"); + modal.classList.remove("hidden"); + + document.getElementById("custom-modal-ok").onclick = () => { + modal.classList.add("hidden"); + callback(true); + }; + cancelBtn.onclick = () => { + modal.classList.add("hidden"); + callback(false); + }; + }, + + // Aktualisiert den Titel im Browser-Tab + updateDocumentTitle(stationName) { + document.title = stationName ? `${stationName} - Radioplayer` : "Radioplayer"; + }, + + // Füllt die Informationen im Footer aus + initializeFooter() { + const yearEl = document.getElementById('current-year'); + const serverEl = document.getElementById('server-name'); + if (yearEl) yearEl.textContent = new Date().getFullYear(); + if (serverEl) serverEl.textContent = window.location.hostname; + } +}; \ No newline at end of file