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 `
+ ${j.status !== 'importing' ? `