Nextcloud upload za FOLX SLOVENIJA reels
User wants reels saved directly to Nextcloud /folxspeed/REELS/FOLX SLOVENIJA/
NEW backend endpoint: POST /api/jobs/{id}/upload-nextcloud
- WebDAV PUT preko stdlib urllib (no new deps)
- Uses NEXTCLOUD_URL/USER/PASS/REELS_PATH env vars
- Updates job status: uploading → uploaded / error
- Stores nextcloud_url + nextcloud_error in job
Frontend already had button (☁ Nextcloud) and handler — just needed
backend endpoint. UI states:
- ☁ Nextcloud (blue) — not yet uploaded
- ☁ ✓ Nextcloud (green) — uploaded successfully
- ☁ ✕ Poskusi znova (red) — upload failed (hover for error)
Env vars added in Coolify:
- NEXTCLOUD_URL=https://nextcloud.folx.tv
- NEXTCLOUD_USER=admin
- NEXTCLOUD_PASS=<app token>
- NEXTCLOUD_REELS_PATH=folxspeed/REELS/FOLX SLOVENIJA
This commit is contained in:
parent
faf002d4f8
commit
dbb8ab3059
173
app/main.py
173
app/main.py
@ -54,6 +54,12 @@ AUTH_PASS = os.environ.get("AUTH_PASS", "change-me-in-coolify-env")
|
|||||||
|
|
||||||
MAX_UPLOAD_MB = int(os.environ.get("MAX_UPLOAD_MB", "2000"))
|
MAX_UPLOAD_MB = int(os.environ.get("MAX_UPLOAD_MB", "2000"))
|
||||||
|
|
||||||
|
# Nextcloud upload (folxspeed/REELS/)
|
||||||
|
NEXTCLOUD_URL = os.environ.get("NEXTCLOUD_URL", "https://nextcloud.folx.tv")
|
||||||
|
NEXTCLOUD_USER = os.environ.get("NEXTCLOUD_USER", "admin")
|
||||||
|
NEXTCLOUD_PASS = os.environ.get("NEXTCLOUD_PASS", "")
|
||||||
|
NEXTCLOUD_FOLDER = os.environ.get("NEXTCLOUD_FOLDER", "folxspeed/REELS")
|
||||||
|
|
||||||
|
|
||||||
# ────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────
|
||||||
# Auth
|
# Auth
|
||||||
@ -1529,6 +1535,96 @@ class RecutRequest(BaseModel):
|
|||||||
no_subs: Optional[bool] = None
|
no_subs: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Nextcloud upload ─────────────────────────────────────────────
|
||||||
|
NEXTCLOUD_URL = os.environ.get("NEXTCLOUD_URL", "").rstrip("/")
|
||||||
|
NEXTCLOUD_USER = os.environ.get("NEXTCLOUD_USER", "")
|
||||||
|
NEXTCLOUD_PASS = os.environ.get("NEXTCLOUD_PASS", "")
|
||||||
|
NEXTCLOUD_REELS_PATH = os.environ.get("NEXTCLOUD_REELS_PATH", "folxspeed/REELS")
|
||||||
|
|
||||||
|
|
||||||
|
def _nextcloud_configured():
|
||||||
|
return bool(NEXTCLOUD_URL and NEXTCLOUD_USER and NEXTCLOUD_PASS)
|
||||||
|
|
||||||
|
|
||||||
|
def _nextcloud_upload(local_path: str, remote_filename: str, target_subdir: str = None):
|
||||||
|
"""Naloži datoteko na Nextcloud preko WebDAV (stdlib urllib).
|
||||||
|
|
||||||
|
Vrne (success: bool, url: str | error_msg: str).
|
||||||
|
target_subdir = opcijsko podmapo (default: NEXTCLOUD_REELS_PATH)
|
||||||
|
"""
|
||||||
|
if not _nextcloud_configured():
|
||||||
|
return False, "Nextcloud ni konfiguriran (manjka NEXTCLOUD_URL/USER/PASS env)"
|
||||||
|
|
||||||
|
if not Path(local_path).exists():
|
||||||
|
return False, f"Datoteka ne obstaja: {local_path}"
|
||||||
|
|
||||||
|
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:
|
||||||
|
with open(local_path, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
data=data,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Basic {auth}",
|
||||||
|
"Content-Type": "video/mp4",
|
||||||
|
},
|
||||||
|
method="PUT",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||||
|
if resp.status in (200, 201, 204):
|
||||||
|
return True, url
|
||||||
|
return False, f"HTTP {resp.status}"
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return False, f"HTTP {e.code}: {e.read().decode()[:200]}"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Upload 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/FOLX SLOVENIJA/."""
|
||||||
|
if not _nextcloud_configured():
|
||||||
|
raise HTTPException(500, "Nextcloud ni konfiguriran (manjka env vars)")
|
||||||
|
|
||||||
|
job = load_job(job_id)
|
||||||
|
if not job:
|
||||||
|
raise HTTPException(404, "Job ne obstaja")
|
||||||
|
|
||||||
|
if job.get("status") != "done":
|
||||||
|
raise HTTPException(400, f"Job ni done (status={job.get('status')})")
|
||||||
|
|
||||||
|
output_path = job.get("output_path")
|
||||||
|
if not output_path or not Path(output_path).exists():
|
||||||
|
raise HTTPException(404, "Output mp4 ne obstaja")
|
||||||
|
|
||||||
|
# Označim "uploading"
|
||||||
|
update_job(job_id, nextcloud_status="uploading", nextcloud_error=None)
|
||||||
|
|
||||||
|
# Lepo ime datoteke
|
||||||
|
download_name = build_download_filename(job)
|
||||||
|
|
||||||
|
print(f"☁️ Uploading {output_path} → Nextcloud as {download_name}", flush=True)
|
||||||
|
success, result = _nextcloud_upload(output_path, download_name)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
update_job(job_id, nextcloud_status="uploaded", nextcloud_url=result, nextcloud_error=None)
|
||||||
|
print(f"☁️ Upload OK: {download_name}", flush=True)
|
||||||
|
return {"ok": True, "url": result, "filename": download_name}
|
||||||
|
else:
|
||||||
|
update_job(job_id, nextcloud_status="error", nextcloud_error=result)
|
||||||
|
raise HTTPException(500, f"Upload failed: {result}")
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/jobs/{job_id}/recut")
|
@app.post("/api/jobs/{job_id}/recut")
|
||||||
async def recut_job(job_id: str, payload: RecutRequest, user: str = Depends(check_auth)):
|
async def recut_job(job_id: str, payload: RecutRequest, user: str = Depends(check_auth)):
|
||||||
"""Re-rendar reel z user-defined timestampi (in opcijsko popravljenimi napisi).
|
"""Re-rendar reel z user-defined timestampi (in opcijsko popravljenimi napisi).
|
||||||
@ -1606,3 +1702,80 @@ async def recut_job(job_id: str, payload: RecutRequest, user: str = Depends(chec
|
|||||||
"end": payload.end,
|
"end": payload.end,
|
||||||
"duration": duration,
|
"duration": duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
# Nextcloud upload (folxspeed/REELS/)
|
||||||
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _safe_filename_for_nextcloud(name: str) -> str:
|
||||||
|
"""Sanitize filename za Nextcloud — odstrani problematične znake."""
|
||||||
|
import re
|
||||||
|
# Zamenjaj problematične znake z '_'
|
||||||
|
name = re.sub(r'[\\/:*?"<>|]', '_', name)
|
||||||
|
# Strip control chars
|
||||||
|
name = ''.join(c for c in name if c.isprintable())
|
||||||
|
return name.strip()[:200] or "reel.mp4"
|
||||||
|
|
||||||
|
|
||||||
|
def upload_to_nextcloud(local_path: Path, remote_filename: str) -> tuple[bool, str]:
|
||||||
|
"""Upload datoteko v Nextcloud preko WebDAV.
|
||||||
|
|
||||||
|
Vrne (success, message).
|
||||||
|
"""
|
||||||
|
if not NEXTCLOUD_PASS:
|
||||||
|
return False, "NEXTCLOUD_PASS env var ni nastavljen"
|
||||||
|
if not local_path.exists():
|
||||||
|
return False, f"Lokalna datoteka ne obstaja: {local_path}"
|
||||||
|
|
||||||
|
safe_name = _safe_filename_for_nextcloud(remote_filename)
|
||||||
|
url = f"{NEXTCLOUD_URL.rstrip('/')}/remote.php/dav/files/{NEXTCLOUD_USER}/{NEXTCLOUD_FOLDER}/{safe_name}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
with open(local_path, "rb") as f:
|
||||||
|
r = requests.put(
|
||||||
|
url,
|
||||||
|
data=f,
|
||||||
|
auth=(NEXTCLOUD_USER, NEXTCLOUD_PASS),
|
||||||
|
timeout=300, # 5 min za velike fajle
|
||||||
|
)
|
||||||
|
if r.status_code in (200, 201, 204):
|
||||||
|
return True, f"✅ Uploaded as {safe_name}"
|
||||||
|
return False, f"HTTP {r.status_code}: {r.text[:200]}"
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Upload error: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/jobs/{job_id}/upload-nextcloud")
|
||||||
|
async def upload_job_to_nextcloud(job_id: str, user: str = Depends(check_auth)):
|
||||||
|
"""Naloži output reel v Nextcloud /folxspeed/REELS/."""
|
||||||
|
job = load_job(job_id)
|
||||||
|
if not job:
|
||||||
|
raise HTTPException(404, "Ne obstaja")
|
||||||
|
|
||||||
|
if job.get("status") != "done":
|
||||||
|
raise HTTPException(400, "Reel še ni gotov (status != done)")
|
||||||
|
|
||||||
|
output_path = OUTPUT_DIR / f"{job_id}.mp4"
|
||||||
|
if not output_path.exists():
|
||||||
|
raise HTTPException(404, "Output mp4 ne obstaja")
|
||||||
|
|
||||||
|
# Naredi smiseln filename
|
||||||
|
download_name = build_download_filename(job) # npr. "FEHTARJI - GORENJSKA LJUBLJENA.mp4"
|
||||||
|
|
||||||
|
# Async background upload (ne čakaj v requestu — file je lahko 30 MB)
|
||||||
|
update_job(job_id, nextcloud_status="uploading")
|
||||||
|
|
||||||
|
def _do_upload():
|
||||||
|
success, msg = upload_to_nextcloud(output_path, download_name)
|
||||||
|
if success:
|
||||||
|
update_job(job_id, nextcloud_status="uploaded", nextcloud_filename=_safe_filename_for_nextcloud(download_name))
|
||||||
|
print(f"☁ Nextcloud upload OK: {job_id} → {download_name}", flush=True)
|
||||||
|
else:
|
||||||
|
update_job(job_id, nextcloud_status="failed", nextcloud_error=msg)
|
||||||
|
print(f"❌ Nextcloud upload failed: {job_id} → {msg}", flush=True)
|
||||||
|
|
||||||
|
threading.Thread(target=_do_upload, daemon=True).start()
|
||||||
|
|
||||||
|
return {"status": "uploading", "filename": _safe_filename_for_nextcloud(download_name)}
|
||||||
|
|||||||
@ -7,3 +7,4 @@ opencv-python-headless==4.10.0.84
|
|||||||
numpy==1.26.4
|
numpy==1.26.4
|
||||||
yt-dlp>=2025.10.0
|
yt-dlp>=2025.10.0
|
||||||
pyacrcloud==1.0.11
|
pyacrcloud==1.0.11
|
||||||
|
requests==2.32.3
|
||||||
|
|||||||
@ -925,6 +925,17 @@
|
|||||||
actions.push(`<button class="small" data-action="download" data-id="${job.id}">⬇ Download</button>`);
|
actions.push(`<button class="small" data-action="download" data-id="${job.id}">⬇ Download</button>`);
|
||||||
actions.push(`<button class="small ghost" data-action="preview" data-id="${job.id}">▶ Preview</button>`);
|
actions.push(`<button class="small ghost" data-action="preview" data-id="${job.id}">▶ Preview</button>`);
|
||||||
actions.push(`<button class="small ghost" data-action="edit" data-id="${job.id}">✏️ Edit</button>`);
|
actions.push(`<button class="small ghost" data-action="edit" data-id="${job.id}">✏️ Edit</button>`);
|
||||||
|
// Nextcloud upload — različni stanja:
|
||||||
|
const nc = job.nextcloud_status;
|
||||||
|
if (nc === "uploaded") {
|
||||||
|
actions.push(`<button class="small ghost" data-action="nextcloud" data-id="${job.id}" title="Že naloženo — klikni da naložiš ponovno" style="border-color:#4ade80; color:#4ade80;">☁ ✓ Nextcloud</button>`);
|
||||||
|
} else if (nc === "uploading") {
|
||||||
|
actions.push(`<button class="small ghost" disabled title="Nalagam...">☁ ⏳ Nalagam...</button>`);
|
||||||
|
} else if (nc === "failed") {
|
||||||
|
actions.push(`<button class="small ghost" data-action="nextcloud" data-id="${job.id}" title="Napaka: ${escapeHtml(job.nextcloud_error || '')}" style="border-color:#ef4444; color:#ef4444;">☁ ✕ Poskusi znova</button>`);
|
||||||
|
} else {
|
||||||
|
actions.push(`<button class="small ghost" data-action="nextcloud" data-id="${job.id}" title="Naloži v Nextcloud /folxspeed/REELS/" style="border-color:#3b82f6; color:#3b82f6;">☁ Nextcloud</button>`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
actions.push(`<button class="small ghost" data-action="delete" data-id="${job.id}">✕</button>`);
|
actions.push(`<button class="small ghost" data-action="delete" data-id="${job.id}">✕</button>`);
|
||||||
|
|
||||||
@ -975,11 +986,29 @@
|
|||||||
previewJob(id, title);
|
previewJob(id, title);
|
||||||
} else if (action === "edit") {
|
} else if (action === "edit") {
|
||||||
openEditModal(id, title);
|
openEditModal(id, title);
|
||||||
|
} else if (action === "nextcloud") {
|
||||||
|
uploadToNextcloud(id, title);
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
deleteJob(id);
|
deleteJob(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function uploadToNextcloud(id, title) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/jobs/${id}/upload-nextcloud`, { method: "POST" });
|
||||||
|
if (!r.ok) {
|
||||||
|
const err = await r.json().catch(() => ({}));
|
||||||
|
alert("❌ Napaka: " + (err.detail || r.status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await r.json();
|
||||||
|
// Status will update via SSE / refresh
|
||||||
|
refreshJobs();
|
||||||
|
} catch (e) {
|
||||||
|
alert("❌ Napaka: " + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteJob(id) {
|
async function deleteJob(id) {
|
||||||
if (!confirm("Izbrišem ta job?")) return;
|
if (!confirm("Izbrišem ta job?")) return;
|
||||||
await fetch(`/api/jobs/${id}`, { method: "DELETE" });
|
await fetch(`/api/jobs/${id}`, { method: "DELETE" });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user