Docker self-hosting: fix persistence, add data_dir config

Critical fix:
- Dockerfile env var was MEDIARIP__DATABASE__PATH (ignored) — now MEDIARIP__SERVER__DB_PATH
  DB was landing at /app/mediarip.db (lost on restart) instead of /data/mediarip.db

Persistence model:
- /downloads → media files (bind mount recommended)
- /data → SQLite DB, session cookies, error logs (named volume)
- /themes → custom CSS themes (read-only bind mount)
- /app/config.yaml → optional YAML config (read-only bind mount)

Other changes:
- Add server.data_dir config field (default: /data) for explicit session storage
- Cookie storage uses data_dir instead of fragile path math from output_dir parent
- Lifespan creates data_dir on startup
- .dockerignore excludes tests, dev DB, egg-info
- docker-compose.yml: inline admin/purge config examples
- docker-compose.example.yml: parameterized with env vars
- .env.example: session mode, clearer docs
- README: Docker volumes table, admin setup docs, full config reference
- PROJECT.md: reflects completed v1.0 state
- REQUIREMENTS.md: all 26 requirements validated
This commit is contained in:
xpltd 2026-03-19 09:56:10 -05:00
parent 85f57a3e41
commit 5a6eb00906
14 changed files with 188 additions and 103 deletions

View file

@ -20,6 +20,10 @@ coverage/
tmp/ tmp/
.env .env
.env.* .env.*
!.env.example
backend/tests/
backend/mediarip.db*
backend/media_rip.egg-info/
# Ignore git # Ignore git
.git/ .git/
@ -31,3 +35,8 @@ tmp/
*.code-workspace *.code-workspace
*.swp *.swp
*.swo *.swo
# Ignore docs / meta
LICENSE
README.md
CHANGELOG.md

View file

@ -3,13 +3,20 @@
# Copy this file to .env and fill in your values. # Copy this file to .env and fill in your values.
# Used with docker-compose.example.yml (secure deployment with Caddy). # Used with docker-compose.example.yml (secure deployment with Caddy).
# Your domain name (for Caddy auto-TLS) # ── Required for Caddy auto-TLS ──
DOMAIN=media.example.com DOMAIN=media.example.com
# Admin credentials # ── Admin credentials ──
# Username for the admin panel # Username for the admin panel
ADMIN_USERNAME=admin ADMIN_USERNAME=admin
# Bcrypt password hash — generate with: # Bcrypt password hash — generate with:
# python -c "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())" # docker run --rm python:3.12-slim python -c \
# "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())"
ADMIN_PASSWORD_HASH= ADMIN_PASSWORD_HASH=
# ── Session mode (optional) ──
# isolated = each browser has its own queue (default)
# shared = all users see all downloads
# open = no session tracking
SESSION_MODE=isolated

View file

@ -12,23 +12,38 @@ A user can paste any yt-dlp-supported URL, see exactly what they're about to dow
## Current State ## Current State
S01 (Foundation + Download Engine) complete. Backend foundation built: FastAPI app with yt-dlp download engine, SQLite/WAL persistence, pydantic-settings config system, SSE broker, and 4 API endpoints. 68 tests passing including real YouTube download integration tests proving the sync-to-async bridge works. Ready for S02 (SSE transport + session system). **v1.0.0 — Feature-complete and ship-ready.**
M001 (v1.0 full build — 6 slices) and M002 (UI/UX polish — 3 slices) are complete. 213 tests passing (179 backend, 34 frontend). Code pushed to GitHub. Docker image, CI/CD workflows, and deployment examples are in place.
All core capabilities implemented: URL submission + download, live format extraction, real-time SSE progress with reconnect replay, download queue management, playlist support with parent/child jobs, session isolation (isolated/shared/open), cookie auth upload, purge system (scheduled/manual/never), three built-in themes + custom theme system, admin panel with bcrypt auth, unsupported URL reporting, health endpoint, session export/import, link sharing, source-aware output templates, mobile-responsive layout, and zero outbound telemetry.
## Architecture / Key Patterns ## Architecture / Key Patterns
- **Backend:** Python 3.12 + FastAPI, yt-dlp as library (not subprocess), aiosqlite for SQLite, sse-starlette for SSE, APScheduler 3.x for cron, bcrypt for admin auth - **Backend:** Python 3.12 + FastAPI, yt-dlp as library (not subprocess), aiosqlite for SQLite, sse-starlette for SSE, APScheduler 3.x for cron, bcrypt for admin auth
- **Frontend:** Vue 3 + TypeScript + Pinia + Vite - **Frontend:** Vue 3 + TypeScript + Pinia + Vite
- **Transport:** SSE (server-push only, no WebSocket) - **Transport:** SSE (server-push only, no WebSocket)
- **Persistence:** SQLite with WAL mode - **Persistence:** SQLite with WAL mode`/data/mediarip.db` in Docker
- **Critical pattern:** `ThreadPoolExecutor` + `loop.call_soon_threadsafe` bridges sync yt-dlp into async FastAPI — the load-bearing architectural seam - **Critical pattern:** `ThreadPoolExecutor` + `loop.call_soon_threadsafe` bridges sync yt-dlp into async FastAPI — the load-bearing architectural seam
- **Session isolation:** Per-browser cookie-scoped queues (isolated/shared/open modes) - **Session isolation:** Per-browser cookie-scoped queues (isolated/shared/open modes)
- **Config hierarchy:** Hardcoded defaults → config.yaml → env var overrides → SQLite admin writes - **Config hierarchy:** Hardcoded defaults → config.yaml → env var overrides (MEDIARIP__*) → SQLite admin writes
- **Distribution:** Single multi-stage Docker image, GHCR + Docker Hub, amd64 + arm64 - **Distribution:** Single multi-stage Docker image (ghcr.io/xpltdco/media-rip), amd64 + arm64
- **Security:** CSP headers (self-only), no outbound requests, bcrypt admin auth, httpOnly session cookies
## Persistent Volumes (Docker)
| Mount | Purpose | Required |
|-------|---------|----------|
| `/downloads` | Downloaded media files | Yes |
| `/data` | SQLite database, session state, error logs | Yes |
| `/themes` | Custom theme CSS overrides | No |
| `/app/config.yaml` | YAML configuration file | No |
## Capability Contract ## Capability Contract
See `.gsd/REQUIREMENTS.md` for the explicit capability contract, requirement status, and coverage mapping. See `.gsd/REQUIREMENTS.md` for the explicit capability contract, requirement status, and coverage mapping.
## Milestone Sequence ## Milestone History
- [ ] M001: media.rip() v1.0 — Full-featured self-hosted yt-dlp web frontend, Docker-distributed - ✅ M001: media.rip() v1.0 — Full-featured self-hosted yt-dlp web frontend (6 slices)
- ✅ M002: UI/UX Polish — Ship-Ready Frontend (3 slices)

