- Python 45.9%
- JavaScript 21%
- CSS 17.1%
- HTML 13%
- Shell 2.2%
- Other 0.8%
| .forgejo/workflows | ||
| accounts | ||
| library | ||
| scripts | ||
| videoz | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| CHANGELOG.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| entrypoint.sh | ||
| LICENSE | ||
| manage.py | ||
| README.md | ||
| requirements.txt | ||
| startup.py | ||
videoz
Self-hosted video library with thumbnails, per-user ratings, watch tracking, and a built-in player. Drop a folder of videos onto the host, mount it into the container, and browse it from your phone or laptop.
Features
- Thumbnail grid of every video found under a mounted folder (recursive), with hover-preview slideshow cycling through evenly-spaced frames.
- Filters: All / New / Watched / ≥1–5 stars, plus filename search and pagination. Watched sorts to the bottom; in-progress videos to the top.
- Custom video player UI: pill-grouped controls, click-zone gestures
(single-click play/pause, dbl-click seeks ±15 s on the edges),
scrub bar with buffered range, hover tooltip, and a sprite-sheet
preview thumbnail centred above the bar; picture-in-picture,
stats overlay (
I), full keyboard set (Space/K,J/L,←/→,↑/↓,M,F,P,0–9). - OS media-key integration via the MediaSession API; lock-screen artwork and metadata pulled from the video's thumbnail and filename.
- Per-user resume position, 0–5 star ratings, and watch history.
- Auto-marks watched at a configurable percentage (default 90%); manually toggleable via the Watched / Not-yet-watched badge on the player.
- Hotwire Turbo for SPA-feel navigation (header doesn't flicker between pages); page-loader spinner during Turbo visits.
- Filesystem watcher (
watchdog/inotify) keeps the library in sync in real time, with a full sweep on startup and a 6-hour safety-net rescan. - Rename detection by SHA-1 of the first 8 MiB so renamed files keep their ratings and watch history.
- Tabbed admin area for user management (modal Add-user form) and manual
rescans, serialized through the scanner sidecar via Postgres
LISTEN/NOTIFY. - Hard-delete videos from disk via a custom confirmation modal (staff-only).
- Hardened containers (
read_only,tmpfs:/tmp,cap_drop: [ALL],no-new-privileges); scanner sidecar mounts/videosread-only. - Login throttling via
django-axes(5 fails → 1 h lockout); 12-char minimum password with the full Django validator chain. /healthzendpoint backed bySELECT 1for reverse-proxy and orchestrator probes.- Per-video URLs use a UUID, so the address bar never leaks the on-disk filename.
- Ayu Mirage / Ayu Light themes (header toggle, persisted in
localStorage), JetBrains Mono throughout.
Run with Docker Compose
cp .env.example .env
$EDITOR .env # set passwords + VIDEOZ_HOST_VIDEO_DIR
docker compose pull
docker compose up -d
The host directory you set as VIDEOZ_HOST_VIDEO_DIR is mounted at
/videos inside both the web and scanner containers. Thumbnails and
the SQLite/scan state live in the videoz-data volume at /data.
The videoz-scanner sidecar runs python manage.py run_scanner, which
performs an initial sweep then watches the directory for changes via
inotify. The web container serves HTTP on port 8000 (bound to localhost
by default — front it with your reverse proxy).
Environment
| Variable | Purpose |
|---|---|
DJANGO_SECRET_KEY |
Django session/CSRF secret. |
DJANGO_DEBUG |
false in production. |
DJANGO_ALLOWED_HOSTS |
Comma-separated list (127.0.0.1 and localhost are always included). |
DJANGO_CSRF_TRUSTED_ORIGINS |
Comma-separated, includes scheme. |
POSTGRES_* |
Database connection. Falls back to SQLite at /data/db.sqlite3 if POSTGRES_DB is unset. |
DJANGO_SUPERUSER_USERNAME / _PASSWORD / _EMAIL |
Bootstraps an admin user. The password is written only on first creation; operator changes stick across restarts. Must be ≥12 characters. |
DJANGO_SUPERUSER_FORCE_RESET |
Set to 1 for a one-shot password reset on the next boot. |
VIDEO_ROOT |
Path inside the container for the videos directory (default /videos). |
DATA_DIR |
Path inside the container for thumbnails / SQLite / scan state (default /data). |
SQLITE_PATH |
Override the SQLite file location when not using Postgres. |
VIDEOZ_HOST_VIDEO_DIR |
Host path mounted into /videos by the compose file. |
VIDEOZ_TAG |
Image tag to pull (defaults to latest). |
VIDEOZ_WATCHED_AFTER_PERCENT |
Fraction of duration that auto-marks a video watched (default 0.9, clamped to [0, 1]). |
VIDEOZ_STARTED_AFTER_SECONDS |
Playback position above which a video gets the in-progress badge (default 15). |
VIDEOZ_PREVIEW_FRAMES |
Number of preview frames generated per video for the hover slideshow (default 6). |
VIDEOZ_PREVIEW_HOVER_INTERVAL_MS |
Interval between hover-preview frames in milliseconds (default 600). |
VIDEOZ_SCRUB_INTERVAL_SECONDS |
Gap between frames in the scrub-bar preview sprite (default 15). Smaller = smoother preview, larger sprite. |
VIDEOZ_SCRUB_FRAME_WIDTH |
Pixel width of each frame in the scrub sprite (default 160); height follows aspect ratio. |
VIDEOZ_SCRUB_SPRITE_COLS |
Frames per row in the sprite (default 10); rows scale with video length. |
Development
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
DJANGO_SECRET_KEY=dev VIDEO_ROOT=$PWD/sample DATA_DIR=$PWD/data \
python manage.py migrate
DJANGO_SECRET_KEY=dev VIDEO_ROOT=$PWD/sample DATA_DIR=$PWD/data \
python manage.py runserver
ffmpeg and ffprobe must be on PATH for thumbnails and metadata.
Releases
scripts/release.sh patch # 0.1.0 -> 0.1.1
scripts/release.sh minor # 0.1.0 -> 0.2.0
scripts/release.sh major # 0.1.0 -> 1.0.0
scripts/release.sh 1.4.2 # explicit version
The script bumps videoz/__init__.py, commits, tags, and pushes; the
Forgejo Actions workflow then builds and pushes the image.
License
AGPL-3.0 — see LICENSE.