Add recording of livestreams
This commit is contained in:
@@ -90,6 +90,13 @@
|
||||
<div class="fieldDescription">Password for proxy authentication (leave empty if not required)</div>
|
||||
</div>
|
||||
<br />
|
||||
<h2>Recording Settings</h2>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="RecordingOutputPath">Recording Output Directory</label>
|
||||
<input id="RecordingOutputPath" name="RecordingOutputPath" type="text" is="emby-input" placeholder="e.g., /media/recordings/srf" />
|
||||
<div class="fieldDescription">Directory where sport livestream recordings will be saved (requires ffmpeg)</div>
|
||||
</div>
|
||||
<br />
|
||||
<h2>Network Settings</h2>
|
||||
<div class="inputContainer">
|
||||
<label class="inputLabel inputLabelUnfocused" for="PublicServerUrl">Public Server URL (Optional)</label>
|
||||
@@ -106,6 +113,31 @@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br />
|
||||
<h2>Sport Livestream Recordings</h2>
|
||||
|
||||
<h3>Upcoming Sport Livestreams</h3>
|
||||
<div class="fieldDescription">Select livestreams to record. The recording starts automatically when the stream goes live.</div>
|
||||
<div id="scheduleContainer" style="margin: 10px 0;">
|
||||
<p><em>Loading schedule...</em></p>
|
||||
</div>
|
||||
<button is="emby-button" type="button" class="raised emby-button" onclick="SRFPlayRecordings.loadSchedule()" style="margin-bottom: 20px;">
|
||||
<span>Refresh Schedule</span>
|
||||
</button>
|
||||
|
||||
<h3>Scheduled & Active Recordings</h3>
|
||||
<div id="activeRecordingsContainer" style="margin: 10px 0;">
|
||||
<p><em>Loading...</em></p>
|
||||
</div>
|
||||
|
||||
<h3>Completed Recordings</h3>
|
||||
<div id="completedRecordingsContainer" style="margin: 10px 0;">
|
||||
<p><em>Loading...</em></p>
|
||||
</div>
|
||||
<button is="emby-button" type="button" class="raised emby-button" onclick="SRFPlayRecordings.loadRecordings()" style="margin-bottom: 20px;">
|
||||
<span>Refresh Recordings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
@@ -130,7 +162,12 @@
|
||||
document.querySelector('#ProxyUsername').value = config.ProxyUsername || '';
|
||||
document.querySelector('#ProxyPassword').value = config.ProxyPassword || '';
|
||||
document.querySelector('#PublicServerUrl').value = config.PublicServerUrl || '';
|
||||
document.querySelector('#RecordingOutputPath').value = config.RecordingOutputPath || '';
|
||||
Dashboard.hideLoadingMsg();
|
||||
|
||||
// Load recordings UI
|
||||
SRFPlayRecordings.loadSchedule();
|
||||
SRFPlayRecordings.loadRecordings();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,6 +188,7 @@
|
||||
config.ProxyUsername = document.querySelector('#ProxyUsername').value;
|
||||
config.ProxyPassword = document.querySelector('#ProxyPassword').value;
|
||||
config.PublicServerUrl = document.querySelector('#PublicServerUrl').value;
|
||||
config.RecordingOutputPath = document.querySelector('#RecordingOutputPath').value;
|
||||
ApiClient.updatePluginConfiguration(SRFPlayConfig.pluginUniqueId, config).then(function (result) {
|
||||
Dashboard.processPluginConfigurationUpdateResult(result);
|
||||
});
|
||||
@@ -159,6 +197,204 @@
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
var SRFPlayRecordings = {
|
||||
apiBase: ApiClient.serverAddress() + '/Plugins/SRFPlay/Recording',
|
||||
|
||||
getHeaders: function() {
|
||||
return {
|
||||
'X-Emby-Token': ApiClient.accessToken()
|
||||
};
|
||||
},
|
||||
|
||||
formatDate: function(dateStr) {
|
||||
if (!dateStr) return 'N/A';
|
||||
var d = new Date(dateStr);
|
||||
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
},
|
||||
|
||||
formatSize: function(bytes) {
|
||||
if (!bytes) return '';
|
||||
if (bytes > 1073741824) return (bytes / 1073741824).toFixed(1) + ' GB';
|
||||
if (bytes > 1048576) return (bytes / 1048576).toFixed(0) + ' MB';
|
||||
return (bytes / 1024).toFixed(0) + ' KB';
|
||||
},
|
||||
|
||||
loadSchedule: function() {
|
||||
var container = document.getElementById('scheduleContainer');
|
||||
container.innerHTML = '<p><em>Loading schedule...</em></p>';
|
||||
|
||||
fetch(this.apiBase + '/Schedule', { headers: this.getHeaders() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(programs) {
|
||||
if (!programs || programs.length === 0) {
|
||||
container.innerHTML = '<p>No upcoming sport livestreams found.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<table style="width:100%; border-collapse: collapse;">';
|
||||
html += '<thead><tr style="text-align:left; border-bottom: 1px solid #444;">';
|
||||
html += '<th style="padding: 8px;">Title</th>';
|
||||
html += '<th style="padding: 8px;">Start</th>';
|
||||
html += '<th style="padding: 8px;">End</th>';
|
||||
html += '<th style="padding: 8px;">Action</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
programs.forEach(function(p) {
|
||||
html += '<tr style="border-bottom: 1px solid #333;">';
|
||||
html += '<td style="padding: 8px;">' + (p.title || 'Unknown') + '</td>';
|
||||
html += '<td style="padding: 8px;">' + SRFPlayRecordings.formatDate(p.validFrom || p.date) + '</td>';
|
||||
html += '<td style="padding: 8px;">' + SRFPlayRecordings.formatDate(p.validTo) + '</td>';
|
||||
html += '<td style="padding: 8px;">';
|
||||
html += '<button is="emby-button" type="button" class="raised emby-button" ';
|
||||
html += 'onclick="SRFPlayRecordings.scheduleRecording(\'' + encodeURIComponent(p.urn) + '\')">';
|
||||
html += '<span>Record</span></button>';
|
||||
html += '</td></tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
})
|
||||
.catch(function(err) {
|
||||
container.innerHTML = '<p style="color: #f44;">Error loading schedule: ' + err.message + '</p>';
|
||||
});
|
||||
},
|
||||
|
||||
scheduleRecording: function(encodedUrn) {
|
||||
fetch(this.apiBase + '/Schedule/' + encodedUrn, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders()
|
||||
})
|
||||
.then(function(r) {
|
||||
if (r.ok) {
|
||||
Dashboard.alert('Recording scheduled!');
|
||||
SRFPlayRecordings.loadRecordings();
|
||||
} else {
|
||||
Dashboard.alert('Failed to schedule recording');
|
||||
}
|
||||
})
|
||||
.catch(function(err) { Dashboard.alert('Error: ' + err.message); });
|
||||
},
|
||||
|
||||
loadRecordings: function() {
|
||||
this.loadActiveRecordings();
|
||||
this.loadCompletedRecordings();
|
||||
},
|
||||
|
||||
loadActiveRecordings: function() {
|
||||
var container = document.getElementById('activeRecordingsContainer');
|
||||
container.innerHTML = '<p><em>Loading...</em></p>';
|
||||
|
||||
fetch(this.apiBase + '/All', { headers: this.getHeaders() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(recordings) {
|
||||
var active = recordings.filter(function(r) {
|
||||
return r.state === 0 || r.state === 1 || r.state === 2;
|
||||
});
|
||||
|
||||
if (active.length === 0) {
|
||||
container.innerHTML = '<p>No scheduled or active recordings.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var stateLabels = {0: 'Scheduled', 1: 'Waiting', 2: 'Recording', 3: 'Completed', 4: 'Failed', 5: 'Cancelled'};
|
||||
var stateColors = {0: '#2196F3', 1: '#FF9800', 2: '#4CAF50', 4: '#f44336', 5: '#9E9E9E'};
|
||||
|
||||
var html = '<table style="width:100%; border-collapse: collapse;">';
|
||||
html += '<thead><tr style="text-align:left; border-bottom: 1px solid #444;">';
|
||||
html += '<th style="padding: 8px;">Title</th>';
|
||||
html += '<th style="padding: 8px;">Status</th>';
|
||||
html += '<th style="padding: 8px;">Start</th>';
|
||||
html += '<th style="padding: 8px;">Action</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
active.forEach(function(r) {
|
||||
html += '<tr style="border-bottom: 1px solid #333;">';
|
||||
html += '<td style="padding: 8px;">' + (r.title || 'Unknown') + '</td>';
|
||||
html += '<td style="padding: 8px;"><span style="color:' + (stateColors[r.state] || '#fff') + ';">' + (stateLabels[r.state] || r.state) + '</span></td>';
|
||||
html += '<td style="padding: 8px;">' + SRFPlayRecordings.formatDate(r.validFrom) + '</td>';
|
||||
html += '<td style="padding: 8px;">';
|
||||
if (r.state === 2) {
|
||||
html += '<button is="emby-button" type="button" class="raised emby-button" ';
|
||||
html += 'onclick="SRFPlayRecordings.stopRecording(\'' + r.id + '\')"><span>Stop</span></button>';
|
||||
} else {
|
||||
html += '<button is="emby-button" type="button" class="raised emby-button" ';
|
||||
html += 'onclick="SRFPlayRecordings.cancelRecording(\'' + r.id + '\')"><span>Cancel</span></button>';
|
||||
}
|
||||
html += '</td></tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
})
|
||||
.catch(function(err) {
|
||||
container.innerHTML = '<p style="color: #f44;">Error: ' + err.message + '</p>';
|
||||
});
|
||||
},
|
||||
|
||||
loadCompletedRecordings: function() {
|
||||
var container = document.getElementById('completedRecordingsContainer');
|
||||
container.innerHTML = '<p><em>Loading...</em></p>';
|
||||
|
||||
fetch(this.apiBase + '/Completed', { headers: this.getHeaders() })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(recordings) {
|
||||
if (!recordings || recordings.length === 0) {
|
||||
container.innerHTML = '<p>No completed recordings.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<table style="width:100%; border-collapse: collapse;">';
|
||||
html += '<thead><tr style="text-align:left; border-bottom: 1px solid #444;">';
|
||||
html += '<th style="padding: 8px;">Title</th>';
|
||||
html += '<th style="padding: 8px;">Recorded</th>';
|
||||
html += '<th style="padding: 8px;">Size</th>';
|
||||
html += '<th style="padding: 8px;">File</th>';
|
||||
html += '<th style="padding: 8px;">Action</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
recordings.forEach(function(r) {
|
||||
html += '<tr style="border-bottom: 1px solid #333;">';
|
||||
html += '<td style="padding: 8px;">' + (r.title || 'Unknown') + '</td>';
|
||||
html += '<td style="padding: 8px;">' + SRFPlayRecordings.formatDate(r.recordingStartedAt) + '</td>';
|
||||
html += '<td style="padding: 8px;">' + SRFPlayRecordings.formatSize(r.fileSizeBytes) + '</td>';
|
||||
html += '<td style="padding: 8px; font-size: 0.85em; word-break: break-all;">' + (r.outputPath || '') + '</td>';
|
||||
html += '<td style="padding: 8px;">';
|
||||
html += '<button is="emby-button" type="button" class="raised emby-button" style="background:#f44336;" ';
|
||||
html += 'onclick="SRFPlayRecordings.deleteRecording(\'' + r.id + '\')"><span>Delete</span></button>';
|
||||
html += '</td></tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
})
|
||||
.catch(function(err) {
|
||||
container.innerHTML = '<p style="color: #f44;">Error: ' + err.message + '</p>';
|
||||
});
|
||||
},
|
||||
|
||||
stopRecording: function(id) {
|
||||
fetch(this.apiBase + '/Active/' + id + '/Stop', {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders()
|
||||
}).then(function() { SRFPlayRecordings.loadRecordings(); });
|
||||
},
|
||||
|
||||
cancelRecording: function(id) {
|
||||
fetch(this.apiBase + '/Schedule/' + id, {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders()
|
||||
}).then(function() { SRFPlayRecordings.loadRecordings(); });
|
||||
},
|
||||
|
||||
deleteRecording: function(id) {
|
||||
if (!confirm('Delete this recording and its file?')) return;
|
||||
fetch(this.apiBase + '/Completed/' + id + '?deleteFile=true', {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders()
|
||||
}).then(function() { SRFPlayRecordings.loadRecordings(); });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user