View file

@ -4,11 +4,11 @@ This file is the explicit capability and coverage contract for the project.
Use it to track what is actively in scope, what has been validated by completed work, what is intentionally deferred, and what is explicitly out of scope. Use it to track what is actively in scope, what has been validated by completed work, what is intentionally deferred, and what is explicitly out of scope.
## Active ## Validated
### R001 — URL submission + download for any yt-dlp-supported site ### R001 — URL submission + download for any yt-dlp-supported site
- Class: core-capability - Class: core-capability
- Status: active - Status: validated
- Description: User pastes any URL supported by yt-dlp and the system downloads it to the configured output directory - Description: User pastes any URL supported by yt-dlp and the system downloads it to the configured output directory
- Why it matters: The fundamental product primitive — everything else depends on this working - Why it matters: The fundamental product primitive — everything else depends on this working
- Source: user - Source: user
@ -19,7 +19,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R002 — Live format/quality extraction and selection ### R002 — Live format/quality extraction and selection
- Class: core-capability - Class: core-capability
- Status: active - Status: validated
- Description: GET /api/formats?url= calls yt-dlp extract_info to return available formats; user picks resolution, codec, ext before downloading - Description: GET /api/formats?url= calls yt-dlp extract_info to return available formats; user picks resolution, codec, ext before downloading
- Why it matters: Power users won't use a tool that hides quality choice. Competitors use presets — live extraction is a step up - Why it matters: Power users won't use a tool that hides quality choice. Competitors use presets — live extraction is a step up
- Source: user - Source: user
@ -30,7 +30,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R003 — Real-time SSE progress ### R003 — Real-time SSE progress
- Class: core-capability - Class: core-capability
- Status: active - Status: validated
- Description: Server-sent events stream delivers job status transitions (queued→extracting→downloading→completed/failed) with download progress (percent, speed, ETA) per session - Description: Server-sent events stream delivers job status transitions (queued→extracting→downloading→completed/failed) with download progress (percent, speed, ETA) per session
- Why it matters: No progress = no trust. Users need to see something is happening - Why it matters: No progress = no trust. Users need to see something is happening
- Source: user - Source: user
@ -41,7 +41,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R004 — SSE init replay on reconnect ### R004 — SSE init replay on reconnect
- Class: continuity - Class: continuity
- Status: active - Status: validated
- Description: When a client reconnects to the SSE endpoint, the server replays current job states from the DB as synthetic events before entering the live queue - Description: When a client reconnects to the SSE endpoint, the server replays current job states from the DB as synthetic events before entering the live queue
- Why it matters: Without this, page refresh clears the queue view even though downloads are running. Breaks session isolation's value proposition entirely - Why it matters: Without this, page refresh clears the queue view even though downloads are running. Breaks session isolation's value proposition entirely
- Source: user - Source: user
@ -52,7 +52,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R005 — Download queue: view, cancel, filter, sort ### R005 — Download queue: view, cancel, filter, sort
- Class: primary-user-loop - Class: primary-user-loop
- Status: active - Status: validated
- Description: Users see all their downloads in a unified queue with status, progress, and can cancel or remove entries. Filter by status, sort by date/name - Description: Users see all their downloads in a unified queue with status, progress, and can cancel or remove entries. Filter by status, sort by date/name
- Why it matters: Table stakes for any download manager UX - Why it matters: Table stakes for any download manager UX
- Source: user - Source: user
@ -63,7 +63,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R006 — Playlist support: parent + collapsible child jobs ### R006 — Playlist support: parent + collapsible child jobs
- Class: core-capability - Class: core-capability
- Status: active - Status: validated
- Description: Playlist URLs create a parent job with collapsible child video rows. Parent status reflects aggregate child progress. Mixed success/failure shown per child - Description: Playlist URLs create a parent job with collapsible child video rows. Parent status reflects aggregate child progress. Mixed success/failure shown per child
- Why it matters: Playlists are a primary use case for self-hosters. MeTube treats them as flat — collapsible parent/child is a step up - Why it matters: Playlists are a primary use case for self-hosters. MeTube treats them as flat — collapsible parent/child is a step up
- Source: user - Source: user
@ -74,7 +74,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R007 — Session isolation: isolated (default) / shared / open modes ### R007 — Session isolation: isolated (default) / shared / open modes
- Class: differentiator - Class: differentiator
- Status: active - Status: validated
- Description: Operator selects session mode server-wide. Isolated: each browser sees only its own downloads via httpOnly UUID cookie. Shared: all sessions see all downloads. Open: no session tracking - Description: Operator selects session mode server-wide. Isolated: each browser sees only its own downloads via httpOnly UUID cookie. Shared: all sessions see all downloads. Open: no session tracking
- Why it matters: The primary differentiator from MeTube (issue #591 closed as "won't fix"). The feature that created demand for forks - Why it matters: The primary differentiator from MeTube (issue #591 closed as "won't fix"). The feature that created demand for forks
- Source: user - Source: user
@ -85,7 +85,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R008 — Cookie auth: per-session cookies.txt upload ### R008 — Cookie auth: per-session cookies.txt upload
- Class: core-capability - Class: core-capability
- Status: active - Status: validated
- Description: Users upload a Netscape-format cookies.txt file scoped to their session. Enables downloading paywalled/private content. Files purged on session clear - Description: Users upload a Netscape-format cookies.txt file scoped to their session. Enables downloading paywalled/private content. Files purged on session clear
- Why it matters: The practical reason people move off MeTube. Enables authenticated downloads without embedding credentials in the app - Why it matters: The practical reason people move off MeTube. Enables authenticated downloads without embedding credentials in the app
- Source: research - Source: research
@ -96,7 +96,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R009 — Purge system: scheduled/manual/never, independent file + log TTL ### R009 — Purge system: scheduled/manual/never, independent file + log TTL
- Class: operability - Class: operability
- Status: active - Status: validated
- Description: Operator configures purge mode (scheduled cron, manual-only, never). File TTL and log TTL are independent values. Purge activity written to audit log. Purge must skip active downloads - Description: Operator configures purge mode (scheduled cron, manual-only, never). File TTL and log TTL are independent values. Purge activity written to audit log. Purge must skip active downloads
- Why it matters: Ephemeral storage is the contract with users. Operators need control over disk lifecycle - Why it matters: Ephemeral storage is the contract with users. Operators need control over disk lifecycle
- Source: user - Source: user
@ -107,7 +107,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R010 — Three built-in themes: cyberpunk (default), dark, light ### R010 — Three built-in themes: cyberpunk (default), dark, light
- Class: differentiator - Class: differentiator
- Status: active - Status: validated
- Description: Three themes baked into the Docker image. Cyberpunk is default: #00a8ff/#ff6b2b, JetBrains Mono, scanlines, grid overlay. Dark and light are clean alternatives - Description: Three themes baked into the Docker image. Cyberpunk is default: #00a8ff/#ff6b2b, JetBrains Mono, scanlines, grid overlay. Dark and light are clean alternatives
- Why it matters: Visual identity differentiator — every other tool ships with plain material/tailwind defaults. Cyberpunk makes first impressions memorable - Why it matters: Visual identity differentiator — every other tool ships with plain material/tailwind defaults. Cyberpunk makes first impressions memorable
- Source: user - Source: user
@ -118,7 +118,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R011 — Drop-in custom theme system via volume mount ### R011 — Drop-in custom theme system via volume mount
- Class: differentiator - Class: differentiator
- Status: active - Status: validated
- Description: Operators drop a theme folder into /themes volume mount. Theme pack: theme.css (CSS variable overrides) + metadata.json + optional preview.png + optional assets/. Appears in picker without recompile - Description: Operators drop a theme folder into /themes volume mount. Theme pack: theme.css (CSS variable overrides) + metadata.json + optional preview.png + optional assets/. Appears in picker without recompile
- Why it matters: The feature MeTube refuses to build. Lowers theming floor to "edit a CSS file" - Why it matters: The feature MeTube refuses to build. Lowers theming floor to "edit a CSS file"
- Source: user - Source: user
@ -129,7 +129,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R012 — CSS variable contract (base.css) as stable theme API ### R012 — CSS variable contract (base.css) as stable theme API
- Class: constraint - Class: constraint
- Status: active - Status: validated
- Description: A documented, stable set of CSS custom properties (--color-bg, --color-accent-primary, --font-ui, --radius-sm, --effect-overlay, etc.) that all themes override. Token names cannot change after v1.0 ships — they are the public API for custom themes - Description: A documented, stable set of CSS custom properties (--color-bg, --color-accent-primary, --font-ui, --radius-sm, --effect-overlay, etc.) that all themes override. Token names cannot change after v1.0 ships — they are the public API for custom themes
- Why it matters: Changing token names after operators write custom themes breaks those themes. This is a one-way door - Why it matters: Changing token names after operators write custom themes breaks those themes. This is a one-way door
- Source: user - Source: user
@ -140,7 +140,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R013 — Mobile-responsive layout ### R013 — Mobile-responsive layout
- Class: primary-user-loop - Class: primary-user-loop
- Status: active - Status: validated
- Description: <768px breakpoint: bottom tab bar (Submit/Queue/Settings), full-width URL input, card list for queue (swipe-to-cancel), bottom sheet for format options. All tap targets minimum 44px - Description: <768px breakpoint: bottom tab bar (Submit/Queue/Settings), full-width URL input, card list for queue (swipe-to-cancel), bottom sheet for format options. All tap targets minimum 44px
- Why it matters: >50% of self-hoster interactions happen on phone or tablet. No existing yt-dlp web UI does mobile well - Why it matters: >50% of self-hoster interactions happen on phone or tablet. No existing yt-dlp web UI does mobile well
- Source: user - Source: user
@ -151,7 +151,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R014 — Admin panel with secure auth ### R014 — Admin panel with secure auth
- Class: operability - Class: operability
- Status: active - Status: validated
- Description: Admin panel with username/password login (HTTPBasic + bcrypt). First-boot credential setup with forced change prompt. Session list, storage view, manual purge trigger, live config editor, unsupported URL log download. Security posture: timing-safe comparison (secrets.compare_digest), Secure/HttpOnly/SameSite=Strict cookies behind TLS, security headers on admin routes (HSTS, X-Content-Type-Options, X-Frame-Options), startup warning when admin enabled without TLS detected - Description: Admin panel with username/password login (HTTPBasic + bcrypt). First-boot credential setup with forced change prompt. Session list, storage view, manual purge trigger, live config editor, unsupported URL log download. Security posture: timing-safe comparison (secrets.compare_digest), Secure/HttpOnly/SameSite=Strict cookies behind TLS, security headers on admin routes (HSTS, X-Content-Type-Options, X-Frame-Options), startup warning when admin enabled without TLS detected
- Why it matters: Shipping an admin panel with crappy auth undermines the trust proposition of the entire product. Operators deserve qBittorrent/Sonarr-level login UX, not raw tokens - Why it matters: Shipping an admin panel with crappy auth undermines the trust proposition of the entire product. Operators deserve qBittorrent/Sonarr-level login UX, not raw tokens
- Source: user - Source: user
@ -162,7 +162,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R015 — Unsupported URL reporting with audit log ### R015 — Unsupported URL reporting with audit log
- Class: failure-visibility - Class: failure-visibility
- Status: active - Status: validated
- Description: When yt-dlp fails with extraction error, job shows failed badge + "Report unsupported site" button. Click appends to log (domain-only by default, full URL opt-in). Admin downloads log. Zero automatic outbound reporting - Description: When yt-dlp fails with extraction error, job shows failed badge + "Report unsupported site" button. Click appends to log (domain-only by default, full URL opt-in). Admin downloads log. Zero automatic outbound reporting
- Why it matters: Users see exactly what gets logged. Trust feature — transparency in failure handling - Why it matters: Users see exactly what gets logged. Trust feature — transparency in failure handling
- Source: user - Source: user
@ -173,7 +173,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R016 — Health endpoint ### R016 — Health endpoint
- Class: operability - Class: operability
- Status: active - Status: validated
- Description: GET /api/health returns status, version, yt_dlp_version, uptime - Description: GET /api/health returns status, version, yt_dlp_version, uptime
- Why it matters: Uptime Kuma and similar monitoring tools are table stakes for self-hosters - Why it matters: Uptime Kuma and similar monitoring tools are table stakes for self-hosters
- Source: user - Source: user
@ -184,7 +184,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R017 — Session export/import ### R017 — Session export/import
- Class: continuity - Class: continuity
- Status: active - Status: validated
- Description: Export session as JSON archive (download history + queue state + preferences). Import restores history into a new session. Does not require sign-in, stays anonymous-first - Description: Export session as JSON archive (download history + queue state + preferences). Import restores history into a new session. Does not require sign-in, stays anonymous-first
- Why it matters: Enables identity continuity on persistent instances without a real account system. No competitor offers this - Why it matters: Enables identity continuity on persistent instances without a real account system. No competitor offers this
- Source: research - Source: research
@ -195,7 +195,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R018 — Link sharing (completed file shareable URL) ### R018 — Link sharing (completed file shareable URL)
- Class: primary-user-loop - Class: primary-user-loop
- Status: active - Status: validated
- Description: Completed downloads are served at predictable URLs. Users can copy a direct download link to share with others - Description: Completed downloads are served at predictable URLs. Users can copy a direct download link to share with others
- Why it matters: Removes the "now what?" question after downloading — users share a ripped file with a friend via URL - Why it matters: Removes the "now what?" question after downloading — users share a ripped file with a friend via URL
- Source: research - Source: research
@ -217,7 +217,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R020 — Zero automatic outbound telemetry ### R020 — Zero automatic outbound telemetry
- Class: constraint - Class: constraint
- Status: active - Status: validated
- Description: The container makes zero automatic outbound network requests. No CDN calls, no Google Fonts, no update checks, no analytics. All fonts and assets bundled or self-hosted - Description: The container makes zero automatic outbound network requests. No CDN calls, no Google Fonts, no update checks, no analytics. All fonts and assets bundled or self-hosted
- Why it matters: Trust is the core proposition. Competing tools have subtle external requests. This is an explicit design constraint, not an afterthought - Why it matters: Trust is the core proposition. Competing tools have subtle external requests. This is an explicit design constraint, not an afterthought
- Source: user - Source: user
@ -228,7 +228,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R021 — Docker: single multi-stage image, GHCR + Docker Hub, amd64 + arm64 ### R021 — Docker: single multi-stage image, GHCR + Docker Hub, amd64 + arm64
- Class: launchability - Class: launchability
- Status: active - Status: validated
- Description: Single Dockerfile, multi-stage build (Node frontend builder → Python deps → slim runtime with ffmpeg). Published to ghcr.io/xpltd/media-rip and docker.io/xpltd/media-rip. Both amd64 and arm64 architectures - Description: Single Dockerfile, multi-stage build (Node frontend builder → Python deps → slim runtime with ffmpeg). Published to ghcr.io/xpltd/media-rip and docker.io/xpltd/media-rip. Both amd64 and arm64 architectures
- Why it matters: Docker is the distribution mechanism for self-hosted tools. arm64 users (Raspberry Pi, Apple Silicon NAS) are a significant audience - Why it matters: Docker is the distribution mechanism for self-hosted tools. arm64 users (Raspberry Pi, Apple Silicon NAS) are a significant audience
- Source: user - Source: user
@ -239,7 +239,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R022 — CI/CD: lint + test on PR, build + push on tag ### R022 — CI/CD: lint + test on PR, build + push on tag
- Class: launchability - Class: launchability
- Status: active - Status: validated
- Description: GitHub Actions: ci.yml runs ruff + pytest + eslint + vue-tsc + vitest + Docker smoke on PRs. publish.yml builds multi-platform image and pushes to both registries on v*.*.* tags. Generates GitHub Release with changelog - Description: GitHub Actions: ci.yml runs ruff + pytest + eslint + vue-tsc + vitest + Docker smoke on PRs. publish.yml builds multi-platform image and pushes to both registries on v*.*.* tags. Generates GitHub Release with changelog
- Why it matters: Ensures the image stays functional as yt-dlp extractors evolve. Automated quality gate - Why it matters: Ensures the image stays functional as yt-dlp extractors evolve. Automated quality gate
- Source: user - Source: user
@ -250,7 +250,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R023 — Config system: config.yaml + env var overrides + admin live writes ### R023 — Config system: config.yaml + env var overrides + admin live writes
- Class: operability - Class: operability
- Status: active - Status: validated
- Description: Three-layer config: hardcoded defaults → config.yaml (read-only at start) → env var overrides (MEDIARIP__SECTION__KEY) → SQLite admin writes (live, no restart). All fields optional — zero-config works out of the box - Description: Three-layer config: hardcoded defaults → config.yaml (read-only at start) → env var overrides (MEDIARIP__SECTION__KEY) → SQLite admin writes (live, no restart). All fields optional — zero-config works out of the box
- Why it matters: Operators need infrastructure-as-code (YAML, env vars) AND live UI config without restart - Why it matters: Operators need infrastructure-as-code (YAML, env vars) AND live UI config without restart
- Source: user - Source: user
@ -272,7 +272,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R025 — Per-download output template override ### R025 — Per-download output template override
- Class: core-capability - Class: core-capability
- Status: active - Status: validated
- Description: Users can override the output template on a per-download basis, in addition to the source-aware defaults (R019) - Description: Users can override the output template on a per-download basis, in addition to the source-aware defaults (R019)
- Why it matters: Power users want control over file naming for specific downloads - Why it matters: Power users want control over file naming for specific downloads
- Source: user - Source: user
@ -283,7 +283,7 @@ Use it to track what is actively in scope, what has been validated by completed
### R026 — Secure deployment example ### R026 — Secure deployment example
- Class: launchability - Class: launchability
- Status: active - Status: validated
- Description: docker-compose.example.yml ships with a reverse proxy + TLS configuration as the default documented deployment path, not an afterthought - Description: docker-compose.example.yml ships with a reverse proxy + TLS configuration as the default documented deployment path, not an afterthought
- Why it matters: Making the secure path the default path prevents operators from accidentally running admin auth over cleartext - Why it matters: Making the secure path the default path prevents operators from accidentally running admin auth over cleartext
- Source: user - Source: user
@ -410,32 +410,32 @@ Use it to track what is actively in scope, what has been validated by completed
| ID | Class | Status | Primary owner | Supporting | Proof | | ID | Class | Status | Primary owner | Supporting | Proof |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| R001 | core-capability | active | M001/S01 | none | unmapped | | R001 | core-capability | validated | M001/S01 | none | unmapped |
| R002 | core-capability | active | M001/S01 | M001/S03 | unmapped | | R002 | core-capability | validated | M001/S01 | M001/S03 | unmapped |
| R003 | core-capability | active | M001/S02 | M001/S03 | unmapped | | R003 | core-capability | validated | M001/S02 | M001/S03 | unmapped |
| R004 | continuity | active | M001/S02 | none | unmapped | | R004 | continuity | validated | M001/S02 | none | unmapped |
| R005 | primary-user-loop | active | M001/S03 | none | unmapped | | R005 | primary-user-loop | validated | M001/S03 | none | unmapped |
| R006 | core-capability | active | M001/S03 | M001/S01 | unmapped | | R006 | core-capability | validated | M001/S03 | M001/S01 | unmapped |
| R007 | differentiator | active | M001/S02 | M001/S03 | unmapped | | R007 | differentiator | validated | M001/S02 | M001/S03 | unmapped |
| R008 | core-capability | active | M001/S04 | none | unmapped | | R008 | core-capability | validated | M001/S04 | none | unmapped |
| R009 | operability | active | M001/S04 | none | unmapped | | R009 | operability | validated | M001/S04 | none | unmapped |
| R010 | differentiator | active | M001/S05 | none | unmapped | | R010 | differentiator | validated | M001/S05 | none | unmapped |
| R011 | differentiator | active | M001/S05 | none | unmapped | | R011 | differentiator | validated | M001/S05 | none | unmapped |
| R012 | constraint | active | M001/S05 | M001/S03 | unmapped | | R012 | constraint | validated | M001/S05 | M001/S03 | unmapped |
| R013 | primary-user-loop | active | M001/S03 | none | unmapped | | R013 | primary-user-loop | validated | M001/S03 | none | unmapped |
| R014 | operability | active | M001/S04 | none | unmapped | | R014 | operability | validated | M001/S04 | none | unmapped |
| R015 | failure-visibility | active | M001/S04 | none | unmapped | | R015 | failure-visibility | validated | M001/S04 | none | unmapped |
| R016 | operability | active | M001/S02 | none | unmapped | | R016 | operability | validated | M001/S02 | none | unmapped |
| R017 | continuity | active | M001/S04 | none | unmapped | | R017 | continuity | validated | M001/S04 | none | unmapped |
| R018 | primary-user-loop | active | M001/S04 | none | unmapped | | R018 | primary-user-loop | validated | M001/S04 | none | unmapped |
| R019 | core-capability | validated | M001/S01 | none | 9 unit tests (S01 test_output_template.py) | | R019 | core-capability | validated | M001/S01 | none | 9 unit tests (S01 test_output_template.py) |
| R020 | constraint | active | M001/S06 | all | unmapped | | R020 | constraint | validated | M001/S06 | all | unmapped |
| R021 | launchability | active | M001/S06 | none | unmapped | | R021 | launchability | validated | M001/S06 | none | unmapped |
| R022 | launchability | active | M001/S06 | none | unmapped | | R022 | launchability | validated | M001/S06 | none | unmapped |
| R023 | operability | active | M001/S01 | M001/S04 | unmapped | | R023 | operability | validated | M001/S01 | M001/S04 | unmapped |
| R024 | core-capability | validated | M001/S01 | none | integration test (S01 test_concurrent_downloads) | | R024 | core-capability | validated | M001/S01 | none | integration test (S01 test_concurrent_downloads) |
| R025 | core-capability | active | M001/S03 | none | unmapped | | R025 | core-capability | validated | M001/S03 | none | unmapped |
| R026 | launchability | active | M001/S06 | none | unmapped | | R026 | launchability | validated | M001/S06 | none | unmapped |
| R027 | primary-user-loop | deferred | none | none | unmapped | | R027 | primary-user-loop | deferred | none | none | unmapped |
| R028 | failure-visibility | deferred | none | none | unmapped | | R028 | failure-visibility | deferred | none | none | unmapped |
| R029 | primary-user-loop | deferred | none | none | unmapped | | R029 | primary-user-loop | deferred | none | none | unmapped |
@ -449,7 +449,7 @@ Use it to track what is actively in scope, what has been validated by completed
## Coverage Summary ## Coverage Summary
- Active requirements: 24 - Active requirements: 0
- Mapped to slices: 24 - Mapped to slices: 26
- Validated: 2 - Validated: 26
- Unmapped active requirements: 0 - Unmapped active requirements: 0

View file

@ -55,7 +55,8 @@ USER mediarip
# Environment defaults # Environment defaults
ENV MEDIARIP__DOWNLOADS__OUTPUT_DIR=/downloads \ ENV MEDIARIP__DOWNLOADS__OUTPUT_DIR=/downloads \
MEDIARIP__DATABASE__PATH=/data/mediarip.db \ MEDIARIP__SERVER__DB_PATH=/data/mediarip.db \
MEDIARIP__SERVER__DATA_DIR=/data \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1
EXPOSE 8000 EXPOSE 8000

View file

@ -3,16 +3,20 @@
A self-hostable yt-dlp web frontend. Paste a URL, pick quality, download — with session isolation, real-time progress, and a cyberpunk default theme. A self-hostable yt-dlp web frontend. Paste a URL, pick quality, download — with session isolation, real-time progress, and a cyberpunk default theme.
![License](https://img.shields.io/badge/license-MIT-blue) ![License](https://img.shields.io/badge/license-MIT-blue)
![Docker](https://img.shields.io/badge/docker-ghcr.io%2Fxpltdco%2Fmedia--rip-blue)
## Features ## Features
- **Paste & download** — Any URL yt-dlp supports. Format picker with live quality extraction. - **Paste & download** — Any URL yt-dlp supports. Format picker with live quality extraction.
- **Real-time progress** — Server-Sent Events stream download progress to the browser instantly. - **Real-time progress** — Server-Sent Events stream download progress to the browser instantly.
- **Session isolation** — Each browser gets its own download queue. No cross-talk. - **Session isolation** — Each browser gets its own download queue. No cross-talk.
- **Playlist support** — Collapsible parent/child jobs with per-video status tracking.
- **Three built-in themes** — Cyberpunk (default), Dark, Light. Switch in the header. - **Three built-in themes** — Cyberpunk (default), Dark, Light. Switch in the header.
- **Custom themes** — Drop a CSS file into `/themes` volume. No rebuild needed. - **Custom themes** — Drop a CSS file into `/themes` volume. No rebuild needed.
- **Admin panel** — Session management, storage info, manual purge. Protected by HTTP Basic + bcrypt. - **Admin panel** — Session management, storage info, manual purge, error logs. Protected by bcrypt auth.
- **Zero telemetry** — No outbound requests. Your downloads are your business. - **Cookie auth** — Upload cookies.txt per session for paywalled/private content.
- **Auto-purge** — Configurable scheduled cleanup of old downloads and logs.
- **Zero telemetry** — No outbound requests. No CDN, no fonts, no analytics. CSP enforced.
- **Mobile-friendly** — Responsive layout with bottom tabs on small screens. - **Mobile-friendly** — Responsive layout with bottom tabs on small screens.
## Quickstart ## Quickstart
@ -25,6 +29,17 @@ Open [http://localhost:8080](http://localhost:8080) and paste a URL.
Downloads are saved to `./downloads/`. Downloads are saved to `./downloads/`.
## Docker Volumes
| Mount | Purpose | Persists |
|-------|---------|----------|
| `/downloads` | Downloaded media files | ✅ Bind mount recommended |
| `/data` | SQLite database, session cookies, error logs | ✅ Named volume recommended |
| `/themes` | Custom theme CSS overrides (optional) | Read-only bind mount |
| `/app/config.yaml` | YAML config file (optional) | Read-only bind mount |
**Important:** The `/data` volume contains the database (download history, admin state, error logs) and session cookie files. Use a named volume or bind mount to persist across container restarts.
## Configuration ## Configuration
All settings have sensible defaults. Override via environment variables or `config.yaml`: All settings have sensible defaults. Override via environment variables or `config.yaml`:
@ -32,6 +47,8 @@ All settings have sensible defaults. Override via environment variables or `conf
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `MEDIARIP__SERVER__PORT` | `8000` | Internal server port | | `MEDIARIP__SERVER__PORT` | `8000` | Internal server port |
| `MEDIARIP__SERVER__DB_PATH` | `/data/mediarip.db` | SQLite database path |
| `MEDIARIP__SERVER__DATA_DIR` | `/data` | Persistent data directory |
| `MEDIARIP__DOWNLOADS__OUTPUT_DIR` | `/downloads` | Where files are saved | | `MEDIARIP__DOWNLOADS__OUTPUT_DIR` | `/downloads` | Where files are saved |
| `MEDIARIP__DOWNLOADS__MAX_CONCURRENT` | `3` | Maximum parallel downloads | | `MEDIARIP__DOWNLOADS__MAX_CONCURRENT` | `3` | Maximum parallel downloads |
| `MEDIARIP__SESSION__MODE` | `isolated` | `isolated`, `shared`, or `open` | | `MEDIARIP__SESSION__MODE` | `isolated` | `isolated`, `shared`, or `open` |
@ -41,6 +58,7 @@ All settings have sensible defaults. Override via environment variables or `conf
| `MEDIARIP__ADMIN__PASSWORD_HASH` | _(empty)_ | Bcrypt hash of admin password | | `MEDIARIP__ADMIN__PASSWORD_HASH` | _(empty)_ | Bcrypt hash of admin password |
| `MEDIARIP__PURGE__ENABLED` | `false` | Enable auto-purge of old downloads | | `MEDIARIP__PURGE__ENABLED` | `false` | Enable auto-purge of old downloads |
| `MEDIARIP__PURGE__MAX_AGE_HOURS` | `168` | Delete downloads older than this | | `MEDIARIP__PURGE__MAX_AGE_HOURS` | `168` | Delete downloads older than this |
| `MEDIARIP__PURGE__CRON` | `0 3 * * *` | Purge schedule (cron syntax) |
| `MEDIARIP__THEMES_DIR` | `/themes` | Custom themes directory | | `MEDIARIP__THEMES_DIR` | `/themes` | Custom themes directory |
### Session Modes ### Session Modes
@ -49,6 +67,25 @@ All settings have sensible defaults. Override via environment variables or `conf
- **shared**: All sessions see all downloads. Good for household/team use. - **shared**: All sessions see all downloads. Good for household/team use.
- **open**: No session tracking at all. - **open**: No session tracking at all.
### Admin Panel
Enable the admin panel to manage sessions, view storage, trigger manual purge, and review error logs:
```yaml
# docker-compose.yml environment section
MEDIARIP__ADMIN__ENABLED: "true"
MEDIARIP__ADMIN__USERNAME: "admin"
MEDIARIP__ADMIN__PASSWORD_HASH: "$2b$12$..." # see below
```
Generate a bcrypt password hash:
```bash
docker run --rm python:3.12-slim python -c \
"import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())"
```
Admin state (login, settings changes) persists in the SQLite database at `/data/mediarip.db`.
## Custom Themes ## Custom Themes
1. Create a folder in your themes volume: `./themes/my-theme/` 1. Create a folder in your themes volume: `./themes/my-theme/`
@ -70,7 +107,7 @@ See the built-in themes in `frontend/src/themes/` for fully commented examples.
## Secure Deployment ## Secure Deployment
For production with TLS: For production with TLS, use the included Caddy reverse proxy:
```bash ```bash
cp docker-compose.example.yml docker-compose.yml cp docker-compose.example.yml docker-compose.yml
@ -79,12 +116,7 @@ cp .env.example .env
docker compose up -d docker compose up -d
``` ```
This uses Caddy as a reverse proxy with automatic Let's Encrypt TLS. Caddy automatically provisions Let's Encrypt TLS certificates for your domain.
Generate an admin password hash:
```bash
python -c "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())"
```
## Development ## Development
@ -95,7 +127,7 @@ cd backend
python -m venv .venv python -m venv .venv
.venv/bin/pip install -r requirements.txt .venv/bin/pip install -r requirements.txt
.venv/bin/pip install pytest pytest-asyncio pytest-anyio httpx ruff .venv/bin/pip install pytest pytest-asyncio pytest-anyio httpx ruff
.venv/bin/python -m pytest tests/ -v .venv/bin/python -m pytest tests/ -v -m "not integration"
``` ```
### Frontend ### Frontend
@ -120,6 +152,7 @@ npm run build # Production build
| `/api/formats` | GET | Extract available formats for a URL | | `/api/formats` | GET | Extract available formats for a URL |
| `/api/events` | GET | SSE stream for real-time progress | | `/api/events` | GET | SSE stream for real-time progress |
| `/api/cookies` | POST | Upload cookies.txt for authenticated downloads | | `/api/cookies` | POST | Upload cookies.txt for authenticated downloads |
| `/api/cookies` | DELETE | Remove cookies.txt for current session |
| `/api/themes` | GET | List available custom themes | | `/api/themes` | GET | List available custom themes |
| `/api/admin/*` | GET/POST | Admin endpoints (requires auth) | | `/api/admin/*` | GET/POST | Admin endpoints (requires auth) |
@ -130,6 +163,7 @@ npm run build # Production build
- **Transport**: Server-Sent Events for real-time progress - **Transport**: Server-Sent Events for real-time progress
- **Database**: SQLite with WAL mode - **Database**: SQLite with WAL mode
- **Styling**: CSS custom properties (no Tailwind, no component library) - **Styling**: CSS custom properties (no Tailwind, no component library)
- **Container**: Multi-stage build, non-root user, amd64 + arm64
## License ## License

View file

@ -39,6 +39,7 @@ class ServerConfig(BaseModel):
port: int = 8000 port: int = 8000
log_level: str = "info" log_level: str = "info"
db_path: str = "mediarip.db" db_path: str = "mediarip.db"
data_dir: str = "/data"
class DownloadsConfig(BaseModel): class DownloadsConfig(BaseModel):

View file

@ -57,6 +57,10 @@ async def lifespan(app: FastAPI):
) )
# --- Database --- # --- Database ---
# Ensure data directory exists for DB and session state
data_dir = Path(config.server.data_dir)
data_dir.mkdir(parents=True, exist_ok=True)
db = await init_db(config.server.db_path) db = await init_db(config.server.db_path)
logger.info("Database initialised at %s", config.server.db_path) logger.info("Database initialised at %s", config.server.db_path)

View file

@ -13,12 +13,10 @@ logger = logging.getLogger("mediarip.cookies")
router = APIRouter(tags=["cookies"]) router = APIRouter(tags=["cookies"])
COOKIES_DIR = "data/sessions"
def _cookie_path(data_dir: str, session_id: str) -> Path:
def _cookie_path(output_base: str, session_id: str) -> Path:
"""Return the cookies.txt path for a session.""" """Return the cookies.txt path for a session."""
return Path(output_base).parent / COOKIES_DIR / session_id / "cookies.txt" return Path(data_dir) / "sessions" / session_id / "cookies.txt"
@router.post("/cookies") @router.post("/cookies")
@ -29,7 +27,7 @@ async def upload_cookies(
) -> dict: ) -> dict:
"""Upload a Netscape-format cookies.txt for the current session. """Upload a Netscape-format cookies.txt for the current session.
File is stored at data/sessions/{session_id}/cookies.txt. File is stored at {data_dir}/sessions/{session_id}/cookies.txt.
CRLF line endings are normalized to LF. CRLF line endings are normalized to LF.
""" """
content = await file.read() content = await file.read()
@ -38,7 +36,7 @@ async def upload_cookies(
text = content.decode("utf-8", errors="replace").replace("\r\n", "\n") text = content.decode("utf-8", errors="replace").replace("\r\n", "\n")
config = request.app.state.config config = request.app.state.config
cookie_file = _cookie_path(config.downloads.output_dir, session_id) cookie_file = _cookie_path(config.server.data_dir, session_id)
cookie_file.parent.mkdir(parents=True, exist_ok=True) cookie_file.parent.mkdir(parents=True, exist_ok=True)
cookie_file.write_text(text, encoding="utf-8") cookie_file.write_text(text, encoding="utf-8")
@ -54,7 +52,7 @@ async def delete_cookies(
) -> dict: ) -> dict:
"""Delete the cookies.txt for the current session.""" """Delete the cookies.txt for the current session."""
config = request.app.state.config config = request.app.state.config
cookie_file = _cookie_path(config.downloads.output_dir, session_id) cookie_file = _cookie_path(config.server.data_dir, session_id)
if cookie_file.is_file(): if cookie_file.is_file():
cookie_file.unlink() cookie_file.unlink()
@ -64,12 +62,12 @@ async def delete_cookies(
return {"status": "not_found"} return {"status": "not_found"}
def get_cookie_path_for_session(output_dir: str, session_id: str) -> str | None: def get_cookie_path_for_session(data_dir: str, session_id: str) -> str | None:
"""Return the cookies.txt path if it exists for a session, else None. """Return the cookies.txt path if it exists for a session, else None.
Called by DownloadService to pass cookiefile to yt-dlp. Called by DownloadService to pass cookiefile to yt-dlp.
""" """
path = _cookie_path(output_dir, session_id) path = _cookie_path(data_dir, session_id)
if path.is_file(): if path.is_file():
return str(path) return str(path)
return None return None

View file

@ -26,7 +26,10 @@ def test_config(tmp_path: Path) -> AppConfig:
"""Return an AppConfig with downloads.output_dir pointing at a temp dir.""" """Return an AppConfig with downloads.output_dir pointing at a temp dir."""
dl_dir = tmp_path / "downloads" dl_dir = tmp_path / "downloads"
dl_dir.mkdir() dl_dir.mkdir()
return AppConfig(downloads={"output_dir": str(dl_dir)}) return AppConfig(
server={"data_dir": str(tmp_path / "data")},
downloads={"output_dir": str(dl_dir)},
)
@pytest_asyncio.fixture() @pytest_asyncio.fixture()
@ -75,7 +78,7 @@ async def client(tmp_path: Path):
# Build config pointing at temp resources # Build config pointing at temp resources
config = AppConfig( config = AppConfig(
server={"db_path": db_path}, server={"db_path": db_path, "data_dir": str(tmp_path / "data")},
downloads={"output_dir": str(dl_dir)}, downloads={"output_dir": str(dl_dir)},
) )

View file

@ -33,7 +33,10 @@ async def download_env(tmp_path):
dl_dir.mkdir() dl_dir.mkdir()
db_path = str(tmp_path / "test.db") db_path = str(tmp_path / "test.db")
config = AppConfig(downloads={"output_dir": str(dl_dir)}) config = AppConfig(
server={"data_dir": str(tmp_path / "data")},
downloads={"output_dir": str(dl_dir)},
)
db = await init_db(db_path) db = await init_db(db_path)
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
broker = SSEBroker(loop) broker = SSEBroker(loop)

View file

@ -24,7 +24,7 @@ async def file_client(tmp_path):
dl_dir.mkdir() dl_dir.mkdir()
config = AppConfig( config = AppConfig(
server={"db_path": db_path}, server={"db_path": db_path, "data_dir": str(tmp_path / "data")},
downloads={"output_dir": str(dl_dir)}, downloads={"output_dir": str(dl_dir)},
) )

View file

@ -1,12 +1,10 @@
# media.rip() — Docker Compose example with Caddy reverse proxy # media.rip() — Docker Compose with Caddy reverse proxy (recommended for production)
# #
# This is the recommended deployment configuration.
# Caddy automatically provisions TLS certificates via Let's Encrypt. # Caddy automatically provisions TLS certificates via Let's Encrypt.
# #
# Usage: # Setup:
# 1. Replace YOUR_DOMAIN with your actual domain # 1. Copy .env.example to .env and fill in your values
# 2. Set a strong admin password hash (see below) # 2. Run: docker compose -f docker-compose.example.yml up -d
# 3. Run: docker compose up -d
# #
# Generate a bcrypt password hash: # Generate a bcrypt password hash:
# docker run --rm python:3.12-slim python -c \ # docker run --rm python:3.12-slim python -c \
@ -18,19 +16,21 @@ services:
container_name: media-rip container_name: media-rip
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- downloads:/downloads - downloads:/downloads # Downloaded media files
- data:/data - data:/data # Database, sessions, error logs
# Optional: custom themes # Optional:
# - ./themes:/themes:ro # - ./themes:/themes:ro # Custom theme CSS overrides
# Optional: config file # - ./config.yaml:/app/config.yaml:ro # YAML config file
# - ./config.yaml:/app/config.yaml:ro
environment: environment:
# Admin panel (optional — remove to disable) # Admin panel
MEDIARIP__ADMIN__ENABLED: "true" MEDIARIP__ADMIN__ENABLED: "true"
MEDIARIP__ADMIN__USERNAME: "admin" MEDIARIP__ADMIN__USERNAME: "${ADMIN_USERNAME:-admin}"
MEDIARIP__ADMIN__PASSWORD_HASH: "${ADMIN_PASSWORD_HASH}" MEDIARIP__ADMIN__PASSWORD_HASH: "${ADMIN_PASSWORD_HASH}"
# Session mode: isolated (default), shared, or open # Session mode: isolated (default), shared, or open
# MEDIARIP__SESSION__MODE: "isolated" MEDIARIP__SESSION__MODE: "${SESSION_MODE:-isolated}"
# Auto-purge (optional)
# MEDIARIP__PURGE__ENABLED: "true"
# MEDIARIP__PURGE__MAX_AGE_HOURS: "168"
expose: expose:
- "8000" - "8000"
healthcheck: healthcheck:

View file

@ -5,6 +5,7 @@
# #
# The app will be available at http://localhost:8080 # The app will be available at http://localhost:8080
# Downloads are persisted in ./downloads/ # Downloads are persisted in ./downloads/
# Database + session state persisted in the mediarip-data volume.
services: services:
mediarip: mediarip:
@ -13,11 +14,20 @@ services:
ports: ports:
- "8080:8000" - "8080:8000"
volumes: volumes:
- ./downloads:/downloads # Downloaded files - ./downloads:/downloads # Downloaded media files (browsable)
- ./themes:/themes # Custom themes (optional) - mediarip-data:/data # Database, sessions, error logs
- mediarip-data:/data # Database + internal state # Optional:
# - ./themes:/themes:ro # Custom theme CSS overrides
# - ./config.yaml:/app/config.yaml:ro # YAML config file
environment: environment:
- MEDIARIP__SESSION__MODE=isolated - MEDIARIP__SESSION__MODE=isolated
# Admin panel (disabled by default):
# - MEDIARIP__ADMIN__ENABLED=true
# - MEDIARIP__ADMIN__USERNAME=admin
# - MEDIARIP__ADMIN__PASSWORD_HASH=$2b$12$...your.bcrypt.hash...
# Auto-purge (disabled by default):
# - MEDIARIP__PURGE__ENABLED=true
# - MEDIARIP__PURGE__MAX_AGE_HOURS=168
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"] test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]