diff --git a/app/routes/library_api.py b/app/routes/library_api.py index 1e97405..a4a6fce 100644 --- a/app/routes/library_api.py +++ b/app/routes/library_api.py @@ -1211,6 +1211,18 @@ def setup_library_routes(app: web.Application, config: Config, result = await importer_service.get_job_status(job_id) return web.json_response(result) + async def delete_import_job(request: web.Request) -> web.Response: + """DELETE /api/library/import/{job_id}""" + if not importer_service: + return web.json_response( + {"error": "Import-Service nicht verfuegbar"}, status=500 + ) + job_id = int(request.match_info["job_id"]) + result = await importer_service.delete_job(job_id) + if "error" in result: + return web.json_response(result, status=400) + return web.json_response(result) + async def post_execute_import(request: web.Request) -> web.Response: """POST /api/library/import/{job_id}/execute""" if not importer_service: @@ -1609,6 +1621,9 @@ def setup_library_routes(app: web.Application, config: Config, # Import app.router.add_get("/api/library/import", get_import_jobs) app.router.add_post("/api/library/import", post_create_import) + app.router.add_delete( + "/api/library/import/{job_id}", delete_import_job + ) app.router.add_post( "/api/library/import/{job_id}/analyze", post_analyze_import ) diff --git a/app/services/importer.py b/app/services/importer.py index 6765bf6..93d9094 100644 --- a/app/services/importer.py +++ b/app/services/importer.py @@ -24,6 +24,10 @@ RE_SERIES_FROM_NAME = re.compile( RE_SERIES_FROM_XXx = re.compile( r'^(.+?)[\s._-]+\d{1,2}x\d{2,3}', re.IGNORECASE ) +# "Serienname - Staffel X" oder "Serienname Season X" in Ordnernamen +RE_STAFFEL_DIR = re.compile( + r'^(.+?)[\s._-]+(?:Staffel|Season)\s*(\d{1,2})\s*$', re.IGNORECASE +) class ImporterService: @@ -172,6 +176,32 @@ class ImporterService: logging.error(f"Import-Job erstellen fehlgeschlagen: {e}") return None + async def delete_job(self, job_id: int) -> dict: + """Loescht einen Import-Job (nur wenn nicht gerade importiert wird)""" + if not self._db_pool: + return {"error": "Keine DB-Verbindung"} + try: + async with self._db_pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute( + "SELECT status FROM import_jobs WHERE id = %s", + (job_id,) + ) + row = await cur.fetchone() + if not row: + return {"error": "Job nicht gefunden"} + if row[0] == 'importing': + return {"error": "Job laeuft gerade, kann nicht geloescht werden"} + # Items werden per ON DELETE CASCADE mitgeloescht + await cur.execute( + "DELETE FROM import_jobs WHERE id = %s", (job_id,) + ) + logging.info(f"Import-Job {job_id} geloescht") + return {"message": f"Import-Job {job_id} geloescht"} + except Exception as e: + logging.error(f"Import-Job {job_id} loeschen fehlgeschlagen: {e}") + return {"error": str(e)} + async def analyze_job(self, job_id: int) -> dict: """Analysiert alle Dateien: Erkennung + TVDB-Lookup + Konflikt-Check""" if not self._db_pool: @@ -311,13 +341,25 @@ class ImporterService: ) target_path = os.path.join(target_dir, target_file) - # 5. Konflikt-Check - status = "matched" if series_name and season and episode else "pending" - conflict = None + # 5. Status bestimmen: Nur matched wenn eindeutig identifiziert + if not (series_name and season and episode): + status = "pending" + conflict = "Serie/Staffel/Episode nicht erkannt" + elif self.tvdb.is_configured and not tvdb_id: + # TVDB aktiv aber kein Match -> nicht eindeutig + status = "pending" + conflict = ( + f"Serie '{series_name}' nicht in TVDB gefunden " + f"- bitte manuell zuordnen" + ) + else: + status = "matched" + conflict = None existing_path = None existing_size = None - if os.path.exists(target_path): + # 6. Datei-Konflikt pruefen (nur wenn Status matched) + if status == "matched" and os.path.exists(target_path): existing_path = target_path existing_size = os.path.getsize(target_path) source_size = item["source_size"] @@ -340,7 +382,7 @@ class ImporterService: conflict = "Datei existiert bereits" status = "conflict" - # 6. In DB aktualisieren + # 7. In DB aktualisieren try: async with self._db_pool.acquire() as conn: async with conn.cursor() as cur: @@ -377,9 +419,13 @@ class ImporterService: Ordnernamen als Fallback. Der Ordnername ist oft zuverlaessiger bei Release-Gruppen-Prefixes (z.B. 'tlr-24.s07e01.mkv' vs Ordner '24.S07E01.German.DL.1080p.Bluray.x264-TLR'). + Erkennt auch 'Serienname - Staffel X'-Ordner (haeufig bei DE-Medien). """ filename = os.path.basename(file_path) parent_dir = os.path.basename(os.path.dirname(file_path)) + grandparent_dir = os.path.basename( + os.path.dirname(os.path.dirname(file_path)) + ) # Beide Quellen versuchen info_file = self._parse_name(filename) @@ -399,6 +445,19 @@ class ImporterService: info_dir["series"] = info_file["series"] return info_dir + # "Staffel X" / "Season X" Pattern im Ordnernamen + # z.B. "24 - Staffel 6" -> Serie="24", Staffel=6 + # Episode kommt dann aus dem Dateinamen + staffel_info = self._parse_staffel_dir(parent_dir) + if not staffel_info: + staffel_info = self._parse_staffel_dir(grandparent_dir) + if staffel_info and info_file.get("episode"): + return { + "series": staffel_info["series"], + "season": staffel_info["season"], + "episode": info_file["episode"], + } + # Dateiname hat S/E if info_file.get("season") and info_file.get("episode"): # Ordner-Serienname als Fallback wenn Datei keinen hat @@ -408,6 +467,17 @@ class ImporterService: return info_file + @staticmethod + def _parse_staffel_dir(dir_name: str) -> Optional[dict]: + """Erkennt 'Serienname - Staffel X' Pattern in Ordnernamen""" + m = RE_STAFFEL_DIR.match(dir_name) + if m: + series = m.group(1).replace(".", " ").replace("_", " ").strip() + series = re.sub(r'\s+', ' ', series).rstrip(" -") + if series: + return {"series": series, "season": int(m.group(2))} + return None + def _parse_name(self, name: str) -> dict: """Extrahiert Serienname, Staffel, Episode aus einem Namen""" result = {"series": "", "season": None, "episode": None} diff --git a/app/static/js/library.js b/app/static/js/library.js index 11f761d..65359f5 100644 --- a/app/static/js/library.js +++ b/app/static/js/library.js @@ -2063,12 +2063,15 @@ function loadExistingImportJobs() { j.status === 'importing' ? 'Laeuft' : j.status === 'error' ? 'Fehler' : j.status; const sourceName = j.source_path.split('/').pop(); - return ``; + ${escapeHtml(sourceName)} (${j.processed_files}/${j.total_files}) + ${statusText} + + ${j.status !== 'importing' ? `` : ''} + `; }).join(""); }) .catch(() => { @@ -2076,6 +2079,36 @@ function loadExistingImportJobs() { }); } +function deleteImportJob(jobId, ev) { + if (ev) ev.stopPropagation(); + if (!confirm("Import-Job wirklich loeschen?")) return; + fetch(`/api/library/import/${jobId}`, { method: "DELETE" }) + .then(r => r.json()) + .then(data => { + if (data.error) { + alert("Fehler: " + data.error); + return; + } + loadExistingImportJobs(); + }) + .catch(() => alert("Loeschen fehlgeschlagen")); +} + +function deleteCurrentImportJob() { + if (!currentImportJobId) return; + if (!confirm("Import-Job wirklich loeschen?")) return; + fetch(`/api/library/import/${currentImportJobId}`, { method: "DELETE" }) + .then(r => r.json()) + .then(data => { + if (data.error) { + alert("Fehler: " + data.error); + return; + } + resetImport(); + }) + .catch(() => alert("Loeschen fehlgeschlagen")); +} + function loadImportJob(jobId) { currentImportJobId = jobId; document.getElementById("import-setup").style.display = "none"; @@ -2282,7 +2315,11 @@ function renderImportItems(data) { const hasUnresolved = items.some(i => (i.status === "conflict" && !i.user_action) || i.status === "pending" ); - document.getElementById("btn-start-import").disabled = hasUnresolved; + const btn = document.getElementById("btn-start-import"); + btn.disabled = hasUnresolved; + btn.title = hasUnresolved + ? `${pending} Dateien muessen erst zugeordnet werden` + : "Import starten"; if (!items.length) { list.innerHTML = '
Keine Dateien gefunden
'; diff --git a/app/templates/base.html b/app/templates/base.html index f27fddf..69cb332 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -4,6 +4,7 @@ {% block title %}VideoKonverter{% endblock %} + {% block head %}{% endblock %} diff --git a/app/templates/library.html b/app/templates/library.html index 1089b45..eac78c2 100644 --- a/app/templates/library.html +++ b/app/templates/library.html @@ -387,6 +387,7 @@
+