Tracs validation added. A warning is thrown if the input contains a video strem, but the requested output format doesn't support it
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
@@ -10,11 +10,12 @@ import {
|
||||
Collapse,
|
||||
IconButton,
|
||||
Divider,
|
||||
Alert,
|
||||
} from '@mui/material'
|
||||
// Select is used internally by TextField with select prop, no need to import
|
||||
import { ExpandMore, ExpandLess } from '@mui/icons-material'
|
||||
import { useOptionsStore } from '../stores/optionsStore'
|
||||
|
||||
import { useUploadStore } from '../stores/uploadStore'
|
||||
import { useValidationStore } from '../stores/validationStore'
|
||||
|
||||
interface SectionProps {
|
||||
title: string
|
||||
@@ -52,41 +53,75 @@ const Section: React.FC<SectionProps> = ({ title, children, defaultExpanded = fa
|
||||
|
||||
export const OptionsPanel: React.FC = () => {
|
||||
const { options, setOptions } = useOptionsStore()
|
||||
const { hasVideo } = useUploadStore()
|
||||
const { formatError, setFormatError } = useValidationStore()
|
||||
|
||||
const handleChange = (field: string, value: any) => {
|
||||
setOptions({ [field]: value })
|
||||
// Audio-only formats from backend constants (hardcoded for now)
|
||||
const audioOnlyFormats = ['mp3', 'm4a', 'ogg', 'opus', 'flac', 'wav', 'aac']
|
||||
|
||||
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) {
|
||||
setFormatError(
|
||||
`Format '${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])
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||
⚙️ Options
|
||||
</Typography>
|
||||
|
||||
{/* Tracklist Section */}
|
||||
<Section title="Tracklist Settings" defaultExpanded>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label="Tracklist Format"
|
||||
value={options.tracklist_format}
|
||||
onChange={(e) => handleChange('tracklist_format', e.target.value)}
|
||||
fullWidth
|
||||
size="small"
|
||||
helperText="Placeholders: %ts (timestamp), %tn (track name), %an (author), %al (album), %date, %ext"
|
||||
/>
|
||||
</Box>
|
||||
</Section>
|
||||
{formatError && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
{formatError}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Output Section */}
|
||||
{/* Output Settings */}
|
||||
<Section title="Output Settings" defaultExpanded>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label="Format"
|
||||
select
|
||||
value={options.format}
|
||||
onChange={(e) => handleChange('format', e.target.value)}
|
||||
onChange={handleFormatChange}
|
||||
fullWidth
|
||||
size="small"
|
||||
error={!!formatError}
|
||||
>
|
||||
<MenuItem value="mp3">MP3</MenuItem>
|
||||
<MenuItem value="m4a">M4A</MenuItem>
|
||||
@@ -113,15 +148,18 @@ export const OptionsPanel: React.FC = () => {
|
||||
<MenuItem value="libopus">OPUS</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={options.drop_video}
|
||||
onChange={(e) => handleChange('drop_video', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Drop video streams"
|
||||
/>
|
||||
{hasVideo && (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={options.drop_video}
|
||||
onChange={handleDropVideoChange}
|
||||
/>
|
||||
}
|
||||
label="Drop video streams"
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
@@ -134,7 +172,7 @@ export const OptionsPanel: React.FC = () => {
|
||||
</Box>
|
||||
</Section>
|
||||
|
||||
{/* Filename Section */}
|
||||
{/* Filename Settings */}
|
||||
<Section title="Filename Settings">
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
@@ -190,7 +228,7 @@ export const OptionsPanel: React.FC = () => {
|
||||
</Box>
|
||||
</Section>
|
||||
|
||||
{/* Metadata Section */}
|
||||
{/* Metadata Settings */}
|
||||
<Section title="Metadata Settings">
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
@@ -245,8 +283,25 @@ export const OptionsPanel: React.FC = () => {
|
||||
/>
|
||||
</Box>
|
||||
</Section>
|
||||
|
||||
{/* Tracklist Settings */}
|
||||
<Section title="Tracklist Settings" defaultExpanded>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label="Tracklist Format"
|
||||
value={options.tracklist_format}
|
||||
onChange={(e) => handleChange('tracklist_format', e.target.value)}
|
||||
fullWidth
|
||||
size="small"
|
||||
helperText="Placeholders: %ts (timestamp), %tn (track name), %an (author), %al (album), %date, %ext"
|
||||
/>
|
||||
</Box>
|
||||
</Section>
|
||||
</Paper>
|
||||
)
|
||||
|
||||
// Helper function for option updates
|
||||
function handleChange(field: string, value: any) {
|
||||
setOptions({ [field]: value })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useDropzone } from 'react-dropzone'
|
||||
import { Box, Typography, Paper, LinearProgress, Alert } from '@mui/material'
|
||||
import { CloudUpload, InsertDriveFile } from '@mui/icons-material'
|
||||
import { useUploadStore } from '../stores/uploadStore'
|
||||
import { uploadFile } from '../api/client'
|
||||
import { uploadFile, getTaskInfo } from '../api/client'
|
||||
import { useTaskStore } from '../stores/taskStore'
|
||||
|
||||
const ALLOWED_EXTENSIONS = ['.mp3', '.flac', '.wav', '.m4a', '.ogg', '.opus', '.aac', '.wma', '.aiff', '.alac', '.ac3']
|
||||
|
||||
@@ -22,8 +23,14 @@ export const UploadZone: React.FC = () => {
|
||||
setUploadProgress,
|
||||
setError,
|
||||
setTaskId,
|
||||
setHasVideo,
|
||||
setHasAudio,
|
||||
setHasSubtitle,
|
||||
setAudioCodec,
|
||||
} = useUploadStore()
|
||||
|
||||
const { setTaskId: setTaskIdStore } = useTaskStore()
|
||||
|
||||
const onDrop = useCallback(
|
||||
async (acceptedFiles: File[]) => {
|
||||
if (acceptedFiles.length === 0) return
|
||||
@@ -45,9 +52,23 @@ export const UploadZone: React.FC = () => {
|
||||
|
||||
try {
|
||||
const response = await uploadFile(selectedFile)
|
||||
setTaskId(response.task_id)
|
||||
const taskId = response.task_id
|
||||
setTaskId(taskId)
|
||||
setTaskIdStore(taskId)
|
||||
setUploadProgress(100)
|
||||
setIsUploading(false)
|
||||
|
||||
// Fetch stream info
|
||||
try {
|
||||
const info = await getTaskInfo(taskId)
|
||||
setHasVideo(info.has_video)
|
||||
setHasAudio(info.has_audio)
|
||||
setHasSubtitle(info.has_subtitle)
|
||||
setAudioCodec(info.audio_codec)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch stream info:', err)
|
||||
// Don't block the upload flow if this fails; we'll just assume no video
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || err.message || 'Upload failed')
|
||||
setIsUploading(false)
|
||||
@@ -57,7 +78,7 @@ export const UploadZone: React.FC = () => {
|
||||
setFileSize(0)
|
||||
}
|
||||
},
|
||||
[setFile, setFileName, setFileSize, setIsUploading, setUploadProgress, setError, setTaskId]
|
||||
[setFile, setFileName, setFileSize, setIsUploading, setUploadProgress, setError, setTaskId, setTaskIdStore]
|
||||
)
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
|
||||
Reference in New Issue
Block a user