Files
weircon-front/templates/admin_media.html

258 lines
16 KiB
HTML
Raw Permalink 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.
{% 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 {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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 %}