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:
+164
-1
@@ -1,10 +1,20 @@
|
||||
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
|
||||
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",
|
||||
@@ -12,6 +22,15 @@ app = FastAPI(
|
||||
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,
|
||||
@@ -86,3 +105,147 @@ async def get_devices():
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user