Files
weircon-front/templates/admin_yt.html
T
agw 2c146b0711 feat(yt): overvåg playlister og kanaler, med cycle X på kanaler
Playliste-abonnement henter nye videoer efterhånden som de lægges i playlisten.
Kanal-abonnement holder en rullende buffer: cycle X = de X nyeste videoer ligger
klar, og kommer der en ny, ryger den ældste ud.

X tæller kun kanal-abonnementets egne hentninger. Manuelle hentninger,
playliste-abonnementer og favoritter tæller ikke med — og favoritter slettes
aldrig, så en favoritering frigør en plads i cyklussen til næste video.

Tre ting der måtte designes udenom:

1. `seen` husker hvert video-id abonnementet nogensinde har hentet, også efter
   videoen er slettet igen. Uden det ville set-reglen slette en video efter 7
   dage, hvorefter kanalen stadig har den blandt sine nyeste og henter den igen
   — i en uendelig løkke.
2. Playliste og kanal primer forskelligt ved oprettelse. En playliste kan have
   hundredvis af videoer, og ønsket var NYE videoer, så første tjek markerer det
   eksisterende som kendt uden at hente. En kanal henter derimod de X nyeste med
   det samme; det er hele pointen med cycle X. Resten af bagkataloget markeres
   som kendt, så den aldrig begynder at hente gammelt indhold.
3. enforce_cycles rører intet hvis Emby ikke svarer. Uden favorit-data kunne den
   komme til at slette noget fredet — samme konservative holdning som mediaclean.

Downloads gemmer nu video-id og upload-dato, så cyklussen kan sortere efter hvor
ny videoen ER, ikke hvornår vi tilfældigvis hentede den.

Timeren kører i rækkefølgen: tjek abonnementer -> hent -> håndhæv cyklus ->
slet sete. Cyklussen skal håndhæves efter hentningen, ellers ser den et halvt
bibliotek.

Claude-Session: https://claude.ai/code/session_011WxG8pbLj61jYU1MMaD7VC
2026-09-19 22:32:40 +02:00

181 lines
9.5 KiB
HTML

