Add Admin section to System: reclaim space from unwatched shows
All checks were successful
Deploy / deploy (push) Successful in 48s

Cross-references Sonarr's per-series disk usage against Tautulli's full
play history to surface the 10 largest shows nobody has ever watched,
excluding anything added in the last 6 months so new downloads aren't
flagged before they've had a chance to be watched. Delete removes the
series from Sonarr along with its episode files and folder, behind the
site's usual two-tap arm/confirm rather than a native dialog.

Also dedupes the never-wired /api/tautulli/never-watched endpoint onto
the same helper, and clears the Sonarr/Radarr list caches on delete so
a removed title doesn't linger in other cards until the TTL expires.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPJxtE9tPURsaWDZ6rC4Dt
This commit is contained in:
Barely Removable 2026-09-05 09:32:45 -07:00
parent d38ab1b561
commit cad4cd0a84
3 changed files with 209 additions and 55 deletions

View file

@ -234,6 +234,75 @@
armed = btn;
};
/* ── Admin: delete unwatched (P105) ───────────────────
Removing a series from Sonarr with its files is permanent,
so it shares the same two-tap arming as everything else here
rather than a native confirm(). Each row carries its own byte
count, so the reclaimable total in the field meta can be
recomputed client-side after a delete instead of going stale
until the next page load. */
function fmtBytes(b) {
if (b >= 1073741824) return (b / 1073741824).toFixed(1) + ' GB';
return (b / 1048576).toFixed(0) + ' MB';
}
function settleAdmin() {
var list = document.getElementById('admin-rows');
var meta = document.getElementById('admin-meta');
var rows = list ? list.querySelectorAll('.tt-row') : [];
var total = 0;
for (var i = 0; i < rows.length; i++) total += parseInt(rows[i].dataset.bytes, 10) || 0;
if (meta) meta.textContent = rows.length + ' unwatched · ' + fmtBytes(total) + ' reclaimable';
if (rows.length === 0 && list) {
var empty = document.createElement('p');
empty.className = 'tt-empty';
empty.id = 'admin-empty';
empty.textContent = 'Nothing left to reclaim.';
list.parentNode.replaceChild(empty, list);
}
}
window.armDelete = function (btn) {
if (btn.disabled) return;
if (armed && armed !== btn) disarm();
if (armed === btn) {
var row = btn.closest('.tt-row');
btn.disabled = true;
btn.classList.remove('armed');
btn.textContent = 'DELETING';
armed = null;
fetch(BASE + '/api/delete/show', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sonarr_id: btn.dataset.id })
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (d && d.success) {
if (row && row.parentNode) row.parentNode.removeChild(row);
settleAdmin();
} else {
btn.disabled = false;
btn.classList.add('armed');
btn.textContent = 'FAILED';
}
})
.catch(function () {
btn.disabled = false;
btn.classList.add('armed');
btn.textContent = 'FAILED';
});
return;
}
btn.dataset.label = btn.textContent.trim();
btn.classList.add('armed');
btn.textContent = 'CONFIRM?';
armed = btn;
};
document.addEventListener('click', function (e) {
if (armed && !armed.contains(e.target)) disarm();
});