Upload files to "/"

Neu aufgeteilter Code
This commit is contained in:
simonpipe 2025-12-18 17:17:16 +01:00
parent 3d4f6bb803
commit 6c1b185fc6
5 changed files with 325 additions and 0 deletions

79
meta-handler.js Normal file
View file

@ -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();

69
metadata.php Normal file
View file

@ -0,0 +1,69 @@
<?php
/**
* optional/metadata.php
* Optimierte Version zum Auslesen von Metadaten
*/
header('Access-Control-Allow-Origin: *');
header('Content-Type: application/json');
$url = isset($_GET['url']) ? $_GET['url'] : '';
if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
echo json_encode(['error' => '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']);
}

76
preset-manager.js Normal file
View file

@ -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);
}
};

57
sleeptimer.js Normal file
View file

@ -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 = `<i class="fas fa-hourglass-half"></i> ${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;
}
};

44
ui-helper.js Normal file
View file

@ -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;
}
};