A simple web video player
  • Python 45.9%
  • JavaScript 21%
  • CSS 17.1%
  • HTML 13%
  • Shell 2.2%
  • Other 0.8%
Find a file
Marcel Leismann efc62fc4ba
All checks were successful
build-and-push / test (push) Successful in 36s
build-and-push / build (push) Successful in 2m6s
Release 0.5.0
2026-05-17 23:01:24 +02:00
.forgejo/workflows Run the CI test job inside the catthehacker/ubuntu:act-latest image (same as the build job) so actions/checkout finds Node, and pin Python 3.13 via actions/setup-python. 2026-04-20 00:34:51 +02:00
accounts initial commit 2026-04-17 02:07:27 +02:00
library Wire up toast auto-dismiss and manual close, scoped to the page AbortController so Turbo navigation cleans up mid-fade. 2026-05-17 23:00:38 +02:00
scripts Promote the [Unreleased] CHANGELOG block to a versioned, dated heading at release time, refuse to tag when it's empty, and auto-insert the heading if missing. 2026-04-18 02:07:09 +02:00
videoz Release 0.5.0 2026-05-17 23:01:24 +02:00
.dockerignore initial commit 2026-04-17 02:07:27 +02:00
.env.example Document the three new VIDEOZ_SCRUB_* knobs in the example env. 2026-04-21 21:20:33 +02:00
.gitignore initial commit 2026-04-17 02:07:27 +02:00
CHANGELOG.md Release 0.5.0 2026-05-17 23:01:24 +02:00
docker-compose.yml Add a Python-driven HEALTHCHECK on the videoz service that hits /healthz and harden both services with read_only: true, tmpfs: /tmp, cap_drop: [ALL], and security_opt: [no-new-privileges:true]. The scanner sidecar mounts /videos read-only since deletes still run in the web container. 2026-04-20 00:25:27 +02:00
Dockerfile Switch gunicorn from sync to gthread (2 workers x 8 threads) with a 600 s timeout so long-lived /stream responses stop being killed by the default 30 s sync-worker timeout. 2026-04-21 23:55:05 +02:00
entrypoint.sh initial commit 2026-04-17 02:07:27 +02:00
LICENSE initial commit 2026-04-17 02:07:27 +02:00
manage.py initial commit 2026-04-17 02:07:27 +02:00
README.md Document the scrub-sprite knobs and the new hover-preview feature under [Unreleased]. 2026-04-21 21:21:01 +02:00
requirements.txt Pin django-axes 8.3.1 for login throttling. 2026-04-19 23:32:46 +02:00
startup.py Stop force-resetting the seeded superuser's password on every restart. The password is now written only on first creation, or when DJANGO_SUPERUSER_FORCE_RESET=1 is explicitly set, so admin-changed passwords stick across container restarts. 2026-04-20 00:25:12 +02:00

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 / ≥15 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, 09).
  • 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, 05 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 /videos read-only.
  • Login throttling via django-axes (5 fails → 1 h lockout); 12-char minimum password with the full Django validator chain.
  • /healthz endpoint backed by SELECT 1 for 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.