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

@@ -0,0 +1,53 @@
import React from 'react'
import { Play, Pause, X, ChevronUp } from 'lucide-react'
import { usePlayerStore } from '../../store/playerStore'
import CoverImage from '../common/CoverImage'
import { coverUrl } from '../../api/items'
export default function MiniPlayer() {
const { item, currentTime, duration, isPlaying, setPlaying, stop, setExpanded } = usePlayerStore()
if (!item) return null
const meta = item.media?.metadata || {}
const title = meta.title || item.relPath || ''
const author = meta.authors?.[0]?.name || ''
const pct = duration > 0 ? (currentTime / duration) * 100 : 0
return (
<div className="fixed bottom-0 left-0 right-0 bg-surface border-t border-white/10 z-50">
{/* Progress bar */}
<div className="h-0.5 bg-white/10">
<div className="h-full bg-primary transition-all" style={{ width: `${pct}%` }} />
</div>
<div className="flex items-center gap-3 px-4 py-3">
<div
className="cursor-pointer flex items-center gap-3 flex-1 min-w-0"
onClick={() => setExpanded(true)}
>
<CoverImage
src={item.media?.coverPath ? coverUrl(item.id) : null}
alt={title}
className="w-10 h-10 rounded flex-shrink-0"
/>
<div className="min-w-0">
<p className="text-sm font-medium text-white truncate">{title}</p>
{author && <p className="text-xs text-gray-400 truncate">{author}</p>}
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button
className="w-9 h-9 rounded-full bg-primary text-black flex items-center justify-center"
onClick={() => setPlaying(!isPlaying)}
>
{isPlaying ? <Pause size={16} fill="currentColor" /> : <Play size={16} fill="currentColor" />}
</button>
<button className="text-gray-400 hover:text-white p-1" onClick={stop}>
<X size={18} />
</button>
</div>
</div>
</div>
)
}