diff --git a/main.py b/main.py
index c6de434..e8f1106 100644
--- a/main.py
+++ b/main.py
@@ -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)
+@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")
async def admin_yt_settings(request: Request):
require_admin(request)
diff --git a/templates/admin_yt.html b/templates/admin_yt.html
index 0dfdf7d..6fb6f09 100644
--- a/templates/admin_yt.html
+++ b/templates/admin_yt.html
@@ -126,7 +126,7 @@
Hentede videoer
{% if not ours %}Ingen endnu — indsæt en URL ovenfor.
{% else %}
- | Titel | Størrelse | Status | Slettes |
+ | Titel | Størrelse | Status | Slettes | |
{% for v in ours %}
@@ -142,6 +142,13 @@
{% elif v.delete_after %}om {{ ((v.delete_after - now) / 86400) | round(0, 'ceil') | int }} dage
{% else %}—{% endif %}
+ |
+
+ |
{% endfor %}
diff --git a/ytdl.py b/ytdl.py
index 9311978..717d0d4 100644
--- a/ytdl.py
+++ b/ytdl.py
@@ -488,6 +488,44 @@ async def enforce_cycles() -> dict:
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 ----------------
async def _emby(client, path, **params):
params["api_key"] = config.EMBY_API_KEY