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
+}