Files
claude 9e33a343ab
Some checks failed
release / build-release (push) Has been cancelled
Floatplane channel plugin for Jellyfin 10.10.x: browse, HLS proxy, cookie session, CI release
- IChannel: creators -> sub-channels -> video posts (public listing API, search mapped)
- IRequiresMediaInfoCallback -> signed proxy URLs; stream controller rewrites HLS so the
  AES-128 key is fetched server-side with the session cookie, segments stay on the CDN
- delivery/info cached per token lifetime and throttled (~2/min)
- config page: paste sails.sid + validate, extra creators, quality, search test
- scripts/package.sh -> plugin folder + zip + md5 + manifest.json; Gitea release workflow
- research docs, HAR analysis (HAR itself gitignored), install-pi5.sh

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 09:42:27 +01:00

291 lines
16 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Chaosflix</title>
</head>
<body>
<div id="ChaosflixConfigPage" data-role="page" class="page type-interior pluginConfigurationPage"
data-require="emby-select,emby-checkbox">
<div data-role="content">
<div class="content-primary">
<form id="ChaosflixConfigForm">
<div class="verticalSection">
<h2 class="sectionTitle">Chaosflix Settings</h2>
<div class="selectContainer">
<label class="selectLabel" for="PreferredQuality">Preferred Quality</label>
<select is="emby-select" id="PreferredQuality" class="emby-select-withcolor">
<option value="High">High (1080p)</option>
<option value="Standard">Standard (576p)</option>
</select>
</div>
<div class="selectContainer">
<label class="selectLabel" for="PreferredFormat">Preferred Format</label>
<select is="emby-select" id="PreferredFormat" class="emby-select-withcolor">
<option value="Mp4">MP4 (H.264)</option>
<option value="WebM">WebM (VP9)</option>
</select>
<div class="fieldDescription">WebM costs a full re-encode on clients that can only transcode to H.264 — the Android app is one of them.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input is="emby-checkbox" type="checkbox" id="CompatibleDefaultVersion" />
<span>Start on a version every client can play untouched</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
Jellyfin hands every client the same list of versions, so the plugin cannot order it per device. With this on a talk starts on its H.264 MP4, which no client has to re-encode, and the preferred format only orders the versions behind it. Leave it off if your clients decode VP9 but not H.264.
</div>
</div>
<div class="selectContainer">
<label class="selectLabel" for="PreferredLanguage">Preferred Language</label>
<select is="emby-select" id="PreferredLanguage" class="emby-select-withcolor">
<option value="">Original</option>
<option value="deu">Deutsch</option>
<option value="eng">English</option>
</select>
</div>
<div class="inputContainer">
<label class="inputLabel" for="ConferenceFilter">Conferences</label>
<input is="emby-input" type="text" id="ConferenceFilter" placeholder="congress, gpn, 38c3" />
<div class="fieldDescription">Leave empty for every conference media.ccc.de publishes. Otherwise list what you follow, separated by commas: a series as it appears in the media.ccc.de address ("congress", "gpn", "easterhegg") keeps every edition including future ones, an acronym ("38c3") keeps exactly that one. This also narrows Popular and Recommended.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="StreamingBaseUrl">Streaming Endpoint</label>
<input is="emby-input" type="text" id="StreamingBaseUrl" placeholder="https://streaming.media.ccc.de" />
<div class="fieldDescription">Where the plugin asks which rooms are on air. Leave empty for streaming.media.ccc.de. A "🔴 Live now" folder appears while a congress is running and disappears again afterwards.</div>
</div>
<div class="inputContainer">
<label class="inputLabel" for="ApiBaseUrl">API Endpoint</label>
<input is="emby-input" type="text" id="ApiBaseUrl" placeholder="https://api.media.ccc.de/public" />
<div class="fieldDescription">Leave empty for media.ccc.de. Set it to use a mirror.</div>
</div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save</span>
</button>
</div>
</form>
<div class="verticalSection">
<h2 class="sectionTitle">Status</h2>
<div class="fieldDescription">What the plugin thinks is true right now. Nothing here talks to media.ccc.de until you ask it to.</div>
<div id="ChaosflixStatus" class="paperList" style="padding: 0.5em 1em;">Loading…</div>
<div>
<button is="emby-button" type="button" id="ChaosflixRefresh" class="raised emby-button">
<span>Refresh</span>
</button>
<button is="emby-button" type="button" id="ChaosflixCheckApi" class="raised emby-button">
<span>Check endpoint now</span>
</button>
<button is="emby-button" type="button" id="ChaosflixClearCaches" class="raised emby-button">
<span>Clear caches</span>
</button>
</div>
<div id="ChaosflixApiCheck" class="fieldDescription"></div>
</div>
<div class="verticalSection">
<h2 class="sectionTitle">Check a talk</h2>
<div class="fieldDescription">Resolves one talk the way playback does — API lookup, recording choice, signed proxy url, proxy request, probe — and names the stage that fails.</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="ChaosflixTalkId">Talk guid, slug or media.ccc.de url</label>
<input is="emby-input" type="text" id="ChaosflixTalkId" />
</div>
<div>
<button is="emby-button" type="button" id="ChaosflixCheckTalk" class="raised emby-button">
<span>Check this talk</span>
</button>
</div>
<div id="ChaosflixTalkCheck" class="fieldDescription"></div>
</div>
</div>
</div>
<script type="text/javascript">
var ChaosflixConfig = {
pluginUniqueId: 'c4a05f11-4ccc-4b00-bdea-dbeef1337000',
statusEndpoint: 'api/ChaosflixStatus'
};
(function () {
var page = document.querySelector('#ChaosflixConfigPage');
function statusUrl(action) {
return ApiClient.getUrl(ChaosflixConfig.statusEndpoint + (action ? '/' + action : ''));
}
function post(action, query) {
return ApiClient.ajax({
type: 'POST',
url: statusUrl(action) + (query || ''),
dataType: 'json'
});
}
// Talk titles and error messages come from media.ccc.de, so every value
// is written as text and never as markup.
function row(container, label, value) {
var line = document.createElement('div');
var name = document.createElement('strong');
name.textContent = label + ': ';
line.appendChild(name);
line.appendChild(document.createTextNode(value));
container.appendChild(line);
}
function percent(value) {
return value === null || value === undefined ? 'n/a' : value + '%';
}
function renderStatus(status) {
var target = page.querySelector('#ChaosflixStatus');
target.textContent = '';
row(target, 'Plugin version', status.Version);
row(target, 'Endpoint', status.ApiBaseUrl + (status.ApiBaseUrlIsDefault ? ' (default)' : ' (configured)'));
var sync = status.Sync;
if (!sync.Registered) {
row(target, 'Sync task', 'not registered with this server');
} else if (!sync.LastRunUtc) {
row(target, 'Sync task', sync.State + ', has not run yet');
} else {
row(target, 'Sync task', sync.LastResult + ' — ' + new Date(sync.LastRunUtc).toLocaleString() +
', took ' + sync.DurationSeconds + 's (now ' + sync.State + ')');
if (sync.Error) {
row(target, 'Sync error', sync.Error);
}
}
var cache = status.ApiCache;
row(target, 'API cache', cache.Entries + '/' + cache.Capacity + ' entries — ' +
cache.Conferences + ' conferences, ' + cache.Events + ' talks, ' +
cache.Searches + ' searches, ' + cache.Redirects + ' CDN mirrors');
row(target, 'API cache hit rate', percent(cache.HitRatePercent) +
' (' + cache.Hits + ' hits, ' + cache.Misses + ' misses since server start)');
var probe = status.ProbeCache;
row(target, 'Probe cache', probe.Entries + '/' + probe.Capacity + ' stream layouts, hit rate ' +
percent(probe.HitRatePercent) + ' (' + probe.Hits + ' hits, ' + probe.Misses + ' misses)');
row(target, 'Media sources handed out', probe.ServedItems +
' talks a configuration change or a cache clear would invalidate');
}
function loadStatus() {
return ApiClient.getJSON(statusUrl()).then(renderStatus, function () {
page.querySelector('#ChaosflixStatus').textContent = 'The status endpoint did not answer.';
});
}
function renderTalkCheck(result) {
var target = page.querySelector('#ChaosflixTalkCheck');
target.textContent = '';
row(target, result.Ok ? 'Result' : 'Failed at',
result.Ok ? 'resolved end to end' : result.FailedStage);
if (result.Title) {
row(target, 'Talk', result.Title);
}
result.Stages.forEach(function (stage) {
row(target, (stage.Ok ? '✓ ' : '✗ ') + stage.Name,
stage.Detail + (stage.ElapsedMs ? ' (' + stage.ElapsedMs + ' ms)' : ''));
});
}
page.addEventListener('pageshow', function () {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(ChaosflixConfig.pluginUniqueId).then(function (config) {
page.querySelector('#PreferredQuality').value = config.PreferredQuality || 'High';
page.querySelector('#PreferredFormat').value = config.PreferredFormat || 'Mp4';
page.querySelector('#PreferredLanguage').value = config.PreferredLanguage || '';
page.querySelector('#CompatibleDefaultVersion').checked = config.CompatibleDefaultVersion === true;
page.querySelector('#ConferenceFilter').value = config.ConferenceFilter || '';
page.querySelector('#StreamingBaseUrl').value = config.StreamingBaseUrl || '';
page.querySelector('#ApiBaseUrl').value = config.ApiBaseUrl || '';
Dashboard.hideLoadingMsg();
});
// Cheap by construction: the endpoint only reads counters the plugin
// already keeps. The checks below are the ones that cost something,
// and they run on click.
loadStatus();
});
page.querySelector('#ChaosflixRefresh').addEventListener('click', loadStatus);
page.querySelector('#ChaosflixCheckApi').addEventListener('click', function () {
var target = page.querySelector('#ChaosflixApiCheck');
target.textContent = 'Asking the endpoint…';
post('CheckApi').then(function (result) {
target.textContent = '';
row(target, result.Ok ? 'Reachable' : 'Unreachable', result.Url);
row(target, 'Round trip', result.ElapsedMs + ' ms');
if (result.Ok) {
row(target, 'Answer', result.StatusCode + ', ' + result.ConferenceCount + ' conferences listed');
} else {
row(target, 'Error', (result.StatusCode ? result.StatusCode + ' ' : '') + (result.Error || 'no answer'));
}
}, function () {
target.textContent = 'The check could not be run.';
});
});
page.querySelector('#ChaosflixClearCaches').addEventListener('click', function () {
Dashboard.showLoadingMsg();
post('ClearCaches').then(function (status) {
renderStatus(status);
Dashboard.hideLoadingMsg();
}, function () {
Dashboard.hideLoadingMsg();
page.querySelector('#ChaosflixStatus').textContent = 'Clearing the caches failed.';
});
});
page.querySelector('#ChaosflixCheckTalk').addEventListener('click', function () {
var id = page.querySelector('#ChaosflixTalkId').value.trim();
var target = page.querySelector('#ChaosflixTalkCheck');
if (!id) {
target.textContent = 'Enter a talk guid, slug or media.ccc.de url first.';
return;
}
target.textContent = 'Resolving ' + id + '…';
post('CheckTalk', '?id=' + encodeURIComponent(id)).then(renderTalkCheck, function () {
target.textContent = 'The check could not be run.';
});
});
page.querySelector('#ChaosflixConfigForm').addEventListener('submit', function (e) {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(ChaosflixConfig.pluginUniqueId).then(function (config) {
config.PreferredQuality = page.querySelector('#PreferredQuality').value;
config.PreferredFormat = page.querySelector('#PreferredFormat').value;
config.PreferredLanguage = page.querySelector('#PreferredLanguage').value;
config.CompatibleDefaultVersion = page.querySelector('#CompatibleDefaultVersion').checked;
config.ConferenceFilter = page.querySelector('#ConferenceFilter').value;
config.StreamingBaseUrl = page.querySelector('#StreamingBaseUrl').value;
config.ApiBaseUrl = page.querySelector('#ApiBaseUrl').value;
ApiClient.updatePluginConfiguration(ChaosflixConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
loadStatus();
});
});
e.preventDefault();
return false;
});
})();
</script>
</div>
</body>
</html>