diff --git a/.env b/.env new file mode 100644 index 0000000..d2fb8ce --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +# Backend +DEBUG=0 +MAX_UPLOAD_SIZE_MB=500 +TEMP_DIR=/tmp/audio_splitter_web +CLEANUP_AFTER_SECONDS=3600 + +# Frontend (development) +VITE_BACKEND_URL=http://localhost:8000 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..56e65a8 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,29 @@ +services: + backend: + build: + context: . # Project root (contains audio_splitter/) + dockerfile: web/backend/Dockerfile + ports: + - "8000:8000" + volumes: + - /tmp/audio_splitter_web:/tmp/audio_splitter_web + environment: + - PYTHONUNBUFFERED=1 + - DEBUG=1 + restart: unless-stopped + + frontend: + build: + context: ./web/frontend + dockerfile: Dockerfile.dev + ports: + - "5173:5173" + volumes: + - ./web/frontend:/app + - /app/node_modules + environment: + - BACKEND_URL=http://backend:8000 + - VITE_BACKEND_URL=http://backend:8000 + depends_on: + - backend + restart: unless-stopped diff --git a/web/backend/.dockerignore b/web/backend/.dockerignore new file mode 100644 index 0000000..514fe0f --- /dev/null +++ b/web/backend/.dockerignore @@ -0,0 +1,18 @@ +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +*.so +*.egg +*.egg-info +dist +build +.venv +venv +.env +.git +.gitignore +README.md +Dockerfile +.dockerignore diff --git a/web/backend/Dockerfile b/web/backend/Dockerfile new file mode 100644 index 0000000..92675a5 --- /dev/null +++ b/web/backend/Dockerfile @@ -0,0 +1,54 @@ +FROM python:3.13-slim + +# Install FFmpeg and system dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + ffmpeg \ + ca-certificates \ + && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Set Python environment variables +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Set working directory +WORKDIR /app + +# ------------------------------------------------------------ +# Copy 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 web/backend /app/backend + +# ------------------------------------------------------------ +# Copy and install the 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 + +# ------------------------------------------------------------ +# Expose port and start +# ------------------------------------------------------------ +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/main.py b/web/backend/main.py index 7834f4b..c4d48b8 100644 --- a/web/backend/main.py +++ b/web/backend/main.py @@ -7,7 +7,7 @@ from fastapi.middleware.cors import CORSMiddleware from backend.config import settings from backend.api import upload, split, status, download from backend.api.websocket import router as websocket_router -from backend.services import progress_publisher # Import the module +from backend.services import progress_publisher app = FastAPI( title="Audio Splitter Web API", @@ -24,9 +24,12 @@ app.add_middleware( allow_headers=["*"], ) -# Store the main event loop in the progress_publisher module -# This avoids a circular import -progress_publisher.MAIN_LOOP = asyncio.get_running_loop() + +@app.on_event("startup") +async def startup_event(): + """Store the main event loop for use in other threads.""" + progress_publisher.MAIN_LOOP = asyncio.get_running_loop() + # Include routers app.include_router(upload.router) diff --git a/web/backend/requirements-web.txt b/web/backend/requirements-web.txt new file mode 100644 index 0000000..2a4fd0a --- /dev/null +++ b/web/backend/requirements-web.txt @@ -0,0 +1,8 @@ +fastapi>=0.104.0 +uvicorn[standard]>=0.24.0 +python-multipart>=0.0.6 +aiofiles>=23.2.0 +pydantic>=2.5.0 +pydantic-settings>=2.0.0 +python-dotenv>=1.0.0 +websockets>=12.0 \ No newline at end of file diff --git a/web/backend/services/progress_publisher.py b/web/backend/services/progress_publisher.py index d9fa45c..8ffb782 100644 --- a/web/backend/services/progress_publisher.py +++ b/web/backend/services/progress_publisher.py @@ -5,7 +5,7 @@ from typing import Dict, Set from fastapi import WebSocket -# This will be set by main.py when the app starts +# This will be set by main.py during startup MAIN_LOOP = None active_connections: Dict[str, Set[WebSocket]] = {} @@ -29,13 +29,19 @@ def publish_progress(task_id: str, progress: int, message: str, status: str = "p } to_remove = set() + # Get the loop to use: either the stored one or try to get the current loop + loop = MAIN_LOOP + if loop is None: + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # No running loop, fallback to default event loop + loop = asyncio.get_event_loop() + for websocket in active_connections.get(task_id, set()): try: - # Use the stored main loop, or fallback to getting the current loop - loop = MAIN_LOOP or asyncio.get_running_loop() asyncio.run_coroutine_threadsafe(websocket.send_json(data), loop) except Exception: - # Client disconnected or other error to_remove.add(websocket) # Clean up disconnected clients diff --git a/web/frontend/.dockerignore b/web/frontend/.dockerignore new file mode 100644 index 0000000..551c85d --- /dev/null +++ b/web/frontend/.dockerignore @@ -0,0 +1,12 @@ +node_modules +dist +.git +.gitignore +*.log +.env +.env.local +.DS_Store +Dockerfile +Dockerfile.dev +.dockerignore +nginx.conf diff --git a/web/frontend/Dockerfile b/web/frontend/Dockerfile new file mode 100644 index 0000000..e40cb5c --- /dev/null +++ b/web/frontend/Dockerfile @@ -0,0 +1,27 @@ +# Stage 1: Build +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json* ./ +RUN npm install + +# Copy the application code and build +COPY . . +RUN npm run build + +# Stage 2: Production (nginx) +FROM nginx:alpine + +# Copy built assets from builder +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose the port +EXPOSE 80 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/web/frontend/Dockerfile.dev b/web/frontend/Dockerfile.dev new file mode 100644 index 0000000..48bfbcd --- /dev/null +++ b/web/frontend/Dockerfile.dev @@ -0,0 +1,17 @@ +FROM node:20-alpine + +# Set working directory +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json* ./ +RUN npm install + +# Copy the application code +COPY . . + +# Expose the port +EXPOSE 5173 + +# Start the development server +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/web/frontend/vite.config.ts b/web/frontend/vite.config.ts index 8f98b99..0d5e171 100644 --- a/web/frontend/vite.config.ts +++ b/web/frontend/vite.config.ts @@ -1,19 +1,24 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +// Backend URL for proxy (default to localhost for local dev) +const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000' + export default defineConfig({ plugins: [react()], server: { proxy: { '/api': { - target: 'http://localhost:8000', + target: BACKEND_URL, changeOrigin: true, + secure: false, }, '/ws': { - target: 'ws://localhost:8000', + target: BACKEND_URL.replace(/^http/, 'ws'), ws: true, changeOrigin: true, + secure: false, }, }, }, -}) \ No newline at end of file +}) diff --git a/web/nginx/nginx.conf b/web/nginx/nginx.conf new file mode 100644 index 0000000..1cd1f20 --- /dev/null +++ b/web/nginx/nginx.conf @@ -0,0 +1,34 @@ +server { + listen 80; + server_name _; + + # Root directory for static files + root /usr/share/nginx/html; + index index.html; + + # Serve React app + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to backend + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Proxy WebSocket requests to backend + location /ws/ { + proxy_pass http://backend:8000/ws/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +}