Files
rippy/docker/api/main.py
T
Hitonabi 518155051f 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
2026-07-21 17:17:35 +02:00

252 lines
6.9 KiB
Python

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))