When no custom directory is set, recordings go to an "SRF Recordings" folder in the chosen library, or the first TV Shows library, so they show up without adding a library by hand. The settings page gets a library picker, a check that the directory is writable, and help for sandboxed systemd services. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
566 lines
39 KiB
HTML
566 lines
39 KiB
HTML
<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 class="content-primary">
|
|
<form id="SRFPlayConfigForm">
|
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
<h3>Channels</h3>
|
|
<div class="fieldDescription">Every Swiss broadcaster has its own channel tile: SRF (German), RTS (French), RSI (Italian), RTR (Romansh) and SWI (swissinfo.ch). They are always available — pin the ones you use from the Jellyfin home screen.</div>
|
|
</div>
|
|
<div class="selectContainer">
|
|
<label class="selectLabel" for="QualityPreference">Quality Preference</label>
|
|
<select is="emby-select" id="QualityPreference" name="QualityPreference" class="emby-select-withcolor emby-select">
|
|
<option id="optAuto" value="Auto">Automatic</option>
|
|
<option id="optSD" value="SD">Standard Definition</option>
|
|
<option id="optHD" value="HD">High Definition</option>
|
|
</select>
|
|
<div class="fieldDescription">Preferred video quality for playback</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="ContentRefreshIntervalHours">Content Refresh Interval (hours)</label>
|
|
<input id="ContentRefreshIntervalHours" name="ContentRefreshIntervalHours" type="number" is="emby-input" min="1" max="168" />
|
|
<div class="fieldDescription">How often to check for new content (1-168 hours)</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="ExpirationCheckIntervalHours">Expiration Check Interval (hours)</label>
|
|
<input id="ExpirationCheckIntervalHours" name="ExpirationCheckIntervalHours" type="number" is="emby-input" min="1" max="168" />
|
|
<div class="fieldDescription">How often to check for expired content (1-168 hours)</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="CacheDurationMinutes">Cache Duration (minutes)</label>
|
|
<input id="CacheDurationMinutes" name="CacheDurationMinutes" type="number" is="emby-input" min="5" max="1440" />
|
|
<div class="fieldDescription">How long to cache metadata (5-1440 minutes)</div>
|
|
</div>
|
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
<label class="emby-checkbox-label">
|
|
<input id="EnableLatestContent" name="EnableLatestContent" type="checkbox" is="emby-checkbox" />
|
|
<span>Enable Latest Content</span>
|
|
</label>
|
|
<div class="fieldDescription">Automatically discover and add latest videos</div>
|
|
</div>
|
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
<label class="emby-checkbox-label">
|
|
<input id="EnableTrendingContent" name="EnableTrendingContent" type="checkbox" is="emby-checkbox" />
|
|
<span>Enable Trending Content</span>
|
|
</label>
|
|
<div class="fieldDescription">Automatically discover and add trending videos</div>
|
|
</div>
|
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
<label class="emby-checkbox-label">
|
|
<input id="GenerateTitleCards" name="GenerateTitleCards" type="checkbox" is="emby-checkbox" />
|
|
<span>Generate Title Cards</span>
|
|
</label>
|
|
<div class="fieldDescription">Generate custom thumbnails with the content title instead of using SRF-provided images</div>
|
|
</div>
|
|
<br />
|
|
<h2>Proxy Settings</h2>
|
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
<label class="emby-checkbox-label">
|
|
<input id="UseProxy" name="UseProxy" type="checkbox" is="emby-checkbox" />
|
|
<span>Use Proxy</span>
|
|
</label>
|
|
<div class="fieldDescription">Route all SRF API requests through a proxy server</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="ProxyAddress">Proxy Address</label>
|
|
<input id="ProxyAddress" name="ProxyAddress" type="text" is="emby-input" />
|
|
<div class="fieldDescription">Proxy server address (e.g., http://proxy.example.com:8080 or socks5://proxy.example.com:1080)</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="ProxyUsername">Proxy Username (Optional)</label>
|
|
<input id="ProxyUsername" name="ProxyUsername" type="text" is="emby-input" autocomplete="off" />
|
|
<div class="fieldDescription">Username for proxy authentication (leave empty if not required)</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="ProxyPassword">Proxy Password (Optional)</label>
|
|
<input id="ProxyPassword" name="ProxyPassword" type="password" is="emby-input" autocomplete="off" />
|
|
<div class="fieldDescription">Password for proxy authentication (leave empty if not required)</div>
|
|
</div>
|
|
<br />
|
|
<h2>Recording Settings</h2>
|
|
<div class="selectContainer">
|
|
<label class="selectLabel" for="RecordingLibraryId">Record Into Library</label>
|
|
<select is="emby-select" id="RecordingLibraryId" name="RecordingLibraryId" class="emby-select-withcolor emby-select">
|
|
<option value="">Automatic (first TV Shows library)</option>
|
|
</select>
|
|
<div class="fieldDescription">Recordings are saved to an "SRF Recordings" folder inside this library, so they appear there after the next library scan (requires ffmpeg).</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="RecordingOutputPath">Custom Recording Directory (Optional)</label>
|
|
<input id="RecordingOutputPath" name="RecordingOutputPath" type="text" is="emby-input" placeholder="e.g., /media/recordings/srf" />
|
|
<div class="fieldDescription">Overrides the library above. Add this directory as a library yourself if you want recordings to appear in Jellyfin.</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<button is="emby-button" type="button" class="raised emby-button" onclick="SRFPlayRecordingOutput.check()">
|
|
<span>Check Recording Directory</span>
|
|
</button>
|
|
<div id="RecordingOutputStatus" class="fieldDescription" style="margin-top: 0.5em;"></div>
|
|
<details class="fieldDescription" style="margin-top: 0.5em;">
|
|
<summary>Recording directory not writable?</summary>
|
|
<p>The Jellyfin user needs write access to the directory and execute (traverse) access to every parent directory. Check with <code>namei -l /path/to/recordings</code>.</p>
|
|
<p>On Linux the packaged systemd service may be sandboxed, so it can read your media folders but not write to them. Allow the directory with <code>sudo systemctl edit jellyfin.service</code> and add:</p>
|
|
<pre>[Service]
|
|
ReadWritePaths=/path/to/recordings</pre>
|
|
<p>then run <code>sudo systemctl daemon-reload && sudo systemctl restart jellyfin</code>. In Docker, make sure the directory is mounted read-write.</p>
|
|
</details>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="DisplayTimeZone">Display Time Zone</label>
|
|
<input id="DisplayTimeZone" name="DisplayTimeZone" type="text" is="emby-input" placeholder="e.g., Europe/Zurich" />
|
|
<div class="fieldDescription">Time zone for start times in upcoming livestream names and for recording file names. Leave empty to use the server's time zone.</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="LiveStartSegmentsBack">Live Start Offset (segments)</label>
|
|
<input id="LiveStartSegmentsBack" name="LiveStartSegmentsBack" type="number" is="emby-input" min="0" max="20" />
|
|
<div class="fieldDescription">How many segments back from the live edge livestreams start playing. Fixes jumpy/stalling playback at the start on Android TV. Minimum 3 (RFC requirement); 0 disables it. Default 3.</div>
|
|
</div>
|
|
<br />
|
|
<h2>Continue Watching Cleanup</h2>
|
|
<div class="fieldDescription" style="margin-bottom: 1em;">
|
|
Livestreams and abandoned playback otherwise stay pinned in every user's
|
|
"Continue Watching" row forever. Clearing a resume point only resets the
|
|
playback position — nothing is deleted and the item stays unwatched.
|
|
</div>
|
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
<label class="emby-checkbox-label">
|
|
<input id="CleanUpResumePoints" name="CleanUpResumePoints" type="checkbox" is="emby-checkbox" />
|
|
<span>Clean up stale resume points</span>
|
|
</label>
|
|
<div class="fieldDescription checkboxFieldDescription">Runs daily at 4 AM (Scheduled Tasks → "Clean Up SRF Play Continue Watching")</div>
|
|
</div>
|
|
<div class="checkboxContainer checkboxContainer-withDescription">
|
|
<label class="emby-checkbox-label">
|
|
<input id="ClearLiveStreamResumePoints" name="ClearLiveStreamResumePoints" type="checkbox" is="emby-checkbox" />
|
|
<span>Never keep a resume point for livestreams</span>
|
|
</label>
|
|
<div class="fieldDescription checkboxFieldDescription">Clears the position as soon as a livestream stops playing, rather than waiting for the daily task</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="ResumePointMaxAgeDays">Maximum Resume Point Age (days)</label>
|
|
<input id="ResumePointMaxAgeDays" name="ResumePointMaxAgeDays" type="number" is="emby-input" min="0" max="3650" />
|
|
<div class="fieldDescription">Clear resume points not touched for this many days. 0 disables the rule. Default 30.</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="ResumePointMinPositionSeconds">Minimum Resume Position (seconds)</label>
|
|
<input id="ResumePointMinPositionSeconds" name="ResumePointMinPositionSeconds" type="number" is="emby-input" min="0" max="3600" />
|
|
<div class="fieldDescription">Positions at or below this are treated as an accidental start and cleared. 0 disables the rule. Default 60.</div>
|
|
</div>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="ResumePointCompletedPercent">Finished Threshold (%)</label>
|
|
<input id="ResumePointCompletedPercent" name="ResumePointCompletedPercent" type="number" is="emby-input" min="0" max="100" />
|
|
<div class="fieldDescription">Playback at or beyond this share of the runtime counts as finished. Only applies when the runtime is known. 0 disables the rule. Default 92.</div>
|
|
</div>
|
|
<br />
|
|
<h2>Network Settings</h2>
|
|
<div class="inputContainer">
|
|
<label class="inputLabel inputLabelUnfocused" for="PublicServerUrl">Public Server URL (Optional)</label>
|
|
<input id="PublicServerUrl" name="PublicServerUrl" type="text" is="emby-input" placeholder="e.g., https://jellyfin.example.com:8920" />
|
|
<div class="fieldDescription">
|
|
The public/external URL for remote clients (Android, iOS, etc.) to access streaming proxy.
|
|
<br />If not set, the plugin will use Jellyfin's automatic URL detection which may return local addresses.
|
|
<br /><strong>Important for Android/remote playback!</strong>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
|
|
<span>Save</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<br />
|
|
<h2>Continue Watching Maintenance</h2>
|
|
<div class="fieldDescription">Run the cleanup right now instead of waiting for the daily task. Only SRF Play items are affected.</div>
|
|
<div id="resumeCleanupResult" style="margin: 10px 0;"></div>
|
|
<button is="emby-button" type="button" class="raised emby-button" onclick="SRFPlayMaintenance.cleanupResumePoints(false)" style="margin-bottom: 20px;">
|
|
<span>Clean Up Stale Entries Now</span>
|
|
</button>
|
|
<button is="emby-button" type="button" class="raised emby-button" onclick="SRFPlayMaintenance.cleanupResumePoints(true)" style="margin-bottom: 20px;">
|
|
<span>Clear All SRF Play Entries</span>
|
|
</button>
|
|
|
|
<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">
|
|
var SRFPlayConfig = {
|
|
pluginUniqueId: 'a4b12f86-8c3d-4e9a-b7f2-1d5e6c8a9b4f'
|
|
};
|
|
|
|
document.querySelector('#SRFPlayConfigPage')
|
|
.addEventListener('pageshow', function() {
|
|
Dashboard.showLoadingMsg();
|
|
ApiClient.getPluginConfiguration(SRFPlayConfig.pluginUniqueId).then(function (config) {
|
|
document.querySelector('#QualityPreference').value = config.QualityPreference;
|
|
document.querySelector('#ContentRefreshIntervalHours').value = config.ContentRefreshIntervalHours;
|
|
document.querySelector('#ExpirationCheckIntervalHours').value = config.ExpirationCheckIntervalHours;
|
|
document.querySelector('#CacheDurationMinutes').value = config.CacheDurationMinutes;
|
|
document.querySelector('#EnableLatestContent').checked = config.EnableLatestContent;
|
|
document.querySelector('#EnableTrendingContent').checked = config.EnableTrendingContent;
|
|
document.querySelector('#GenerateTitleCards').checked = config.GenerateTitleCards !== false;
|
|
document.querySelector('#UseProxy').checked = config.UseProxy || false;
|
|
document.querySelector('#ProxyAddress').value = config.ProxyAddress || '';
|
|
document.querySelector('#ProxyUsername').value = config.ProxyUsername || '';
|
|
document.querySelector('#ProxyPassword').value = config.ProxyPassword || '';
|
|
document.querySelector('#PublicServerUrl').value = config.PublicServerUrl || '';
|
|
document.querySelector('#RecordingOutputPath').value = config.RecordingOutputPath || '';
|
|
SRFPlayRecordingOutput.loadLibraries(config.RecordingLibraryId || '');
|
|
document.querySelector('#DisplayTimeZone').value = config.DisplayTimeZone || '';
|
|
document.querySelector('#LiveStartSegmentsBack').value = config.LiveStartSegmentsBack != null ? config.LiveStartSegmentsBack : 3;
|
|
document.querySelector('#CleanUpResumePoints').checked = config.CleanUpResumePoints !== false;
|
|
document.querySelector('#ClearLiveStreamResumePoints').checked = config.ClearLiveStreamResumePoints !== false;
|
|
document.querySelector('#ResumePointMaxAgeDays').value = config.ResumePointMaxAgeDays != null ? config.ResumePointMaxAgeDays : 30;
|
|
document.querySelector('#ResumePointMinPositionSeconds').value = config.ResumePointMinPositionSeconds != null ? config.ResumePointMinPositionSeconds : 60;
|
|
document.querySelector('#ResumePointCompletedPercent').value = config.ResumePointCompletedPercent != null ? config.ResumePointCompletedPercent : 92;
|
|
Dashboard.hideLoadingMsg();
|
|
|
|
// Load recordings UI
|
|
SRFPlayRecordings.loadSchedule();
|
|
SRFPlayRecordings.loadRecordings();
|
|
});
|
|
});
|
|
|
|
document.querySelector('#SRFPlayConfigForm')
|
|
.addEventListener('submit', function(e) {
|
|
Dashboard.showLoadingMsg();
|
|
ApiClient.getPluginConfiguration(SRFPlayConfig.pluginUniqueId).then(function (config) {
|
|
config.QualityPreference = document.querySelector('#QualityPreference').value;
|
|
config.ContentRefreshIntervalHours = parseInt(document.querySelector('#ContentRefreshIntervalHours').value);
|
|
config.ExpirationCheckIntervalHours = parseInt(document.querySelector('#ExpirationCheckIntervalHours').value);
|
|
config.CacheDurationMinutes = parseInt(document.querySelector('#CacheDurationMinutes').value);
|
|
config.EnableLatestContent = document.querySelector('#EnableLatestContent').checked;
|
|
config.EnableTrendingContent = document.querySelector('#EnableTrendingContent').checked;
|
|
config.GenerateTitleCards = document.querySelector('#GenerateTitleCards').checked;
|
|
config.UseProxy = document.querySelector('#UseProxy').checked;
|
|
config.ProxyAddress = document.querySelector('#ProxyAddress').value;
|
|
config.ProxyUsername = document.querySelector('#ProxyUsername').value;
|
|
config.ProxyPassword = document.querySelector('#ProxyPassword').value;
|
|
config.PublicServerUrl = document.querySelector('#PublicServerUrl').value;
|
|
config.RecordingOutputPath = document.querySelector('#RecordingOutputPath').value.trim();
|
|
config.RecordingLibraryId = document.querySelector('#RecordingLibraryId').value;
|
|
config.DisplayTimeZone = document.querySelector('#DisplayTimeZone').value.trim();
|
|
config.LiveStartSegmentsBack = parseInt(document.querySelector('#LiveStartSegmentsBack').value) || 0;
|
|
config.CleanUpResumePoints = document.querySelector('#CleanUpResumePoints').checked;
|
|
config.ClearLiveStreamResumePoints = document.querySelector('#ClearLiveStreamResumePoints').checked;
|
|
config.ResumePointMaxAgeDays = parseInt(document.querySelector('#ResumePointMaxAgeDays').value) || 0;
|
|
config.ResumePointMinPositionSeconds = parseInt(document.querySelector('#ResumePointMinPositionSeconds').value) || 0;
|
|
config.ResumePointCompletedPercent = parseInt(document.querySelector('#ResumePointCompletedPercent').value) || 0;
|
|
ApiClient.updatePluginConfiguration(SRFPlayConfig.pluginUniqueId, config).then(function (result) {
|
|
Dashboard.processPluginConfigurationUpdateResult(result);
|
|
SRFPlayRecordingOutput.check();
|
|
});
|
|
});
|
|
|
|
e.preventDefault();
|
|
return false;
|
|
});
|
|
|
|
var SRFPlayRecordingOutput = {
|
|
loadLibraries: function(selectedId) {
|
|
var select = document.querySelector('#RecordingLibraryId');
|
|
ApiClient.getVirtualFolders().then(function(folders) {
|
|
while (select.options.length > 1) {
|
|
select.remove(1);
|
|
}
|
|
(folders || []).forEach(function(folder) {
|
|
if (!folder.Locations || !folder.Locations.length) {
|
|
return;
|
|
}
|
|
var option = document.createElement('option');
|
|
option.value = folder.ItemId;
|
|
option.textContent = folder.Name + ' (' + folder.Locations[0] + ')';
|
|
select.appendChild(option);
|
|
});
|
|
select.value = selectedId;
|
|
if (select.value !== selectedId) {
|
|
// Chosen library was removed; fall back to automatic
|
|
select.value = '';
|
|
}
|
|
SRFPlayRecordingOutput.check();
|
|
});
|
|
},
|
|
|
|
check: function() {
|
|
var status = document.querySelector('#RecordingOutputStatus');
|
|
status.textContent = 'Checking…';
|
|
ApiClient.getJSON(ApiClient.getUrl('Plugins/SRFPlay/Recording/OutputInfo')).then(function(info) {
|
|
var sources = {
|
|
custom: 'custom directory',
|
|
library: 'library "' + info.libraryName + '"',
|
|
tvLibrary: 'first TV Shows library "' + info.libraryName + '"',
|
|
fallback: 'no TV Shows library found, using the Jellyfin user\'s home. Add this directory as a library to see recordings'
|
|
};
|
|
status.textContent = '';
|
|
var line = document.createElement('div');
|
|
line.textContent = (info.writable ? '\u2714 ' : '\u2718 ') + info.path + ' (' + (sources[info.source] || info.source) + ')';
|
|
line.style.color = info.writable ? '' : '#e57373';
|
|
status.appendChild(line);
|
|
if (!info.writable) {
|
|
var err = document.createElement('div');
|
|
err.textContent = 'Not writable: ' + (info.error || 'unknown error') + '. Saved settings apply; see the help below.';
|
|
err.style.color = '#e57373';
|
|
status.appendChild(err);
|
|
}
|
|
}).catch(function() {
|
|
status.textContent = 'Could not check the recording directory.';
|
|
});
|
|
}
|
|
};
|
|
|
|
var SRFPlayMaintenance = {
|
|
cleanupResumePoints: function(clearAll) {
|
|
if (clearAll && !confirm('Clear the playback position of every SRF Play item for all users? Nothing is deleted and items stay unwatched.')) {
|
|
return;
|
|
}
|
|
|
|
var target = document.querySelector('#resumeCleanupResult');
|
|
target.innerHTML = '<p><em>Cleaning up...</em></p>';
|
|
|
|
fetch(ApiClient.serverAddress() + '/Plugins/SRFPlay/Maintenance/ResumePoints/Cleanup?clearAll=' + (clearAll ? 'true' : 'false'), {
|
|
method: 'POST',
|
|
headers: { 'Authorization': 'MediaBrowser Token="' + ApiClient.accessToken() + '"' }
|
|
})
|
|
.then(function(response) {
|
|
if (!response.ok) {
|
|
throw new Error('HTTP ' + response.status);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(function(result) {
|
|
var byReason = result.clearedByReason || {};
|
|
var reasons = Object.keys(byReason)
|
|
.map(function(key) { return key + ': ' + byReason[key]; })
|
|
.join(', ');
|
|
target.innerHTML = '<p>Cleared <strong>' + result.cleared + '</strong> of ' + result.inspected +
|
|
' SRF Play resume point(s) across ' + result.usersChecked + ' user(s).' +
|
|
(reasons ? ' (' + reasons + ')' : '') + '</p>';
|
|
})
|
|
.catch(function(error) {
|
|
target.innerHTML = '<p style="color: #d44;">Cleanup failed: ' + error.message + '</p>';
|
|
});
|
|
}
|
|
};
|
|
|
|
var SRFPlayRecordings = {
|
|
apiBase: ApiClient.serverAddress() + '/Plugins/SRFPlay/Recording',
|
|
|
|
getHeaders: function() {
|
|
return {
|
|
'Authorization': 'MediaBrowser 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 activeStates = ['Scheduled', 'WaitingForStream', 'Recording', 0, 1, 2];
|
|
var active = recordings.filter(function(r) {
|
|
return activeStates.indexOf(r.state) !== -1;
|
|
});
|
|
|
|
if (active.length === 0) {
|
|
container.innerHTML = '<p>No scheduled or active recordings.</p>';
|
|
return;
|
|
}
|
|
|
|
var stateLabels = {'Scheduled': 'Scheduled', 'WaitingForStream': 'Waiting', 'Recording': 'Recording', 'Completed': 'Completed', 'Failed': 'Failed', 'Cancelled': 'Cancelled', 0: 'Scheduled', 1: 'Waiting', 2: 'Recording', 3: 'Completed', 4: 'Failed', 5: 'Cancelled'};
|
|
var stateColors = {'Scheduled': '#2196F3', 'WaitingForStream': '#FF9800', 'Recording': '#4CAF50', 'Failed': '#f44336', 'Cancelled': '#9E9E9E', 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 || r.state === 'Recording') {
|
|
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>
|