Files
Audiolib/backend/app/main.py
Audiolib 52c10a7518 Phase 5-9: Matching-Engine, Podcast-Support, Web-Interface + Player
Backend:
- Matching-Orchestrator mit deutschen Serien-Patterns (drei ???, TKKG, ...)
- Vollständige MusicBrainz-Integration (Tracklist → Kapitel, Cover Art Archive)
- OpenLibrary + Google Books als Fallback-Quellen
- Auto-Accept (≥0.75) vs zu_prüfen (0.5-0.75) vs kein Match
- Manuelles Matching: GET /api/items/:id/match/search, POST apply
- RSS-Feed-Manager: feedparser, iTunes Search, periodisches Update
- APScheduler für Podcast-Feed-Updates (konfigurierbares Intervall)
- Podcast-Router: Feed-URL setzen, Episoden, Feed-Suche
- HLS: FFmpeg läuft als Background-Task, wartet auf ersten Segment
- main.py: APScheduler + neue Router eingebunden

Frontend (React + Vite + Tailwind + HLS.js):
- Login-Seite mit Fehlerbehandlung
- Library-Seite: Grid/Listen-Ansicht, Suche, Tag-Filter, Pagination, Scan
- BookCard: Cover, Fortschrittsbalken, zu_prüfen Badge, Quick-Play
- BookDetail: Metadaten, Matching-Panel, Kapitel-Liste, Lesezeichen
- AudioPlayer: HLS.js, Kapitel-Marker auf Fortschrittsbalken, Speed,
  Sleep-Timer, Lesezeichen, Keyboard-Shortcuts (Space/Arrows)
- MiniPlayer: persistent an Fußzeile, expandierbar
- PodcastDetail: Feed-URL, iTunes-Suche, Episoden-Liste
- Admin-Panel: Benutzer/Bibliotheken/Einstellungen verwalten
- App.tsx: React Router, Auth-Guard, Player-Overlay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 13:11:04 +02:00

114 lines
3.5 KiB
Python

import uuid
import logging
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from .database import init_db, AsyncSessionLocal
from .config import get_settings
from .models import User, Library
from .services.auth import hash_password, create_token
from .services.file_watcher import start_file_watcher, stop_file_watcher
from .services.podcast_feed import update_all_feeds
from sqlalchemy import select
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
logger = logging.getLogger(__name__)
_scheduler = AsyncIOScheduler()
async def _seed_admin():
settings = get_settings()
async with AsyncSessionLocal() as db:
result = await db.execute(select(User).where(User.is_admin == True))
if result.scalar_one_or_none():
return
logger.info(f"Lege Admin-User an: {settings.admin_username}")
admin = User(
id=str(uuid.uuid4()),
username=settings.admin_username,
email=settings.admin_email,
password_hash=hash_password(settings.admin_password),
is_admin=True,
)
db.add(admin)
await db.flush()
admin.token = create_token(admin.id)
await db.commit()
logger.info("Admin-User angelegt.")
async def _seed_default_library():
settings = get_settings()
async with AsyncSessionLocal() as db:
result = await db.execute(select(Library))
if result.scalar_one_or_none():
return
folder_id = str(uuid.uuid4())
lib = Library(
id=str(uuid.uuid4()),
name="Hörbücher",
display_name="Hörbücher",
folders=[{"id": folder_id, "fullPath": settings.audiofiles_path}],
media_type="book",
settings={"icon": "headphones", "provider": "google"},
)
db.add(lib)
await db.commit()
logger.info(f"Standard-Library angelegt: {settings.audiofiles_path}")
@asynccontextmanager
async def lifespan(app: FastAPI):
settings = get_settings()
for d in [settings.hls_cache_dir, settings.covers_dir, settings.log_dir]:
os.makedirs(d, exist_ok=True)
await init_db()
await _seed_admin()
await _seed_default_library()
await start_file_watcher()
# Podcast-Feed-Scheduler
_scheduler.add_job(update_all_feeds, "interval", hours=settings.podcast_update_interval_hours, id="feed_update")
_scheduler.start()
logger.info("Audiolib gestartet.")
yield
stop_file_watcher()
_scheduler.shutdown(wait=False)
logger.info("Audiolib gestoppt.")
app = FastAPI(title="Audiolib", version="2.4.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
settings = get_settings()
if os.path.exists(settings.covers_dir):
app.mount("/covers", StaticFiles(directory=settings.covers_dir), name="covers")
from .routers import auth, libraries, items, stream, me, users, settings as settings_router
from .routers import matching, podcasts
app.include_router(auth.router)
app.include_router(libraries.router)
app.include_router(items.router)
app.include_router(stream.router)
app.include_router(me.router)
app.include_router(users.router)
app.include_router(settings_router.router)
app.include_router(matching.router)
app.include_router(podcasts.router)