FEATURE: propagate improvements from CLI and web-API versions to web-frontend
This commit is contained in:
@@ -68,16 +68,15 @@ const App: React.FC = () => {
|
|||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSplitDisabled = !taskId || !isValid || entries.length === 0 || isProcessing || !!formatError
|
const isSplitDisabled =
|
||||||
|
!taskId ||
|
||||||
|
!isValid ||
|
||||||
|
entries.length === 0 ||
|
||||||
|
isProcessing ||
|
||||||
|
!!formatError
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider
|
<ThemeProvider theme={createTheme({ palette: { mode: theme } })}>
|
||||||
theme={createTheme({
|
|
||||||
palette: {
|
|
||||||
mode: theme,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Layout>
|
<Layout>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
// web/frontend/src/api/client.ts
|
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { TracklistEntry, SplitOptions, TaskStatus, FormatsResponse } from '../types'
|
import {
|
||||||
|
TracklistEntry,
|
||||||
|
SplitOptions,
|
||||||
|
TaskStatus,
|
||||||
|
FormatsResponse,
|
||||||
|
UploadResponse,
|
||||||
|
SplitResponse,
|
||||||
|
} from '../types'
|
||||||
|
|
||||||
export const api = axios.create({
|
export const api = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
@@ -10,16 +15,14 @@ export const api = axios.create({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const uploadFile = async (file: File): Promise<{ task_id: string; filename: string; size: number }> => {
|
export const uploadFile = async (file: File): Promise<UploadResponse> => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
|
|
||||||
const response = await api.post('/upload', formData, {
|
const response = await api.post('/upload', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,12 +30,32 @@ export const startSplit = async (
|
|||||||
task_id: string,
|
task_id: string,
|
||||||
tracklist: TracklistEntry[],
|
tracklist: TracklistEntry[],
|
||||||
options: SplitOptions
|
options: SplitOptions
|
||||||
): Promise<{ task_id: string; status: string }> => {
|
): Promise<SplitResponse> => {
|
||||||
const response = await api.post('/split', {
|
// Build request payload (only send fields that are not undefined)
|
||||||
|
const payload: any = {
|
||||||
task_id,
|
task_id,
|
||||||
tracklist,
|
tracklist,
|
||||||
options,
|
container: options.container, // null means auto-detect
|
||||||
})
|
audio_codec: options.audio_codec,
|
||||||
|
video_codec: options.video_codec,
|
||||||
|
subtitle_codec: options.subtitle_codec,
|
||||||
|
video_quality: options.video_quality,
|
||||||
|
drop_video: options.drop_video,
|
||||||
|
drop_subs: options.drop_subs,
|
||||||
|
number_tracks: options.number_tracks,
|
||||||
|
replace_bad_chars: options.replace_bad_chars,
|
||||||
|
replacement_char: options.replacement_char,
|
||||||
|
bad_chars: options.bad_chars,
|
||||||
|
skip_existing: options.skip_existing,
|
||||||
|
output_template: options.output_template,
|
||||||
|
album: options.album || null,
|
||||||
|
comment: options.comment || null,
|
||||||
|
no_comment: options.no_comment,
|
||||||
|
comment_stream: options.comment_stream,
|
||||||
|
merge_comments: options.merge_comments,
|
||||||
|
comment_separator: options.comment_separator,
|
||||||
|
}
|
||||||
|
const response = await api.post('/split', payload)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +72,7 @@ export const getDownloadZipUrl = (task_id: string): string => {
|
|||||||
return `/api/download/${task_id}/splits.zip`
|
return `/api/download/${task_id}/splits.zip`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New functions for format validation feature
|
export const getFormats = async (): Promise<FormatsResponse> => {
|
||||||
export const getFormatInfo = async (): Promise<{ formats: Array<{ name: string; audio_only: boolean }> }> => {
|
|
||||||
const response = await api.get('/formats')
|
const response = await api.get('/formats')
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
@@ -70,9 +92,3 @@ export const getRecommendedFormat = async (task_id: string): Promise<{ format: s
|
|||||||
const response = await api.get(`/info/recommended-format/${task_id}`)
|
const response = await api.get(`/info/recommended-format/${task_id}`)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: fetch containers and codecs
|
|
||||||
export const getFormats = async (): Promise<FormatsResponse> => {
|
|
||||||
const response = await api.get('/formats')
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -17,6 +17,7 @@ import { useOptionsStore } from '../stores/optionsStore'
|
|||||||
import { useUploadStore } from '../stores/uploadStore'
|
import { useUploadStore } from '../stores/uploadStore'
|
||||||
import { useValidationStore } from '../stores/validationStore'
|
import { useValidationStore } from '../stores/validationStore'
|
||||||
import { getFormats } from '../api/client'
|
import { getFormats } from '../api/client'
|
||||||
|
import { SplitOptions } from '../types'
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------
|
||||||
// Section component (collapsible)
|
// Section component (collapsible)
|
||||||
@@ -29,7 +30,6 @@ interface SectionProps {
|
|||||||
|
|
||||||
const Section: React.FC<SectionProps> = ({ title, children, defaultExpanded = false }) => {
|
const Section: React.FC<SectionProps> = ({ title, children, defaultExpanded = false }) => {
|
||||||
const [expanded, setExpanded] = React.useState(defaultExpanded)
|
const [expanded, setExpanded] = React.useState(defaultExpanded)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box sx={{ mb: 2 }}>
|
||||||
<Box
|
<Box
|
||||||
@@ -56,7 +56,7 @@ const Section: React.FC<SectionProps> = ({ title, children, defaultExpanded = fa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------
|
||||||
// Main OptionsPanel component
|
// Main OptionsPanel
|
||||||
// ------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------
|
||||||
export const OptionsPanel: React.FC = () => {
|
export const OptionsPanel: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -64,57 +64,103 @@ export const OptionsPanel: React.FC = () => {
|
|||||||
setOptions,
|
setOptions,
|
||||||
containers,
|
containers,
|
||||||
codecs,
|
codecs,
|
||||||
|
compatibility,
|
||||||
setContainers,
|
setContainers,
|
||||||
setCodecs,
|
setCodecs,
|
||||||
|
setCompatibility,
|
||||||
} = useOptionsStore()
|
} = useOptionsStore()
|
||||||
|
|
||||||
const { hasVideo } = useUploadStore()
|
const { hasVideo } = useUploadStore()
|
||||||
const { formatError, setFormatError } = useValidationStore()
|
const { formatError, setFormatError } = useValidationStore()
|
||||||
|
|
||||||
// Fetch containers and codecs from backend on mount
|
// Fetch formats on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchFormats = async () => {
|
const fetchFormats = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getFormats()
|
const data = await getFormats()
|
||||||
setContainers(data.containers)
|
setContainers(data.containers || [])
|
||||||
setCodecs(data.codecs)
|
setCodecs(data.codecs || [])
|
||||||
|
setCompatibility(data.compatibility || {})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch formats:', err)
|
console.error('Failed to fetch formats:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchFormats()
|
fetchFormats()
|
||||||
}, [setContainers, setCodecs])
|
}, [setContainers, setCodecs, setCompatibility])
|
||||||
|
|
||||||
|
// --------------------------------------------------------------
|
||||||
|
// Filter codec options based on selected container
|
||||||
|
// --------------------------------------------------------------
|
||||||
|
const filteredAudioCodecs = useMemo(() => {
|
||||||
|
const entry = compatibility?.[options.container || '']
|
||||||
|
if (!entry) return codecs.filter(c => c.supports_transcoding)
|
||||||
|
const audioList = entry.audio
|
||||||
|
if (audioList === null) return codecs.filter(c => c.supports_transcoding)
|
||||||
|
return codecs.filter(c => c.supports_transcoding && audioList.includes(c.name))
|
||||||
|
}, [compatibility, options.container, codecs])
|
||||||
|
|
||||||
|
const filteredVideoCodecs = useMemo(() => {
|
||||||
|
const entry = compatibility?.[options.container || '']
|
||||||
|
if (!entry) return codecs.filter(c => c.supports_video)
|
||||||
|
const videoList = entry.video
|
||||||
|
if (videoList === null) return codecs.filter(c => c.supports_video)
|
||||||
|
return codecs.filter(c => c.supports_video && videoList.includes(c.name))
|
||||||
|
}, [compatibility, options.container, codecs])
|
||||||
|
|
||||||
|
// --------------------------------------------------------------
|
||||||
|
// Validate compatibility
|
||||||
|
// --------------------------------------------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
const selectedContainer = containers.find(c => c.name === options.container)
|
||||||
|
if (selectedContainer?.audio_only && hasVideo && !options.drop_video) {
|
||||||
|
setFormatError(
|
||||||
|
`Container '${options.container}' does not support video streams. Please enable "Drop video" or choose a container that supports video.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check audio codec compatibility
|
||||||
|
if (options.audio_codec && options.audio_codec !== 'copy' && options.container) {
|
||||||
|
const entry = compatibility?.[options.container]
|
||||||
|
if (entry) {
|
||||||
|
const audioList = entry.audio
|
||||||
|
if (audioList !== null && !audioList.includes(options.audio_codec)) {
|
||||||
|
setFormatError(
|
||||||
|
`Audio codec '${options.audio_codec}' is not supported by container '${options.container}'.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check video codec compatibility
|
||||||
|
if (options.video_codec && options.video_codec !== 'copy' && options.container && hasVideo && !options.drop_video) {
|
||||||
|
const entry = compatibility?.[options.container]
|
||||||
|
if (entry) {
|
||||||
|
const videoList = entry.video
|
||||||
|
if (videoList !== null && !videoList.includes(options.video_codec)) {
|
||||||
|
setFormatError(
|
||||||
|
`Video codec '${options.video_codec}' is not supported by container '${options.container}'.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormatError(null)
|
||||||
|
}, [options.container, options.audio_codec, options.video_codec, options.drop_video, hasVideo, compatibility, containers, setFormatError])
|
||||||
|
|
||||||
// --------------------------------------------------------------
|
// --------------------------------------------------------------
|
||||||
// Handlers
|
// Handlers
|
||||||
// --------------------------------------------------------------
|
// --------------------------------------------------------------
|
||||||
const handleFormatChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (field: keyof SplitOptions, value: any) => {
|
||||||
const newFormat = e.target.value
|
|
||||||
setOptions({ format: newFormat })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDropVideoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const checked = e.target.checked
|
|
||||||
setOptions({ drop_video: checked })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = (field: string, value: any) => {
|
|
||||||
setOptions({ [field]: value })
|
setOptions({ [field]: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------
|
const handleContainerChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
// Format validation – re-run when relevant state changes
|
const value = e.target.value === '' ? null : e.target.value
|
||||||
// --------------------------------------------------------------
|
handleChange('container', value)
|
||||||
useEffect(() => {
|
}
|
||||||
const selectedContainer = containers.find(c => c.name === options.format)
|
|
||||||
if (selectedContainer?.audio_only && hasVideo && !options.drop_video) {
|
|
||||||
setFormatError(
|
|
||||||
`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)
|
|
||||||
}
|
|
||||||
}, [containers, options.format, options.drop_video, hasVideo, setFormatError])
|
|
||||||
|
|
||||||
// --------------------------------------------------------------
|
// --------------------------------------------------------------
|
||||||
// Render
|
// Render
|
||||||
@@ -140,56 +186,92 @@ export const OptionsPanel: React.FC = () => {
|
|||||||
<TextField
|
<TextField
|
||||||
label="Container"
|
label="Container"
|
||||||
select
|
select
|
||||||
value={options.format}
|
value={options.container ?? ''}
|
||||||
onChange={handleFormatChange}
|
onChange={handleContainerChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
error={!!formatError}
|
error={!!formatError}
|
||||||
helperText={
|
helperText={
|
||||||
formatError || "Determines the output file extension and container structure."
|
formatError || "Select a container (auto-detect if empty)."
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{containers.map((container) => (
|
<MenuItem value="">Auto-detect</MenuItem>
|
||||||
<MenuItem key={container.name} value={container.name}>
|
{containers.map((c) => (
|
||||||
{container.name.toUpperCase()} ({container.extension})
|
<MenuItem key={c.name} value={c.name}>
|
||||||
|
{c.name.toUpperCase()} ({c.extension})
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
{/* Audio Codec (Transcode) dropdown */}
|
{/* Audio Codec dropdown */}
|
||||||
<TextField
|
<TextField
|
||||||
label="Audio Codec (Transcode)"
|
label="Audio Codec"
|
||||||
select
|
select
|
||||||
value={options.transcode_to || ''}
|
value={options.audio_codec}
|
||||||
onChange={(e) => handleChange('transcode_to', e.target.value || undefined)}
|
onChange={(e) => handleChange('audio_codec', e.target.value)}
|
||||||
fullWidth
|
fullWidth
|
||||||
size="small"
|
size="small"
|
||||||
helperText="Select an audio codec to re-encode, or keep 'Copy' to preserve the original."
|
helperText="Audio codec (copy = keep original)"
|
||||||
>
|
>
|
||||||
<MenuItem value="">Copy (no transcoding)</MenuItem>
|
<MenuItem value="copy">Copy (original)</MenuItem>
|
||||||
{codecs
|
{filteredAudioCodecs.map((c) => (
|
||||||
.filter(c => c.supports_transcoding)
|
<MenuItem key={c.name} value={c.name}>
|
||||||
.map((codec) => (
|
{c.name.toUpperCase()}
|
||||||
<MenuItem key={codec.name} value={codec.ffmpeg}>
|
</MenuItem>
|
||||||
{codec.name.toUpperCase()} (→ {codec.recommended_container})
|
))}
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|
||||||
{/* Drop video stream (only shown if video present) */}
|
{/* Video Codec dropdown */}
|
||||||
|
<TextField
|
||||||
|
label="Video Codec"
|
||||||
|
select
|
||||||
|
value={options.video_codec}
|
||||||
|
onChange={(e) => handleChange('video_codec', e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
helperText="Video codec (copy = keep original)"
|
||||||
|
disabled={!hasVideo || options.drop_video}
|
||||||
|
>
|
||||||
|
<MenuItem value="copy">Copy (original)</MenuItem>
|
||||||
|
{filteredVideoCodecs.map((c) => (
|
||||||
|
<MenuItem key={c.name} value={c.name}>
|
||||||
|
{c.name.toUpperCase()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
{/* Video Quality */}
|
||||||
|
<TextField
|
||||||
|
label="Video Quality"
|
||||||
|
type="number"
|
||||||
|
value={options.video_quality ?? ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value === '' ? null : parseInt(e.target.value, 10)
|
||||||
|
handleChange('video_quality', val)
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
disabled={!hasVideo || options.drop_video}
|
||||||
|
helperText="Optional quality value (encoder-specific; e.g., 1-31 for libx264, 0-10 for Theora)"
|
||||||
|
InputProps={{
|
||||||
|
inputProps: { min: 0, max: 51, step: 1 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Drop video (if video present) */}
|
||||||
{hasVideo && (
|
{hasVideo && (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
checked={options.drop_video}
|
checked={options.drop_video}
|
||||||
onChange={handleDropVideoChange}
|
onChange={(e) => handleChange('drop_video', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Drop video streams"
|
label="Drop video streams"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Drop subtitle streams */}
|
{/* Drop subtitles */}
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
@@ -293,7 +375,7 @@ export const OptionsPanel: React.FC = () => {
|
|||||||
type="number"
|
type="number"
|
||||||
value={options.comment_stream ?? ''}
|
value={options.comment_stream ?? ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleChange('comment_stream', e.target.value === '' ? null : parseInt(e.target.value))
|
handleChange('comment_stream', e.target.value === '' ? null : parseInt(e.target.value, 10))
|
||||||
}
|
}
|
||||||
size="small"
|
size="small"
|
||||||
disabled={options.no_comment}
|
disabled={options.no_comment}
|
||||||
|
|||||||
@@ -70,9 +70,10 @@ export const UploadZone: React.FC = () => {
|
|||||||
|
|
||||||
// Fetch recommended format and update options
|
// Fetch recommended format and update options
|
||||||
try {
|
try {
|
||||||
|
// Inside UploadZone.tsx, after fetching recommended format:
|
||||||
const rec = await getRecommendedFormat(taskId)
|
const rec = await getRecommendedFormat(taskId)
|
||||||
// Update only the format; keep other options (e.g., transcode_to) as defaults
|
// Update options store with container (not format)
|
||||||
setOptions({ format: rec.format })
|
setOptions({ container: rec.format })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to fetch recommended format, using default', err)
|
console.warn('Failed to fetch recommended format, using default', err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// web/frontend/src/stores/optionsStore.ts
|
|
||||||
|
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { SplitOptions, ContainerInfo, CodecInfo } from '../types'
|
import { SplitOptions, ContainerInfo, CodecInfo } from '../types'
|
||||||
import {
|
import {
|
||||||
@@ -18,24 +16,16 @@ import {
|
|||||||
DEFAULT_NUMBER_TRACKS,
|
DEFAULT_NUMBER_TRACKS,
|
||||||
DEFAULT_REPLACE_BAD_CHARS,
|
DEFAULT_REPLACE_BAD_CHARS,
|
||||||
DEFAULT_SKIP_EXISTING,
|
DEFAULT_SKIP_EXISTING,
|
||||||
DEFAULT_TRANSCODE_TO,
|
|
||||||
DEFAULT_TRACKLIST_FORMAT,
|
DEFAULT_TRACKLIST_FORMAT,
|
||||||
} from '../constants/generated'
|
} from '../constants/generated'
|
||||||
|
|
||||||
// Extend the state
|
// We use DEFAULT_FORMAT only as a fallback; container default is null (auto-detect)
|
||||||
interface OptionsState {
|
|
||||||
options: SplitOptions
|
|
||||||
containers: ContainerInfo[]
|
|
||||||
codecs: CodecInfo[]
|
|
||||||
setOptions: (options: Partial<SplitOptions>) => void
|
|
||||||
setContainers: (containers: ContainerInfo[]) => void
|
|
||||||
setCodecs: (codecs: CodecInfo[]) => void
|
|
||||||
reset: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_OPTIONS: SplitOptions = {
|
const DEFAULT_OPTIONS: SplitOptions = {
|
||||||
format: DEFAULT_FORMAT,
|
container: null,
|
||||||
transcode_to: DEFAULT_TRANSCODE_TO ?? '',
|
audio_codec: 'copy',
|
||||||
|
video_codec: 'copy',
|
||||||
|
subtitle_codec: 'copy',
|
||||||
|
video_quality: null,
|
||||||
drop_video: DEFAULT_DROP_VIDEO,
|
drop_video: DEFAULT_DROP_VIDEO,
|
||||||
drop_subs: DEFAULT_DROP_SUBS,
|
drop_subs: DEFAULT_DROP_SUBS,
|
||||||
number_tracks: DEFAULT_NUMBER_TRACKS,
|
number_tracks: DEFAULT_NUMBER_TRACKS,
|
||||||
@@ -53,20 +43,35 @@ const DEFAULT_OPTIONS: SplitOptions = {
|
|||||||
tracklist_format: DEFAULT_TRACKLIST_FORMAT,
|
tracklist_format: DEFAULT_TRACKLIST_FORMAT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface OptionsState {
|
||||||
|
options: SplitOptions
|
||||||
|
containers: ContainerInfo[]
|
||||||
|
codecs: CodecInfo[]
|
||||||
|
compatibility: Record<string, { audio: string[] | null; video: string[] | null }>
|
||||||
|
setOptions: (newOptions: Partial<SplitOptions>) => void
|
||||||
|
setContainers: (containers: ContainerInfo[]) => void
|
||||||
|
setCodecs: (codecs: CodecInfo[]) => void
|
||||||
|
setCompatibility: (compat: Record<string, { audio: string[] | null; video: string[] | null }>) => void
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
export const useOptionsStore = create<OptionsState>((set) => ({
|
export const useOptionsStore = create<OptionsState>((set) => ({
|
||||||
options: { ...DEFAULT_OPTIONS },
|
options: { ...DEFAULT_OPTIONS },
|
||||||
containers: [],
|
containers: [],
|
||||||
codecs: [],
|
codecs: [],
|
||||||
|
compatibility: {},
|
||||||
setOptions: (newOptions) =>
|
setOptions: (newOptions) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
options: { ...state.options, ...newOptions },
|
options: { ...state.options, ...newOptions },
|
||||||
})),
|
})),
|
||||||
setContainers: (containers) => set({ containers }),
|
setContainers: (containers) => set({ containers }),
|
||||||
setCodecs: (codecs) => set({ codecs }),
|
setCodecs: (codecs) => set({ codecs }),
|
||||||
|
setCompatibility: (compatibility) => set({ compatibility }),
|
||||||
reset: () =>
|
reset: () =>
|
||||||
set({
|
set({
|
||||||
options: { ...DEFAULT_OPTIONS },
|
options: { ...DEFAULT_OPTIONS },
|
||||||
containers: [],
|
containers: [],
|
||||||
codecs: [],
|
codecs: [],
|
||||||
|
compatibility: {},
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ export interface TaskStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SplitOptions {
|
export interface SplitOptions {
|
||||||
format: string
|
container: string | null // null = auto-detect
|
||||||
transcode_to?: string
|
audio_codec: string // 'copy' or encoder name
|
||||||
|
video_codec: string // 'copy' or encoder name
|
||||||
|
subtitle_codec: string // 'copy' or encoder name
|
||||||
|
video_quality: number | null // encoder-specific integer
|
||||||
drop_video: boolean
|
drop_video: boolean
|
||||||
drop_subs: boolean
|
drop_subs: boolean
|
||||||
number_tracks: boolean
|
number_tracks: boolean
|
||||||
@@ -38,7 +41,36 @@ export interface SplitOptions {
|
|||||||
comment_stream: number | null
|
comment_stream: number | null
|
||||||
merge_comments: boolean
|
merge_comments: boolean
|
||||||
comment_separator: string
|
comment_separator: string
|
||||||
tracklist_format: string
|
tracklist_format: string // for frontend parsing
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerInfo {
|
||||||
|
name: string
|
||||||
|
ffmpeg: string
|
||||||
|
extension: string
|
||||||
|
audio_only: boolean
|
||||||
|
supports_video: boolean
|
||||||
|
supports_subs: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodecInfo {
|
||||||
|
name: string
|
||||||
|
ffmpeg: string
|
||||||
|
recommended_container: string
|
||||||
|
recommended_extension: string
|
||||||
|
supports_transcoding: boolean
|
||||||
|
supports_video: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompatibilityEntry {
|
||||||
|
audio: string[] | null
|
||||||
|
video: string[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormatsResponse {
|
||||||
|
containers: ContainerInfo[]
|
||||||
|
codecs: CodecInfo[]
|
||||||
|
compatibility: Record<string, CompatibilityEntry>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadResponse {
|
export interface UploadResponse {
|
||||||
@@ -51,22 +83,3 @@ export interface SplitResponse {
|
|||||||
task_id: string
|
task_id: string
|
||||||
status: string
|
status: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContainerInfo {
|
|
||||||
name: string
|
|
||||||
ffmpeg: string
|
|
||||||
extension: string
|
|
||||||
audio_only: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CodecInfo {
|
|
||||||
name: string
|
|
||||||
ffmpeg: string
|
|
||||||
recommended_container: string
|
|
||||||
supports_transcoding: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FormatsResponse {
|
|
||||||
containers: ContainerInfo[]
|
|
||||||
codecs: CodecInfo[]
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user