Track-Auswahl-Tabelle, nativer Windows-Worker (via UI angeboten), Anleitungs-Tab
Ampel / ampel (push) Successful in 29s
Ampel / ampel (push) Successful in 29s
Volle Titel-Auswahl vor dem Rip (Etappe-12-Rest):
- worker.tasks.scan_tracks: makemkvcon info -> Titel-Liste (Dauer/Groesse/
Kapitel, apdefs-Attr 8/9/11) in die settings-Tabelle 'tracks:<device>';
API: POST /devices/{n}/scan-tracks + GET /devices/{n}/tracks (Polling).
- Rip-Dialog: 'Disc scannen' -> Tabelle mit Checkboxen, Vorauswahl ab
5 min, Groessen-Summe; gewaehlte Titel als meta.titles in den Job.
- rip_titel_auswahl: ein makemkvcon-Aufruf je Titel (mkv kann nur einen
Titel oder all), Fortschritt anteilig. Parser-Tests dabei.
Nativer Windows-Worker OHNE Docker (Commander-Wunsch):
- Die Rippy-Instanz versorgt sich selbst: GET /worker-setup/windows
liefert install.ps1, /worker-setup/paket den Worker-Code als Zip
(api-Dockerfile kopiert docker/worker/*.py als worker_dist/ ins Image).
- install.ps1: Python-Check, venv + requirements, HandBrakeCLI 1.9.2 vom
offiziellen GitHub-Release (URL per HEAD verifiziert), start-worker.bat
mit celery -Q transcode --pool=solo (Celery-Doku: Windows nur solo),
optional -Autostart via schtasks onlogon.
- tasks.py laedt jetzt auch ohne fcntl (Import-Guard) — rip_disc ist auf
solchen Workern hart verriegelt (Klartext-Fehler statt Crash).
- Einstellungen -> Worker: Varianten-Umschalter Linux(Docker)/Windows(nativ)
mit passenden Copy-Paste-Befehlen.
Anleitungs-Tab: Rippy erklaert sich selbst (Disc-Weg, Serien, NAS,
Media-Server, Benachrichtigungen, Key, Worker, Downloads, FAQ).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -337,6 +337,7 @@ class JobCreateRequest(BaseModel):
|
||||
series: Optional[str] = None # Serien-Flow: Ablage <Serie>/Season NN
|
||||
season: Optional[int] = None
|
||||
main_feature_only: Optional[bool] = None # pro Rip; None = Setting gilt
|
||||
titles: Optional[List[int]] = None # exakte Titel-Auswahl (Track-Tabelle)
|
||||
|
||||
|
||||
MEDIA_ROOT = "/app/media"
|
||||
@@ -390,6 +391,10 @@ async def create_job(request: JobCreateRequest):
|
||||
meta_dict["season"] = max(1, int(request.season or 1))
|
||||
if request.main_feature_only is not None:
|
||||
meta_dict["main_feature_only"] = request.main_feature_only
|
||||
if request.titles:
|
||||
titel_liste = sorted({int(t) for t in request.titles if int(t) >= 0})[:200]
|
||||
if titel_liste:
|
||||
meta_dict["titles"] = titel_liste
|
||||
meta_json = json.dumps(meta_dict) if meta_dict else None
|
||||
|
||||
job_id = str(uuid.uuid4())
|
||||
@@ -542,6 +547,34 @@ async def eject_device(name: str):
|
||||
return {"status": "ejected", "device": device_path}
|
||||
|
||||
|
||||
@app.post("/devices/{name}/scan-tracks")
|
||||
async def scan_tracks_starten(name: str):
|
||||
"""Titel-Scan der eingelegten Disc anstoßen (Track-Auswahl-Tabelle).
|
||||
|
||||
Läuft als Worker-Task (nur der hat makemkvcon + Laufwerk); das UI pollt
|
||||
GET /devices/{name}/tracks. Dauert je nach Disc 20–120 s.
|
||||
"""
|
||||
device_path = f"/dev/{name}"
|
||||
if device_path not in device_discovery.list_optical_devices():
|
||||
raise HTTPException(status_code=404, detail=f"Laufwerk {device_path} nicht gefunden")
|
||||
if await asyncio.to_thread(db.has_active_job, device_path):
|
||||
raise HTTPException(status_code=409, detail="Auf diesem Laufwerk läuft gerade ein Job")
|
||||
|
||||
await asyncio.to_thread(db.save_settings, {"status": "running"}, f"tracks:{device_path}")
|
||||
celery_client.send_task("worker.tasks.scan_tracks", args=[device_path])
|
||||
return {"status": "scanning"}
|
||||
|
||||
|
||||
@app.get("/devices/{name}/tracks")
|
||||
async def scan_tracks_ergebnis(name: str):
|
||||
"""Ergebnis des Titel-Scans (Polling-Ziel des UI)."""
|
||||
device_path = f"/dev/{name}"
|
||||
daten = await asyncio.to_thread(db.get_settings, f"tracks:{device_path}")
|
||||
if not daten:
|
||||
return {"status": "none"}
|
||||
return daten
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/retry-transcode")
|
||||
async def retry_transcode(job_id: str):
|
||||
"""Stößt die Kompression eines Jobs neu an — OHNE die Disc neu zu rippen.
|
||||
@@ -902,6 +935,50 @@ async def browse_mkdir(request: MkdirRequest):
|
||||
return {"path": ziel}
|
||||
|
||||
|
||||
@app.get("/worker-setup/windows")
|
||||
async def worker_setup_windows():
|
||||
"""Windows-Installer-Skript für den nativen Transcode-Worker.
|
||||
|
||||
Das UI (Einstellungen → Worker → Windows) zeigt den passenden
|
||||
Zwei-Zeilen-Aufruf — alles kommt von dieser Rippy-Instanz selbst.
|
||||
"""
|
||||
pfad = "worker_dist/install.ps1"
|
||||
if not os.path.isfile(pfad):
|
||||
raise HTTPException(status_code=404, detail="Installer nicht im Image — API neu bauen")
|
||||
return FileResponse(pfad, media_type="text/plain", filename="install-rippy-worker.ps1")
|
||||
|
||||
|
||||
@app.get("/worker-setup/paket")
|
||||
async def worker_setup_paket():
|
||||
"""Worker-Quellcode als Zip — der Windows-Installer lädt ihn von hier.
|
||||
|
||||
Kein git, kein Docker auf der Zielmaschine nötig: die Rippy-Instanz
|
||||
versorgt ihre Worker selbst (Dateien liegen via Dockerfile im Image).
|
||||
"""
|
||||
def baue():
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
if not os.path.isdir("worker_dist"):
|
||||
return None
|
||||
puffer = io.BytesIO()
|
||||
with zipfile.ZipFile(puffer, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for name in sorted(os.listdir("worker_dist")):
|
||||
if name.endswith(".py") and not name.startswith(("test_", "conftest")):
|
||||
z.write(os.path.join("worker_dist", name), name)
|
||||
elif name == "requirements.txt":
|
||||
z.write(os.path.join("worker_dist", name), name)
|
||||
return puffer.getvalue()
|
||||
|
||||
inhalt = await asyncio.to_thread(baue)
|
||||
if not inhalt:
|
||||
raise HTTPException(status_code=404, detail="Worker-Paket nicht im Image — API neu bauen")
|
||||
return Response(
|
||||
content=inhalt, media_type="application/zip",
|
||||
headers={"Content-Disposition": 'attachment; filename="rippy-worker.zip"'},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/system/info")
|
||||
async def system_info():
|
||||
"""System-Selbstauskunft (Einstellungen → System): Werkzeug-Versionen der
|
||||
|
||||
Reference in New Issue
Block a user