Files
jellyfin-plugin-recently-ad…/Jellyfin.Plugin.RecentlyAddedGuard/Configuration/configPage.html
T
Jonas MenzelandClaude Opus 5.5 d431486095
build / build (push) Successful in 1m10s
release / release (push) Failing after 1m36s
feat: Recently Added Guard plugin for Jellyfin 12.2
Keeps Radarr/Sonarr quality upgrades out of "Recently Added" by keeping
a ledger of the first date added per logical medium (movie or episode,
identified by provider ids and the series) and restoring it on upgrades.

- core logic without Jellyfin types (Reconciler, MediaKeys, GuardProcessor)
- thread-safe ledger with atomic, debounced persistence
- event processing via channel and background worker, seed on first start
- scheduled task "Ledger synchronisieren", status API, configuration page
- xUnit tests and a Jellyfin 12.2 integration test
- Gitea workflows: build/test, release with jprm and manifest branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:10:02 +02:00

211 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Recently Added Guard</title>
</head>
<body>
<div id="RecentlyAddedGuardConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-checkbox">
<style>
#RecentlyAddedGuardConfigPage .ragStats { display: grid; grid-template-columns: repeat(auto-fit, minmax(11em, 1fr)); gap: 0.75em; margin: 1em 0; }
#RecentlyAddedGuardConfigPage .ragStat { padding: 0.75em 1em; border-radius: 0.3em; background: rgba(128, 128, 128, 0.12); }
#RecentlyAddedGuardConfigPage .ragStatValue { font-size: 1.6em; font-weight: 600; }
#RecentlyAddedGuardConfigPage .ragStatLabel { opacity: 0.75; font-size: 0.9em; }
#RecentlyAddedGuardConfigPage .ragTableWrap { overflow-x: auto; }
#RecentlyAddedGuardConfigPage table { width: 100%; border-collapse: collapse; }
#RecentlyAddedGuardConfigPage th, #RecentlyAddedGuardConfigPage td { text-align: left; padding: 0.45em 0.6em; border-bottom: 1px solid rgba(128, 128, 128, 0.25); white-space: nowrap; }
#RecentlyAddedGuardConfigPage td.ragName { white-space: normal; }
#RecentlyAddedGuardConfigPage .ragHint { opacity: 0.75; }
</style>
<div data-role="content">
<div class="content-primary">
<h2 class="sectionTitle">Recently Added Guard</h2>
<p class="ragHint">
Bewahrt das erste Hinzufügedatum jedes Films und jeder Episode. Qualitäts-Upgrades aus Radarr und Sonarr
behalten so ihr ursprüngliches Datum und erscheinen nicht erneut unter „Zuletzt hinzugefügt“.
</p>
<form id="RecentlyAddedGuardConfigForm">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="RagGraceDays">Frist in Tagen</label>
<input id="RagGraceDays" name="GraceDays" type="number" is="emby-input" min="0" max="3650" step="1" required />
<div class="fieldDescription">
Taucht ein gelöschter Film oder eine gelöschte Episode innerhalb dieser Frist wieder auf, gilt das als Upgrade und
das alte Datum wird wiederhergestellt. Nach Ablauf gilt das Medium als neu. Standard: 14.
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="RagToleranceSeconds">Toleranz in Sekunden</label>
<input id="RagToleranceSeconds" name="ToleranceSeconds" type="number" is="emby-input" min="0" max="86400" step="1" required />
<div class="fieldDescription">Kleinere Abweichungen des Datums werden ignoriert. Standard: 60.</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="RagEnableMovies" name="EnableMovies" type="checkbox" is="emby-checkbox" />
<span>Filme schützen</span>
</label>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="RagEnableEpisodes" name="EnableEpisodes" type="checkbox" is="emby-checkbox" />
<span>Episoden schützen</span>
</label>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="RagDryRun" name="DryRun" type="checkbox" is="emby-checkbox" />
<span>Testmodus (DryRun)</span>
</label>
<div class="fieldDescription checkboxFieldDescription">Upgrades nur protokollieren, kein Datum ändern.</div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Speichern</span>
</button>
</div>
</form>
<div class="verticalSection" style="margin-top: 2.5em;">
<h2 class="sectionTitle">Status</h2>
<div class="ragStats">
<div class="ragStat"><div id="RagLedgerEntries" class="ragStatValue">–</div><div class="ragStatLabel">Ledger-Einträge</div></div>
<div class="ragStat"><div id="RagLiveEntries" class="ragStatValue">–</div><div class="ragStatLabel">davon mit Items</div></div>
<div class="ragStat"><div id="RagRemovedEntries" class="ragStatValue">–</div><div class="ragStatLabel">in der Frist</div></div>
<div class="ragStat"><div id="RagCorrections" class="ragStatValue">–</div><div class="ragStatLabel">Korrekturen seit Start</div></div>
</div>
<p id="RagSyncInfo" class="ragHint"></p>
<button is="emby-button" type="button" id="RagSyncButton" class="raised block emby-button">
<span>Ledger synchronisieren</span>
</button>
<h3 style="margin-top: 2em;">Letzte Korrekturen</h3>
<div class="ragTableWrap">
<table>
<thead>
<tr>
<th>Name</th>
<th>Altes Datum</th>
<th>Neues Datum</th>
<th>Zeitpunkt</th>
</tr>
</thead>
<tbody id="RagCorrectionRows">
<tr><td colspan="4" class="ragHint">Lädt …</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script type="text/javascript">
(function () {
var pluginId = '6fdfedef-adde-4ac3-a873-62a057c95574';
var page = document.querySelector('#RecentlyAddedGuardConfigPage');
var form = page.querySelector('#RecentlyAddedGuardConfigForm');
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function formatDate(value) {
if (!value) {
return '–';
}
var date = new Date(value);
return isNaN(date.getTime()) ? escapeHtml(value) : date.toLocaleString('de-DE');
}
function loadConfiguration() {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(pluginId).then(function (config) {
page.querySelector('#RagGraceDays').value = config.GraceDays;
page.querySelector('#RagToleranceSeconds').value = config.ToleranceSeconds;
page.querySelector('#RagEnableMovies').checked = config.EnableMovies;
page.querySelector('#RagEnableEpisodes').checked = config.EnableEpisodes;
page.querySelector('#RagDryRun').checked = config.DryRun;
Dashboard.hideLoadingMsg();
});
}
function renderStatus(status) {
page.querySelector('#RagLedgerEntries').textContent = status.LedgerEntries;
page.querySelector('#RagLiveEntries').textContent = status.LiveEntries;
page.querySelector('#RagRemovedEntries').textContent = status.RemovedEntries;
page.querySelector('#RagCorrections').textContent = status.DryRun
? status.CorrectionsSinceStart + ' (+' + status.DryRunDetectionsSinceStart + ' DryRun)'
: status.CorrectionsSinceStart;
var info = status.SeedCompleted
? 'Letzte Synchronisation: ' + formatDate(status.LastSyncUtc) + '.'
: 'Das Ledger wird gerade zum ersten Mal befüllt (Seed).';
if (status.DryRun) {
info += ' Testmodus aktiv: Upgrades werden nur protokolliert.';
}
page.querySelector('#RagSyncInfo').textContent = info;
var rows = (status.RecentCorrections || []).map(function (c) {
return '<tr>'
+ '<td class="ragName">' + escapeHtml(c.Name) + (c.DryRun ? ' <span class="ragHint">(DryRun)</span>' : '') + '</td>'
+ '<td>' + formatDate(c.PreviousDate) + '</td>'
+ '<td>' + formatDate(c.RestoredDate) + '</td>'
+ '<td>' + formatDate(c.CorrectedAt) + '</td>'
+ '</tr>';
});
page.querySelector('#RagCorrectionRows').innerHTML = rows.length
? rows.join('')
: '<tr><td colspan="4" class="ragHint">Seit dem Start wurden keine Upgrades korrigiert.</td></tr>';
}
function loadStatus() {
return ApiClient.getJSON(ApiClient.getUrl('RecentlyAddedGuard/Status')).then(renderStatus, function () {
page.querySelector('#RagSyncInfo').textContent = 'Status konnte nicht geladen werden.';
});
}
page.addEventListener('pageshow', function () {
loadConfiguration();
loadStatus();
});
form.addEventListener('submit', function (e) {
e.preventDefault();
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(pluginId).then(function (config) {
config.GraceDays = parseInt(page.querySelector('#RagGraceDays').value, 10) || 0;
config.ToleranceSeconds = parseInt(page.querySelector('#RagToleranceSeconds').value, 10) || 0;
config.EnableMovies = page.querySelector('#RagEnableMovies').checked;
config.EnableEpisodes = page.querySelector('#RagEnableEpisodes').checked;
config.DryRun = page.querySelector('#RagDryRun').checked;
ApiClient.updatePluginConfiguration(pluginId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
loadStatus();
});
});
return false;
});
page.querySelector('#RagSyncButton').addEventListener('click', function () {
var button = this;
button.disabled = true;
ApiClient.ajax({ type: 'POST', url: ApiClient.getUrl('RecentlyAddedGuard/Sync') }).then(function () {
page.querySelector('#RagSyncInfo').textContent = 'Synchronisation gestartet. Fortschritt unter Geplante Aufgaben.';
setTimeout(loadStatus, 5000);
}, function () {
page.querySelector('#RagSyncInfo').textContent = 'Synchronisation konnte nicht gestartet werden.';
}).then(function () {
button.disabled = false;
});
});
})();
</script>
</div>
</body>
</html>