0ea5f10b31
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)
256 lines
9.7 KiB
Python
256 lines
9.7 KiB
Python
"""Pre-Scan-Modul: Liest Disc-TOC ohne Ripping und schlägt Metadaten vor.
|
|
|
|
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
|
|
|
|
|
|
class PreScanResult:
|
|
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:
|
|
return {
|
|
"disc_type": self.disc_type,
|
|
"title": self.title,
|
|
"year": self.year,
|
|
"confidence": self.confidence,
|
|
"metadata": self.metadata,
|
|
"tracks": self.tracks
|
|
}
|
|
|
|
|
|
class PreScan:
|
|
def __init__(self):
|
|
self.tmdb = TMDBClient()
|
|
self.musicbrainz = MusicBrainzClient()
|
|
self.thetvdb = TheTVDBClient()
|
|
|
|
def scan(self, device_path: str) -> PreScanResult:
|
|
"""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
|