Upload files to "/"
Neu aufgeteilter Code
This commit is contained in:
parent
3d4f6bb803
commit
6c1b185fc6
5 changed files with 325 additions and 0 deletions
79
meta-handler.js
Normal file
79
meta-handler.js
Normal 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
69
metadata.php
Normal 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
76
preset-manager.js
Normal 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
57
sleeptimer.js
Normal 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
44
ui-helper.js
Normal 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;
|
||||
}
|
||||
};
|
||||
Loading…
Reference in a new issue