258 lines
16 KiB
HTML
258 lines
16 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Admin — medie-oprydning{% endblock %}
|
||
{% block body %}
|
||
<h1>Medie-oprydning</h1>
|
||
<p class="lead">Finder film & serier, der <strong>aldrig er set</strong> af nogen og blev tilføjet for længe siden,
|
||
og sletter dem automatisk efter en karensperiode — medmindre du beholder dem. Behold via Emby-favorit,
|
||
<code>keep</code>-tag i Sonarr/Radarr, hele samlinger eller knappen her; alle vises samlet.</p>
|
||
{% if saved %}<div class="flash">{{ saved }}</div>{% endif %}
|
||
|
||
{% if unconfigured %}
|
||
<div class="err">Ikke konfigureret endnu. Sæt <code>EMBY_URL/EMBY_API_KEY</code>,
|
||
<code>RADARR_URL/RADARR_API_KEY</code> og <code>SONARR_URL/SONARR_API_KEY</code> i <code>/etc/portal.env</code>.</div>
|
||
{% else %}
|
||
|
||
{% if not watch_ok %}<div class="err">⚠ Emby svarede ikke — se-data mangler, så intet markeres som kandidat (sikkerhed).</div>{% endif %}
|
||
|
||
{# ---- topkort: disk + status ---- #}
|
||
{% set cands = items | selectattr('status','equalto','candidate') | list %}
|
||
{% set leavingrows = items | selectattr('delete_after') | list %}
|
||
{% set cand_size = cands | sum(attribute='size') %}
|
||
<div class="grid" style="grid-template-columns:repeat(auto-fill,minmax(15rem,1fr));margin-top:1.4rem">
|
||
<div class="stat">
|
||
<div class="muted" style="font-size:.8rem;text-transform:uppercase;letter-spacing:.08em">Slow-lager</div>
|
||
{% if disk.ok %}
|
||
<div class="n">{{ disk.used_pct }}%</div>
|
||
<div class="bar"><span style="width:{{ disk.used_pct }}%;{% if disk.used_pct >= settings.disk_high %}background:#f87171{% endif %}"></span></div>
|
||
<div class="muted" style="font-size:.85rem;margin-top:.4rem">{{ human(disk.free) }} fri af {{ human(disk.total) }}</div>
|
||
{% else %}<div class="muted">utilgængelig</div>{% endif %}
|
||
</div>
|
||
<div class="stat"><div class="muted" style="font-size:.8rem;text-transform:uppercase;letter-spacing:.08em">Kandidater nu</div>
|
||
<div class="n">{{ cands|length }}</div>
|
||
<div class="muted" style="font-size:.85rem;margin-top:.4rem">frigør {{ human(cand_size) }}</div></div>
|
||
<div class="stat"><div class="muted" style="font-size:.8rem;text-transform:uppercase;letter-spacing:.08em">På vej ud</div>
|
||
<div class="n">{{ leavingrows|length }}</div>
|
||
<div class="muted" style="font-size:.85rem;margin-top:.4rem">i karens</div></div>
|
||
<div class="stat"><div class="muted" style="font-size:.8rem;text-transform:uppercase;letter-spacing:.08em">Sletning</div>
|
||
<div class="n" style="font-size:1.4rem;margin-top:.4rem">{% if settings.enabled %}<span style="color:#3ecf8e">AKTIV</span>{% else %}<span style="color:#f5a623">dry-run</span>{% endif %}</div>
|
||
<div class="muted" style="font-size:.85rem;margin-top:.4rem">{% if settings.disk_throttle %}kun over {{ settings.disk_high }}%{% else %}altid efter karens{% endif %}</div></div>
|
||
</div>
|
||
|
||
{# ---- indstillinger (redigerbare defaults) ---- #}
|
||
<details style="margin-top:1.6rem" {% if not settings.enabled %}open{% endif %}>
|
||
<summary style="cursor:pointer;font-weight:600">⚙️ Indstillinger</summary>
|
||
<form method="post" action="/admin/media/settings" style="margin-top:1rem">
|
||
<div class="setgrid">
|
||
<label class="sw"><input type="checkbox" name="enabled" {% if settings.enabled %}checked{% endif %}> <span>Slet automatisk (ellers kun dry-run)</span></label>
|
||
<label class="sw"><input type="checkbox" name="disk_throttle" {% if settings.disk_throttle %}checked{% endif %}> <span>Slet kun når lageret er fyldt</span></label>
|
||
<label class="sw"><input type="checkbox" name="do_movies" {% if settings.do_movies %}checked{% endif %}> <span>Inkludér film</span></label>
|
||
<label class="sw"><input type="checkbox" name="do_series" {% if settings.do_series %}checked{% endif %}> <span>Inkludér serier</span></label>
|
||
</div>
|
||
<div class="setgrid" style="margin-top:.8rem">
|
||
<label class="fld"><span>Alder før kandidat (dage)</span><input type="number" name="age_days" min="1" max="3650" value="{{ settings.age_days }}"></label>
|
||
<label class="fld"><span>Karens før sletning (dage)</span><input type="number" name="grace_days" min="0" max="365" value="{{ settings.grace_days }}"></label>
|
||
<label class="fld"><span>Start sletning over (% fyldt)</span><input type="number" name="disk_high" min="5" max="100" value="{{ settings.disk_high }}"></label>
|
||
<label class="fld"><span>Stop sletning under (% fyldt)</span><input type="number" name="disk_low" min="1" max="99" value="{{ settings.disk_low }}"></label>
|
||
</div>
|
||
<p style="margin-top:1rem;display:flex;gap:.6rem;flex-wrap:wrap">
|
||
<button class="btn" type="submit">Gem indstillinger</button>
|
||
<button class="btn secondary" type="submit" formaction="/admin/media/run" formmethod="post">Kør simulering nu</button>
|
||
</p>
|
||
</form>
|
||
</details>
|
||
|
||
{# ---- samlinger: behold hele samlinger (også fremtidige film) ---- #}
|
||
{% set kept_colls = collections | selectattr('kept') | list %}
|
||
<details style="margin-top:1rem">
|
||
<summary style="cursor:pointer;font-weight:600">📦 Samlinger — behold hele samlinger{% if kept_colls %} ({{ kept_colls|length }} valgt){% endif %}</summary>
|
||
<p class="muted" style="margin:.6rem 0;font-size:.9rem">Markér en samling, så <em>alle</em> dens film — også dem du tilføjer senere — aldrig slettes.</p>
|
||
<div class="filter" style="max-width:22rem;margin:0 0 .4rem"><span class="mag">⌕</span><input id="cfilter" type="search" placeholder="Søg samling…" autocomplete="off"></div>
|
||
<div id="clist" class="clist"></div>
|
||
</details>
|
||
<script id="cdata" type="application/json">{{ collections|tojson }}</script>
|
||
|
||
{# ---- filter ---- #}
|
||
<div class="opstools" style="margin-top:1.8rem">
|
||
<div class="filter" style="margin:0;flex:1;min-width:11rem">
|
||
<span class="mag">⌕</span>
|
||
<input id="mfilter" type="search" placeholder="Søg titel…" autocomplete="off">
|
||
</div>
|
||
<div class="segmented" id="mmode" role="tablist">
|
||
<button type="button" data-f="candidate" class="on">Kandidater</button>
|
||
<button type="button" data-f="leaving">På vej ud</button>
|
||
<button type="button" data-f="kept">Beholdt</button>
|
||
<button type="button" data-f="all">Alle</button>
|
||
</div>
|
||
</div>
|
||
|
||
<table style="margin-top:1rem">
|
||
<thead><tr><th>Titel</th><th style="width:64px">Type</th><th style="width:90px">Størrelse</th><th style="width:104px">Tilføjet</th><th style="width:170px">Status</th><th style="width:96px"></th></tr></thead>
|
||
<tbody id="mtbody"></tbody>
|
||
</table>
|
||
<div id="mpager" class="pager"></div>
|
||
<p id="mnomatch" class="muted" style="display:none;margin-top:1rem">Ingen titler matcher.</p>
|
||
<script id="mdata" type="application/json">{{ items|tojson }}</script>
|
||
|
||
{% if log %}
|
||
<h2>Seneste sletninger</h2>
|
||
<table><tbody>
|
||
{% for l in log %}<tr><td>{{ l.title }}</td><td style="width:64px">{{ 'Film' if l.type=='movie' else 'Serie' }}</td><td style="width:90px">{{ human(l.size) }}</td></tr>{% endfor %}
|
||
</tbody></table>
|
||
{% endif %}
|
||
|
||
<style>
|
||
.bar { height:.5rem; background:var(--surface2); border-radius:999px; overflow:hidden; margin-top:.5rem; }
|
||
.bar span { display:block; height:100%; background:var(--accent); border-radius:999px; }
|
||
.setgrid { display:grid; gap:.7rem; grid-template-columns:repeat(auto-fill,minmax(15rem,1fr)); }
|
||
.sw { display:flex; align-items:center; gap:.55rem; cursor:pointer; }
|
||
.sw input { width:auto; }
|
||
.fld { display:flex; flex-direction:column; gap:.3rem; font-size:.85rem; color:var(--muted); }
|
||
.fld input { width:100%; }
|
||
.mbadge { display:inline-block; font-size:.74rem; font-weight:600; border-radius:999px; padding:.1rem .6rem; border:1px solid var(--border); white-space:nowrap; }
|
||
.mbadge.amber { color:#f5a623; border-color:color-mix(in srgb,#f5a623 50%,transparent); background:color-mix(in srgb,#f5a623 12%,transparent); }
|
||
.mbadge.red { color:#f87171; border-color:color-mix(in srgb,#f87171 55%,transparent); background:color-mix(in srgb,#f87171 14%,transparent); }
|
||
.mbadge.green { color:#3ecf8e; border-color:color-mix(in srgb,#3ecf8e 40%,transparent); background:color-mix(in srgb,#3ecf8e 10%,transparent); }
|
||
.mbadge.muted { color:var(--muted); }
|
||
.segmented { display:inline-flex; border:1px solid var(--border); border-radius:.7rem; overflow:hidden; }
|
||
.segmented button { background:transparent; color:var(--muted); border:0; padding:.5rem .9rem; cursor:pointer; font:inherit; font-size:.85rem; }
|
||
.segmented button.on { background:var(--surface2); color:var(--text); }
|
||
.opstools { display:flex; gap:.8rem; align-items:center; flex-wrap:wrap; }
|
||
.pager { display:flex; gap:.9rem; align-items:center; margin-top:1rem; }
|
||
.pager button { padding:.35rem .8rem; }
|
||
.pager button:disabled { opacity:.4; cursor:default; }
|
||
td .btn[disabled] { opacity:.6; cursor:default; }
|
||
.clist { display:flex; flex-direction:column; gap:.4rem; margin-top:.4rem; max-height:24rem; overflow:auto; }
|
||
.crow { display:flex; align-items:center; gap:.8rem; padding:.45rem .7rem; border:1px solid var(--border); border-radius:.7rem; background:color-mix(in srgb,var(--surface) 70%,transparent); }
|
||
.crow.on { border-color:color-mix(in srgb,#3ecf8e 45%,transparent); background:color-mix(in srgb,#3ecf8e 8%,transparent); }
|
||
.crow .cname { flex:1; font-weight:600; font-size:.9rem; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||
.crow .ccount { font-size:.8rem; flex:0 0 auto; }
|
||
</style>
|
||
<script>
|
||
(function(){
|
||
var all = JSON.parse(document.getElementById('mdata').textContent || '[]');
|
||
var colls = JSON.parse(document.getElementById('cdata').textContent || '[]');
|
||
var now = {{ now|default(0) }};
|
||
var PAGE = 25, st = { q:'', mode:'candidate', page:1 };
|
||
var tb = document.getElementById('mtbody'), pager = document.getElementById('mpager'),
|
||
nomatch = document.getElementById('mnomatch'), clist = document.getElementById('clist');
|
||
function esc(s){ return String(s).replace(/[&<>"]/g, function(c){
|
||
return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); }
|
||
function human(n){ var u=['B','KB','MB','GB','TB'], i=0; while(n>=1024 && i<4){ n/=1024; i++; }
|
||
return n.toFixed(1)+' '+u[i]; }
|
||
function leaving(it){ return it.delete_after != null; }
|
||
|
||
// ---- hovedtabel ----
|
||
function match(it){
|
||
var okq = !st.q || it.title.toLowerCase().indexOf(st.q) > -1;
|
||
var okm = st.mode==='all'
|
||
|| (st.mode==='candidate' && it.status==='candidate' && !leaving(it))
|
||
|| (st.mode==='leaving' && leaving(it))
|
||
|| (st.mode==='kept' && it.kept);
|
||
return okq && okm;
|
||
}
|
||
function badge(it){
|
||
if (leaving(it)) return '<span class="mbadge red">På vej ud · '+Math.ceil((it.delete_after-now)/86400)+'d</span>';
|
||
if (it.status==='candidate') return '<span class="mbadge amber">Kandidat</span>';
|
||
if (it.status==='kept') return '<span class="mbadge green">Beholdt'+((it.keep_sources&&it.keep_sources.length)?' · '+esc(it.keep_sources.join(', ')):'')+'</span>';
|
||
if (it.status==='watched') return '<span class="mbadge muted">Set</span>';
|
||
if (it.status==='tooNew') return '<span class="mbadge muted">For ny</span>';
|
||
return '<span class="mbadge muted">Ingen fil</span>';
|
||
}
|
||
function row(it){
|
||
var btn = it.kept
|
||
? '<button class="btn secondary keepbtn" data-key="'+esc(it.key)+'" data-on="0" style="padding:.35rem .7rem;font-size:.82rem">Frigiv</button>'
|
||
: '<button class="btn keepbtn" data-key="'+esc(it.key)+'" data-on="1" style="padding:.35rem .7rem;font-size:.82rem">Behold</button>';
|
||
return '<tr><td>'+esc(it.title)+'</td><td>'+(it.type==='movie'?'Film':'Serie')+'</td><td>'+human(it.size)+'</td>'+
|
||
'<td class="muted" style="font-size:.85rem">'+esc((it.added||'').slice(0,10))+'</td><td>'+badge(it)+'</td><td>'+btn+'</td></tr>';
|
||
}
|
||
function render(){
|
||
var list = all.filter(match);
|
||
var pages = Math.max(1, Math.ceil(list.length/PAGE));
|
||
if (st.page > pages) st.page = pages;
|
||
tb.innerHTML = list.slice((st.page-1)*PAGE, st.page*PAGE).map(row).join('');
|
||
nomatch.style.display = list.length ? 'none' : 'block';
|
||
pager.innerHTML = list.length > PAGE
|
||
? '<button class="btn secondary" data-pg="prev"'+(st.page<=1?' disabled':'')+'>‹</button>'+
|
||
'<span class="muted">Side '+st.page+' af '+pages+' · '+list.length+' titler</span>'+
|
||
'<button class="btn secondary" data-pg="next"'+(st.page>=pages?' disabled':'')+'>›</button>'
|
||
: (list.length ? '<span class="muted">'+list.length+' titler</span>' : '');
|
||
}
|
||
document.getElementById('mfilter').addEventListener('input', function(e){
|
||
st.q = e.target.value.trim().toLowerCase(); st.page = 1; render(); });
|
||
document.getElementById('mmode').addEventListener('click', function(e){
|
||
var b = e.target.closest('button[data-f]'); if (!b) return;
|
||
st.mode = b.getAttribute('data-f'); st.page = 1;
|
||
this.querySelectorAll('button').forEach(function(x){ x.classList.toggle('on', x===b); });
|
||
render();
|
||
});
|
||
pager.addEventListener('click', function(e){
|
||
var b = e.target.closest('button[data-pg]'); if (!b || b.disabled) return;
|
||
st.page += b.getAttribute('data-pg')==='next' ? 1 : -1; render();
|
||
});
|
||
tb.addEventListener('click', function(e){
|
||
var b = e.target.closest('.keepbtn'); if (!b) return;
|
||
var key = b.getAttribute('data-key'), on = b.getAttribute('data-on')==='1';
|
||
b.disabled = true; b.textContent = '…';
|
||
fetch('/api/media/keep', { method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({ key:key, on:on }) })
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res){
|
||
if (!res || !res.ok) throw 0;
|
||
var it = all.filter(function(x){ return x.key===key; })[0]; if (!it) return;
|
||
var hadFav = (it.keep_sources||[]).indexOf('favorit') > -1;
|
||
var hadColl = (it.keep_sources||[]).indexOf('samling') > -1;
|
||
if (on){ var s=[]; if(hadFav)s.push('favorit'); if(hadColl)s.push('samling'); s.push('tag','portal');
|
||
it.keep_sources=s; it.kept=true; it.delete_after=null; }
|
||
else { var s2=[]; if(hadFav)s2.push('favorit'); if(hadColl)s2.push('samling');
|
||
it.keep_sources=s2; it.kept=s2.length>0; }
|
||
it.status = it.kept ? 'kept' : it.base_status;
|
||
render();
|
||
})
|
||
.catch(function(){ b.disabled = false; b.textContent = on?'Behold':'Frigiv';
|
||
alert('Kunne ikke gemme — prøv igen.'); });
|
||
});
|
||
|
||
// ---- samlinger ----
|
||
var cq = '';
|
||
function crow(c){
|
||
var btn = c.kept
|
||
? '<button class="btn secondary collbtn" data-tmdb="'+esc(c.tmdb)+'" data-title="'+esc(c.title)+'" data-on="0" style="padding:.3rem .7rem;font-size:.82rem">Beholdes</button>'
|
||
: '<button class="btn collbtn" data-tmdb="'+esc(c.tmdb)+'" data-title="'+esc(c.title)+'" data-on="1" style="padding:.3rem .7rem;font-size:.82rem">Behold</button>';
|
||
return '<div class="crow'+(c.kept?' on':'')+'"><span class="cname">'+esc(c.title)+'</span><span class="ccount muted">'+c.count+' film</span>'+btn+'</div>';
|
||
}
|
||
function crender(){
|
||
var list = colls.filter(function(c){ return !cq || c.title.toLowerCase().indexOf(cq) > -1; });
|
||
clist.innerHTML = list.length ? list.map(crow).join('') : '<p class="muted">Ingen samlinger.</p>';
|
||
}
|
||
document.getElementById('cfilter').addEventListener('input', function(e){
|
||
cq = e.target.value.trim().toLowerCase(); crender(); });
|
||
clist.addEventListener('click', function(e){
|
||
var b = e.target.closest('.collbtn'); if (!b) return;
|
||
var tmdb = b.getAttribute('data-tmdb'), title = b.getAttribute('data-title'), on = b.getAttribute('data-on')==='1';
|
||
b.disabled = true; b.textContent = '…';
|
||
fetch('/api/media/collection-keep', { method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({ tmdb:tmdb, title:title, on:on }) })
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(res){
|
||
if (!res || !res.ok) throw 0;
|
||
var c = colls.filter(function(x){ return x.tmdb===tmdb; })[0]; if (c) c.kept = on;
|
||
all.forEach(function(it){
|
||
if (it.coll !== tmdb) return;
|
||
var s = (it.keep_sources||[]).filter(function(x){ return x!=='samling'; });
|
||
if (on) s.push('samling');
|
||
it.keep_sources = s; it.kept = s.length>0; it.status = it.kept ? 'kept' : it.base_status;
|
||
if (it.kept) it.delete_after = null;
|
||
});
|
||
crender(); render();
|
||
})
|
||
.catch(function(){ b.disabled = false; b.textContent = on?'Behold':'Beholdes';
|
||
alert('Kunne ikke gemme — prøv igen.'); });
|
||
});
|
||
|
||
crender();
|
||
render();
|
||
})();
|
||
</script>
|
||
{% endif %}
|
||
{% endblock %}
|