Speicherziel-Wahl: Rip-Ziel pro Job frei waehlbar (Etappe 16 / Task 16)
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
- POST /jobs nimmt target_dir (validiert unter /app/media, .. fliegt raus) - GET /storage-targets: Verzeichnisse unter /app/media inkl. Mount-Flag und freiem Platz — NFS/SMB-Shares unter /srv/rippy/media erscheinen dank rslave-Bind automatisch - jobs.target_dir (Mini-Migration via ADD COLUMN IF NOT EXISTS) - Worker: rip_disc(target_dir) mit eigener Validierung; CD/Video/ Transcode-Pfade legen im gewaehlten Ziel ab - UI: "Rippen starten" oeffnet den Ziel-Dialog (Filme/Serien/Musik oder eigener Pfad); Modal-Bugfix: customPath wurde still ignoriert Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+60
-4
@@ -6,6 +6,8 @@ from typing import List, Optional, Dict
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
|
||||
import db
|
||||
@@ -187,6 +189,23 @@ class JobCreateRequest(BaseModel):
|
||||
device_path: Optional[str] = None
|
||||
device: Optional[str] = None # Alias, so schickt es das UI
|
||||
title: Optional[str] = None
|
||||
target_dir: Optional[str] = None # Ablageziel unter /app/media (frei wählbar)
|
||||
|
||||
|
||||
MEDIA_ROOT = "/app/media"
|
||||
|
||||
|
||||
def _validiere_ziel(target_dir: Optional[str]) -> Optional[str]:
|
||||
"""Ziel muss unter /app/media liegen — Pfad-Ausbrüche (..) fliegen raus."""
|
||||
if not target_dir:
|
||||
return None
|
||||
normalisiert = os.path.normpath(target_dir)
|
||||
if not normalisiert.startswith(MEDIA_ROOT):
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"Ziel muss unter {MEDIA_ROOT} liegen (Shares dort einhängen)",
|
||||
)
|
||||
return normalisiert
|
||||
|
||||
|
||||
@app.post("/jobs", status_code=201)
|
||||
@@ -201,12 +220,49 @@ async def create_job(request: JobCreateRequest):
|
||||
raise HTTPException(status_code=422, detail="device_path fehlt")
|
||||
if device_path not in device_discovery.list_optical_devices():
|
||||
raise HTTPException(status_code=404, detail=f"Laufwerk {device_path} nicht gefunden")
|
||||
ziel = _validiere_ziel(request.target_dir)
|
||||
|
||||
job_id = str(uuid.uuid4())
|
||||
await asyncio.to_thread(db.insert_job, job_id, device_path, None, request.title)
|
||||
await asyncio.to_thread(db.add_log, "info", "api", f"Job {job_id} angelegt für {device_path}")
|
||||
start_rip(device_path, job_id)
|
||||
return {"id": job_id, "status": "pending", "device": device_path}
|
||||
await asyncio.to_thread(db.insert_job, job_id, device_path, None, request.title, ziel)
|
||||
await asyncio.to_thread(
|
||||
db.add_log, "info", "api",
|
||||
f"Job {job_id} angelegt für {device_path}" + (f" → {ziel}" if ziel else ""),
|
||||
)
|
||||
start_rip(device_path, job_id, ziel)
|
||||
return {"id": job_id, "status": "pending", "device": device_path, "target_dir": ziel}
|
||||
|
||||
|
||||
@app.get("/storage-targets")
|
||||
async def storage_targets():
|
||||
"""Verfügbare Ablageziele: Verzeichnisse unter /app/media inkl. Mounts.
|
||||
|
||||
NFS/SMB-Shares, die auf der VM unter /srv/rippy/media eingehängt werden,
|
||||
tauchen hier automatisch auf (rslave-Bind in docker-compose).
|
||||
"""
|
||||
def sammle():
|
||||
ziele = []
|
||||
try:
|
||||
eintraege = sorted(os.listdir(MEDIA_ROOT))
|
||||
except OSError:
|
||||
return ziele
|
||||
for name in eintraege:
|
||||
pfad = os.path.join(MEDIA_ROOT, name)
|
||||
if not os.path.isdir(pfad):
|
||||
continue
|
||||
try:
|
||||
nutzung = shutil.disk_usage(pfad)
|
||||
frei_gb = round(nutzung.free / 1024**3, 1)
|
||||
except OSError:
|
||||
frei_gb = None
|
||||
ziele.append({
|
||||
"name": name,
|
||||
"path": pfad,
|
||||
"is_mount": os.path.ismount(pfad),
|
||||
"free_gb": frei_gb,
|
||||
})
|
||||
return ziele
|
||||
|
||||
return await asyncio.to_thread(sammle)
|
||||
|
||||
|
||||
@app.post("/devices/{name}/eject")
|
||||
|
||||
Reference in New Issue
Block a user