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>
This commit is contained in:
Audiolib
2026-05-26 13:11:04 +02:00
parent dfbb397e46
commit 52c10a7518
32 changed files with 2987 additions and 223 deletions

View File

@@ -1,32 +1,32 @@
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
import os
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",
)
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 # Admin existiert bereits
return
logger.info(f"Lege Admin-User an: {settings.admin_username}")
admin = User(
id=str(uuid.uuid4()),
@@ -37,7 +37,6 @@ async def _seed_admin():
)
db.add(admin)
await db.flush()
# Token mit echter ID erstellen
admin.token = create_token(admin.id)
await db.commit()
logger.info("Admin-User angelegt.")
@@ -48,8 +47,7 @@ async def _seed_default_library():
async with AsyncSessionLocal() as db:
result = await db.execute(select(Library))
if result.scalar_one_or_none():
return # Bereits eine Library vorhanden
return
folder_id = str(uuid.uuid4())
lib = Library(
id=str(uuid.uuid4()),
@@ -66,29 +64,28 @@ async def _seed_default_library():
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
settings = get_settings()
os.makedirs(settings.hls_cache_dir, exist_ok=True)
os.makedirs(settings.covers_dir, exist_ok=True)
os.makedirs(settings.log_dir, exist_ok=True)
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
# Shutdown
stop_file_watcher()
_scheduler.shutdown(wait=False)
logger.info("Audiolib gestoppt.")
app = FastAPI(
title="Audiolib",
version="2.4.0",
description="Selbst gehosteter Audiobook-Server, API-kompatibel mit Audiobookshelf",
lifespan=lifespan,
)
app = FastAPI(title="Audiolib", version="2.4.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
@@ -98,13 +95,12 @@ app.add_middleware(
allow_headers=["*"],
)
# Cover-Dateien direkt ausliefern
settings = get_settings()
if os.path.exists(settings.covers_dir):
app.mount("/covers", StaticFiles(directory=settings.covers_dir), name="covers")
# Router registrieren
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)
@@ -113,3 +109,5 @@ 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)