import { useEffect, useMemo, useState } from 'react'; import { Dialog } from 'primereact/dialog'; import { Dropdown } from 'primereact/dropdown'; import { Slider } from 'primereact/slider'; import { Button } from 'primereact/button'; import { ProgressBar } from 'primereact/progressbar'; import { Tag } from 'primereact/tag'; import { InputText } from 'primereact/inputtext'; import { AUDIOBOOK_FORMATS, AUDIOBOOK_FORMAT_SCHEMAS, getDefaultAudiobookFormatOptions } from '../config/audiobookFormatSchemas'; import { getStatusLabel, getStatusSeverity } from '../utils/statusPresentation'; function normalizeJobId(value) { const parsed = Number(value); if (!Number.isFinite(parsed) || parsed <= 0) { return null; } return Math.trunc(parsed); } function normalizeFormat(value) { const raw = String(value || '').trim().toLowerCase(); return AUDIOBOOK_FORMATS.some((entry) => entry.value === raw) ? raw : 'mp3'; } function isFieldVisible(field, values) { if (!field?.showWhen) { return true; } return values?.[field.showWhen.field] === field.showWhen.value; } function buildFormatOptions(format, existingOptions = {}) { return { ...getDefaultAudiobookFormatOptions(format), ...(existingOptions && typeof existingOptions === 'object' ? existingOptions : {}) }; } function formatChapterTime(secondsValue) { const totalSeconds = Number(secondsValue || 0); if (!Number.isFinite(totalSeconds) || totalSeconds < 0) { return '-'; } const rounded = Math.max(0, Math.round(totalSeconds)); const hours = Math.floor(rounded / 3600); const minutes = Math.floor((rounded % 3600) / 60); const seconds = rounded % 60; if (hours > 0) { return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; } return `${minutes}:${String(seconds).padStart(2, '0')}`; } function stripHtml(value) { return String(value || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); } function truncateDescription(value, maxLength = 220) { const normalized = stripHtml(value); if (!normalized || normalized.length <= maxLength) { return normalized; } return `${normalized.slice(0, maxLength).trim()}...`; } function normalizeChapterTitle(value, index) { const normalized = String(value || '').replace(/\s+/g, ' ').trim(); return normalized || `Kapitel ${index}`; } function normalizeEditableChapters(chapters = []) { const source = Array.isArray(chapters) ? chapters : []; return source.map((chapter, index) => { const safeIndex = Number(chapter?.index); const resolvedIndex = Number.isFinite(safeIndex) && safeIndex > 0 ? Math.trunc(safeIndex) : index + 1; return { index: resolvedIndex, title: normalizeChapterTitle(chapter?.title, resolvedIndex), startSeconds: Number(chapter?.startSeconds || 0), endSeconds: Number(chapter?.endSeconds || 0), startMs: Number(chapter?.startMs || 0), endMs: Number(chapter?.endMs || 0) }; }); } function FormatField({ field, value, onChange, disabled }) { if (field.type === 'slider') { return (
{field.description ? {field.description} : null} onChange(field.key, event.value)} min={field.min} max={field.max} step={field.step || 1} disabled={disabled} />
); } if (field.type === 'select') { return (
{field.description ? {field.description} : null} onChange(field.key, event.value)} disabled={disabled} />
); } return null; } export default function AudiobookConfigPanel({ pipeline, onStart, onCancel, onRetry, onDeleteJob, busy }) { const context = pipeline?.context && typeof pipeline.context === 'object' ? pipeline.context : {}; const state = String(pipeline?.state || 'IDLE').trim().toUpperCase() || 'IDLE'; const jobId = normalizeJobId(context?.jobId); const metadata = context?.selectedMetadata && typeof context.selectedMetadata === 'object' ? context.selectedMetadata : {}; const audiobookConfig = context?.audiobookConfig && typeof context.audiobookConfig === 'object' ? context.audiobookConfig : (context?.mediaInfoReview && typeof context.mediaInfoReview === 'object' ? context.mediaInfoReview : {}); const initialFormat = normalizeFormat(audiobookConfig?.format); const chapters = Array.isArray(metadata?.chapters) ? metadata.chapters : (Array.isArray(context?.chapters) ? context.chapters : []); const [format, setFormat] = useState(initialFormat); const [formatOptions, setFormatOptions] = useState(() => buildFormatOptions(initialFormat, audiobookConfig?.formatOptions)); const [editableChapters, setEditableChapters] = useState(() => normalizeEditableChapters(chapters)); const [descriptionDialogVisible, setDescriptionDialogVisible] = useState(false); useEffect(() => { const nextFormat = normalizeFormat(audiobookConfig?.format); setFormat(nextFormat); setFormatOptions(buildFormatOptions(nextFormat, audiobookConfig?.formatOptions)); }, [jobId, audiobookConfig?.format, JSON.stringify(audiobookConfig?.formatOptions || {})]); useEffect(() => { setEditableChapters(normalizeEditableChapters(chapters)); }, [jobId, JSON.stringify(chapters || [])]); const schema = AUDIOBOOK_FORMAT_SCHEMAS[format] || AUDIOBOOK_FORMAT_SCHEMAS.mp3; const canStart = Boolean(jobId) && (state === 'READY_TO_START' || state === 'ERROR' || state === 'CANCELLED'); const isRunning = state === 'ENCODING'; const isFinished = state === 'FINISHED'; const progress = Number.isFinite(Number(pipeline?.progress)) ? Math.max(0, Math.min(100, Number(pipeline.progress))) : 0; const outputPath = String(context?.outputPath || '').trim() || null; const isSplitOutput = format === 'mp3' || format === 'flac'; const currentChapter = context?.currentChapter && typeof context.currentChapter === 'object' ? context.currentChapter : null; const completedChapterCount = Number(context?.completedChapterCount ?? -1); const chapterTotal = Number(currentChapter?.total || editableChapters.length || 0); const statusLabel = getStatusLabel(state); const statusSeverity = getStatusSeverity(state); const description = String(metadata?.description || '').trim(); const descriptionStripped = stripHtml(description); const descriptionPreview = truncateDescription(description); const posterUrl = String(metadata?.poster || '').trim() || null; const visibleFields = useMemo( () => (Array.isArray(schema?.fields) ? schema.fields.filter((field) => isFieldVisible(field, formatOptions)) : []), [schema, formatOptions] ); return (
{posterUrl ? (
{metadata?.title
) : null}
Titel: {metadata?.title || '-'}
Autor: {metadata?.author || '-'}
Sprecher: {metadata?.narrator || '-'}
Jahr: {metadata?.year || '-'}
Kapitel: {editableChapters.length || '-'}
{descriptionPreview ? (
Beschreibung: {descriptionPreview} {descriptionStripped.length > descriptionPreview.length ? (
) : null}
{metadata?.durationMs ? : null} {posterUrl ? : null}
{ const nextFormat = normalizeFormat(event.value); setFormat(nextFormat); setFormatOptions(buildFormatOptions(nextFormat, {})); }} disabled={busy || isRunning} />
{visibleFields.map((field) => ( { setFormatOptions((prev) => ({ ...prev, [key]: nextValue })); }} disabled={busy || isRunning} /> ))} m4b erzeugt eine Datei mit bearbeitbaren Kapiteln. mp3 und flac werden kapitelweise als einzelne Dateien erzeugt.

Kapitel

{editableChapters.length === 0 ? ( Keine Kapitel in der Quelle erkannt. ) : (
{editableChapters.map((chapter, index) => (
#{chapter.index || index + 1} {formatChapterTime(chapter.startSeconds)} - {formatChapterTime(chapter.endSeconds)}
{ const nextTitle = event.target.value; setEditableChapters((prev) => prev.map((entry, entryIndex) => ( entryIndex === index ? { ...entry, title: nextTitle } : entry ))); }} disabled={busy || isRunning} />
))}
)}
{isRunning ? (
{Math.round(progress)}%{currentChapter ? ` | Kapitel ${currentChapter.index}/${currentChapter.total}: ${currentChapter.title}` : ''}
) : null} {isSplitOutput && (isRunning || isFinished) && editableChapters.length > 0 ? (
Kapitel-Status ({completedChapterCount >= 0 ? completedChapterCount : (isFinished ? editableChapters.length : 0)}/{chapterTotal || editableChapters.length} fertig)
{editableChapters.map((chapter, idx) => { const chIdx = chapter.index || idx + 1; const isDone = isFinished || (completedChapterCount >= 0 && chIdx <= completedChapterCount); const isActive = !isDone && currentChapter?.index === chIdx; const statusIcon = isDone ? 'pi pi-check-circle' : isActive ? 'pi pi-spin pi-spinner' : 'pi pi-circle'; const statusClass = isDone ? 'chapter-status-done' : isActive ? 'chapter-status-active' : 'chapter-status-pending'; return (
#{String(chIdx).padStart(2, '0')} {chapter.title}
); })}
) : null} {outputPath ? (
Ausgabe: {outputPath}
) : null}
{canStart ? (
setDescriptionDialogVisible(false)} >
Keine Beschreibung vorhanden.

' }} />
); }