from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse from fastapi import WebSocket, WebSocketDisconnect from pydantic import BaseModel from typing import List, Optional, Dict from datetime import datetime import os import subprocess import asyncio import json from .config import settings from .cache import init_cache, set from .prescan import PreScan from .nfo_generator import NFOGenerator from .image_downloader import ImageDownloader app = FastAPI( title="Rippy API", description="API für das automatische Ripping-System", version="1.0.0" ) # SSE-Connections sse_connections: List = [] @app.on_event("startup") async def startup_event(): """Initialisiere Cache beim Start.""" init_cache() # CORS hinzufügen app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) class Job(BaseModel): id: str type: str status: str device: str startTime: str endTime: Optional[str] = None progress: int = 0 class Device(BaseModel): id: str name: str type: str path: str status: str @app.get("/health") async def health_check(): return {"status": "ok", "service": "api"} @app.get("/") async def root(): return { "name": "Rippy", "version": "1.0.0", "description": "Automatisches Ripping-System für CD, DVD und Blu-ray" } @app.get("/jobs", response_model=List[Job]) async def get_jobs(): """Holt alle Jobs.""" return [] @app.get("/devices", response_model=List[Device]) async def get_devices(): """Holt alle Geräte.""" devices = [] try: result = subprocess.run( ["ls", "-la", "/dev/disc/"], capture_output=True, text=True, timeout=5 ) for line in result.stdout.strip().split('\n')[1:]: if line and 'total' not in line: parts = line.split() if len(parts) >= 9: name = parts[-1] devices.append(Device( id=name, name=f"Laufwerk {name}", type="dvd", path=f"/dev/disc/{name}", status="ready" )) except Exception: pass return devices # SSE-Stream für Echtzeit-Updates @app.get("/stream/jobs") async def job_stream(): """SSE-Stream für Job-Updates.""" async def event_generator(): while True: if sse_connections: # Job-Status aktualisieren jobs = await get_jobs() yield f"data: {json.dumps([j.dict() for j in jobs])}\n\n" await asyncio.sleep(1) return StreamingResponse(event_generator(), media_type="text/event-stream") # Metadaten-Lookup Endpoints class MetadataLookupRequest(BaseModel): title: str year: Optional[int] = None disc_type: str = "dvd" @app.post("/metadata/lookup") async def lookup_metadata(request: MetadataLookupRequest): """Suche Metadaten für Disc.""" prescan = PreScan() # Dummy device für Pre-Scan device = "/dev/dvd" if request.disc_type in ["dvd", "bluray"] else "/dev/cdrom" result = prescan.scan(device) return { "title": result.title, "year": result.year, "confidence": result.confidence, "metadata": result.metadata, "tracks": result.tracks } @app.post("/metadata/confirm") async def confirm_metadata(title: str, year: Optional[int] = None, metadata: Dict = None): """Bestätige Metadaten.""" # In Cache speichern cache_key = f"confirmed:{title}:{year}" set(cache_key, {"title": title, "year": year, "metadata": metadata or {}}) return {"status": "confirmed", "key": cache_key} # Pre-Scan Endpoint class PreScanRequest(BaseModel): device_path: str @app.post("/prescan") async def run_prescan(request: PreScanRequest): """Führe Pre-Scan durch.""" try: prescan = PreScan() result = prescan.scan(request.device_path) return result.to_dict() except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # Jellyfin-Formatierung Endpoints class JellyfinFormatRequest(BaseModel): title: str year: Optional[int] metadata: Dict disc_type: str output_dir: str @app.post("/jellyfin/format") async def jellyfin_format(request: JellyfinFormatRequest): """Formatiere für Jellyfin (NFO + Images).""" try: nfo_gen = NFOGenerator() img_downloader = ImageDownloader() # Ordnerstruktur erstellen output_path = Path(request.output_dir) if request.disc_type in ["dvd", "bluray"]: # Film-Formatierung title = request.metadata.get("title", request.title) year = request.year or request.metadata.get("year") # movie.nfo movie_nfo = nfo_gen.generate_movie_nfo( title=title, year=year or 2000, overview=request.metadata.get("overview", ""), rating=request.metadata.get("rating", 0), runtime=request.metadata.get("runtime", 0), genres=request.metadata.get("genres", []), director=request.metadata.get("director", ""), actors=request.metadata.get("actors", []) ) nfo_path = output_path / "movie.nfo" nfo_gen.save_nfo(movie_nfo, nfo_path) # Poster und Fanart img_downloader.download_poster(title, output_path, 500) img_downloader.download_fanart(title, output_path, 1920) return { "status": "formatted", "nfo_path": str(nfo_path), "poster_path": str(output_path / "poster.jpg"), "fanart_path": str(output_path / "fanart.jpg") } else: # Audio-Formatierung artist = request.metadata.get("artist", "Unknown Artist") album = title # album.nfo album_nfo = nfo_gen.generate_album_nfo( title=album, artist=artist, year=year or 2000, genres=request.metadata.get("genres", []) ) nfo_path = output_path / "album.nfo" nfo_gen.save_nfo(album_nfo, nfo_path) # Album-Cover img_downloader.download_music_images(artist, album, output_path) return { "status": "formatted", "nfo_path": str(nfo_path), "album_cover_path": str(output_path / "album.jpg") } except Exception as e: raise HTTPException(status_code=500, detail=str(e))