{% extends "base.html" %}
{% block title %}Admin — YouTube offline{% endblock %}
{% block body %}
<h1>YouTube offline</h1>
<p class="lead">Indsæt en YouTube-URL, så hentes videoen til Emby-biblioteket
<strong>{{ settings.library_name }}</strong> — klar til at se uden net, fx i et fly.
Når du har <strong>markeret den som set</strong> i Emby, slettes den efter
{{ settings.grace_days }} dage. <strong>Favoritter slettes aldrig.</strong>
Halvt sete videoer rører oprydningen ikke.</p>
{% if saved %}<div class="flash">{{ saved }}</div>{% endif %}
{% if unconfigured %}
<div class="err">Ikke konfigureret endnu. Kræver <code>EMBY_URL/EMBY_API_KEY</code> i
<code>/etc/portal.env</code>, <code>yt-dlp</code> installeret, og at download-mappen
<code>{{ download_dir }}</code> findes i containeren.</div>
{% else %}
{% if inv_error %}<div class="err">⚠ Emby: {{ inv_error }} — oprydningen står stille indtil den kan læse se-data (sikkerhed).</div>{% endif %}
{# ---- hent ny ---- #}
<form method="post" action="/admin/yt/add" style="margin-top:1.4rem;display:flex;gap:.6rem;flex-wrap:wrap;align-items:center">
<input name="url" placeholder="https://www.youtube.com/watch?v=…" required
style="flex:1 1 26rem;min-width:16rem">
<label class="sw" style="flex:0 0 auto"><input type="checkbox" name="playlist"> <span>Hele playlisten</span></label>
<button class="btn" type="submit">Hent</button>
</form>
{# ---- topkort ---- #}
<div class="grid" style="grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));margin-top:1.4rem">
<div class="stat"><div class="muted" style="font-size:.8rem;text-transform:uppercase;letter-spacing:.08em">I biblioteket</div>
<div class="n">{{ ours|length }}</div>
<div class="muted" style="font-size:.85rem;margin-top:.4rem">hentet af portalen · {{ human(ours_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">{{ leaving|length }}</div>
<div class="muted" style="font-size:.85rem;margin-top:.4rem">set · i karens</div></div>
<div class="stat"><div class="muted" style="font-size:.8rem;text-transform:uppercase;letter-spacing:.08em">I kø</div>
<div class="n">{{ queued }}</div>
<div class="muted" style="font-size:.85rem;margin-top:.4rem">{% if running %}1 i gang{% else %}ingen i gang{% endif %}</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">fra</span>{% endif %}</div>
<div class="muted" style="font-size:.85rem;margin-top:.4rem">{{ settings.grace_days }} dages karens</div></div>
</div>
{# ---- indstillinger ---- #}
<details style="margin-top:1.6rem">
<summary style="cursor:pointer;font-weight:600">⚙️ Indstillinger</summary>
<form method="post" action="/admin/yt/settings" style="margin-top:1rem">
<div class="setgrid">
<label class="sw"><input type="checkbox" name="enabled" {% if settings.enabled %}checked{% endif %}> <span>Slet sete videoer automatisk</span></label>
<label class="sw"><input type="checkbox" name="sponsorblock" {% if settings.sponsorblock %}checked{% endif %}> <span>SponsorBlock — klip sponsorater ud</span></label>
<label class="sw"><input type="checkbox" name="subtitles" {% if settings.subtitles %}checked{% endif %}> <span>Undertekster hvis de findes</span></label>
<label class="sw"><input type="checkbox" name="metadata" {% if settings.metadata %}checked{% endif %}> <span>Metadata, miniature &amp; kapitler</span></label>
</div>
<div class="setgrid" style="margin-top:.8rem">
<label class="fld"><span>Karens efter set (dage)</span><input type="number" name="grace_days" min="0" max="365" value="{{ settings.grace_days }}"></label>
<label class="fld"><span>Undertekst-sprog</span><input name="sub_langs" value="{{ settings.sub_langs }}"></label>
<label class="fld"><span>Emby-bibliotek</span><input name="library_name" value="{{ settings.library_name }}"></label>
<label class="fld"><span>Kvalitet (yt-dlp -f)</span><input name="quality" value="{{ settings.quality }}"></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/yt/run" formmethod="post">Kør oprydning nu</button>
</p>
</form>
</details>
{# ---- abonnementer ---- #}
<h2 style="margin-top:2rem">Overvågede playlister &amp; kanaler</h2>
<p class="muted" style="margin-top:.3rem">
En <strong>playliste</strong> henter nye videoer efterhånden som de lægges i den —
det der allerede ligger i playlisten, når du opretter overvågningen, hentes ikke.
En <strong>kanal</strong> holder de <strong>X nyeste</strong> videoer liggende: kommer der en ny,
ryger den ældste ud. Favoritter tæller <em>ikke</em> med i X og slettes aldrig, så en
favoritering frigør en plads. Manuelle hentninger og playlister tæller heller ikke med.
</p>
<form method="post" action="/admin/yt/sub/add" style="margin-top:1rem;display:flex;gap:.6rem;flex-wrap:wrap;align-items:center">
<input name="url" placeholder="Playliste- eller kanal-URL" required style="flex:1 1 22rem;min-width:14rem">
<select name="kind" style="flex:0 0 auto">
<option value="">Gæt selv</option>
<option value="playlist">Playliste</option>
<option value="channel">Kanal</option>
</select>
<label class="fld" style="flex:0 0 9rem"><span>cycle X (kanal)</span>
<input type="number" name="cycle" min="1" max="100" placeholder="fx 3"></label>
<button class="btn" type="submit">Overvåg</button>
<button class="btn secondary" type="submit" formaction="/admin/yt/subs/check">Tjek alle nu</button>
</form>
{% if subs %}
<table class="wt-table" style="margin-top:1rem">
<thead><tr><th>Navn</th><th style="width:7rem">Type</th><th style="width:12rem">Cyklus</th>
<th style="width:10rem">Sidst tjekket</th><th style="width:12rem"></th></tr></thead>
<tbody>
{% for sb in subs %}
<tr>
<td>{{ sb.title }}<br><span class="muted" style="font-size:.8rem">{{ sb.url }}</span></td>
<td>{% if sb.kind == 'channel' %}Kanal{% else %}Playliste{% endif %}</td>
<td class="muted">
{% if sb.kind == 'channel' and sb.cycle %}
{{ sb.in_cycle }} af {{ sb.cycle }} i cyklus
{% if sb.favorites %}<br>+ {{ sb.favorites }} favorit{% if sb.favorites != 1 %}ter{% endif %} (fredet){% endif %}
{% else %}
{{ sb.on_disk }} hentet
{% endif %}
</td>
<td class="muted">{% if sb.last_check %}{{ ((now - sb.last_check) / 3600) | round(0) | int }} t siden{% else %}aldrig{% endif %}</td>
<td>
<form method="post" action="/admin/yt/sub/{{ sb.id }}/save" style="display:flex;gap:.4rem;align-items:center">
{% if sb.kind == 'channel' %}<input type="number" name="cycle" min="1" max="100" value="{{ sb.cycle }}" style="width:4.2rem">{% endif %}
<label class="sw" style="margin:0"><input type="checkbox" name="enabled" {% if sb.enabled %}checked{% endif %}> <span>aktiv</span></label>
<button class="btn" type="submit">Gem</button>
<button class="btn secondary" type="submit" formaction="/admin/yt/sub/{{ sb.id }}/del"
onclick="return confirm('Fjern overvågningen af {{ sb.title }}? Allerede hentede videoer bliver liggende.')">✕</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="muted">Ingen overvågninger endnu.</p>
{% endif %}
{# ---- vores videoer ---- #}
<h2 style="margin-top:2rem">Hentede videoer</h2>
{% if not ours %}<p class="muted">Ingen endnu — indsæt en URL ovenfor.</p>{% else %}
<table class="wt-table" style="margin-top:.8rem">
<thead><tr><th>Titel</th><th style="width:7rem">Størrelse</th><th style="width:9rem">Status</th><th style="width:11rem">Slettes</th></tr></thead>
<tbody>
{% for v in ours %}
<tr>
<td>{{ v.name }}</td>
<td class="muted">{{ human(v.size) }}</td>
<td>
{% if v.favorite %}<span style="color:#3ecf8e">★ favorit — fredet</span>
{% elif v.played %}<span style="color:#f5a623">set</span>
{% else %}<span class="muted">ikke set</span>{% endif %}
</td>
<td class="muted">
{% if v.favorite %}aldrig
{% elif v.delete_after %}om {{ ((v.delete_after - now) / 86400) | round(0, 'ceil') | int }} dage
{% else %}—{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{# ---- kø & historik ---- #}
{% if jobs %}
<h2 style="margin-top:2rem">Kø &amp; historik</h2>
<table class="wt-table" style="margin-top:.8rem">
<thead><tr><th>URL / titel</th><th style="width:8rem">Status</th><th>Besked</th></tr></thead>
<tbody>
{% for j in jobs %}
<tr>
<td>{{ j.title or j.url }}</td>
<td>{% if j.status == 'done' %}<span style="color:#3ecf8e">hentet</span>
{% elif j.status == 'error' %}<span style="color:#f87171">fejl</span>
{% elif j.status == 'running' %}i gang…
{% elif j.status == 'deleted' %}<span class="muted">slettet</span>
{% else %}i kø{% endif %}</td>
<td class="muted">{{ j.error }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if log %}
<h2 style="margin-top:2rem">Log</h2>
<ul class="muted" style="margin-top:.6rem;line-height:1.7">
{% for l in log %}<li>{{ l.text }}</li>{% endfor %}
</ul>
{% endif %}
{% endif %}
{% endblock %}