Record into a Jellyfin library by default; configurable recording location
🏗️ Build Plugin / build (push) Successful in 12m31s
Nightly Build / nightly-build (push) Successful in 2m44s
🧪 Test Plugin / test (push) Successful in 11m14s
🚀 Release Plugin / build-and-release (push) Successful in 12m33s

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>
This commit is contained in:
2026-09-26 19:35:55 -04:00
co-authored by Claude Opus 5.5
parent 9467cb28c7
commit aa47de9912
6 changed files with 229 additions and 12 deletions
@@ -77,10 +77,31 @@
</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 &quot;SRF Recordings&quot; 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">Recording Output Directory</label>
<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">Directory where sport livestream recordings will be saved (requires ffmpeg)</div>
<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 &amp;&amp; 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>
@@ -205,6 +226,7 @@
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;
@@ -236,7 +258,8 @@
config.ProxyUsername = document.querySelector('#ProxyUsername').value;
config.ProxyPassword = document.querySelector('#ProxyPassword').value;
config.PublicServerUrl = document.querySelector('#PublicServerUrl').value;
config.RecordingOutputPath = document.querySelector('#RecordingOutputPath').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;
@@ -246,6 +269,7 @@
config.ResumePointCompletedPercent = parseInt(document.querySelector('#ResumePointCompletedPercent').value) || 0;
ApiClient.updatePluginConfiguration(SRFPlayConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
SRFPlayRecordingOutput.check();
});
});
@@ -253,6 +277,58 @@
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.')) {