From 8a1e2a53357acc8523cda319ce4bc369d6867c75 Mon Sep 17 00:00:00 2001 From: Maxim Vershinin Date: Thu, 20 Aug 2026 12:47:58 +0500 Subject: [PATCH 1/3] FIX: aligns line number with the corresponding lines in the tracklist editor --- .../src/components/TracklistEditor.tsx | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/web/frontend/src/components/TracklistEditor.tsx b/web/frontend/src/components/TracklistEditor.tsx index d878f45..3f49bb7 100644 --- a/web/frontend/src/components/TracklistEditor.tsx +++ b/web/frontend/src/components/TracklistEditor.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import { Box, Paper, TextField, Typography, Alert } from '@mui/material' import { useDropzone } from 'react-dropzone' import { useTracklistStore } from '../stores/tracklistStore' @@ -10,6 +10,9 @@ export const TracklistEditor: React.FC = () => { const { options } = useOptionsStore() const [isDragging, setIsDragging] = useState(false) + const textAreaRef = useRef(null) + const lineNumbersRef = useRef(null) + const validate = (text: string) => { const result = parseAndValidateTracklist(text, options.tracklist_format) setEntries(result.entries) @@ -23,10 +26,29 @@ export const TracklistEditor: React.FC = () => { validate(text) } + const handleScroll = useCallback(() => { + if (lineNumbersRef.current && textAreaRef.current) { + lineNumbersRef.current.scrollTop = textAreaRef.current.scrollTop + } + }, []) + + useEffect(() => { + const textArea = textAreaRef.current + if (textArea) { + textArea.addEventListener('scroll', handleScroll) + return () => textArea.removeEventListener('scroll', handleScroll) + } + }, [handleScroll]) + + useEffect(() => { + if (rawText) { + validate(rawText) + } + }, [options.tracklist_format]) + const onDrop = useCallback( (acceptedFiles: File[]) => { if (acceptedFiles.length === 0) return - const file = acceptedFiles[0] const reader = new FileReader() reader.onload = (event) => { @@ -48,13 +70,6 @@ export const TracklistEditor: React.FC = () => { multiple: false, }) - // Re-validate when tracklist format changes - useEffect(() => { - if (rawText) { - validate(rawText) - } - }, [options.tracklist_format]) - const lineCount = rawText.split('\n').filter(line => line.trim() !== '').length return ( @@ -82,12 +97,13 @@ export const TracklistEditor: React.FC = () => { - + {/* Line numbers column */} { textAlign: 'right', userSelect: 'none', overflow: 'hidden', + paddingTop: '8.5px', + paddingBottom: '8.5px', + scrollbarWidth: 'none', + '&::-webkit-scrollbar': { display: 'none' }, + whiteSpace: 'nowrap', // Prevent wrapping of line numbers }} > {rawText.split('\n').map((_, i) => ( @@ -102,7 +123,7 @@ export const TracklistEditor: React.FC = () => { ))} - {/* Editor text area */} + {/* Editor text area – now with horizontal scroll and no wrap */} { onChange={handleTextChange} placeholder={`Enter your tracklist here...\n\nExample (format: ${options.tracklist_format}):\n00:00 Intro\n01:30 Song One - Artist A\n04:20-06:45 Another Song - Artist B`} variant="outlined" + inputRef={textAreaRef} sx={{ '& .MuiInputBase-root': { fontFamily: 'monospace', fontSize: '14px', lineHeight: 1.7, + overflowX: 'auto', // Enable horizontal scroll + }, + '& .MuiInputBase-input': { + paddingTop: '8.5px', + paddingBottom: '8.5px', + whiteSpace: 'nowrap', // Prevent wrapping + overflowX: 'auto', }, }} error={!isValid && errors.length > 0} From 364fc7a8fef22133b0e667fb6e5728fe74854d1d Mon Sep 17 00:00:00 2001 From: Maxim Vershinin Date: Thu, 20 Aug 2026 19:59:11 +0500 Subject: [PATCH 2/3] FEATURE: adds automatic format detected to the frontend --- web/backend/api/info.py | 41 ++++++++++++++++++++-- web/frontend/src/api/client.ts | 5 +++ web/frontend/src/components/UploadZone.tsx | 17 +++++++-- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/web/backend/api/info.py b/web/backend/api/info.py index 261fa80..3c5a609 100644 --- a/web/backend/api/info.py +++ b/web/backend/api/info.py @@ -5,7 +5,12 @@ from fastapi import APIRouter, HTTPException from backend.config import settings from backend.services.file_manager import FileManager from backend.services.task_manager import task_manager -from backend.ffmpeg import get_stream_info + +# Import core functions +from audio_splitter.ffmpeg import get_stream_info, get_container_format, get_audio_codec +from audio_splitter.formats import determine_default_format +from audio_splitter.constants import FORMAT_INFO +from audio_splitter.defaults import DEFAULT_FORMAT router = APIRouter(prefix="/api", tags=["info"]) @@ -13,7 +18,8 @@ router = APIRouter(prefix="/api", tags=["info"]) @router.get("/info/{task_id}") async def get_task_info(task_id: str): """ - Return stream information (has_audio, has_video, has_subtitle) for the uploaded file. + Return stream information (has_audio, has_video, has_subtitle, audio_codec) + for the uploaded file. """ if not task_manager.has_task(task_id): raise HTTPException(status_code=404, detail=f"Task {task_id} not found") @@ -33,3 +39,34 @@ async def get_task_info(task_id: str): } except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to retrieve stream info: {str(e)}") + + +@router.get("/info/recommended-format/{task_id}") +async def get_recommended_format(task_id: str): + """ + Return the recommended output format (container name) for the uploaded file, + based on its container and audio codec. + """ + if not task_manager.has_task(task_id): + raise HTTPException(status_code=404, detail=f"Task {task_id} not found") + + input_path = FileManager.get_input_path(task_id) + if not input_path or not input_path.exists(): + raise HTTPException(status_code=404, detail="Input file not found") + + try: + container = get_container_format(str(input_path)) + codec = get_audio_codec(str(input_path)) + + fmt = determine_default_format(container, codec) + + # Fallback if detection fails or format is unsupported + if fmt is None: + fmt = DEFAULT_FORMAT + if fmt not in FORMAT_INFO: + fmt = "mp3" # ultimate fallback + + return {"format": fmt} + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to determine format: {str(e)}") diff --git a/web/frontend/src/api/client.ts b/web/frontend/src/api/client.ts index 39cb635..8677837 100644 --- a/web/frontend/src/api/client.ts +++ b/web/frontend/src/api/client.ts @@ -63,3 +63,8 @@ export const getTaskInfo = async (task_id: string): Promise<{ const response = await api.get(`/info/${task_id}`) return response.data } + +export const getRecommendedFormat = async (task_id: string): Promise<{ format: string }> => { + const response = await api.get(`/info/recommended-format/${task_id}`) + return response.data +} diff --git a/web/frontend/src/components/UploadZone.tsx b/web/frontend/src/components/UploadZone.tsx index 4432f85..c49c47f 100644 --- a/web/frontend/src/components/UploadZone.tsx +++ b/web/frontend/src/components/UploadZone.tsx @@ -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, getTaskInfo } from '../api/client' +import { useOptionsStore } from '../stores/optionsStore' // NEW +import { uploadFile, getTaskInfo, getRecommendedFormat } from '../api/client' // NEW import { useTaskStore } from '../stores/taskStore' const ALLOWED_EXTENSIONS = ['.mp3', '.flac', '.wav', '.m4a', '.ogg', '.opus', '.aac', '.wma', '.aiff', '.alac', '.ac3'] @@ -30,6 +31,7 @@ export const UploadZone: React.FC = () => { } = useUploadStore() const { setTaskId: setTaskIdStore } = useTaskStore() + const { setOptions } = useOptionsStore() // NEW const onDrop = useCallback( async (acceptedFiles: File[]) => { @@ -65,9 +67,18 @@ export const UploadZone: React.FC = () => { setHasAudio(info.has_audio) setHasSubtitle(info.has_subtitle) setAudioCodec(info.audio_codec) + + // Fetch recommended format and update options + try { + const rec = await getRecommendedFormat(taskId) + // Update only the format; keep other options (e.g., transcode_to) as defaults + setOptions({ format: rec.format }) + } catch (err) { + console.warn('Failed to fetch recommended format, using default', err) + } } 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 + // Don't block upload flow; user can manually change options } } catch (err: any) { setError(err.response?.data?.detail || err.message || 'Upload failed') @@ -78,7 +89,7 @@ export const UploadZone: React.FC = () => { setFileSize(0) } }, - [setFile, setFileName, setFileSize, setIsUploading, setUploadProgress, setError, setTaskId, setTaskIdStore] + [setFile, setFileName, setFileSize, setIsUploading, setUploadProgress, setError, setTaskId, setTaskIdStore, setHasVideo, setHasAudio, setHasSubtitle, setAudioCodec, setOptions] ) const { getRootProps, getInputProps, isDragActive } = useDropzone({ From 4843ac1cb5f29db8517948da7044665779dc708e Mon Sep 17 00:00:00 2001 From: Maxim Vershinin Date: Thu, 20 Aug 2026 20:21:15 +0500 Subject: [PATCH 3/3] FEATURE: clarifes 'format' option in output section of the frontend --- web/frontend/src/components/OptionsPanel.tsx | 27 ++++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/web/frontend/src/components/OptionsPanel.tsx b/web/frontend/src/components/OptionsPanel.tsx index 62e4732..0fd3ef8 100644 --- a/web/frontend/src/components/OptionsPanel.tsx +++ b/web/frontend/src/components/OptionsPanel.tsx @@ -115,32 +115,37 @@ export const OptionsPanel: React.FC = () => {
- MP3 - M4A - MKV - MP4 - OGG - OPUS - FLAC - WAV - AAC + MP3 (.mp3) + M4A (.m4a) + MKV (.mkv) + MP4 (.mp4) + OGG (.ogg) + OPUS (.opus) + FLAC (.flac) + WAV (.wav) + AAC (.aac) + {/* Transcode option – remains unchanged but clearly labeled */} handleChange('transcode_to', e.target.value || undefined)} fullWidth size="small" + helperText="Select an audio codec to re-encode, or keep 'Copy' to preserve the original." > Copy (no transcoding) MP3 (LAME)