Delete job: cascade delete povsod (Nextcloud + dedup DB + S3 + lokal)

PROBLEM: '✕' gumb je do zdaj brisal samo lokalne fajle + S3, ampak
NE Nextcloud upload (rel je ostal na folxspeed/REELS/{TV}/) in NE
dedup DB zapis (zato se enaka pesem ni mogla več upload-ati).

NEW BACKEND:
- _nextcloud_delete(filename, target_subdir) helper preko WebDAV DELETE
  (404 šteje kot success — če že ne obstaja, OK)
- delete_job() razširjen:
  1. Nextcloud delete (če nextcloud_status='uploaded' ali ima nextcloud_url)
  2. Dedup DB remove (processed_videos zapis za tisto TV postajo)
  3. Lokal + S3 delete vseh workfile-ov (kot prej)
  4. Glob za yt-dlp artifacte ({job_id}_yt*) — info.json, .part, .f137.mp4
  5. Job metadata
- Response: {deleted, nextcloud_delete: 'ok'|'not_found'|'fail: msg', nextcloud_filename}

NEW FRONTEND:
- buildJobEl() doda data-nc-status atribut na kartico
- deleteJob() bere dataset.ncStatus za Nextcloud info
- Confirm dialog detail razložen seznam KAJ se zbriše + opozorilo če Nextcloud
- Če Nextcloud delete ni uspel po API klicu, alert
This commit is contained in:
Claude 2026-05-03 14:52:40 +00:00
parent 79f611ba73
commit 12e8edba93
2 changed files with 99 additions and 11 deletions

View File

