Dateigröße pro Aufnahme entkoppelt - feste Größe nach Abschluss
Die Dateigröße wird jetzt pro Aufnahme korrekt berechnet statt das gesamte Verzeichnis zu summieren. Bei abgeschlossenen Aufnahmen wird die Größe einmalig gespeichert und nicht mehr durch laufende Aufnahmen verändert. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1ad87c7186
commit
851ff086ac
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>stream-recorder</title>
|
||||
<title>Stream-Recorder</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
@@ -30,6 +30,7 @@
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 52px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -338,15 +339,38 @@
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 50;
|
||||
text-align: center;
|
||||
padding: 12px 16px;
|
||||
background: rgba(15, 17, 23, 0.96);
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.site-footer a {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.site-footer a:last-child {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<header>
|
||||
<h1><span class="rec-dot" id="recDot"></span> stream-recorder</h1>
|
||||
<h1><span class="rec-dot" id="recDot"></span> Stream-Recorder</h1>
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('dashboard')">Dashboard</button>
|
||||
<button class="tab" onclick="switchTab('jobs')">Jobs</button>
|
||||
<button class="tab" onclick="switchTab('stations')">Sender</button>
|
||||
<button class="tab" onclick="switchTab('logs')">Logs</button>
|
||||
<button class="tab" onclick="switchTab('settings')">Einstellungen</button>
|
||||
</div>
|
||||
@@ -373,6 +397,15 @@
|
||||
<div id="jobsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Stations -->
|
||||
<div class="panel" id="panel-stations">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
||||
<p class="section-title" style="margin:0">Senderbibliothek</p>
|
||||
<button class="btn btn-primary btn-sm" onclick="openStationModal()">+ Neuer Sender</button>
|
||||
</div>
|
||||
<div id="stationsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="panel" id="panel-logs">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
||||
@@ -402,6 +435,12 @@
|
||||
<div class="modal-overlay" id="quickRecordModal">
|
||||
<div class="modal">
|
||||
<div class="modal-title">Schnellaufnahme</div>
|
||||
<div class="form-group">
|
||||
<label>Senderbibliothek</label>
|
||||
<select id="qr-station" onchange="applyStationToQuickRecord()">
|
||||
<option value="">URL manuell eingeben</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Stream-URL *</label>
|
||||
<input type="text" id="qr-url" placeholder="rtmp://... oder https://...m3u8">
|
||||
@@ -442,6 +481,12 @@
|
||||
<input type="hidden" id="job-edit-id">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group full">
|
||||
<label>Senderbibliothek</label>
|
||||
<select id="job-station" onchange="applyStationToJob()">
|
||||
<option value="">URL manuell eingeben</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label>Stream-URL *</label>
|
||||
<input type="text" id="job-url" placeholder="rtmp://... oder https://...m3u8">
|
||||
@@ -574,6 +619,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Station Modal -->
|
||||
<div class="modal-overlay" id="stationModal">
|
||||
<div class="modal">
|
||||
<div class="modal-title" id="stationModalTitle">Neuer Sender</div>
|
||||
<input type="hidden" id="station-edit-id">
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Name *</label>
|
||||
<input type="text" id="station-name" placeholder="Sendername">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Typ</label>
|
||||
<select id="station-type">
|
||||
<option value="auto">Automatisch</option>
|
||||
<option value="rtmp">RTMP</option>
|
||||
<option value="hls">HLS</option>
|
||||
<option value="mp3">MP3</option>
|
||||
<option value="aac">AAC</option>
|
||||
<option value="ogg">OGG</option>
|
||||
<option value="http">HTTP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label>Stream-URL *</label>
|
||||
<input type="text" id="station-url" placeholder="rtmp://... oder https://...m3u8">
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<label>Beschreibung (optional)</label>
|
||||
<textarea id="station-description" rows="3" placeholder="z.B. Hauptstream, Wochenendprogramm..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn" onclick="closeModal('stationModal')">Abbrechen</button>
|
||||
<button class="btn btn-primary" onclick="saveStation()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Extend Modal -->
|
||||
<div class="modal-overlay" id="extendModal">
|
||||
<div class="modal" style="max-width:400px">
|
||||
@@ -602,6 +685,7 @@
|
||||
|
||||
<script>
|
||||
const API = '';
|
||||
let stationsCache = [];
|
||||
|
||||
async function api(path, opts = {}) {
|
||||
const res = await fetch(API + path, {
|
||||
@@ -620,12 +704,20 @@ function switchTab(name) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||
document.querySelector(`#panel-${name}`).classList.add('active');
|
||||
const tabLabel = {
|
||||
dashboard: 'dash',
|
||||
jobs: 'jobs',
|
||||
stations: 'sender',
|
||||
logs: 'logs',
|
||||
settings: 'einst',
|
||||
}[name] || name;
|
||||
document.querySelectorAll('.tab').forEach(t => {
|
||||
if (t.textContent.toLowerCase().includes(name === 'dashboard' ? 'dash' : name === 'settings' ? 'einst' : name))
|
||||
if (t.textContent.toLowerCase().includes(tabLabel))
|
||||
t.classList.add('active');
|
||||
});
|
||||
if (name === 'logs') loadLogs();
|
||||
if (name === 'jobs') loadJobs();
|
||||
if (name === 'stations') loadStations();
|
||||
if (name === 'settings') loadSettings();
|
||||
}
|
||||
|
||||
@@ -706,6 +798,127 @@ function renderRecent(recs) {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Stations
|
||||
async function loadStationOptions() {
|
||||
try {
|
||||
stationsCache = await api('/api/stations');
|
||||
populateStationSelects();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function populateStationSelects() {
|
||||
['qr-station', 'job-station'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
const selected = el.value;
|
||||
el.innerHTML = '<option value="">URL manuell eingeben</option>' + stationsCache.map(s =>
|
||||
`<option value="${s.id}">${esc(s.name)}${s.description ? ' - ' + esc(s.description) : ''}</option>`
|
||||
).join('');
|
||||
if (stationsCache.some(s => String(s.id) === selected)) el.value = selected;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadStations() {
|
||||
try {
|
||||
await loadStationOptions();
|
||||
const el = document.getElementById('stationsList');
|
||||
if (!stationsCache.length) {
|
||||
el.innerHTML = '<div class="empty"><div class="empty-icon">📻</div>Keine Sender gespeichert<br><button class="btn btn-primary" style="margin-top:12px" onclick="openStationModal()">Ersten Sender anlegen</button></div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = stationsCache.map(s => `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
${esc(s.name)}
|
||||
<span class="badge badge-scheduled">${esc(s.stream_type || 'auto')}</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-sm" onclick="openStationModal(${s.id})">Bearbeiten</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteStation(${s.id})">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
${s.description ? `<div class="meta" style="margin-bottom:4px"><span class="meta-item" style="color:var(--text)">${esc(s.description)}</span></div>` : ''}
|
||||
<div class="meta">
|
||||
<span class="meta-item">${esc(s.stream_url)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function openStationModal(id = null) {
|
||||
const station = id ? stationsCache.find(s => s.id === id) : null;
|
||||
document.getElementById('station-edit-id').value = station ? station.id : '';
|
||||
document.getElementById('stationModalTitle').textContent = station ? 'Sender bearbeiten' : 'Neuer Sender';
|
||||
document.getElementById('station-name').value = station ? station.name : '';
|
||||
document.getElementById('station-url').value = station ? station.stream_url : '';
|
||||
document.getElementById('station-type').value = station ? station.stream_type || 'auto' : 'auto';
|
||||
document.getElementById('station-description').value = station ? station.description || '' : '';
|
||||
openModal('stationModal');
|
||||
}
|
||||
|
||||
async function saveStation() {
|
||||
const editId = document.getElementById('station-edit-id').value;
|
||||
const data = {
|
||||
name: document.getElementById('station-name').value,
|
||||
stream_url: document.getElementById('station-url').value,
|
||||
stream_type: document.getElementById('station-type').value,
|
||||
description: document.getElementById('station-description').value,
|
||||
};
|
||||
if (!data.name || !data.stream_url) {
|
||||
alert('Name und URL sind Pflichtfelder');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (editId) {
|
||||
await api(`/api/stations/${editId}`, { method: 'PUT', body: JSON.stringify(data) });
|
||||
} else {
|
||||
await api('/api/stations', { method: 'POST', body: JSON.stringify(data) });
|
||||
}
|
||||
closeModal('stationModal');
|
||||
loadStations();
|
||||
} catch (e) {
|
||||
alert('Fehler: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteStation(id) {
|
||||
if (!confirm('Sender wirklich löschen?')) return;
|
||||
await api(`/api/stations/${id}`, { method: 'DELETE' });
|
||||
loadStations();
|
||||
}
|
||||
|
||||
function applyStationToQuickRecord() {
|
||||
const id = document.getElementById('qr-station').value;
|
||||
const station = stationsCache.find(s => String(s.id) === id);
|
||||
if (!station) return;
|
||||
document.getElementById('qr-url').value = station.stream_url;
|
||||
document.getElementById('qr-type').value = station.stream_type || 'auto';
|
||||
if (!document.getElementById('qr-name').value) {
|
||||
document.getElementById('qr-name').value = station.name;
|
||||
}
|
||||
}
|
||||
|
||||
function applyStationToJob() {
|
||||
const id = document.getElementById('job-station').value;
|
||||
const station = stationsCache.find(s => String(s.id) === id);
|
||||
if (!station) return;
|
||||
document.getElementById('job-url').value = station.stream_url;
|
||||
document.getElementById('job-type').value = station.stream_type || 'auto';
|
||||
if (!document.getElementById('job-name').value) {
|
||||
document.getElementById('job-name').value = station.name;
|
||||
}
|
||||
if (!document.getElementById('job-description').value && station.description) {
|
||||
document.getElementById('job-description').value = station.description;
|
||||
}
|
||||
}
|
||||
|
||||
// Jobs
|
||||
async function loadJobs() {
|
||||
try {
|
||||
@@ -748,8 +961,10 @@ async function loadJobs() {
|
||||
}
|
||||
|
||||
function openJobModal(job = null) {
|
||||
loadStationOptions();
|
||||
document.getElementById('job-edit-id').value = job ? job.id : '';
|
||||
document.getElementById('jobModalTitle').textContent = job ? 'Job bearbeiten' : 'Neuer Job';
|
||||
document.getElementById('job-station').value = '';
|
||||
document.getElementById('job-url').value = job ? job.stream_url : '';
|
||||
document.getElementById('job-name').value = job ? job.name : '';
|
||||
document.getElementById('job-type').value = job ? job.stream_type : 'auto';
|
||||
@@ -915,6 +1130,8 @@ function toggleMetadata() {
|
||||
|
||||
// Quick Record
|
||||
function openQuickRecord() {
|
||||
loadStationOptions();
|
||||
document.getElementById('qr-station').value = '';
|
||||
document.getElementById('qr-url').value = '';
|
||||
document.getElementById('qr-name').value = '';
|
||||
document.getElementById('qr-type').value = 'auto';
|
||||
@@ -1121,10 +1338,11 @@ document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
});
|
||||
|
||||
// Auto-refresh
|
||||
loadStationOptions();
|
||||
loadDashboard();
|
||||
setInterval(loadDashboard, 5000);
|
||||
</script>
|
||||
<footer style="text-align:center;padding:32px 16px 16px;color:var(--text-dim);font-size:0.75rem">
|
||||
<footer class="site-footer">
|
||||
stream-recorder © <a href="https://www.patrick-asmus.de" target="_blank" rel="noopener" style="color:var(--text-dim)">Patrick Asmus</a> · <a href="https://git.techniverse.net/scriptos/stream-recorder" target="_blank" rel="noopener" style="color:var(--accent)">Source</a>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user