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>
211 lines
12 KiB
HTML
211 lines
12 KiB
HTML
<!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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
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>
|