Files
rippy/docker/api/prescan/prescan.py
T
Hitonabi 0ea5f10b31
Ampel / ampel (push) Failing after 12m31s
Fix-Runde nach Review: alle Befunde behoben + echte Tests
- 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)
2026-07-22 19:31:48 +02:00

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