Compare commits

...
1 Commits
Author SHA1 Message Date
agw fbdb257079 feat(yt): slet-knap på hentede videoer
CI / check (push) Successful in 23s
CD / deploy (push) Successful in 14s
Hver række under "Hentede videoer" på /admin/yt har nu en Slet-knap (med
bekræftelse; nævner det, hvis videoen er favorit). Sletningen bruger samme
sikkerhedsnet som oprydningen: kun filer portalen selv har hentet, og
undertekster/miniature slettes med. Er det jobbets sidste fil, markeres jobbet
som slettet, og Emby-biblioteket opdateres. Video-id'et bliver i
abonnementets "seen", så en overvåget kanal ikke henter den igen.

Claude-Session: https://claude.ai/code/session_012LCTmU2ewo8tnM1dByYAoG
2026-10-06 13:38:56 +02:00
3 changed files with 60 additions and 1 deletions
+14
View File
@@ -1069,6 +1069,20 @@ async def admin_yt_add(request: Request, url: str = Form(""), playlist: str = Fo
return RedirectResponse("/admin/yt?saved=Lagt i kø — hentningen er i gang", status_code=303) return RedirectResponse("/admin/yt?saved=Lagt i kø — hentningen er i gang", status_code=303)
@app.post("/admin/yt/slet")
async def admin_yt_delete(request: Request):
require_admin(request)
f = await request.form()
res = yt.delete_video(f.get("path") or "")
if not res["ok"]:
return RedirectResponse(f"/admin/yt?saved={urllib.parse.quote(res['error'])}", status_code=303)
cache.clear("yt:")
await yt.refresh_library()
return RedirectResponse(
"/admin/yt?saved=" + urllib.parse.quote(f"Slettet: {res['title']} ({yt._human(res['size'])})"),
status_code=303)
@app.post("/admin/yt/settings") @app.post("/admin/yt/settings")
async def admin_yt_settings(request: Request): async def admin_yt_settings(request: Request):
require_admin(request) require_admin(request)
+8 -1
View File
@@ -126,7 +126,7 @@
<h2 style="margin-top:2rem">Hentede videoer</h2> <h2 style="margin-top:2rem">Hentede videoer</h2>
{% if not ours %}<p class="muted">Ingen endnu — indsæt en URL ovenfor.</p>{% else %} {% if not ours %}<p class="muted">Ingen endnu — indsæt en URL ovenfor.</p>{% else %}
<table class="wt-table" style="margin-top:.8rem"> <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> <thead><tr><th>Titel</th><th style="width:7rem">Størrelse</th><th style="width:9rem">Status</th><th style="width:11rem">Slettes</th><th style="width:5.5rem"></th></tr></thead>
<tbody> <tbody>
{% for v in ours %} {% for v in ours %}
<tr> <tr>
@@ -142,6 +142,13 @@
{% elif v.delete_after %}om {{ ((v.delete_after - now) / 86400) | round(0, 'ceil') | int }} dage {% elif v.delete_after %}om {{ ((v.delete_after - now) / 86400) | round(0, 'ceil') | int }} dage
{% else %}—{% endif %} {% else %}—{% endif %}
</td> </td>
<td>
<form method="post" action="/admin/yt/slet" style="margin:0"
onsubmit="return confirm({{ ('Slet „' ~ v.name ~ '“ nu?' ~ (' Den er markeret som favorit.' if v.favorite else '')) | tojson | forceescape }})">
<input type="hidden" name="path" value="{{ v.path }}">
<button class="wt-btn" type="submit">Slet</button>
</form>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
+38
View File
@@ -488,6 +488,44 @@ async def enforce_cycles() -> dict:
return res return res
def delete_video(path: str) -> dict:
"""Slet én video manuelt (knappen på /admin/yt), med samme sikkerhedsnet som
oprydningen: KUN filer portalen selv har hentet, og sidecars med.
Video-id'et bliver stående i abonnementets `seen`, så en overvåget kanal ikke
henter den igen. Er det jobbets sidste fil, markeres jobbet som slettet.
"""
real = os.path.realpath(path or "")
if real not in owned_paths():
return {"ok": False, "error": "Den video har portalen ikke selv hentet, så den røres ikke"}
st = get_state()
try:
size = file_size(real)
for side in _sidecars(real):
os.remove(side)
if os.path.exists(real):
os.remove(real)
except OSError as e:
return {"ok": False, "error": f"Kunne ikke slette: {e}"}
st["leaving"].pop(real, None)
titel = os.path.basename(real)
for j in st["jobs"].values():
paths = [p for p in (j.get("paths") or ([j["path"]] if j.get("path") else [])) if p]
if real not in {os.path.realpath(p) for p in paths}:
continue
titel = j.get("title") or titel
rest = [p for p in paths if os.path.realpath(p) != real]
if j.get("paths") is not None:
j["paths"] = rest
if j.get("videos"):
j["videos"] = [v for v in j["videos"] if os.path.realpath(v.get("path") or "") != real]
if not rest:
j["status"] = "deleted"
_log(st, f"Slettet manuelt: {titel} ({_human(size)})")
save_state(st)
return {"ok": True, "title": titel, "size": size}
# ---------------- Emby ---------------- # ---------------- Emby ----------------
async def _emby(client, path, **params): async def _emby(client, path, **params):
params["api_key"] = config.EMBY_API_KEY params["api_key"] = config.EMBY_API_KEY