Fix-Runde nach Review: alle Befunde behoben + echte Tests
Ampel / ampel (push) Failing after 12m31s

- Metadaten-Preview WIEDERHERGESTELLT (Stub ueberschattete echte
  prescan-Implementierung), tote Altmodule geloescht
- Celery update_state statt Phantom-Task/erfundener API
- abcde-Kommando korrigiert (CD-Ripping war nie funktionsfaehig)
- JWT: fester Schluessel Pflicht, echtes Logout, Cleanup nur Abgelaufene
- main.py: crashende Endpoints (Path/secrets/api_keys), year-Bug,
  Admin-Login aus .env
- Ruff gruen (29 Funde), Tests: auth/cache_keys/ripping_helpers,
  Placebo-test_health raus
- SAVEPOINT: offene MakeMKV-Entscheidung SICHTBAR gemacht (Regel B)
This commit is contained in:
Hitonabi
2026-07-22 19:31:48 +02:00
parent 728f650bf8
commit 0ea5f10b31
22 changed files with 722 additions and 719 deletions
+238 -22
View File
@@ -1,22 +1,40 @@
"""Pre-Scan module."""
"""Pre-Scan-Modul: Liest Disc-TOC ohne Ripping und schlägt Metadaten vor.
from typing import Dict, Optional
from dataclasses import dataclass, field
WIEDERHERGESTELLT 22.07.2026: Beim SoC-Refactoring wurde die echte Implementierung
(flache prescan.py) durch einen Stub überschattet — die Metadaten-Preview (Muss-Feature)
lieferte seitdem immer „Unknown Disc". Dies ist die echte Logik, bereinigt.
"""
import subprocess
from typing import Dict, List
from clients.tmdb import TMDBClient
from clients.musicbrainz import MusicBrainzClient
from clients.thetvdb import TheTVDBClient
from cache import get, set as cache_set
from cache.keys import generate_prescan_key
@dataclass
class PreScanResult:
"""Result of a pre-scan."""
title: str
year: Optional[int] = None
confidence: float = 0.0
metadata: Dict = field(default_factory=dict)
tracks: list = field(default_factory=list)
def __init__(
self,
disc_type: str = "DVD",
title: str = "Unknown",
year: int = None,
confidence: float = 0.0,
metadata: Dict = None,
tracks: List[Dict] = None
):
self.disc_type = disc_type
self.title = title
self.year = year
self.confidence = confidence
self.metadata = metadata or {}
self.tracks = tracks or []
def to_dict(self) -> Dict:
"""Convert to dictionary."""
return {
"disc_type": self.disc_type,
"title": self.title,
"year": self.year,
"confidence": self.confidence,
@@ -26,14 +44,212 @@ class PreScanResult:
class PreScan:
"""Pre-scan for discs."""
def __init__(self):
self.tmdb = TMDBClient()
self.musicbrainz = MusicBrainzClient()
self.thetvdb = TheTVDBClient()
def scan(self, device_path: str) -> PreScanResult:
"""Scan a disc and return metadata."""
return PreScanResult(
title="Unknown Disc",
year=None,
confidence=0.0,
metadata={},
tracks=[]
"""Führe Pre-Scan durch."""
disc_type = self._detect_disc_type(device_path)
toc = self._read_toc(device_path, disc_type)
if disc_type == "CD":
return self._scan_audio(device_path, toc)
return self._scan_video(device_path, toc)
def _detect_disc_type(self, device_path: str) -> str:
"""Erkenne Disc-Typ (CD/DVD/Blu-ray) über die Medien-Größe."""
try:
result = subprocess.run(
["isosize", "-x", device_path],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
size = int(result.stdout.strip())
if size < 700 * 1024 * 1024:
return "CD"
elif size < 15 * 1024 * 1024 * 1024:
return "DVD"
else:
return "Blu-ray"
except Exception:
pass
return "DVD"
def _read_toc(self, device_path: str, disc_type: str) -> Dict:
"""Lese TOC (Table of Contents)."""
toc = {
"title": None,
"year": None,
"tracks": [],
"duration": 0
}
try:
if disc_type == "CD":
result = subprocess.run(
["cdparanoia", "-Q", device_path],
capture_output=True,
text=True,
timeout=10
)
for line in result.stdout.split('\n'):
if 'track' in line.lower():
parts = line.split()
if len(parts) >= 4:
toc["tracks"].append({
"track_number": parts[1],
"title": " ".join(parts[3:]) if len(parts) > 3 else "Track",
"duration": 0
})
else:
# makemkvcon liest nur den TOC — KEIN Ripping
result = subprocess.run(
["makemkvcon", "--minlength=300", "--progress=off", "info", device_path],
capture_output=True,
text=True,
timeout=30
)
for line in result.stdout.split('\n'):
if line.startswith('DRV:'):
parts = line.split(',')
if len(parts) >= 4:
toc["title"] = parts[3].strip().strip('"')
elif line.startswith('TINFO:'):
parts = line.split(',')
if len(parts) >= 5:
toc["tracks"].append({
"title": parts[4].strip().strip('"') if len(parts) > 4 else "Title",
"duration": int(parts[2]) if len(parts) > 2 else 0
})
except Exception as e:
print(f"Pre-Scan TOC Error: {e}")
return toc
def _scan_audio(self, device_path: str, toc: Dict) -> PreScanResult:
"""Pre-Scan für Audio-CD."""
cache_key = generate_prescan_key(device_path, is_audio=True)
cached = get(cache_key)
if cached:
return PreScanResult(**cached)
confidence = 0.5
artist = None
album = toc["title"] or "Unknown Album"
if " - " in album:
parts = album.split(" - ", 1)
if len(parts) == 2:
artist = parts[0]
album = parts[1]
if artist:
artists = self.musicbrainz.search_artist(artist)
if artists:
confidence = 0.8
releases = self.musicbrainz.search_release(album, artist)
if releases:
confidence = 0.9
release_id = releases[0]["id"]
release_info = self.musicbrainz.get_release_info(release_id)
if release_info:
for medium in release_info.get("media", []):
for track in medium.get("tracks", []):
toc["tracks"].append({
"track_number": track.get("position", 0),
"title": track.get("title", "Unknown"),
"duration": track.get("duration", 0) // 1000
})
if not artist:
movies = self.tmdb.search_movie(album)
if movies:
confidence = 0.7
movie_details = self.tmdb.get_movie_details(movies[0]["id"])
if movie_details:
album = f"Soundtrack - {movie_details.get('title', album)}"
toc["title"] = album
result = PreScanResult(
disc_type="CD",
title=album,
tracks=toc["tracks"],
confidence=confidence
)
cache_set(cache_key, result.to_dict())
return result
def _scan_video(self, device_path: str, toc: Dict) -> PreScanResult:
"""Pre-Scan für DVD/Blu-ray."""
cache_key = generate_prescan_key(device_path, is_audio=False)
cached = get(cache_key)
if cached:
return PreScanResult(**cached)
title = toc["title"] or "Unknown Title"
confidence = 0.0
metadata = {}
matched = False
movies = self.tmdb.search_movie(title)
if movies:
for movie in movies:
if movie.get("title", "").lower() == title.lower():
confidence = 0.95
movie_details = self.tmdb.get_movie_details(movie["id"])
if movie_details:
metadata = {
"type": "movie",
"id": movie["id"],
"title": movie_details.get("title", title),
"year": int(movie_details.get("release_date", "0")[:4]) if movie_details.get("release_date") else None,
"overview": movie_details.get("overview", ""),
"poster_path": movie_details.get("poster_path", ""),
"backdrop_path": movie_details.get("backdrop_path", ""),
"runtime": movie_details.get("runtime", 0),
"genres": [g["name"] for g in movie_details.get("genres", [])]
}
matched = True
break
if not matched:
tv_shows = self.tmdb.search_tv(title)
if tv_shows:
for show in tv_shows:
if show.get("name", "").lower() == title.lower():
confidence = 0.9
tv_details = self.tmdb.get_tv_details(show["id"])
if tv_details:
metadata = {
"type": "tv",
"id": show["id"],
"title": tv_details.get("name", title),
"year": int(tv_details.get("first_air_date", "0")[:4]) if tv_details.get("first_air_date") else None,
"overview": tv_details.get("overview", ""),
"poster_path": tv_details.get("poster_path", ""),
"backdrop_path": tv_details.get("backdrop_path", ""),
"genres": [g["name"] for g in tv_details.get("genres", [])]
}
matched = True
break
if not matched:
confidence = 0.3
metadata = {
"type": "unknown",
"title": title,
"year": None
}
result = PreScanResult(
disc_type=toc.get("disc_type", "DVD"),
title=title,
year=metadata.get("year"),
confidence=confidence,
metadata=metadata,
tracks=toc.get("tracks", [])
)
cache_set(cache_key, result.to_dict())
return result