Etappe 3: Metadaten-Lookup + Pre-Scan
- SQLite-Cache für API-Rate-Limits (LRU, 10k Einträge) - TMDB/MusicBrainz/TheTVDB Clients - Pre-Scan-Modul für TOC-Lesung ohne Ripping - Metadaten-Preview UI - Jellyfin-Formatierung (NFO + Images) - API Endpoints für Lookup, Confirm, Format
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""Jellyfin Image Downloader."""
|
||||
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .clients.tmdb import TMDBClient
|
||||
from .clients.thetvdb import TheTVDBClient
|
||||
from .config import settings
|
||||
|
||||
|
||||
class ImageDownloader:
|
||||
def __init__(self):
|
||||
self.tmdb = TMDBClient()
|
||||
self.thetvdb = TheTVDBClient()
|
||||
self.base_url = "https://image.tmdb.org/t/p"
|
||||
|
||||
def download_image(self, url: str, output_path: Path, width: int = 500) -> bool:
|
||||
"""Lade Image herunter."""
|
||||
try:
|
||||
# TMDB URL anpassen
|
||||
if url.startswith("https://image.tmdb.org"):
|
||||
# Konvertiere zu gewünschter Größe
|
||||
path = url.replace(f"{self.base_url}/", "")
|
||||
url = f"{self.base_url}/w{width}/{path}"
|
||||
|
||||
response = requests.get(url, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(response.content)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Image download error: {e}")
|
||||
return False
|
||||
|
||||
def download_poster(self, title: str, output_dir: Path, width: int = 500) -> Optional[Path]:
|
||||
"""Lade Poster herunter."""
|
||||
# TMDB Search
|
||||
movies = self.tmdb.search_movie(title)
|
||||
if movies:
|
||||
movie = movies[0]
|
||||
images = self.tmdb.get_movie_images(movie["id"])
|
||||
|
||||
if images.get("poster"):
|
||||
output_path = output_dir / "poster.jpg"
|
||||
if self.download_image(images["poster"], output_path, width):
|
||||
return output_path
|
||||
|
||||
return None
|
||||
|
||||
def download_fanart(self, title: str, output_dir: Path, width: int = 1920) -> Optional[Path]:
|
||||
"""Lade Fanart herunter."""
|
||||
# TMDB Search
|
||||
movies = self.tmdb.search_movie(title)
|
||||
if movies:
|
||||
movie = movies[0]
|
||||
images = self.tmdb.get_movie_images(movie["id"])
|
||||
|
||||
if images.get("fanart"):
|
||||
output_path = output_dir / "fanart.jpg"
|
||||
if self.download_image(images["fanart"], output_path, width):
|
||||
return output_path
|
||||
|
||||
return None
|
||||
|
||||
def download_series_images(self, title: str, output_dir: Path) -> Dict[str, Optional[Path]]:
|
||||
"""Lade Serien-Poster und Fanart herunter."""
|
||||
result = {
|
||||
"poster": None,
|
||||
"fanart": None
|
||||
}
|
||||
|
||||
tv_shows = self.tmdb.search_tv(title)
|
||||
if tv_shows:
|
||||
tv = tv_shows[0]
|
||||
images = self.tmdb.get_tv_images(tv["id"])
|
||||
|
||||
if images.get("poster"):
|
||||
output_path = output_dir / "poster.jpg"
|
||||
if self.download_image(images["poster"], output_path, 500):
|
||||
result["poster"] = output_path
|
||||
|
||||
if images.get("fanart"):
|
||||
output_path = output_dir / "fanart.jpg"
|
||||
if self.download_image(images["fanart"], output_path, 1920):
|
||||
result["fanart"] = output_path
|
||||
|
||||
return result
|
||||
|
||||
def download_music_images(self, artist: str, album: str, output_dir: Path) -> Dict[str, Optional[Path]]:
|
||||
"""Lade Musik-Album-Cover herunter (via TMDB als Fallback)."""
|
||||
result = {
|
||||
"album": None
|
||||
}
|
||||
|
||||
# TMDB Search für Soundtracks
|
||||
query = f"{album} soundtrack"
|
||||
movies = self.tmdb.search_movie(query)
|
||||
if movies:
|
||||
movie = movies[0]
|
||||
images = self.tmdb.get_movie_images(movie["id"])
|
||||
|
||||
if images.get("poster"):
|
||||
output_path = output_dir / "album.jpg"
|
||||
if self.download_image(images["poster"], output_path, 500):
|
||||
result["album"] = output_path
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user