Web interface merge #1
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: . # Project root (contains audio_splitter/)
|
context: .
|
||||||
dockerfile: web/backend/Dockerfile
|
dockerfile: web/backend/Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- /tmp/audio_splitter_web:/tmp/audio_splitter_web
|
- /tmp/audio_splitter_web:/tmp/audio_splitter_web # Bind mount
|
||||||
environment:
|
environment:
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
- DEBUG=1
|
- DEBUG=1
|
||||||
@@ -15,9 +15,9 @@ services:
|
|||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./web/frontend
|
context: ./web/frontend
|
||||||
dockerfile: Dockerfile.dev
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "5173:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./web/frontend:/app
|
- ./web/frontend:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
|
|||||||
+12
-21
@@ -1,10 +1,11 @@
|
|||||||
FROM python:3.13-slim
|
FROM python:3.13-slim
|
||||||
|
|
||||||
# Install FFmpeg and system dependencies
|
# Install FFmpeg, system dependencies, and gosu from APT
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
gosu \
|
||||||
&& \
|
&& \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
@@ -16,39 +17,29 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# Copy requirements and install Python dependencies
|
||||||
# Copy and install Python dependencies
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
COPY web/backend/requirements-web.txt .
|
COPY web/backend/requirements-web.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements-web.txt
|
RUN pip install --no-cache-dir -r requirements-web.txt
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# Copy backend code
|
||||||
# Copy the backend application code
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
COPY web/backend /app/backend
|
COPY web/backend /app/backend
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# Copy and install audio_splitter package
|
||||||
# Copy and install the audio_splitter package
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
COPY audio_splitter /app/audio_splitter
|
COPY audio_splitter /app/audio_splitter
|
||||||
COPY setup.py pyproject.toml README.md /app/
|
COPY setup.py pyproject.toml README.md /app/
|
||||||
|
|
||||||
# Install audio_splitter as a package
|
|
||||||
RUN pip install --no-cache-dir /app
|
RUN pip install --no-cache-dir /app
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# Create a non-root user
|
# Create a non-root user
|
||||||
# ------------------------------------------------------------
|
|
||||||
RUN addgroup --system --gid 1000 appgroup && \
|
RUN addgroup --system --gid 1000 appgroup && \
|
||||||
adduser --system --uid 1000 --ingroup appgroup appuser && \
|
adduser --system --uid 1000 --ingroup appgroup appuser && \
|
||||||
chown -R appuser:appgroup /app
|
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
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# Set entrypoint
|
||||||
# Expose port and start
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
# Expose port
|
||||||
EXPOSE 8000
|
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"]
|
|
||||||
|
|||||||
Executable
+21
@@ -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
|
||||||
@@ -18,7 +18,7 @@ FROM nginx:alpine
|
|||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
# Copy nginx configuration
|
# 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 the port
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
|
client_max_body_size 500M;
|
||||||
# Root directory for static files
|
# Root directory for static files
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ThemeProvider, createTheme, CssBaseline } from '@mui/material'
|
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 { PlayArrow } from '@mui/icons-material'
|
||||||
import { Layout } from './components/Layout'
|
import { Layout } from './components/Layout'
|
||||||
import { UploadZone } from './components/UploadZone'
|
import { UploadZone } from './components/UploadZone'
|
||||||
@@ -18,11 +18,17 @@ import { startSplit } from './api/client'
|
|||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const { theme } = useUIStore()
|
const { theme } = useUIStore()
|
||||||
const { taskId, file } = useUploadStore()
|
const { taskId } = useUploadStore()
|
||||||
const { entries, isValid } = useTracklistStore()
|
const { entries, isValid } = useTracklistStore()
|
||||||
const { options } = useOptionsStore()
|
const { options } = useOptionsStore()
|
||||||
const { isProcessing, status, setStatus, setProgress, setMessage, setError, setIsProcessing, addLog, reset } =
|
const {
|
||||||
useTaskStore()
|
isProcessing,
|
||||||
|
setTaskId, // <-- Add this
|
||||||
|
setError,
|
||||||
|
setIsProcessing,
|
||||||
|
addLog,
|
||||||
|
reset,
|
||||||
|
} = useTaskStore()
|
||||||
|
|
||||||
// Connect WebSocket when taskId is available and processing
|
// Connect WebSocket when taskId is available and processing
|
||||||
useWebSocket(taskId && isProcessing ? taskId : null)
|
useWebSocket(taskId && isProcessing ? taskId : null)
|
||||||
@@ -44,6 +50,8 @@ const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Set taskId in taskStore so DownloadSection can use it
|
||||||
|
setTaskId(taskId)
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
addLog('🚀 Starting split...')
|
addLog('🚀 Starting split...')
|
||||||
|
|
||||||
@@ -106,8 +114,8 @@ const App: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{status && <ProgressDisplay />}
|
<ProgressDisplay />
|
||||||
{status === 'done' && <DownloadSection />}
|
<DownloadSection />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { TracklistEntry, SplitOptions, TaskStatus } from '../types'
|
||||||
|
|
||||||
export const api = axios.create({
|
export const api = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Box, Paper, Typography, Button, List, ListItem, ListItemText, IconButton, Divider } from '@mui/material'
|
import { Paper, Typography, Button, List, ListItem, ListItemText, IconButton, Divider } from '@mui/material'
|
||||||
import { Download, FileDownload, FolderZip } from '@mui/icons-material'
|
import { Download, FolderZip } from '@mui/icons-material'
|
||||||
import { useTaskStore } from '../stores/taskStore'
|
import { useTaskStore } from '../stores/taskStore'
|
||||||
import { getDownloadUrl, getDownloadZipUrl } from '../api/client'
|
import { getDownloadZipUrl } from '../api/client'
|
||||||
import { formatFileSize } from '../utils/formatters'
|
import { formatFileSize } from '../utils/formatters'
|
||||||
|
|
||||||
export const DownloadSection: React.FC = () => {
|
export const DownloadSection: React.FC = () => {
|
||||||
const { taskId, tracks, status } = useTaskStore()
|
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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we get here, we have tracks
|
||||||
|
console.log('[DownloadSection] Showing tracks:', tracks)
|
||||||
|
|
||||||
const handleDownloadZip = () => {
|
const handleDownloadZip = () => {
|
||||||
const url = getDownloadZipUrl(taskId!)
|
const url = getDownloadZipUrl(taskId)
|
||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +29,7 @@ export const DownloadSection: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ p: 3 }}>
|
<Paper sx={{ p: 3, mt: 3 }}>
|
||||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
📥 Download Results
|
📥 Download Results
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Paper,
|
Paper,
|
||||||
Typography,
|
Typography,
|
||||||
TextField,
|
TextField,
|
||||||
Select,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -12,9 +11,11 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Divider,
|
Divider,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
// Select is used internally by TextField with select prop, no need to import
|
||||||
import { ExpandMore, ExpandLess } from '@mui/icons-material'
|
import { ExpandMore, ExpandLess } from '@mui/icons-material'
|
||||||
import { useOptionsStore } from '../stores/optionsStore'
|
import { useOptionsStore } from '../stores/optionsStore'
|
||||||
|
|
||||||
|
|
||||||
interface SectionProps {
|
interface SectionProps {
|
||||||
title: string
|
title: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useDropzone } from 'react-dropzone'
|
|||||||
import { useTracklistStore } from '../stores/tracklistStore'
|
import { useTracklistStore } from '../stores/tracklistStore'
|
||||||
import { useOptionsStore } from '../stores/optionsStore'
|
import { useOptionsStore } from '../stores/optionsStore'
|
||||||
import { parseAndValidateTracklist } from '../utils/validators'
|
import { parseAndValidateTracklist } from '../utils/validators'
|
||||||
import { TracklistEntry } from '../types'
|
|
||||||
|
|
||||||
export const TracklistEditor: React.FC = () => {
|
export const TracklistEditor: React.FC = () => {
|
||||||
const { rawText, errors, isValid, setRawText, setEntries, setErrors, setIsValid } = useTracklistStore()
|
const { rawText, errors, isValid, setRawText, setEntries, setErrors, setIsValid } = useTracklistStore()
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Box, Typography, Paper, LinearProgress, Alert } from '@mui/material'
|
|||||||
import { CloudUpload, InsertDriveFile } from '@mui/icons-material'
|
import { CloudUpload, InsertDriveFile } from '@mui/icons-material'
|
||||||
import { useUploadStore } from '../stores/uploadStore'
|
import { useUploadStore } from '../stores/uploadStore'
|
||||||
import { uploadFile } from '../api/client'
|
import { uploadFile } from '../api/client'
|
||||||
import { useTaskStore } from '../stores/taskStore'
|
|
||||||
|
|
||||||
const ALLOWED_EXTENSIONS = ['.mp3', '.flac', '.wav', '.m4a', '.ogg', '.opus', '.aac', '.wma', '.aiff', '.alac', '.ac3']
|
const ALLOWED_EXTENSIONS = ['.mp3', '.flac', '.wav', '.m4a', '.ogg', '.opus', '.aac', '.wma', '.aiff', '.alac', '.ac3']
|
||||||
|
|
||||||
@@ -25,8 +24,6 @@ export const UploadZone: React.FC = () => {
|
|||||||
setTaskId,
|
setTaskId,
|
||||||
} = useUploadStore()
|
} = useUploadStore()
|
||||||
|
|
||||||
const { setTaskId: setTaskIdStore, setIsProcessing } = useTaskStore()
|
|
||||||
|
|
||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
async (acceptedFiles: File[]) => {
|
async (acceptedFiles: File[]) => {
|
||||||
if (acceptedFiles.length === 0) return
|
if (acceptedFiles.length === 0) return
|
||||||
@@ -49,7 +46,6 @@ export const UploadZone: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const response = await uploadFile(selectedFile)
|
const response = await uploadFile(selectedFile)
|
||||||
setTaskId(response.task_id)
|
setTaskId(response.task_id)
|
||||||
setTaskIdStore(response.task_id)
|
|
||||||
setUploadProgress(100)
|
setUploadProgress(100)
|
||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -61,7 +57,7 @@ export const UploadZone: React.FC = () => {
|
|||||||
setFileSize(0)
|
setFileSize(0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setFile, setFileName, setFileSize, setIsUploading, setUploadProgress, setError, setTaskId, setTaskIdStore]
|
[setFile, setFileName, setFileSize, setIsUploading, setUploadProgress, setError, setTaskId]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useTaskStore } from '../stores/taskStore'
|
import { useTaskStore } from '../stores/taskStore'
|
||||||
import { useUIStore } from '../stores/uiStore'
|
import { useUIStore } from '../stores/uiStore'
|
||||||
|
import { getStatus } from '../api/client'
|
||||||
|
|
||||||
export const useWebSocket = (taskId: string | null) => {
|
export const useWebSocket = (taskId: string | null) => {
|
||||||
const wsRef = useRef<WebSocket | null>(null)
|
const wsRef = useRef<WebSocket | null>(null)
|
||||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const reconnectAttempts = useRef(0)
|
const reconnectAttempts = useRef(0)
|
||||||
|
const pollingRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setStatus,
|
setStatus,
|
||||||
@@ -19,6 +21,55 @@ export const useWebSocket = (taskId: string | null) => {
|
|||||||
} = useTaskStore()
|
} = useTaskStore()
|
||||||
const { setWsConnected } = useUIStore()
|
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(() => {
|
useEffect(() => {
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
if (wsRef.current) {
|
if (wsRef.current) {
|
||||||
@@ -26,6 +77,11 @@ export const useWebSocket = (taskId: string | null) => {
|
|||||||
wsRef.current = null
|
wsRef.current = null
|
||||||
}
|
}
|
||||||
setWsConnected(false)
|
setWsConnected(false)
|
||||||
|
// Clear polling
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearTimeout(pollingRef.current)
|
||||||
|
pollingRef.current = null
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,11 +97,17 @@ export const useWebSocket = (taskId: string | null) => {
|
|||||||
clearTimeout(reconnectTimeoutRef.current)
|
clearTimeout(reconnectTimeoutRef.current)
|
||||||
reconnectTimeoutRef.current = null
|
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) => {
|
ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
|
console.log('[WebSocket] Message:', data)
|
||||||
|
|
||||||
if (data.type === 'status') {
|
if (data.type === 'status') {
|
||||||
const statusData = data.data
|
const statusData = data.data
|
||||||
@@ -58,7 +120,6 @@ export const useWebSocket = (taskId: string | null) => {
|
|||||||
if (statusData.tracks) {
|
if (statusData.tracks) {
|
||||||
setTracks(statusData.tracks)
|
setTracks(statusData.tracks)
|
||||||
}
|
}
|
||||||
// If status is done or error, stop processing
|
|
||||||
if (statusData.status === 'done' || statusData.status === 'error') {
|
if (statusData.status === 'done' || statusData.status === 'error') {
|
||||||
setIsProcessing(false)
|
setIsProcessing(false)
|
||||||
}
|
}
|
||||||
@@ -67,13 +128,12 @@ export const useWebSocket = (taskId: string | null) => {
|
|||||||
setStatus(progressData.status)
|
setStatus(progressData.status)
|
||||||
setProgress(progressData.progress)
|
setProgress(progressData.progress)
|
||||||
setMessage(progressData.message)
|
setMessage(progressData.message)
|
||||||
if (progressData.status === 'done') {
|
|
||||||
addLog('✅ Split complete!')
|
|
||||||
setIsProcessing(false)
|
|
||||||
// If tracks are included, update them
|
|
||||||
if (progressData.tracks) {
|
if (progressData.tracks) {
|
||||||
setTracks(progressData.tracks)
|
setTracks(progressData.tracks)
|
||||||
}
|
}
|
||||||
|
if (progressData.status === 'done') {
|
||||||
|
addLog('✅ Split complete!')
|
||||||
|
setIsProcessing(false)
|
||||||
} else if (progressData.status === 'error') {
|
} else if (progressData.status === 'error') {
|
||||||
setError(progressData.message)
|
setError(progressData.message)
|
||||||
addLog(`❌ Error: ${progressData.message}`)
|
addLog(`❌ Error: ${progressData.message}`)
|
||||||
@@ -83,7 +143,7 @@ export const useWebSocket = (taskId: string | null) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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}`)
|
console.log(`WebSocket disconnected for task ${taskId}`)
|
||||||
setWsConnected(false)
|
setWsConnected(false)
|
||||||
|
|
||||||
// If task is still in processing state, reconnect
|
// If task is not done and we have a taskId, start polling
|
||||||
// We'll check via polling if needed
|
// We check the status store to see if it's already done
|
||||||
if (reconnectAttempts.current < 5) {
|
if (taskId && status !== 'done' && status !== 'error') {
|
||||||
reconnectTimeoutRef.current = setTimeout(() => {
|
console.log('[WebSocket] Disconnected while processing, starting polling...')
|
||||||
reconnectAttempts.current += 1
|
setTimeout(pollStatus, 1000)
|
||||||
connect()
|
|
||||||
}, 3000)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
ws.onerror = (error) => {
|
||||||
console.error('WebSocket error:', error)
|
console.error('[WebSocket] Error:', error)
|
||||||
// onclose will handle reconnection
|
// onclose will handle reconnection
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +178,13 @@ export const useWebSocket = (taskId: string | null) => {
|
|||||||
clearTimeout(reconnectTimeoutRef.current)
|
clearTimeout(reconnectTimeoutRef.current)
|
||||||
reconnectTimeoutRef.current = null
|
reconnectTimeoutRef.current = null
|
||||||
}
|
}
|
||||||
|
if (pollingRef.current) {
|
||||||
|
clearTimeout(pollingRef.current)
|
||||||
|
pollingRef.current = null
|
||||||
|
}
|
||||||
setWsConnected(false)
|
setWsConnected(false)
|
||||||
}
|
}
|
||||||
}, [taskId, setStatus, setProgress, setMessage, setError, setTracks, addLog, setWsConnected, setIsProcessing])
|
}, [taskId, setStatus, setProgress, setMessage, setError, setTracks, addLog, setWsConnected, setIsProcessing, status])
|
||||||
|
|
||||||
return wsRef.current
|
return wsRef.current
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { TaskStatus, TrackInfo } from '../types'
|
import { TrackInfo } from '../types'
|
||||||
|
|
||||||
interface TaskState {
|
interface TaskState {
|
||||||
taskId: string | null
|
taskId: string | null
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// web/frontend/src/utils/validators.ts
|
// web/frontend/src/utils/validators.ts
|
||||||
|
|
||||||
import { TracklistEntry } from '../types'
|
import { TracklistEntry } from '../types'
|
||||||
import { parseTracklistWithFormat } from './parser'
|
import { parseTracklistWithFormat } from './parser'
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": false,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
|
|||||||
Reference in New Issue
Block a user