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:
parent
79f611ba73
commit
12e8edba93
92
app/main.py
92
app/main.py
@ -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}/."""
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user