Files
Audiolib/backend/app/services/hls.py
Audiolib 14ffee3051 Initial commit: Phase 1 – Projektstruktur, DB-Schema, Core-API
- FastAPI-Backend mit vollständiger ABS v2.x API-Kompatibilität
- SQLAlchemy-Models: User, Library, LibraryItem, BookFile, Chapter,
  Podcast, PodcastEpisode, MediaProgress, Bookmark, PlaybackSession
- Auth: JWT-Login (/login, /logout, /api/authorize)
- Library + Items Endpoints inkl. camelCase ABS-Response-Format
- HLS-Streaming via FFmpeg (POST /api/items/:id/play, Session-Sync)
- Me/Progress Endpoints + Lesezeichen
- User-Management + Server-Settings (Admin)
- Library-Scanner (MP3/WAV Discovery, Hintergrund-Task)
- File Watcher (watchdog, 30s Debounce)
- Matching-Skelett (MusicBrainz, OpenLibrary, Google Books – Phase 5)
- Docker-Setup: backend (Python 3.12+FFmpeg), frontend (React/Vite),
  nginx Reverse-Proxy auf Port 3000
- setup.sh: Installiert Docker auf Debian/Ubuntu, richtet .env ein

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

109 lines
3.3 KiB
Python

import os
import asyncio
import uuid
import shutil
from pathlib import Path
from typing import Optional
from ..config import get_settings
HLS_SEGMENT_DURATION = 10 # Sekunden pro Segment
async def create_hls_session(
session_id: str,
audio_files: list[str],
start_time: float = 0.0,
) -> str:
"""
Erstellt HLS-Segmente via FFmpeg für die gegebenen Audio-Dateien.
Gibt den Pfad zum HLS-Verzeichnis zurück.
"""
settings = get_settings()
session_dir = os.path.join(settings.hls_cache_dir, session_id)
os.makedirs(session_dir, exist_ok=True)
playlist_path = os.path.join(session_dir, "output.m3u8")
if len(audio_files) == 1:
input_path = audio_files[0]
else:
# Mehrere Dateien: Concat-Liste erstellen
concat_file = os.path.join(session_dir, "concat.txt")
with open(concat_file, "w", encoding="utf-8") as f:
for af in audio_files:
safe_path = af.replace("\\", "/")
f.write(f"file '{safe_path}'\n")
input_path = concat_file
if len(audio_files) == 1:
cmd = [
"ffmpeg", "-y",
"-ss", str(start_time),
"-i", input_path,
"-c:a", "aac",
"-b:a", "192k",
"-ac", "2",
"-hls_time", str(HLS_SEGMENT_DURATION),
"-hls_list_size", "0",
"-hls_segment_filename", os.path.join(session_dir, "seg%05d.ts"),
"-hls_flags", "independent_segments",
playlist_path,
]
else:
cmd = [
"ffmpeg", "-y",
"-f", "concat", "-safe", "0",
"-i", input_path,
"-ss", str(start_time),
"-c:a", "aac",
"-b:a", "192k",
"-ac", "2",
"-hls_time", str(HLS_SEGMENT_DURATION),
"-hls_list_size", "0",
"-hls_segment_filename", os.path.join(session_dir, "seg%05d.ts"),
"-hls_flags", "independent_segments",
playlist_path,
]
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.PIPE,
)
_, stderr = await proc.communicate()
if proc.returncode != 0:
error_msg = stderr.decode(errors="replace") if stderr else "unknown error"
raise RuntimeError(f"FFmpeg fehler: {error_msg}")
return session_dir
def cleanup_hls_session(session_id: str):
settings = get_settings()
session_dir = os.path.join(settings.hls_cache_dir, session_id)
if os.path.exists(session_dir):
shutil.rmtree(session_dir, ignore_errors=True)
def get_hls_session_path(session_id: str) -> Optional[str]:
settings = get_settings()
session_dir = os.path.join(settings.hls_cache_dir, session_id)
playlist = os.path.join(session_dir, "output.m3u8")
return session_dir if os.path.exists(playlist) else None
def parse_m3u8_duration(playlist_path: str) -> float:
"""Berechnet Gesamtdauer aus M3U8-Playlist."""
total = 0.0
try:
with open(playlist_path, "r") as f:
for line in f:
if line.startswith("#EXTINF:"):
duration_str = line.split(":")[1].split(",")[0]
total += float(duration_str)
except Exception:
pass
return total