Jikan/MAL-Quelle + Job-Titel + Abbrechen-Knopf + Dark-Hover + Ordner-Verwaltung
Ampel / ampel (push) Failing after 38s
Ampel / ampel (push) Failing after 38s
Commander-Befunde (Screenshots 23.07. abends):
- Jikan (MyAnimeList, kostenlos OHNE Key) als Anime-Quelle vor OMDb —
waehlt per Titel-Aehnlichkeit (SequenceMatcher, Schwelle 0.55) statt
blind Treffer 1; damit trifft "Evangelion 2.22" den exakten Film
- Job-Titel: POST /jobs uebernimmt den erkannten Disc-Titel aus dem
DISC_CACHE — Schluss mit "Unbekannt" in der Queue
- Kooperativer Job-Abbruch: POST /jobs/{id}/cancel setzt canceling,
der Worker prueft das Flag bei jedem Fortschritts-Update und killt
den Encoder-Prozess sauber (RipAbbruch); UI-Knopf "Abbrechen" fuer
laufende, Status-Badge "Wird abgebrochen"
- Dark-Mode: Job-Zeilen-Hover war hart hell (hover:bg-slate-50)
- Speicherziele: lokaler Ordner-Browser mit "Ordner anlegen"
(GET /browse + POST /browse/mkdir, nur unter /app/media)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -116,6 +116,17 @@ def get_settings(key: str = "ui") -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def get_job_status(job_id: str) -> str:
|
||||
"""Nur der Status — der Worker prüft damit kooperative Abbruch-Anfragen."""
|
||||
from sqlalchemy import select
|
||||
|
||||
with engine.connect() as conn:
|
||||
zeile = conn.execute(
|
||||
select(jobs.c.status).where(jobs.c.id == job_id)
|
||||
).first()
|
||||
return zeile[0] if zeile else ""
|
||||
|
||||
|
||||
def update_job(job_id: str, **fields) -> None:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(jobs.update().where(jobs.c.id == job_id).values(**fields))
|
||||
|
||||
+35
-15
@@ -20,6 +20,11 @@ import tempfile
|
||||
RIP_OUTPUT_DIR = os.getenv("RIP_OUTPUT_DIR", "/app/media")
|
||||
|
||||
|
||||
class RipAbbruch(Exception):
|
||||
"""Kooperativer Abbruch: vom Fortschritts-Callback geworfen, wenn der
|
||||
Nutzer den Job abgebrochen hat (Status 'canceling' in der DB)."""
|
||||
|
||||
|
||||
def check_makemkv_installed() -> bool:
|
||||
"""Prüft, ob makemkvcon installiert ist."""
|
||||
return shutil.which("makemkvcon") is not None
|
||||
@@ -124,10 +129,15 @@ def run_handbrake(input_path: str, output_path: str, preset: str = DEFAULT_HB_PR
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
for line in process.stdout:
|
||||
progress = get_progress_from_line(line)
|
||||
if progress > 0 and progress_cb:
|
||||
progress_cb(progress)
|
||||
try:
|
||||
for line in process.stdout:
|
||||
progress = get_progress_from_line(line)
|
||||
if progress > 0 and progress_cb:
|
||||
progress_cb(progress)
|
||||
except RipAbbruch:
|
||||
process.kill()
|
||||
process.wait()
|
||||
return {"status": "cancelled", "error": "Abgebrochen durch Nutzer"}
|
||||
|
||||
process.wait()
|
||||
|
||||
@@ -185,15 +195,20 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None) -> dict:
|
||||
)
|
||||
|
||||
letzte_meldung = ""
|
||||
for line in process.stdout:
|
||||
progress = get_progress_from_prgv(line)
|
||||
if progress >= 0 and progress_cb:
|
||||
progress_cb(progress)
|
||||
elif line.startswith("MSG:"):
|
||||
# MSG:code,flags,count,"message",... — Klartext ist Feld 4
|
||||
teile = line.split(",", 4)
|
||||
if len(teile) >= 4:
|
||||
letzte_meldung = teile[3].strip('"')
|
||||
try:
|
||||
for line in process.stdout:
|
||||
progress = get_progress_from_prgv(line)
|
||||
if progress >= 0 and progress_cb:
|
||||
progress_cb(progress)
|
||||
elif line.startswith("MSG:"):
|
||||
# MSG:code,flags,count,"message",... — Klartext ist Feld 4
|
||||
teile = line.split(",", 4)
|
||||
if len(teile) >= 4:
|
||||
letzte_meldung = teile[3].strip('"')
|
||||
except RipAbbruch:
|
||||
process.kill()
|
||||
process.wait()
|
||||
return {"status": "cancelled", "error": "Abgebrochen durch Nutzer"}
|
||||
|
||||
process.wait()
|
||||
|
||||
@@ -267,8 +282,13 @@ def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = N
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
for line in process.stdout:
|
||||
melde(50, line.strip()[:200])
|
||||
try:
|
||||
for line in process.stdout:
|
||||
melde(50, line.strip()[:200])
|
||||
except RipAbbruch:
|
||||
process.kill()
|
||||
process.wait()
|
||||
return {"status": "cancelled", "error": "Abgebrochen durch Nutzer"}
|
||||
|
||||
process.wait()
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from detection import detect_disc_type
|
||||
from ripping import (
|
||||
DEFAULT_HB_PRESET,
|
||||
RIP_OUTPUT_DIR,
|
||||
RipAbbruch,
|
||||
rip_cd,
|
||||
rip_video,
|
||||
run_handbrake,
|
||||
@@ -40,8 +41,20 @@ def _zielbasis(target_dir, disc_type: str) -> str:
|
||||
return os.path.join(RIP_OUTPUT_DIR, disc_type)
|
||||
|
||||
|
||||
def _abbruch_angefordert(job_id: str) -> bool:
|
||||
"""Kooperativer Abbruch: hat der Nutzer über die API abgebrochen?"""
|
||||
return db.get_job_status(job_id) == "canceling"
|
||||
|
||||
|
||||
def _job_abschliessen(job_id: str, ergebnis: dict) -> None:
|
||||
"""Schreibt den Endzustand eines Jobs (completed/failed) nach Postgres."""
|
||||
if ergebnis.get("status") == "cancelled":
|
||||
db.update_job(
|
||||
job_id, status="failed", error="Abgebrochen durch Nutzer",
|
||||
finished_at=db.utcnow(),
|
||||
)
|
||||
db.add_log("warning", "worker", f"Job {job_id}: abgebrochen — Rohdaten bleiben erhalten")
|
||||
return
|
||||
if ergebnis.get("status") == "success":
|
||||
db.update_job(
|
||||
job_id,
|
||||
@@ -69,6 +82,11 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
|
||||
dort eingehängte Shares (NFS/SMB) sind damit direkt wählbar.
|
||||
"""
|
||||
db.init_db()
|
||||
|
||||
if _abbruch_angefordert(job_id):
|
||||
_job_abschliessen(job_id, {"status": "cancelled"})
|
||||
return {"status": "cancelled"}
|
||||
|
||||
disc_type = detect_disc_type(device_path)
|
||||
|
||||
if disc_type in ("no_disc", "unknown"):
|
||||
@@ -90,6 +108,8 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
|
||||
if progress == letzter[0]:
|
||||
return
|
||||
letzter[0] = progress
|
||||
if _abbruch_angefordert(job_id):
|
||||
raise RipAbbruch()
|
||||
self.update_state(
|
||||
state="PROGRESS",
|
||||
meta={"progress": progress, "status": "ripping", "message": message},
|
||||
@@ -162,14 +182,23 @@ def transcode_files(self, job_id: str, raw_dir: str, final_dir: str):
|
||||
)
|
||||
|
||||
anzahl = len(quellen)
|
||||
letzter = [-1]
|
||||
for index, quelle in enumerate(quellen):
|
||||
ziel = os.path.join(final_dir, os.path.basename(quelle))
|
||||
|
||||
def datei_fortschritt(p, _index=index):
|
||||
gesamt = int((_index * 100 + p) / anzahl)
|
||||
if gesamt == letzter[0]:
|
||||
return
|
||||
letzter[0] = gesamt
|
||||
if _abbruch_angefordert(job_id):
|
||||
raise RipAbbruch()
|
||||
db.update_job(job_id, progress=min(99, gesamt))
|
||||
|
||||
hb = run_handbrake(quelle, ziel, preset=preset, progress_cb=datei_fortschritt)
|
||||
if hb.get("status") == "cancelled":
|
||||
_job_abschliessen(job_id, hb)
|
||||
return hb
|
||||
if hb.get("status") != "success":
|
||||
ergebnis = {
|
||||
"status": "error",
|
||||
|
||||
Reference in New Issue
Block a user