FEATURE: all versions use single source of truth for codec/container/file_extension info
This commit is contained in:
@@ -16,7 +16,11 @@ import { ExpandMore, ExpandLess } from '@mui/icons-material'
|
||||
import { useOptionsStore } from '../stores/optionsStore'
|
||||
import { useUploadStore } from '../stores/uploadStore'
|
||||
import { useValidationStore } from '../stores/validationStore'
|
||||
import { getFormats } from '../api/client'
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Section component (collapsible)
|
||||
// ------------------------------------------------------------------------------
|
||||
interface SectionProps {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
@@ -51,54 +55,70 @@ const Section: React.FC<SectionProps> = ({ title, children, defaultExpanded = fa
|
||||
)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// Main OptionsPanel component
|
||||
// ------------------------------------------------------------------------------
|
||||
export const OptionsPanel: React.FC = () => {
|
||||
const { options, setOptions } = useOptionsStore()
|
||||
const {
|
||||
options,
|
||||
setOptions,
|
||||
containers,
|
||||
codecs,
|
||||
setContainers,
|
||||
setCodecs,
|
||||
} = useOptionsStore()
|
||||
|
||||
const { hasVideo } = useUploadStore()
|
||||
const { formatError, setFormatError } = useValidationStore()
|
||||
|
||||
// Audio-only formats from backend constants (hardcoded for now)
|
||||
const audioOnlyFormats = ['mp3', 'm4a', 'ogg', 'opus', 'flac', 'wav', 'aac']
|
||||
// Fetch containers and codecs from backend on mount
|
||||
useEffect(() => {
|
||||
const fetchFormats = async () => {
|
||||
try {
|
||||
const data = await getFormats()
|
||||
setContainers(data.containers)
|
||||
setCodecs(data.codecs)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch formats:', err)
|
||||
}
|
||||
}
|
||||
fetchFormats()
|
||||
}, [setContainers, setCodecs])
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// Handlers
|
||||
// --------------------------------------------------------------
|
||||
const handleFormatChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newFormat = e.target.value
|
||||
setOptions({ format: newFormat })
|
||||
|
||||
// Validate format
|
||||
if (audioOnlyFormats.includes(newFormat) && hasVideo && !options.drop_video) {
|
||||
setFormatError(
|
||||
`Format '${newFormat}' does not support video streams. Please enable "Drop video" or choose a container that supports video (e.g., MKV, MP4).`
|
||||
)
|
||||
} else {
|
||||
setFormatError(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDropVideoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const checked = e.target.checked
|
||||
setOptions({ drop_video: checked })
|
||||
// Re-validate format
|
||||
if (audioOnlyFormats.includes(options.format) && hasVideo && !checked) {
|
||||
}
|
||||
|
||||
const handleChange = (field: string, value: any) => {
|
||||
setOptions({ [field]: value })
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// Format validation – re-run when relevant state changes
|
||||
// --------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
const selectedContainer = containers.find(c => c.name === options.format)
|
||||
if (selectedContainer?.audio_only && hasVideo && !options.drop_video) {
|
||||
setFormatError(
|
||||
`Format '${options.format}' does not support video streams. Please enable "Drop video" or choose a container that supports video (e.g., MKV, MP4).`
|
||||
`Container '${options.format}' does not support video streams. Please enable "Drop video" or choose a container that supports video (e.g., MKV, MP4).`
|
||||
)
|
||||
} else {
|
||||
setFormatError(null)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-validate when hasVideo changes (e.g., after upload)
|
||||
useEffect(() => {
|
||||
const shouldShowError = audioOnlyFormats.includes(options.format) && hasVideo && !options.drop_video
|
||||
const newError = shouldShowError
|
||||
? `Format '${options.format}' does not support video streams. Please enable "Drop video" or choose a container that supports video (e.g., MKV, MP4).`
|
||||
: null
|
||||
|
||||
// Only update if the error state actually changes
|
||||
if (newError !== formatError) {
|
||||
setFormatError(newError)
|
||||
}
|
||||
}, [hasVideo, options.format, options.drop_video, formatError, setFormatError])
|
||||
}, [containers, options.format, options.drop_video, hasVideo, setFormatError])
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// Render
|
||||
// --------------------------------------------------------------
|
||||
return (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||
@@ -111,9 +131,12 @@ export const OptionsPanel: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Output Settings */}
|
||||
{/* ------------------------------------------------------------------------
|
||||
Output Settings
|
||||
------------------------------------------------------------------------ */}
|
||||
<Section title="Output Settings" defaultExpanded>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{/* Container dropdown */}
|
||||
<TextField
|
||||
label="Container"
|
||||
select
|
||||
@@ -126,18 +149,14 @@ export const OptionsPanel: React.FC = () => {
|
||||
formatError || "Determines the output file extension and container structure."
|
||||
}
|
||||
>
|
||||
<MenuItem value="mp3">MP3 (.mp3)</MenuItem>
|
||||
<MenuItem value="m4a">M4A (.m4a)</MenuItem>
|
||||
<MenuItem value="mkv">MKV (.mkv)</MenuItem>
|
||||
<MenuItem value="mp4">MP4 (.mp4)</MenuItem>
|
||||
<MenuItem value="ogg">OGG (.ogg)</MenuItem>
|
||||
<MenuItem value="opus">OPUS (.opus)</MenuItem>
|
||||
<MenuItem value="flac">FLAC (.flac)</MenuItem>
|
||||
<MenuItem value="wav">WAV (.wav)</MenuItem>
|
||||
<MenuItem value="aac">AAC (.aac)</MenuItem>
|
||||
{containers.map((container) => (
|
||||
<MenuItem key={container.name} value={container.name}>
|
||||
{container.name.toUpperCase()} ({container.extension})
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{/* Transcode option – remains unchanged but clearly labeled */}
|
||||
{/* Audio Codec (Transcode) dropdown */}
|
||||
<TextField
|
||||
label="Audio Codec (Transcode)"
|
||||
select
|
||||
@@ -148,11 +167,16 @@ export const OptionsPanel: React.FC = () => {
|
||||
helperText="Select an audio codec to re-encode, or keep 'Copy' to preserve the original."
|
||||
>
|
||||
<MenuItem value="">Copy (no transcoding)</MenuItem>
|
||||
<MenuItem value="libmp3lame">MP3 (LAME)</MenuItem>
|
||||
<MenuItem value="aac">AAC</MenuItem>
|
||||
<MenuItem value="libopus">OPUS</MenuItem>
|
||||
{codecs
|
||||
.filter(c => c.supports_transcoding)
|
||||
.map((codec) => (
|
||||
<MenuItem key={codec.name} value={codec.ffmpeg}>
|
||||
{codec.name.toUpperCase()} (→ {codec.recommended_container})
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
{/* Drop video stream (only shown if video present) */}
|
||||
{hasVideo && (
|
||||
<FormControlLabel
|
||||
control={
|
||||
@@ -165,6 +189,7 @@ export const OptionsPanel: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Drop subtitle streams */}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
@@ -177,7 +202,9 @@ export const OptionsPanel: React.FC = () => {
|
||||
</Box>
|
||||
</Section>
|
||||
|
||||
{/* Filename Settings */}
|
||||
{/* ------------------------------------------------------------------------
|
||||
Filename Settings
|
||||
------------------------------------------------------------------------ */}
|
||||
<Section title="Filename Settings">
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
@@ -233,7 +260,9 @@ export const OptionsPanel: React.FC = () => {
|
||||
</Box>
|
||||
</Section>
|
||||
|
||||
{/* Metadata Settings */}
|
||||
{/* ------------------------------------------------------------------------
|
||||
Metadata Settings
|
||||
------------------------------------------------------------------------ */}
|
||||
<Section title="Metadata Settings">
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
@@ -289,7 +318,9 @@ export const OptionsPanel: React.FC = () => {
|
||||
</Box>
|
||||
</Section>
|
||||
|
||||
{/* Tracklist Settings */}
|
||||
{/* ------------------------------------------------------------------------
|
||||
Tracklist Settings
|
||||
------------------------------------------------------------------------ */}
|
||||
<Section title="Tracklist Settings" defaultExpanded>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
@@ -304,9 +335,4 @@ export const OptionsPanel: React.FC = () => {
|
||||
</Section>
|
||||
</Paper>
|
||||
)
|
||||
|
||||
// Helper function for option updates
|
||||
function handleChange(field: string, value: any) {
|
||||
setOptions({ [field]: value })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user