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:
2026-08-03 11:26:17 +05:00
parent 1d1f8563c0
commit dcdae41706
11 changed files with 330 additions and 52 deletions
+13 -12
View File
@@ -1,6 +1,6 @@
import React from 'react'
import { ThemeProvider, createTheme, CssBaseline } from '@mui/material'
import { Box, Grid, Button, CircularProgress } from '@mui/material'
import { Box, Grid, Button, CircularProgress, Typography } from '@mui/material'
import { PlayArrow } from '@mui/icons-material'
import { Layout } from './components/Layout'
import { UploadZone } from './components/UploadZone'
@@ -13,6 +13,7 @@ import { useTracklistStore } from './stores/tracklistStore'
import { useOptionsStore } from './stores/optionsStore'
import { useTaskStore } from './stores/taskStore'
import { useUIStore } from './stores/uiStore'
import { useValidationStore } from './stores/validationStore'
import { useWebSocket } from './hooks/useWebSocket'
import { startSplit } from './api/client'
@@ -23,14 +24,14 @@ const App: React.FC = () => {
const { options } = useOptionsStore()
const {
isProcessing,
setTaskId, // <-- Add this
setTaskId,
setError,
setIsProcessing,
addLog,
reset,
} = useTaskStore()
const { formatError } = useValidationStore()
// Connect WebSocket when taskId is available and processing
useWebSocket(taskId && isProcessing ? taskId : null)
const handleSplit = async () => {
@@ -50,7 +51,6 @@ const App: React.FC = () => {
}
try {
// Set taskId in taskStore so DownloadSection can use it
setTaskId(taskId)
setIsProcessing(true)
addLog('🚀 Starting split...')
@@ -68,7 +68,7 @@ const App: React.FC = () => {
reset()
}
const isSplitDisabled = !taskId || !isValid || entries.length === 0 || isProcessing
const isSplitDisabled = !taskId || !isValid || entries.length === 0 || isProcessing || !!formatError
return (
<ThemeProvider
@@ -81,22 +81,15 @@ const App: React.FC = () => {
<CssBaseline />
<Layout>
<Grid container spacing={3}>
{/* Upload Section */}
<Grid item xs={12} md={6}>
<UploadZone />
</Grid>
{/* Tracklist Editor */}
<Grid item xs={12} md={6}>
<TracklistEditor />
</Grid>
{/* Options Panel */}
<Grid item xs={12} md={4}>
<OptionsPanel />
</Grid>
{/* Progress / Split Controls */}
<Grid item xs={12} md={8}>
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<Button
@@ -114,6 +107,14 @@ const App: React.FC = () => {
</Button>
</Box>
{formatError && (
<Box sx={{ mb: 2, p: 2, bgcolor: 'warning.light', borderRadius: 1 }}>
<Typography color="warning.dark" variant="body2">
{formatError}
</Typography>
</Box>
)}
<ProgressDisplay />
<DownloadSection />
</Grid>
+17
View File
@@ -46,3 +46,20 @@ export const getDownloadUrl = (task_id: string): string => {
export const getDownloadZipUrl = (task_id: string): string => {
return `/api/download/${task_id}/splits.zip`
}
// New functions for format validation feature
export const getFormatInfo = async (): Promise<{ formats: Array<{ name: string; audio_only: boolean }> }> => {
const response = await api.get('/formats')
return response.data
}
export const getTaskInfo = async (task_id: string): Promise<{
task_id: string
has_audio: boolean
has_video: boolean
has_subtitle: boolean
audio_codec: string | null
}> => {
const response = await api.get(`/info/${task_id}`)
return response.data
}
+88 -33
View File
@@ -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 })
}
}
+24 -3
View File
@@ -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({
+22 -1
View File
@@ -8,6 +8,11 @@ interface UploadState {
isUploading: boolean
uploadProgress: number
error: string | null
// New fields for stream info
hasVideo: boolean
hasAudio: boolean
hasSubtitle: boolean
audioCodec: string | null
setFile: (file: File | null) => void
setTaskId: (taskId: string | null) => void
@@ -16,6 +21,10 @@ interface UploadState {
setIsUploading: (isUploading: boolean) => void
setUploadProgress: (progress: number) => void
setError: (error: string | null) => void
setHasVideo: (hasVideo: boolean) => void
setHasAudio: (hasAudio: boolean) => void
setHasSubtitle: (hasSubtitle: boolean) => void
setAudioCodec: (audioCodec: string | null) => void
reset: () => void
}
@@ -27,6 +36,10 @@ export const useUploadStore = create<UploadState>((set) => ({
isUploading: false,
uploadProgress: 0,
error: null,
hasVideo: false,
hasAudio: false,
hasSubtitle: false,
audioCodec: null,
setFile: (file) => set({ file }),
setTaskId: (taskId) => set({ taskId }),
@@ -35,6 +48,10 @@ export const useUploadStore = create<UploadState>((set) => ({
setIsUploading: (isUploading) => set({ isUploading }),
setUploadProgress: (uploadProgress) => set({ uploadProgress }),
setError: (error) => set({ error }),
setHasVideo: (hasVideo) => set({ hasVideo }),
setHasAudio: (hasAudio) => set({ hasAudio }),
setHasSubtitle: (hasSubtitle) => set({ hasSubtitle }),
setAudioCodec: (audioCodec) => set({ audioCodec }),
reset: () =>
set({
file: null,
@@ -44,5 +61,9 @@ export const useUploadStore = create<UploadState>((set) => ({
isUploading: false,
uploadProgress: 0,
error: null,
hasVideo: false,
hasAudio: false,
hasSubtitle: false,
audioCodec: null,
}),
}))
}))
@@ -0,0 +1,11 @@
import { create } from 'zustand'
interface ValidationState {
formatError: string | null
setFormatError: (error: string | null) => void
}
export const useValidationStore = create<ValidationState>((set) => ({
formatError: null,
setFormatError: (error) => set({ formatError: error }),
}))