Initial Commit

This commit is contained in:
Brian McGonagill 2026-03-09 17:42:26 -05:00
commit e2f743e8bc
11 changed files with 3104 additions and 0 deletions

125
README.md Normal file
View file

@ -0,0 +1,125 @@
# VideoPress — FFmpeg Video Compressor
A web-based video compression tool served via **Gunicorn + gevent** (WSGI),
containerised with **Docker**. FFmpeg compresses video files to approximately
1/3 their original size. All file-system access is restricted to a single
configurable media root for security.
---
## Quick start with Docker (recommended)
```bash
# 1. Build the image
docker build -t videopress .
# 2. Run — replace /your/video/path with the real path on your host
docker run -d \
--name videopress \
--restart unless-stopped \
-p 8080:8080 \
-v /your/video/path:/media \
videopress
# 3. Open http://localhost:8080
```
### With docker compose
```bash
# Edit the volume path in docker-compose.yml (or export the env var):
export MEDIA_HOST_PATH=/your/video/path
docker compose up -d
```
---
## Bare-metal (without Docker)
### Requirements
| Tool | Version |
|------|---------|
| Python | 3.8+ |
| FFmpeg + FFprobe | any recent |
| pip packages | see requirements.txt |
```bash
# Install Python dependencies
pip install -r requirements.txt
# Development server (Flask built-in — not for production)
./start.sh
# Production server (Gunicorn + gevent)
MEDIA_ROOT=/your/video/path ./start.sh --prod
# or on a custom port:
MEDIA_ROOT=/your/video/path ./start.sh --prod 9000
```
---
## Security model
Every API call that accepts a path validates it with `safe_path()` before any
OS operation. `safe_path()` resolves symlinks and asserts the result is inside
`MEDIA_ROOT`. Requests that attempt directory traversal are rejected with
HTTP 403. The container runs as a non-root user (UID 1000).
---
## Architecture
```
Browser ──HTTP──▶ Gunicorn (gevent worker)
├─ GET / → index.html
├─ GET /api/config → {"media_root": ...}
├─ GET /api/browse → directory listing
├─ POST /api/scan → ffprobe metadata
├─ POST /api/compress/start → launch ffmpeg thread
├─ GET /api/compress/progress/<id> ← SSE stream
└─ POST /api/compress/cancel/<id>
```
**Why gevent?** SSE (`/api/compress/progress`) is a long-lived streaming
response. Standard Gunicorn sync workers block for its entire duration.
Gevent workers use cooperative greenlets so a single worker process can
handle many concurrent SSE streams and normal requests simultaneously.
**Why workers=1?** Job state lives in an in-process Python dict. Multiple
worker *processes* would not share it. One gevent worker with many greenlets
is sufficient for this workload. To scale to multiple workers, replace the
in-process job store with Redis.
---
## Environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| `MEDIA_ROOT` | `/media` | Root directory the app may access |
| `PORT` | `8080` | Port Gunicorn listens on |
| `LOG_LEVEL` | `info` | Gunicorn log level |
---
## File layout
```
videocompressor/
├── app.py ← Flask application + all API routes
├── wsgi.py ← Gunicorn entry point (imports app from app.py)
├── gunicorn.conf.py ← Gunicorn configuration (gevent, timeout, logging)
├── requirements.txt ← Python dependencies
├── Dockerfile ← Two-stage Docker build
├── docker-compose.yml ← Volume mapping, port, env vars
├── start.sh ← Helper script (dev + prod modes)
├── README.md
├── templates/
│ └── index.html
└── static/
├── css/main.css
└── js/app.js
```