FEATURE: all versions use single source of truth for codec/container/file_extension info

This commit is contained in:
2026-08-21 14:58:55 +05:00
parent d08045bf02
commit 7af2600ed8
7 changed files with 183 additions and 110 deletions
+77 -51
View File
@@ -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 })
}
}