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,4 +1,3 @@
"""Google Books-Matching — Phase 5."""
import httpx
from .base import MatchResult
@@ -10,26 +9,52 @@ async def search_google_books(title: str, author: str | None = None) -> list[Mat
if author:
q += f' inauthor:"{author}"'
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(f"{GB_BASE}/volumes", params={"q": q, "maxResults": 5, "langRestrict": "de"})
resp.raise_for_status()
data = resp.json()
async with httpx.AsyncClient(timeout=12) as client:
try:
r = await client.get(
f"{GB_BASE}/volumes",
params={"q": q, "maxResults": 5, "langRestrict": "de", "printType": "books"},
)
r.raise_for_status()
data = r.json()
except Exception:
return []
results = []
for item in data.get("items", []):
vol = item.get("volumeInfo", {})
authors = vol.get("authors", [])
results.append(
MatchResult(
source="google_books",
source_id=item.get("id", ""),
title=vol.get("title", title),
author=authors[0] if authors else None,
description=vol.get("description"),
publisher=vol.get("publisher"),
publish_year=int(vol.get("publishedDate", "0")[:4]) if vol.get("publishedDate") else None,
language=vol.get("language"),
confidence=0.5,
cover_url = None
image_links = vol.get("imageLinks", {})
if image_links:
cover_url = (
image_links.get("extraLarge")
or image_links.get("large")
or image_links.get("medium")
or image_links.get("thumbnail", "").replace("zoom=1", "zoom=3")
)
)
year = None
pub_date = vol.get("publishedDate", "")
if pub_date and len(pub_date) >= 4:
try:
year = int(pub_date[:4])
except ValueError:
pass
results.append(MatchResult(
source="google_books",
source_id=item.get("id", ""),
title=vol.get("title", title),
subtitle=vol.get("subtitle"),
author=authors[0] if authors else None,
description=vol.get("description"),
publisher=vol.get("publisher"),
publish_year=year,
language=vol.get("language"),
genres=vol.get("categories", []),
cover_url=cover_url,
confidence=0.5,
))
return results