The PRAGMA journal_mode=WAL returns 'wal' even on CIFS mounts where
WAL shared-memory actually fails. Now we do a concrete test write after
setting WAL mode; if it fails, we close the connection, remove stale
WAL/SHM files, reopen in DELETE mode.
When the data directory is on a CIFS/SMB mount (or other filesystem
lacking mmap shared-memory support), SQLite WAL mode fails with
'locking protocol' or 'readonly database' errors. The init_db function
now detects this and falls back to DELETE journal mode automatically.
Admin Settings:
- Theme section: pick Dark Theme, Light Theme, and Default Mode
- 5 dark options (Cyberpunk/Dark/Midnight/Hacker/Neon)
- 4 light options (Light/Paper/Arctic/Solarized)
- Persisted in SQLite — survives container rebuilds
- Served via /api/config/public so frontend loads admin defaults
Visitor behavior:
- Page loads with admin's chosen default (dark or light theme)
- Sun/moon icon toggles between admin's dark and light pair
- Preference stored in cookie — persists within browser session
- No theme dropdown for visitors — admin controls the pair
Header icon simplified back to clean dark/light toggle
API Key (Sonarr/Radarr style):
- Admin panel → Settings: Generate / Show / Copy / Regenerate / Revoke
- Persisted in SQLite via settings system
- When set, POST /api/downloads requires X-API-Key header or browser origin
- Browser users unaffected (X-Requested-With: XMLHttpRequest auto-sent)
- No key configured = open access (backward compatible)
Container hardening:
- Strip SUID/SGID bits from all binaries in image
- Make /app source directory read-only (only /downloads and /data writable)
Download endpoint:
- New _check_api_access guard on POST /api/downloads
- Timing-safe key comparison via secrets.compare_digest
Version:
- New app/__version__.py with 'dev' fallback for local dev
- Dockerfile injects APP_VERSION build arg from CI tag
- Health endpoint and footer now show actual release version
- Test updated to accept 'dev' in non-Docker environments
File size:
- Capture filesize/filesize_approx from yt-dlp extract_info
- Write to DB via update_job_progress and broadcast via SSE
- New 'Size' column in download table (hidden on mobile)
- formatSize helper: bytes → human-readable (KB/MB/GB)
- Frontend store picks up filesize from SSE events
- New MEDIARIP__ADMIN__PASSWORD env var accepts plaintext password
- Hashed via bcrypt on startup, plaintext cleared from memory immediately
- PASSWORD_HASH still works for backward compatibility (takes precedence)
- Removes the 'docker run python bcrypt' ceremony from setup flow
- Updated README, docker-compose, .env.example to use plaintext
- Wire up get_cookie_path_for_session() in download opts — session
cookies.txt is now passed to yt-dlp as cookiefile when present
- Add YtdlpConfig with extractor_args field, configurable via
config.yaml or MEDIARIP__YTDLP__EXTRACTOR_ARGS env var
(e.g. {"youtube": {"player_client": ["web_safari"]}})
- Inject extractor_args into all three yt-dlp call sites:
_enqueue_single, _extract_info, _extract_url_info
- Enhance 403 error messages with actionable guidance directing
users to upload cookies.txt
- create_session uses INSERT OR IGNORE to handle concurrent requests
with same session cookie (race when multiple requests arrive before
the first INSERT commits)
- Widen actions column 110px→130px to fit 3 action buttons without
overflowing (was causing 4px horizontal scrollbar)
- Widen status column 100px→120px for DOWNLOADING badge breathing room
- Admin enabled by default (was opt-in via env var)
- New /admin/status (public) and /admin/setup (first-run only) endpoints
- Setup endpoint locked after first use (returns 403)
- Admin password persisted to SQLite config table (survives restarts)
- Change password now persists to DB (was in-memory only)
- Frontend router guard forces /admin redirect until setup is complete
- AdminSetup.vue wizard: username + password + confirm
- Public config exposes admin_enabled/admin_setup_complete for frontend
- TLS warning only fires when password is actually configured
Backend:
- New error_log table: url, domain, error, format_id, media_type,
session_id, created_at
- log_download_error() called when yt-dlp throws during download
- GET /admin/errors returns recent entries (limit 200)
- DELETE /admin/errors clears all entries
- Manual purge also clears error log
- Domain extracted from URL via urlparse for grouping
Frontend:
- New 'Errors' tab in admin panel (Sessions, Storage, Errors, Settings)
- Each error entry shows: domain, timestamp, full URL, error message,
format/media type metadata
- Red left border + error-colored message for visual scanning
- Clear Log button to wipe entries
- Empty state: 'No errors logged.'
Error entries contain enough context (full URL, error message, domain,
format, media type) to paste into an LLM for domain-specific debugging.
Settings tab reorganized into 3 sections:
- Appearance & Defaults: welcome message + output formats + Save
- Privacy & Data: privacy mode toggle + manual purge
- Security: change password
Manual purge fix:
- purge_all=True clears ALL completed/failed jobs regardless of age
- Previously only cleared jobs older than max_age_hours (7 days),
so recent downloads were never purged on manual trigger
SSE broadcast for purge:
- Added SSEBroker.publish_all() for cross-session broadcasts
- Purge endpoint sends job_removed events for each deleted job
- Frontend queue clears in real-time when admin purges
Privacy Mode feature:
- Toggle in Admin > Settings enables automatic purge of download
history, session logs, and files after configurable retention period
- Default retention: 24 hours when privacy mode is on
- Configurable 1-8760 hours via number input
- When enabled, starts purge scheduler (every 30 min) if not running
- When disabled, data persists indefinitely
Admin panel consolidation:
- Removed separate 'Purge' tab — manual purge moved to Settings
- Settings tab order: Privacy Mode > Manual Purge > Welcome Message >
Output Formats > Change Password
- Toggle switch UI with accent color and smooth animation
- Retention input with left accent border and unit label
Backend:
- PurgeConfig: added privacy_mode (bool) and privacy_retention_hours
- Purge service: uses privacy_retention_hours when privacy mode active
- PUT /admin/settings: accepts privacy_mode + privacy_retention_hours
- GET /config/public: exposes privacy settings to frontend
- Runtime overrides passed to purge service via config._runtime_overrides