From e23b0f44ce4401f9e3507227a2a3f36d544fa730 Mon Sep 17 00:00:00 2001 From: Maxim Vershinin Date: Mon, 3 Aug 2026 10:46:01 +0500 Subject: [PATCH] Download section didn't appear. Resolved --- Dockerfile | 2 +- docker-compose.yaml | 8 +- web/backend/Dockerfile | 33 +++---- web/backend/docker-entrypoint.sh | 21 +++++ web/frontend/Dockerfile | 2 +- web/{ => frontend}/nginx/nginx.conf | 1 + web/frontend/src/App.tsx | 22 +++-- web/frontend/src/api/client.ts | 3 +- .../src/components/DownloadSection.tsx | 20 ++-- web/frontend/src/components/OptionsPanel.tsx | 3 +- .../src/components/TracklistEditor.tsx | 1 - web/frontend/src/components/UploadZone.tsx | 8 +- web/frontend/src/hooks/useWebSocket.ts | 94 +++++++++++++++---- web/frontend/src/stores/taskStore.ts | 4 +- web/frontend/src/utils/validators.ts | 1 - web/frontend/tsconfig.json | 6 +- 16 files changed, 157 insertions(+), 72 deletions(-) create mode 100755 web/backend/docker-entrypoint.sh rename web/{ => frontend}/nginx/nginx.conf (96%) diff --git a/Dockerfile b/Dockerfile index 40cb482..6a8b800 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,4 +54,4 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] # Default command (shows help if no arguments provided) -CMD ["--help"] \ No newline at end of file +CMD ["--help"] diff --git a/docker-compose.yaml b/docker-compose.yaml index 56e65a8..bd7a218 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,12 +1,12 @@ services: backend: build: - context: . # Project root (contains audio_splitter/) + context: . dockerfile: web/backend/Dockerfile ports: - "8000:8000" volumes: - - /tmp/audio_splitter_web:/tmp/audio_splitter_web + - /tmp/audio_splitter_web:/tmp/audio_splitter_web # Bind mount environment: - PYTHONUNBUFFERED=1 - DEBUG=1 @@ -15,9 +15,9 @@ services: frontend: build: context: ./web/frontend - dockerfile: Dockerfile.dev + dockerfile: Dockerfile ports: - - "5173:5173" + - "5173:80" volumes: - ./web/frontend:/app - /app/node_modules diff --git a/web/backend/Dockerfile b/web/backend/Dockerfile index 92675a5..ebc3c50 100644 --- a/web/backend/Dockerfile +++ b/web/backend/Dockerfile @@ -1,10 +1,11 @@ FROM python:3.13-slim -# Install FFmpeg and system dependencies +# Install FFmpeg, system dependencies, and gosu from APT RUN apt-get update && \ apt-get install -y --no-install-recommends \ ffmpeg \ ca-certificates \ + gosu \ && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* @@ -16,39 +17,29 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ # Set working directory WORKDIR /app -# ------------------------------------------------------------ -# Copy and install Python dependencies -# ------------------------------------------------------------ +# Copy requirements and install Python dependencies COPY web/backend/requirements-web.txt . RUN pip install --no-cache-dir -r requirements-web.txt -# ------------------------------------------------------------ -# Copy the backend application code -# ------------------------------------------------------------ +# Copy backend code COPY web/backend /app/backend -# ------------------------------------------------------------ -# Copy and install the audio_splitter package -# ------------------------------------------------------------ +# Copy and install audio_splitter package COPY audio_splitter /app/audio_splitter COPY setup.py pyproject.toml README.md /app/ - -# Install audio_splitter as a package RUN pip install --no-cache-dir /app -# ------------------------------------------------------------ # Create a non-root user -# ------------------------------------------------------------ RUN addgroup --system --gid 1000 appgroup && \ adduser --system --uid 1000 --ingroup appgroup appuser && \ chown -R appuser:appgroup /app -USER appuser +# Copy entrypoint script +COPY web/backend/docker-entrypoint.sh /docker-entrypoint.sh +RUN chmod +x /docker-entrypoint.sh -# ------------------------------------------------------------ -# Expose port and start -# ------------------------------------------------------------ +# Set entrypoint +ENTRYPOINT ["/docker-entrypoint.sh"] + +# Expose port EXPOSE 8000 - -# PYTHONPATH is already /app by default because we set WORKDIR /app -CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/web/backend/docker-entrypoint.sh b/web/backend/docker-entrypoint.sh new file mode 100755 index 0000000..e361187 --- /dev/null +++ b/web/backend/docker-entrypoint.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e + +# Detect if we are running as root (default) +if [ "$(id -u)" = "0" ]; then + # Ensure the temp directory exists and set ownership + if [ -d "/tmp/audio_splitter_web" ]; then + echo "Setting ownership of /tmp/audio_splitter_web to appuser:appgroup" + chown -R appuser:appgroup /tmp/audio_splitter_web + else + echo "Creating /tmp/audio_splitter_web and setting ownership" + mkdir -p /tmp/audio_splitter_web + chown -R appuser:appgroup /tmp/audio_splitter_web + fi + + # Drop privileges and run uvicorn using gosu + exec gosu appuser uvicorn backend.main:app --host 0.0.0.0 --port 8000 +else + # If not root, just run directly + exec uvicorn backend.main:app --host 0.0.0.0 --port 8000 +fi diff --git a/web/frontend/Dockerfile b/web/frontend/Dockerfile index e40cb5c..3db19ff 100644 --- a/web/frontend/Dockerfile +++ b/web/frontend/Dockerfile @@ -18,7 +18,7 @@ FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html # Copy nginx configuration -COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf # Expose the port EXPOSE 80 diff --git a/web/nginx/nginx.conf b/web/frontend/nginx/nginx.conf similarity index 96% rename from web/nginx/nginx.conf rename to web/frontend/nginx/nginx.conf index 1cd1f20..8e851cb 100644 --- a/web/nginx/nginx.conf +++ b/web/frontend/nginx/nginx.conf @@ -2,6 +2,7 @@ server { listen 80; server_name _; + client_max_body_size 500M; # Root directory for static files root /usr/share/nginx/html; index index.html; diff --git a/web/frontend/src/App.tsx b/web/frontend/src/App.tsx index e70e399..89b93f9 100644 --- a/web/frontend/src/App.tsx +++ b/web/frontend/src/App.tsx @@ -1,6 +1,6 @@ import React from 'react' import { ThemeProvider, createTheme, CssBaseline } from '@mui/material' -import { Box, Grid, Button, Alert, CircularProgress } from '@mui/material' +import { Box, Grid, Button, CircularProgress } from '@mui/material' import { PlayArrow } from '@mui/icons-material' import { Layout } from './components/Layout' import { UploadZone } from './components/UploadZone' @@ -18,11 +18,17 @@ import { startSplit } from './api/client' const App: React.FC = () => { const { theme } = useUIStore() - const { taskId, file } = useUploadStore() + const { taskId } = useUploadStore() const { entries, isValid } = useTracklistStore() const { options } = useOptionsStore() - const { isProcessing, status, setStatus, setProgress, setMessage, setError, setIsProcessing, addLog, reset } = - useTaskStore() + const { + isProcessing, + setTaskId, // <-- Add this + setError, + setIsProcessing, + addLog, + reset, + } = useTaskStore() // Connect WebSocket when taskId is available and processing useWebSocket(taskId && isProcessing ? taskId : null) @@ -44,6 +50,8 @@ const App: React.FC = () => { } try { + // Set taskId in taskStore so DownloadSection can use it + setTaskId(taskId) setIsProcessing(true) addLog('🚀 Starting split...') @@ -106,8 +114,8 @@ const App: React.FC = () => { - {status && } - {status === 'done' && } + + @@ -115,4 +123,4 @@ const App: React.FC = () => { ) } -export default App \ No newline at end of file +export default App diff --git a/web/frontend/src/api/client.ts b/web/frontend/src/api/client.ts index bf09776..e9375d6 100644 --- a/web/frontend/src/api/client.ts +++ b/web/frontend/src/api/client.ts @@ -1,4 +1,5 @@ import axios from 'axios' +import { TracklistEntry, SplitOptions, TaskStatus } from '../types' export const api = axios.create({ baseURL: '/api', @@ -44,4 +45,4 @@ export const getDownloadUrl = (task_id: string): string => { export const getDownloadZipUrl = (task_id: string): string => { return `/api/download/${task_id}/splits.zip` -} \ No newline at end of file +} diff --git a/web/frontend/src/components/DownloadSection.tsx b/web/frontend/src/components/DownloadSection.tsx index 86d1f60..edcc7ce 100644 --- a/web/frontend/src/components/DownloadSection.tsx +++ b/web/frontend/src/components/DownloadSection.tsx @@ -1,19 +1,25 @@ import React from 'react' -import { Box, Paper, Typography, Button, List, ListItem, ListItemText, IconButton, Divider } from '@mui/material' -import { Download, FileDownload, FolderZip } from '@mui/icons-material' +import { Paper, Typography, Button, List, ListItem, ListItemText, IconButton, Divider } from '@mui/material' +import { Download, FolderZip } from '@mui/icons-material' import { useTaskStore } from '../stores/taskStore' -import { getDownloadUrl, getDownloadZipUrl } from '../api/client' +import { getDownloadZipUrl } from '../api/client' import { formatFileSize } from '../utils/formatters' export const DownloadSection: React.FC = () => { const { taskId, tracks, status } = useTaskStore() - if (status !== 'done' || tracks.length === 0) { + console.log('[DownloadSection] Rendering:', { status, tracks, taskId }) + + // Check if we should show the download section + if (status !== 'done' || !tracks || tracks.length === 0 || !taskId) { return null } + // If we get here, we have tracks + console.log('[DownloadSection] Showing tracks:', tracks) + const handleDownloadZip = () => { - const url = getDownloadZipUrl(taskId!) + const url = getDownloadZipUrl(taskId) window.open(url, '_blank') } @@ -23,7 +29,7 @@ export const DownloadSection: React.FC = () => { } return ( - + 📥 Download Results @@ -64,4 +70,4 @@ export const DownloadSection: React.FC = () => { ) -} \ No newline at end of file +} diff --git a/web/frontend/src/components/OptionsPanel.tsx b/web/frontend/src/components/OptionsPanel.tsx index cd762b8..a10dcdf 100644 --- a/web/frontend/src/components/OptionsPanel.tsx +++ b/web/frontend/src/components/OptionsPanel.tsx @@ -4,7 +4,6 @@ import { Paper, Typography, TextField, - Select, MenuItem, FormControlLabel, Switch, @@ -12,9 +11,11 @@ import { IconButton, Divider, } 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' + interface SectionProps { title: string children: React.ReactNode diff --git a/web/frontend/src/components/TracklistEditor.tsx b/web/frontend/src/components/TracklistEditor.tsx index 38f48f4..d878f45 100644 --- a/web/frontend/src/components/TracklistEditor.tsx +++ b/web/frontend/src/components/TracklistEditor.tsx @@ -4,7 +4,6 @@ import { useDropzone } from 'react-dropzone' import { useTracklistStore } from '../stores/tracklistStore' import { useOptionsStore } from '../stores/optionsStore' import { parseAndValidateTracklist } from '../utils/validators' -import { TracklistEntry } from '../types' export const TracklistEditor: React.FC = () => { const { rawText, errors, isValid, setRawText, setEntries, setErrors, setIsValid } = useTracklistStore() diff --git a/web/frontend/src/components/UploadZone.tsx b/web/frontend/src/components/UploadZone.tsx index 3721846..433dd74 100644 --- a/web/frontend/src/components/UploadZone.tsx +++ b/web/frontend/src/components/UploadZone.tsx @@ -4,7 +4,6 @@ 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 { useTaskStore } from '../stores/taskStore' const ALLOWED_EXTENSIONS = ['.mp3', '.flac', '.wav', '.m4a', '.ogg', '.opus', '.aac', '.wma', '.aiff', '.alac', '.ac3'] @@ -25,8 +24,6 @@ export const UploadZone: React.FC = () => { setTaskId, } = useUploadStore() - const { setTaskId: setTaskIdStore, setIsProcessing } = useTaskStore() - const onDrop = useCallback( async (acceptedFiles: File[]) => { if (acceptedFiles.length === 0) return @@ -49,7 +46,6 @@ export const UploadZone: React.FC = () => { try { const response = await uploadFile(selectedFile) setTaskId(response.task_id) - setTaskIdStore(response.task_id) setUploadProgress(100) setIsUploading(false) } catch (err: any) { @@ -61,7 +57,7 @@ export const UploadZone: React.FC = () => { setFileSize(0) } }, - [setFile, setFileName, setFileSize, setIsUploading, setUploadProgress, setError, setTaskId, setTaskIdStore] + [setFile, setFileName, setFileSize, setIsUploading, setUploadProgress, setError, setTaskId] ) const { getRootProps, getInputProps, isDragActive } = useDropzone({ @@ -140,4 +136,4 @@ export const UploadZone: React.FC = () => { )} ) -} \ No newline at end of file +} diff --git a/web/frontend/src/hooks/useWebSocket.ts b/web/frontend/src/hooks/useWebSocket.ts index 9526505..4332646 100644 --- a/web/frontend/src/hooks/useWebSocket.ts +++ b/web/frontend/src/hooks/useWebSocket.ts @@ -1,11 +1,13 @@ import { useEffect, useRef } from 'react' import { useTaskStore } from '../stores/taskStore' import { useUIStore } from '../stores/uiStore' +import { getStatus } from '../api/client' export const useWebSocket = (taskId: string | null) => { const wsRef = useRef(null) - const reconnectTimeoutRef = useRef(null) + const reconnectTimeoutRef = useRef | null>(null) const reconnectAttempts = useRef(0) + const pollingRef = useRef | null>(null) const { setStatus, @@ -19,6 +21,55 @@ export const useWebSocket = (taskId: string | null) => { } = useTaskStore() const { setWsConnected } = useUIStore() + const pollStatus = async () => { + if (!taskId) return + + try { + const response = await getStatus(taskId) + console.log('[Polling] Status response:', response) + + // Update all state fields together + setStatus(response.status) + setProgress(response.progress) + setMessage(response.message) + + // Explicitly set tracks if present + if (response.tracks && response.tracks.length > 0) { + console.log('[Polling] Setting tracks:', response.tracks) + setTracks(response.tracks) + } + + if (response.status === 'done') { + console.log('[Polling] Split complete, tracks set:', response.tracks) + setIsProcessing(false) + // Ensure tracks are set one more time (safety) + if (response.tracks && response.tracks.length > 0) { + setTracks(response.tracks) + } + return + } + + if (response.status === 'error') { + setError(response.error || 'Split failed') + setIsProcessing(false) + return + } + + // Still processing – poll again + if (pollingRef.current) { + clearTimeout(pollingRef.current) + } + pollingRef.current = setTimeout(pollStatus, 2000) + } catch (error) { + console.error('[Polling] Error:', error) + if (pollingRef.current) { + clearTimeout(pollingRef.current) + } + pollingRef.current = setTimeout(pollStatus, 3000) + } + } + + useEffect(() => { if (!taskId) { if (wsRef.current) { @@ -26,6 +77,11 @@ export const useWebSocket = (taskId: string | null) => { wsRef.current = null } setWsConnected(false) + // Clear polling + if (pollingRef.current) { + clearTimeout(pollingRef.current) + pollingRef.current = null + } return } @@ -41,11 +97,17 @@ export const useWebSocket = (taskId: string | null) => { clearTimeout(reconnectTimeoutRef.current) reconnectTimeoutRef.current = null } + // Start polling when connection is established + // This ensures we get the final status even if WebSocket fails + setTimeout(() => { + pollStatus() + }, 1000) } ws.onmessage = (event) => { try { const data = JSON.parse(event.data) + console.log('[WebSocket] Message:', data) if (data.type === 'status') { const statusData = data.data @@ -58,7 +120,6 @@ export const useWebSocket = (taskId: string | null) => { if (statusData.tracks) { setTracks(statusData.tracks) } - // If status is done or error, stop processing if (statusData.status === 'done' || statusData.status === 'error') { setIsProcessing(false) } @@ -67,13 +128,12 @@ export const useWebSocket = (taskId: string | null) => { setStatus(progressData.status) setProgress(progressData.progress) setMessage(progressData.message) + if (progressData.tracks) { + setTracks(progressData.tracks) + } if (progressData.status === 'done') { addLog('✅ Split complete!') setIsProcessing(false) - // If tracks are included, update them - if (progressData.tracks) { - setTracks(progressData.tracks) - } } else if (progressData.status === 'error') { setError(progressData.message) addLog(`❌ Error: ${progressData.message}`) @@ -83,7 +143,7 @@ export const useWebSocket = (taskId: string | null) => { } } } catch (error) { - console.error('Failed to parse WebSocket message:', error) + console.error('[WebSocket] Failed to parse message:', error) } } @@ -91,18 +151,16 @@ export const useWebSocket = (taskId: string | null) => { console.log(`WebSocket disconnected for task ${taskId}`) setWsConnected(false) - // If task is still in processing state, reconnect - // We'll check via polling if needed - if (reconnectAttempts.current < 5) { - reconnectTimeoutRef.current = setTimeout(() => { - reconnectAttempts.current += 1 - connect() - }, 3000) + // If task is not done and we have a taskId, start polling + // We check the status store to see if it's already done + if (taskId && status !== 'done' && status !== 'error') { + console.log('[WebSocket] Disconnected while processing, starting polling...') + setTimeout(pollStatus, 1000) } } ws.onerror = (error) => { - console.error('WebSocket error:', error) + console.error('[WebSocket] Error:', error) // onclose will handle reconnection } @@ -120,9 +178,13 @@ export const useWebSocket = (taskId: string | null) => { clearTimeout(reconnectTimeoutRef.current) reconnectTimeoutRef.current = null } + if (pollingRef.current) { + clearTimeout(pollingRef.current) + pollingRef.current = null + } setWsConnected(false) } - }, [taskId, setStatus, setProgress, setMessage, setError, setTracks, addLog, setWsConnected, setIsProcessing]) + }, [taskId, setStatus, setProgress, setMessage, setError, setTracks, addLog, setWsConnected, setIsProcessing, status]) return wsRef.current } diff --git a/web/frontend/src/stores/taskStore.ts b/web/frontend/src/stores/taskStore.ts index 205de06..55f2852 100644 --- a/web/frontend/src/stores/taskStore.ts +++ b/web/frontend/src/stores/taskStore.ts @@ -1,5 +1,5 @@ import { create } from 'zustand' -import { TaskStatus, TrackInfo } from '../types' +import { TrackInfo } from '../types' interface TaskState { taskId: string | null @@ -51,4 +51,4 @@ export const useTaskStore = create((set) => ({ logs: [], isProcessing: false, }), -})) \ No newline at end of file +})) diff --git a/web/frontend/src/utils/validators.ts b/web/frontend/src/utils/validators.ts index a901011..539b6dc 100644 --- a/web/frontend/src/utils/validators.ts +++ b/web/frontend/src/utils/validators.ts @@ -1,5 +1,4 @@ // web/frontend/src/utils/validators.ts - import { TracklistEntry } from '../types' import { parseTracklistWithFormat } from './parser' diff --git a/web/frontend/tsconfig.json b/web/frontend/tsconfig.json index d0104ed..17f43b1 100644 --- a/web/frontend/tsconfig.json +++ b/web/frontend/tsconfig.json @@ -12,10 +12,10 @@ "noEmit": true, "jsx": "react-jsx", "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + "noUnusedLocals": false, + "noUnusedParameters": false, "noFallthroughCasesInSwitch": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] -} \ No newline at end of file +}