Fix settings no longer redirect.
All checks were successful
🏗️ Build Plugin / build (push) Successful in 1m16s
Latest Release / latest-release (push) Successful in 42s
🧪 Test Plugin / test (push) Successful in 27s
🚀 Release Plugin / build-and-release (push) Successful in 42s

This commit is contained in:
Duncan Tourolle 2026-06-27 08:50:49 +02:00
parent a39a10636b
commit 10a76f8d45
3 changed files with 328 additions and 329 deletions

View File

@ -1,11 +1,4 @@
<!DOCTYPE html> <div id="SRFPlayConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<html lang="en">
<head>
<meta charset="utf-8">
<title>SRF Play</title>
</head>
<body>
<div id="SRFPlayConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<div data-role="content"> <div data-role="content">
<div class="content-primary"> <div class="content-primary">
<form id="SRFPlayConfigForm"> <form id="SRFPlayConfigForm">
@ -398,5 +391,3 @@
}; };
</script> </script>
</div> </div>
</body>
</html>

View File

@ -1,344 +1,346 @@
<!DOCTYPE html> <div id="SRFPlayRecordingsPage" data-role="page" class="page type-interior pluginConfigurationPage"
<html lang="en"> data-require="emby-button,emby-input">
<head> <style>
<meta charset="utf-8"> #SRFPlayRecordingsPage .srfrec-wrap { padding: 0 1em; }
<meta name="viewport" content="width=device-width, initial-scale=1.0"> #SRFPlayRecordingsPage table { width: 100%; border-collapse: collapse; margin: 10px 0 20px; }
<title>SRF Sport Recordings</title> #SRFPlayRecordingsPage th { text-align: left; padding: 10px 8px; border-bottom: 2px solid #444; color: #aaa; font-size: 0.85em; text-transform: uppercase; }
<style> #SRFPlayRecordingsPage td { padding: 10px 8px; border-bottom: 1px solid #2a2a3e; }
* { box-sizing: border-box; margin: 0; padding: 0; } #SRFPlayRecordingsPage tr:hover { background: rgba(255,255,255,0.04); }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #e0e0e0; padding: 20px; } #SRFPlayRecordingsPage .btn { padding: 6px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85em; color: #fff; transition: opacity 0.2s; }
h1 { color: #fff; margin-bottom: 8px; font-size: 1.8em; } #SRFPlayRecordingsPage .btn:hover { opacity: 0.85; }
h2 { color: #ccc; margin: 24px 0 12px; font-size: 1.3em; border-bottom: 1px solid #333; padding-bottom: 6px; } #SRFPlayRecordingsPage .btn-record { background: #4CAF50; }
.subtitle { color: #999; margin-bottom: 20px; font-size: 0.9em; } #SRFPlayRecordingsPage .btn-stop { background: #FF9800; }
table { width: 100%; border-collapse: collapse; margin: 10px 0 20px; } #SRFPlayRecordingsPage .btn-cancel { background: #9E9E9E; }
th { text-align: left; padding: 10px 8px; border-bottom: 2px solid #444; color: #aaa; font-size: 0.85em; text-transform: uppercase; } #SRFPlayRecordingsPage .btn-delete { background: #f44336; }
td { padding: 10px 8px; border-bottom: 1px solid #2a2a3e; } #SRFPlayRecordingsPage .btn-refresh { background: #2196F3; margin-bottom: 16px; }
tr:hover { background: #2a2a3e; } #SRFPlayRecordingsPage .status { padding: 3px 8px; border-radius: 3px; font-size: 0.8em; font-weight: 600; color: #fff; }
.btn { padding: 6px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 0.85em; color: #fff; transition: opacity 0.2s; } #SRFPlayRecordingsPage .status-scheduled { background: #1565C0; }
.btn:hover { opacity: 0.85; } #SRFPlayRecordingsPage .status-waiting { background: #E65100; }
.btn-record { background: #4CAF50; } #SRFPlayRecordingsPage .status-recording { background: #2E7D32; }
.btn-stop { background: #FF9800; } #SRFPlayRecordingsPage .status-failed { background: #C62828; }
.btn-cancel { background: #9E9E9E; } #SRFPlayRecordingsPage .srfrec-msg { padding: 12px; color: #999; font-style: italic; }
.btn-delete { background: #f44336; } #SRFPlayRecordingsPage .srfrec-error { color: #f44; }
.btn-refresh { background: #2196F3; margin-bottom: 16px; } #SRFPlayRecordingsPage .login-form { max-width: 400px; margin: 40px auto; }
.status { padding: 3px 8px; border-radius: 3px; font-size: 0.8em; font-weight: 600; } #SRFPlayRecordingsPage .login-form input { width: 100%; padding: 10px; margin: 8px 0; background: #2a2a3e; border: 1px solid #444; color: #fff; border-radius: 4px; font-size: 1em; }
.status-scheduled { background: #1565C0; } #SRFPlayRecordingsPage .login-form .btn { width: 100%; padding: 12px; font-size: 1em; margin-top: 12px; }
.status-waiting { background: #E65100; } </style>
.status-recording { background: #2E7D32; } <div data-role="content">
.status-failed { background: #C62828; } <div class="content-primary srfrec-wrap">
.msg { padding: 12px; color: #999; font-style: italic; } <div id="srfrecLoginSection" style="display:none;">
.error { color: #f44; } <div class="login-form">
.login-form { max-width: 400px; margin: 60px auto; } <h1>SRF Sport Recordings</h1>
.login-form input { width: 100%; padding: 10px; margin: 8px 0; background: #2a2a3e; border: 1px solid #444; color: #fff; border-radius: 4px; font-size: 1em; } <p class="fieldDescription">Sign in to your Jellyfin server</p>
.login-form .btn { width: 100%; padding: 12px; font-size: 1em; margin-top: 12px; } <input type="text" id="srfrecLoginServer" placeholder="Server URL (e.g. http://192.168.1.50:8096)" />
@media (max-width: 600px) { body { padding: 10px; } td, th { padding: 6px 4px; font-size: 0.85em; } } <input type="text" id="srfrecLoginUser" placeholder="Username" />
</style> <input type="password" id="srfrecLoginPass" placeholder="Password" />
</head> <button type="button" class="btn btn-record" onclick="SRFRec.login()">Sign In</button>
<body> <p id="srfrecLoginError" class="srfrec-error" style="margin-top:10px;"></p>
<div id="loginSection" style="display:none;"> </div>
<div class="login-form"> </div>
<h1>SRF Sport Recordings</h1>
<p class="subtitle">Sign in to your Jellyfin server</p> <div id="srfrecMainSection" style="display:none;">
<input type="text" id="loginServer" placeholder="Server URL (e.g. http://192.168.1.50:8096)" /> <h1>SRF Sport Recordings</h1>
<input type="text" id="loginUser" placeholder="Username" /> <p class="fieldDescription">Browse upcoming sport livestreams and schedule recordings. <span id="srfrecUserInfo"></span></p>
<input type="password" id="loginPass" placeholder="Password" />
<button class="btn btn-record" onclick="SRFRec.login()">Sign In</button> <h2>Upcoming Sport Livestreams</h2>
<p id="loginError" class="error" style="margin-top:10px;"></p> <button type="button" class="btn btn-refresh" onclick="SRFRec.loadSchedule()">Refresh Schedule</button>
<div id="srfrecScheduleContainer"><p class="srfrec-msg">Loading schedule...</p></div>
<h2>Scheduled &amp; Active Recordings</h2>
<div id="srfrecActiveRecordingsContainer"><p class="srfrec-msg">Loading...</p></div>
<h2>Completed Recordings</h2>
<button type="button" class="btn btn-refresh" onclick="SRFRec.loadRecordings()">Refresh</button>
<div id="srfrecCompletedRecordingsContainer"><p class="srfrec-msg">Loading...</p></div>
</div>
</div>
</div> </div>
</div>
<div id="mainSection" style="display:none;"> <script>
<h1>SRF Sport Recordings</h1> var SRFRec = {
<p class="subtitle">Browse upcoming sport livestreams and schedule recordings. <span id="userInfo"></span></p> serverUrl: '',
token: '',
<h2>Upcoming Sport Livestreams</h2> init: function() {
<button class="btn btn-refresh" onclick="SRFRec.loadSchedule()">Refresh Schedule</button> // Check if we're inside Jellyfin's web client (ApiClient available)
<div id="scheduleContainer"><p class="msg">Loading schedule...</p></div> if (typeof ApiClient !== 'undefined' && ApiClient.accessToken()) {
this.serverUrl = ApiClient.serverAddress();
<h2>Scheduled &amp; Active Recordings</h2> this.token = ApiClient.accessToken();
<div id="activeRecordingsContainer"><p class="msg">Loading...</p></div> this.showMain();
<h2>Completed Recordings</h2>
<button class="btn btn-refresh" onclick="SRFRec.loadRecordings()">Refresh</button>
<div id="completedRecordingsContainer"><p class="msg">Loading...</p></div>
</div>
<script>
var SRFRec = {
serverUrl: '',
token: '',
init: function() {
// Check if we're inside Jellyfin's web client (ApiClient available)
if (typeof ApiClient !== 'undefined' && ApiClient.accessToken()) {
this.serverUrl = ApiClient.serverAddress();
this.token = ApiClient.accessToken();
this.showMain();
return;
}
// Check localStorage for saved session
var saved = localStorage.getItem('srfRecSession');
if (saved) {
try {
var session = JSON.parse(saved);
this.serverUrl = session.serverUrl;
this.token = session.token;
// Verify token still works
this.verifySession(session);
return; return;
} catch(e) { /* fall through to login */ } }
}
// Show login form // Check localStorage for saved session
var serverInput = document.getElementById('loginServer'); var saved = localStorage.getItem('srfRecSession');
serverInput.value = window.location.origin; if (saved) {
document.getElementById('loginSection').style.display = 'block'; try {
}, var session = JSON.parse(saved);
this.serverUrl = session.serverUrl;
this.token = session.token;
// Verify token still works
this.verifySession(session);
return;
} catch(e) { /* fall through to login */ }
}
verifySession: function(session) { // Show login form
var self = this; var serverInput = document.getElementById('srfrecLoginServer');
fetch(this.serverUrl + '/System/Info', { serverInput.value = window.location.origin;
headers: { 'X-Emby-Token': this.token } document.getElementById('srfrecLoginSection').style.display = 'block';
}).then(function(r) { },
if (r.ok) {
document.getElementById('userInfo').textContent = '(Server: ' + self.serverUrl + ')'; verifySession: function(session) {
self.showMain(); var self = this;
} else { fetch(this.serverUrl + '/System/Info', {
headers: { 'X-Emby-Token': this.token }
}).then(function(r) {
if (r.ok) {
document.getElementById('srfrecUserInfo').textContent = '(Server: ' + self.serverUrl + ')';
self.showMain();
} else {
localStorage.removeItem('srfRecSession');
document.getElementById('srfrecLoginSection').style.display = 'block';
}
}).catch(function() {
localStorage.removeItem('srfRecSession'); localStorage.removeItem('srfRecSession');
document.getElementById('loginSection').style.display = 'block'; document.getElementById('srfrecLoginSection').style.display = 'block';
} });
}).catch(function() { },
localStorage.removeItem('srfRecSession');
document.getElementById('loginSection').style.display = 'block';
});
},
login: function() { login: function() {
var self = this; var self = this;
var server = document.getElementById('loginServer').value.replace(/\/+$/, ''); var server = document.getElementById('srfrecLoginServer').value.replace(/\/+$/, '');
var user = document.getElementById('loginUser').value; var user = document.getElementById('srfrecLoginUser').value;
var pass = document.getElementById('loginPass').value; var pass = document.getElementById('srfrecLoginPass').value;
var errorEl = document.getElementById('loginError'); var errorEl = document.getElementById('srfrecLoginError');
errorEl.textContent = ''; errorEl.textContent = '';
fetch(server + '/Users/AuthenticateByName', { fetch(server + '/Users/AuthenticateByName', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Emby-Authorization': 'MediaBrowser Client="SRF Recordings", Device="Web", DeviceId="srfrec-' + Date.now() + '", Version="1.0.0"' 'X-Emby-Authorization': 'MediaBrowser Client="SRF Recordings", Device="Web", DeviceId="srfrec-' + Date.now() + '", Version="1.0.0"'
}, },
body: JSON.stringify({ Username: user, Pw: pass }) body: JSON.stringify({ Username: user, Pw: pass })
}) })
.then(function(r) { .then(function(r) {
if (!r.ok) throw new Error('Authentication failed'); if (!r.ok) throw new Error('Authentication failed');
return r.json(); return r.json();
}) })
.then(function(data) { .then(function(data) {
self.serverUrl = server; self.serverUrl = server;
self.token = data.AccessToken; self.token = data.AccessToken;
localStorage.setItem('srfRecSession', JSON.stringify({ localStorage.setItem('srfRecSession', JSON.stringify({
serverUrl: server, serverUrl: server,
token: data.AccessToken, token: data.AccessToken,
userName: data.User.Name userName: data.User.Name
})); }));
document.getElementById('userInfo').textContent = '(Signed in as ' + data.User.Name + ')'; document.getElementById('srfrecUserInfo').textContent = '(Signed in as ' + data.User.Name + ')';
self.showMain(); self.showMain();
})
.catch(function(err) {
errorEl.textContent = err.message;
});
},
showMain: function() {
document.getElementById('loginSection').style.display = 'none';
document.getElementById('mainSection').style.display = 'block';
this.loadSchedule();
this.loadRecordings();
},
getHeaders: function() {
return { 'X-Emby-Token': this.token };
},
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 class="msg">Loading schedule...</p>';
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Schedule', { headers: this.getHeaders() })
.then(function(r) { return r.json(); })
.then(function(programs) {
if (!programs || programs.length === 0) {
container.innerHTML = '<p class="msg">No upcoming sport livestreams found.</p>';
return;
}
var html = '<table>';
html += '<thead><tr><th>Title</th><th>Start</th><th>End</th><th>Action</th></tr></thead><tbody>';
programs.forEach(function(p) {
html += '<tr>';
html += '<td>' + (p.title || 'Unknown') + '</td>';
html += '<td>' + SRFRec.formatDate(p.validFrom || p.date) + '</td>';
html += '<td>' + SRFRec.formatDate(p.validTo) + '</td>';
html += '<td><button class="btn btn-record" onclick="SRFRec.scheduleRecording(\'' + encodeURIComponent(p.urn) + '\')">Record</button></td>';
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
}) })
.catch(function(err) { .catch(function(err) {
container.innerHTML = '<p class="error">Error loading schedule: ' + err.message + '</p>'; errorEl.textContent = err.message;
}); });
}, },
scheduleRecording: function(encodedUrn) { showMain: function() {
var self = this; document.getElementById('srfrecLoginSection').style.display = 'none';
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Schedule/' + encodedUrn, { document.getElementById('srfrecMainSection').style.display = 'block';
method: 'POST', this.loadSchedule();
headers: this.getHeaders() this.loadRecordings();
}) },
.then(function(r) {
if (r.ok) {
alert('Recording scheduled!');
self.loadRecordings();
} else {
alert('Failed to schedule recording');
}
})
.catch(function(err) { alert('Error: ' + err.message); });
},
loadRecordings: function() { getHeaders: function() {
this.loadActiveRecordings(); return { 'X-Emby-Token': this.token };
this.loadCompletedRecordings(); },
},
loadActiveRecordings: function() { formatDate: function(dateStr) {
var container = document.getElementById('activeRecordingsContainer'); if (!dateStr) return 'N/A';
container.innerHTML = '<p class="msg">Loading...</p>'; var d = new Date(dateStr);
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
},
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/All', { headers: this.getHeaders() }) formatSize: function(bytes) {
.then(function(r) { return r.json(); }) if (!bytes) return '';
.then(function(recordings) { if (bytes > 1073741824) return (bytes / 1073741824).toFixed(1) + ' GB';
var activeStates = ['Scheduled', 'WaitingForStream', 'Recording', 0, 1, 2]; if (bytes > 1048576) return (bytes / 1048576).toFixed(0) + ' MB';
var active = recordings.filter(function(r) { return (bytes / 1024).toFixed(0) + ' KB';
return activeStates.indexOf(r.state) !== -1; },
});
if (active.length === 0) { loadSchedule: function() {
container.innerHTML = '<p class="msg">No scheduled or active recordings.</p>'; var container = document.getElementById('srfrecScheduleContainer');
return; container.innerHTML = '<p class="srfrec-msg">Loading schedule...</p>';
}
var stateMap = { fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Schedule', { headers: this.getHeaders() })
'Scheduled': {label: 'Scheduled', cls: 'status-scheduled'}, .then(function(r) { return r.json(); })
'WaitingForStream': {label: 'Waiting', cls: 'status-waiting'}, .then(function(programs) {
'Recording': {label: 'Recording', cls: 'status-recording'}, if (!programs || programs.length === 0) {
0: {label: 'Scheduled', cls: 'status-scheduled'}, container.innerHTML = '<p class="srfrec-msg">No upcoming sport livestreams found.</p>';
1: {label: 'Waiting', cls: 'status-waiting'}, return;
2: {label: 'Recording', cls: 'status-recording'}
};
var html = '<table>';
html += '<thead><tr><th>Title</th><th>Status</th><th>Start</th><th>Action</th></tr></thead><tbody>';
active.forEach(function(r) {
var st = stateMap[r.state] || {label: r.state, cls: ''};
html += '<tr>';
html += '<td>' + (r.title || 'Unknown') + '</td>';
html += '<td><span class="status ' + st.cls + '">' + st.label + '</span></td>';
html += '<td>' + SRFRec.formatDate(r.validFrom) + '</td>';
html += '<td>';
if (r.state === 2 || r.state === 'Recording') {
html += '<button class="btn btn-stop" onclick="SRFRec.stopRecording(\'' + r.id + '\')">Stop</button>';
} else {
html += '<button class="btn btn-cancel" onclick="SRFRec.cancelRecording(\'' + r.id + '\')">Cancel</button>';
} }
html += '</td></tr>';
var html = '<table>';
html += '<thead><tr><th>Title</th><th>Start</th><th>End</th><th>Action</th></tr></thead><tbody>';
programs.forEach(function(p) {
html += '<tr>';
html += '<td>' + (p.title || 'Unknown') + '</td>';
html += '<td>' + SRFRec.formatDate(p.validFrom || p.date) + '</td>';
html += '<td>' + SRFRec.formatDate(p.validTo) + '</td>';
html += '<td><button type="button" class="btn btn-record" onclick="SRFRec.scheduleRecording(\'' + encodeURIComponent(p.urn) + '\')">Record</button></td>';
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
})
.catch(function(err) {
container.innerHTML = '<p class="srfrec-error">Error loading schedule: ' + err.message + '</p>';
}); });
},
html += '</tbody></table>'; scheduleRecording: function(encodedUrn) {
container.innerHTML = html; var self = this;
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Schedule/' + encodedUrn, {
method: 'POST',
headers: this.getHeaders()
}) })
.catch(function(err) { .then(function(r) {
container.innerHTML = '<p class="error">Error: ' + err.message + '</p>'; if (r.ok) {
}); alert('Recording scheduled!');
}, self.loadRecordings();
} else {
loadCompletedRecordings: function() { alert('Failed to schedule recording');
var container = document.getElementById('completedRecordingsContainer');
container.innerHTML = '<p class="msg">Loading...</p>';
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Completed', { headers: this.getHeaders() })
.then(function(r) { return r.json(); })
.then(function(recordings) {
if (!recordings || recordings.length === 0) {
container.innerHTML = '<p class="msg">No completed recordings.</p>';
return;
} }
var html = '<table>';
html += '<thead><tr><th>Title</th><th>Recorded</th><th>Size</th><th>Action</th></tr></thead><tbody>';
recordings.forEach(function(r) {
html += '<tr>';
html += '<td>' + (r.title || 'Unknown') + '</td>';
html += '<td>' + SRFRec.formatDate(r.recordingStartedAt) + '</td>';
html += '<td>' + SRFRec.formatSize(r.fileSizeBytes) + '</td>';
html += '<td><button class="btn btn-delete" onclick="SRFRec.deleteRecording(\'' + r.id + '\')">Delete</button></td>';
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
}) })
.catch(function(err) { .catch(function(err) { alert('Error: ' + err.message); });
container.innerHTML = '<p class="error">Error: ' + err.message + '</p>'; },
});
},
stopRecording: function(id) { loadRecordings: function() {
var self = this; this.loadActiveRecordings();
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Active/' + id + '/Stop', { this.loadCompletedRecordings();
method: 'POST', },
headers: this.getHeaders()
}).then(function() { self.loadRecordings(); });
},
cancelRecording: function(id) { loadActiveRecordings: function() {
var self = this; var container = document.getElementById('srfrecActiveRecordingsContainer');
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Schedule/' + id, { container.innerHTML = '<p class="srfrec-msg">Loading...</p>';
method: 'DELETE',
headers: this.getHeaders()
}).then(function() { self.loadRecordings(); });
},
deleteRecording: function(id) { fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/All', { headers: this.getHeaders() })
if (!confirm('Delete this recording and its file?')) return; .then(function(r) { return r.json(); })
var self = this; .then(function(recordings) {
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Completed/' + id + '?deleteFile=true', { var activeStates = ['Scheduled', 'WaitingForStream', 'Recording', 0, 1, 2];
method: 'DELETE', var active = recordings.filter(function(r) {
headers: this.getHeaders() return activeStates.indexOf(r.state) !== -1;
}).then(function() { self.loadRecordings(); }); });
}
};
SRFRec.init(); if (active.length === 0) {
</script> container.innerHTML = '<p class="srfrec-msg">No scheduled or active recordings.</p>';
</body> return;
</html> }
var stateMap = {
'Scheduled': {label: 'Scheduled', cls: 'status-scheduled'},
'WaitingForStream': {label: 'Waiting', cls: 'status-waiting'},
'Recording': {label: 'Recording', cls: 'status-recording'},
0: {label: 'Scheduled', cls: 'status-scheduled'},
1: {label: 'Waiting', cls: 'status-waiting'},
2: {label: 'Recording', cls: 'status-recording'}
};
var html = '<table>';
html += '<thead><tr><th>Title</th><th>Status</th><th>Start</th><th>Action</th></tr></thead><tbody>';
active.forEach(function(r) {
var st = stateMap[r.state] || {label: r.state, cls: ''};
html += '<tr>';
html += '<td>' + (r.title || 'Unknown') + '</td>';
html += '<td><span class="status ' + st.cls + '">' + st.label + '</span></td>';
html += '<td>' + SRFRec.formatDate(r.validFrom) + '</td>';
html += '<td>';
if (r.state === 2 || r.state === 'Recording') {
html += '<button type="button" class="btn btn-stop" onclick="SRFRec.stopRecording(\'' + r.id + '\')">Stop</button>';
} else {
html += '<button type="button" class="btn btn-cancel" onclick="SRFRec.cancelRecording(\'' + r.id + '\')">Cancel</button>';
}
html += '</td></tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
})
.catch(function(err) {
container.innerHTML = '<p class="srfrec-error">Error: ' + err.message + '</p>';
});
},
loadCompletedRecordings: function() {
var container = document.getElementById('srfrecCompletedRecordingsContainer');
container.innerHTML = '<p class="srfrec-msg">Loading...</p>';
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Completed', { headers: this.getHeaders() })
.then(function(r) { return r.json(); })
.then(function(recordings) {
if (!recordings || recordings.length === 0) {
container.innerHTML = '<p class="srfrec-msg">No completed recordings.</p>';
return;
}
var html = '<table>';
html += '<thead><tr><th>Title</th><th>Recorded</th><th>Size</th><th>Action</th></tr></thead><tbody>';
recordings.forEach(function(r) {
html += '<tr>';
html += '<td>' + (r.title || 'Unknown') + '</td>';
html += '<td>' + SRFRec.formatDate(r.recordingStartedAt) + '</td>';
html += '<td>' + SRFRec.formatSize(r.fileSizeBytes) + '</td>';
html += '<td><button type="button" class="btn btn-delete" onclick="SRFRec.deleteRecording(\'' + r.id + '\')">Delete</button></td>';
html += '</tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
})
.catch(function(err) {
container.innerHTML = '<p class="srfrec-error">Error: ' + err.message + '</p>';
});
},
stopRecording: function(id) {
var self = this;
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Active/' + id + '/Stop', {
method: 'POST',
headers: this.getHeaders()
}).then(function() { self.loadRecordings(); });
},
cancelRecording: function(id) {
var self = this;
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Schedule/' + id, {
method: 'DELETE',
headers: this.getHeaders()
}).then(function() { self.loadRecordings(); });
},
deleteRecording: function(id) {
if (!confirm('Delete this recording and its file?')) return;
var self = this;
fetch(this.serverUrl + '/Plugins/SRFPlay/Recording/Completed/' + id + '?deleteFile=true', {
method: 'DELETE',
headers: this.getHeaders()
}).then(function() { self.loadRecordings(); });
}
};
(function() {
var page = document.querySelector('#SRFPlayRecordingsPage');
if (page && typeof page.addEventListener === 'function' && typeof ApiClient !== 'undefined') {
// Inside the Jellyfin dashboard: defer to the page lifecycle so we
// never run during the config page's view and never hijack routing.
page.addEventListener('pageshow', function() { SRFRec.init(); });
} else {
// Standalone (e.g. TV/browser): initialise immediately.
SRFRec.init();
}
})();
</script>
</div>

View File

@ -62,9 +62,15 @@ public class MediaSourceFactory : IMediaSourceFactory
return Task.FromResult<MediaSourceInfo?>(null); return Task.FromResult<MediaSourceInfo?>(null);
} }
// Detect if this is a live stream // A scheduled livestream is only treated as live when the broadcast window is open.
var isLiveStream = chapter.Type == "SCHEDULED_LIVESTREAM" || // Past replays (ValidTo in the past) and upcoming events (ValidFrom in the future)
urn.Contains("livestream", StringComparison.OrdinalIgnoreCase); // are treated as VOD to avoid IsInfiniteStream/IgnoreDts flags and FFmpeg -re mode.
var isScheduledLivestream = chapter.Type == "SCHEDULED_LIVESTREAM" ||
urn.Contains("livestream", StringComparison.OrdinalIgnoreCase);
var now = DateTime.UtcNow;
var isLiveStream = isScheduledLivestream &&
(chapter.ValidFrom == null || chapter.ValidFrom.Value.ToUniversalTime() <= now) &&
(chapter.ValidTo == null || chapter.ValidTo.Value.ToUniversalTime() > now);
// Register stream with UNAUTHENTICATED URL - proxy will authenticate on-demand // Register stream with UNAUTHENTICATED URL - proxy will authenticate on-demand
// This avoids wasting 30-second tokens during category browsing // This avoids wasting 30-second tokens during category browsing