@ -1859,14 +1859,41 @@ async def delete_job(job_id: str, user: str = Depends(check_auth)):
job = load_job(job_id) job = load_job(job_id)
if not job: if not job:
raise HTTPException(404, "Ne obstaja") raise HTTPException(404, "Ne obstaja")
# Glavni input + output (po job records)
# ── 1. Nextcloud (če je bil naložen) ──────────────────────
nc_filename = None
nc_subdir = None
nc_status = None
if job.get("nextcloud_status") == "uploaded" or job.get("nextcloud_url"):
try:
station = job.get("tv_station", "")
nc_subdir = f"{NEXTCLOUD_REELS_PATH.strip('/')}/{_nextcloud_folder_for_station(station)}"
nc_filename = build_download_filename(job)
ok, msg = _nextcloud_delete(nc_filename, target_subdir=nc_subdir)
nc_status = ("ok" if ok else f"fail: {msg}")
print(f"🗑️ NC delete {nc_filename}{nc_status}", flush=True)
except Exception as e:
nc_status = f"error: {e}"
print(f"⚠️ NC delete error: {e}", flush=True)
# ── 2. Dedup DB (processed_videos) ────────────────────────
try:
if nc_filename and job.get("tv_station"):
dedup_remove(nc_filename, job["tv_station"])
# Tudi po job_id — če je shranjen pod drugim filename
# (Glavno: zbriši zapis, da se enaka pesem lahko spet upload-a)
except Exception as e:
print(f"⚠️ Dedup remove error: {e}", flush=True)
# ── 3. Lokal + S3: glavni input + output ──────────────────
for key, kind in (("input_path", "upload"), ("output_path", "output")): for key, kind in (("input_path", "upload"), ("output_path", "output")):
p = job.get(key) p = job.get(key)
if p: if p:
local_p = Path(p) local_p = Path(p)
local_p.unlink(missing_ok=True) local_p.unlink(missing_ok=True)
_delete_from_s3(local_p.name, kind) _delete_from_s3(local_p.name, kind)
# Pomožne datoteke v outputs/ (analysis, subtitles, low-q, waveform)
# ── 4. Pomožne datoteke (analysis, subtitles, low-q, waveform) ──
for fname in ( for fname in (
f"{job_id}.mp4", f"{job_id}.mp4",
f"{job_id}.analysis.json", f"{job_id}.analysis.json",
@ -1877,6 +1904,7 @@ async def delete_job(job_id: str, user: str = Depends(check_auth)):
f = OUTPUT_DIR / fname f = OUTPUT_DIR / fname
f.unlink(missing_ok=True) f.unlink(missing_ok=True)
_delete_from_s3(fname, "output") _delete_from_s3(fname, "output")
# Waveform PNG-ji (več velikosti) — listanje ker imena niso fiksna # Waveform PNG-ji (več velikosti) — listanje ker imena niso fiksna
try: try:
for wf in OUTPUT_DIR.glob(f"{job_id}_waveform_*.png"): for wf in OUTPUT_DIR.glob(f"{job_id}_waveform_*.png"):
@ -1885,16 +1913,26 @@ async def delete_job(job_id: str, user: str = Depends(check_auth)):
_delete_from_s3(wf_name, "output") _delete_from_s3(wf_name, "output")
except Exception: except Exception:
pass pass
# YT info.json
info_json = UPLOAD_DIR / f"{job_id}_yt.info.json" # YT info.json + ostali yt-dlp artifacti (.part, .f137.mp4, ...)
if info_json.exists(): try:
info_json.unlink(missing_ok=True) for f in UPLOAD_DIR.glob(f"{job_id}_yt*"):
_delete_from_s3(f"{job_id}_yt.info.json", "upload") f_name = f.name
# Job metadata f.unlink(missing_ok=True)
_delete_from_s3(f_name, "upload")
except Exception:
pass
# ── 5. Job metadata (lokal + S3) ──────────────────────────
jp = job_path(job_id) jp = job_path(job_id)
jp.unlink(missing_ok=True) jp.unlink(missing_ok=True)
_delete_from_s3(f"{job_id}.json", "job_meta") _delete_from_s3(f"{job_id}.json", "job_meta")
return {"deleted": job_id}
return {
"deleted": job_id,
"nextcloud_delete": nc_status,
"nextcloud_filename": nc_filename,
}
# ─── EDIT FEATURE ──────────────────────────────────────────────── # ─── EDIT FEATURE ────────────────────────────────────────────────
@ -2294,6 +2332,42 @@ def _nextcloud_upload(local_path: str, remote_filename: str, target_subdir: str
return False, f"Upload error: {e}" return False, f"Upload error: {e}"
def _nextcloud_delete(remote_filename: str, target_subdir: str = None):
"""Pobriše datoteko iz Nextclouda preko WebDAV DELETE.
Vrne (success: bool, msg: str). 404 (ne obstaja) tudi šteje kot success.
"""
if not _nextcloud_configured():
return False, "Nextcloud ni konfiguriran"
import urllib.request, urllib.error, base64
from urllib.parse import quote
base_path = (target_subdir or NEXTCLOUD_REELS_PATH).strip("/")
safe_dir = "/".join(quote(p, safe="") for p in base_path.split("/"))
safe_file = quote(remote_filename, safe="")
url = f"{NEXTCLOUD_URL}/remote.php/dav/files/{NEXTCLOUD_USER}/{safe_dir}/{safe_file}"
auth = base64.b64encode(f"{NEXTCLOUD_USER}:{NEXTCLOUD_PASS}".encode()).decode()
try:
req = urllib.request.Request(
url,
headers={"Authorization": f"Basic {auth}"},
method="DELETE",
)
with urllib.request.urlopen(req, timeout=30) as resp:
if resp.status in (200, 204):
return True, "deleted"
return False, f"HTTP {resp.status}"
except urllib.error.HTTPError as e:
if e.code == 404:
return True, "not_found" # že ne obstaja — OK
return False, f"HTTP {e.code}"
except Exception as e:
return False, f"Delete error: {e}"
@app.post("/api/jobs/{job_id}/upload-nextcloud") @app.post("/api/jobs/{job_id}/upload-nextcloud")
async def upload_nextcloud(job_id: str, user: str = Depends(check_auth)): async def upload_nextcloud(job_id: str, user: str = Depends(check_auth)):
"""Naloži dokončan reel na Nextcloud /folxspeed/REELS/{TV STATION}/.""" """Naloži dokončan reel na Nextcloud /folxspeed/REELS/{TV STATION}/."""

View File

@ -1224,6 +1224,7 @@
el.className = "job"; el.className = "job";
el.id = `job-${job.id}`; el.id = `job-${job.id}`;
el.dataset.id = job.id; el.dataset.id = job.id;
if (job.nextcloud_status) el.dataset.ncStatus = job.nextcloud_status;
// Vizualni hint če je že naložen na Nextcloud // Vizualni hint če je že naložen na Nextcloud
if (job.nextcloud_status === "uploaded") { if (job.nextcloud_status === "uploaded") {
@ -1360,8 +1361,21 @@
} }
async function deleteJob(id) { async function deleteJob(id) {
if (!confirm("Izbrišem ta job?")) return; // Najdi job v listu da lahko prikažem ime + Nextcloud status
await fetch(`/api/jobs/${id}`, { method: "DELETE" }); const card = document.getElementById(`job-${id}`);
const title = card ? (card.querySelector(".job-title")?.textContent || id) : id;
const isUploaded = card && card.dataset.ncStatus === "uploaded";
const ncWarn = isUploaded
? "\n\n☁ POZOR: ta rel je bil naložen na Nextcloud — bo zbrisan tudi tam (in iz dedup DB)."
: "";
if (!confirm(`Izbrišem ta job?\n\n"${title}"${ncWarn}\n\nTo zbriše:\n• job iz seznama\n• video + podnapisi (lokal + S3)\n• YouTube original\n• analizo + waveform${isUploaded ? "\n• datoteko iz Nextclouda\n• zapis iz dedup DB (lahko boš ga ponovno naložil)" : ""}`)) return;
const r = await fetch(`/api/jobs/${id}`, { method: "DELETE" });
if (r.ok) {
const data = await r.json().catch(() => ({}));
if (data.nextcloud_delete && data.nextcloud_delete.startsWith("fail")) {
alert(`Job zbrisan, ampak Nextcloud delete ni uspel:\n${data.nextcloud_delete}`);
}
}
refreshJobs(); refreshJobs();
} }