From 12e8edba93daaf05d3c16504e651e3aacd560515 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 14:52:40 +0000 Subject: [PATCH] Delete job: cascade delete povsod (Nextcloud + dedup DB + S3 + lokal) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/main.py | 92 +++++++++++++++++++++++++++++++++++++++----- templates/index.html | 18 ++++++++- 2 files changed, 99 insertions(+), 11 deletions(-) diff --git a/app/main.py b/app/main.py index 995e342..45f6385 100644 --- a/app/main.py +++ b/app/main.py @@ -1859,14 +1859,41 @@ async def delete_job(job_id: str, user: str = Depends(check_auth)): job = load_job(job_id) if not job: 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")): p = job.get(key) if p: local_p = Path(p) local_p.unlink(missing_ok=True) _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 ( f"{job_id}.mp4", 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.unlink(missing_ok=True) _delete_from_s3(fname, "output") + # Waveform PNG-ji (več velikosti) — listanje ker imena niso fiksna try: 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") except Exception: pass - # YT info.json - info_json = UPLOAD_DIR / f"{job_id}_yt.info.json" - if info_json.exists(): - info_json.unlink(missing_ok=True) - _delete_from_s3(f"{job_id}_yt.info.json", "upload") - # Job metadata + + # YT info.json + ostali yt-dlp artifacti (.part, .f137.mp4, ...) + try: + for f in UPLOAD_DIR.glob(f"{job_id}_yt*"): + f_name = f.name + 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.unlink(missing_ok=True) _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 ──────────────────────────────────────────────── @@ -2294,6 +2332,42 @@ def _nextcloud_upload(local_path: str, remote_filename: str, target_subdir: str 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") async def upload_nextcloud(job_id: str, user: str = Depends(check_auth)): """Naloži dokončan reel na Nextcloud /folxspeed/REELS/{TV STATION}/.""" diff --git a/templates/index.html b/templates/index.html index 12dde51..a772aa1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1224,6 +1224,7 @@ el.className = "job"; el.id = `job-${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 if (job.nextcloud_status === "uploaded") { @@ -1360,8 +1361,21 @@ } async function deleteJob(id) { - if (!confirm("Izbrišem ta job?")) return; - await fetch(`/api/jobs/${id}`, { method: "DELETE" }); + // Najdi job v listu da lahko prikažem ime + Nextcloud status + 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(); }