diff --git a/1.png b/1.png
deleted file mode 100644
index 10b11162..00000000
Binary files a/1.png and /dev/null differ
diff --git a/2.png b/2.png
deleted file mode 100644
index 730a9d07..00000000
Binary files a/2.png and /dev/null differ
diff --git a/3.png b/3.png
deleted file mode 100644
index 800592bb..00000000
Binary files a/3.png and /dev/null differ
diff --git a/4.png b/4.png
deleted file mode 100644
index e6d18010..00000000
Binary files a/4.png and /dev/null differ
diff --git a/5.png b/5.png
deleted file mode 100644
index edd44882..00000000
Binary files a/5.png and /dev/null differ
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9de64782..3c768e1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Fixed
+
+- Modular mode PostgreSQL/Redis connection checks: Replaced raw Python socket checks with native tools (`pg_isready` for PostgreSQL and `socket.create_connection` for Redis) in modular deployment mode to prevent indefinite hangs in Docker environments with non-standard networking or DNS configurations. Now properly supports IPv4 and IPv6 configurations. (Fixes #952) - Thanks [@CodeBormen](https://github.com/CodeBormen)
+
+## [0.19.0] - 2026-02-10
+
### Added
- Add system notifications and update checks
@@ -32,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `useWarningsStore` tests for warning suppression functionality
- Code refactoring for improved readability and maintainability - Thanks [@nick4810](https://github.com/nick4810)
- EPG auto-matching: Added advanced options to strip prefixes, suffixes, and custom text from channel names to assist matching; default matching behavior and settings remain unchanged (Closes #771) - Thanks [@CodeBormen](https://github.com/CodeBormen)
+- Redis authentication support for modular deployments: Added support for authentication when connecting to external Redis instances using either password-only authentication (Redis <6) or username + password authentication (Redis 6+ ACL). REDIS_PASSWORD and REDIS_USER environment variables with URL encoding for special characters. (Closes #937) - Thanks [@CodeBormen](https://github.com/CodeBormen)
- Plugin logos: if a plugin ZIP includes `logo.png`, it is surfaced in the Plugins UI and shown next to the plugin name.
- Plugin manifests (`plugin.json`) for safe metadata discovery, plus legacy warnings and folder-name fallbacks when a manifest is missing.
- Plugin stop hooks: Dispatcharr now calls a plugin's optional `stop()` method (or `run("stop")` action) when disabling, deleting, or reloading plugins to allow graceful shutdown.
diff --git a/README.md b/README.md
index 9b359e25..1efaecb3 100644
--- a/README.md
+++ b/README.md
@@ -1,61 +1,87 @@
# π¬ Dispatcharr β Your Ultimate IPTV & Stream Management Companion
+
-
+
---
## π What is Dispatcharr?
-Dispatcharr is an **open-source powerhouse** for managing IPTV streams and EPG data with elegance and control.\
+Dispatcharr is an **open-source powerhouse** for managing IPTV streams, EPG data, and VOD content with elegance and control.\
Born from necessity and built with passion, it started as a personal project by **[OkinawaBoss](https://github.com/OkinawaBoss)** and evolved with contributions from legends like **[dekzter](https://github.com/dekzter)**, **[SergeantPanda](https://github.com/SergeantPanda)** and **Bucatini**.
-> Think of Dispatcharr as the \*arr familyβs IPTV cousin β simple, smart, and designed for streamers who want reliability and flexibility.
+> Think of Dispatcharr as the \*arr family's IPTV cousin β simple, smart, and designed for streamers who want reliability and flexibility.
---
-## π§ͺ Whatβs New in Beta
+## π― What Can I Do With Dispatcharr?
-Dispatcharr has officially entered **BETA**, bringing powerful new features and improvements across the board:
+Dispatcharr empowers you with complete IPTV control. Here are some real-world scenarios:
-β¨ **Proxy Streaming Engine** β Optimize bandwidth, reduce provider connections, and increase stream reliability\
-π **Real-Time Stats Dashboard** β Live insights into stream health and client activity\
-π§ **EPG Auto-Match** β Match program data to channels automatically\
-βοΈ **Streamlink + FFmpeg Support** β Flexible backend options for streaming and recording\
-π¬ **VOD Management** β Full Video on Demand support with movies and TV series\
-π§Ό **UI & UX Enhancements** β Smoother, faster, more responsive interface\
-π **Output Compatibility** β HDHomeRun, M3U, and XMLTV EPG support for Plex, Jellyfin, and more
+π‘ **Consolidate Multiple IPTV Sources**\
+Combine streams from multiple providers into a single interface. Manage, filter, and organize thousands of channels with ease.
+
+πΊ **Integrate with Media Centers**\
+Use HDHomeRun emulation to add virtual tuners to **Plex**, **Emby**, or **Jellyfin**. They'll discover Dispatcharr as a live TV source and can record programs directly to their own DVR libraries.
+
+π‘ **Create a Personal TV Ecosystem**\
+Merge live TV channels with custom EPG guides. Generate XMLTV schedules or use auto-matching to align channels with existing program data. Export as M3U, Xtream Codes API, or HDHomeRun device.
+
+π§ **Transcode & Optimize Streams**\
+Configure output profiles with FFmpeg transcoding to optimize streams for different clients β reduce bandwidth, standardize formats, or add audio normalization.
+
+π **Centralize VPN Access**\
+Run Dispatcharr through a VPN container (like Gluetun) so all streams route through a single VPN connection. Your clients access geo-blocked content without needing individual VPNs, reducing bandwidth overhead and simplifying network management.
+
+π **Monitor & Manage in Real-Time**\
+Track active streams, client connections, and bandwidth usage with live statistics. Monitor buffering events and stream quality. Automatic failover keeps viewers connected when streams failβseamlessly switching to backup sources without interruption.
+
+π₯ **Share Access Safely**\
+Create multiple user accounts with granular permissions. Share streams via M3U playlists or Xtream Codes API while controlling which users access which channels, profiles, or features. Network-based access restrictions available for additional security.
+
+π **Extend with Plugins**\
+Build custom integrations using Dispatcharr's robust plugin system. Automate tasks, connect to external services, or add entirely new workflows.
---
## β¨ Why You'll Love Dispatcharr
-β **Full IPTV Control** β Import, organize, proxy, and monitor IPTV streams on your own terms\
-β **Smart Playlist Handling** β M3U import, filtering, grouping, and failover support\
-β **VOD Content Management** β Organize movies and TV series with metadata and streaming\
-β **Reliable EPG Integration** β Match and manage TV guide data with ease\
-β **Clean & Responsive Interface** β Modern design that gets out of your way\
-β **Fully Self-Hosted** β Total control, zero reliance on third-party services
+β **Stream Proxy & Relay** β Intercept and proxy IPTV streams with real-time client management\
+β **M3U & Xtream Codes** β Import, filter, and organize playlists with multiple backend support\
+β **EPG Matching & Generation** β Auto-match EPG to channels or generate custom TV guides\
+β **Video on Demand** β Stream movies and TV series with rich metadata and IMDB/TMDB integration\
+β **Multi-Format Output** β Export as M3U, XMLTV EPG, Xtream Codes API, or HDHomeRun device\
+β **Real-Time Monitoring** β Live connection stats, bandwidth tracking, and automatic failover\
+β **Stream Profiles** β Configure different stream profiles for various clients and bandwidth requirements\
+β **Flexible Streaming Backends** β VLC, FFmpeg, Streamlink, or custom backends for transcoding and streaming\
+β **Multi-User & Access Control** β Granular permissions and network-based access restrictions\
+β **Plugin System** β Extend functionality with custom plugins for automation and integrations\
+β **Fully Self-Hosted** β Total control, no third-party dependencies
---
-
# Screenshots
-
+
+
+
+
+
+
+
+
-
+---
-
+## π οΈ Troubleshooting & Help
-
+- **General help?** Visit [Dispatcharr Docs](https://dispatcharr.github.io/Dispatcharr-Docs/)
+- **Community support?** Join our [Discord](https://discord.gg/Sp45V5BcxU)
-
+---
-
-
-
-# π Get Started in Minutes
+## π Get Started in Minutes
### π³ Quick Start with Docker (Recommended)
@@ -72,42 +98,50 @@ docker run -d \
---
-### π³ Docker Compose Options
+### π Docker Compose Options
-| Use Case | File | Description |
-| --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
-| **All-in-One Deployment** | [docker-compose.aio.yml](docker/docker-compose.aio.yml) | β Recommended! A simple, all-in-one solution β everything runs in a single container for quick setup. |
-| **Modular Deployment** | [docker-compose.yml](docker/docker-compose.yml) | Separate containers for Dispatcharr, Celery, and Postgres β perfect if you want more granular control. |
-| **Development Environment** | [docker-compose.dev.yml](docker/docker-compose.dev.yml) | Developer-friendly setup with pre-configured ports and settings for contributing and testing. |
+| Use Case | File | Description |
+| --------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
+| **All-in-One Deployment** | [docker-compose.aio.yml](docker/docker-compose.aio.yml) | β Recommended! A simple, all-in-one solution β everything runs in a single container for quick setup. |
+| **Modular Deployment** | [docker-compose.yml](docker/docker-compose.yml) | Separate containers for Dispatcharr, Celery, Redis, and Postgres β perfect if you want more granular control. |
+| **Development Environment** | [docker-compose.dev.yml](docker/docker-compose.dev.yml) | Developer-friendly setup with pre-configured ports and settings for contributing and testing. |
---
-### βοΈ Building from Source (For the Adventurous)
+### π οΈ Building from Source
> β οΈ **Warning**: Not officially supported β but if you're here, you know what you're doing!
-If you are running a Debian based operating system you can install using the `debian_install.sh` script. If you are on another operating system and come up with a script let us know! We would love to add it here!
+If you are running a Debian-based OS, use the `debian_install.sh` script. For other OS, contribute a script and weβll add it!
---
## π€ Want to Contribute?
We welcome **PRs, issues, ideas, and suggestions**!\
-Hereβs how you can join the party:
+Here's how you can join the party:
- Follow our coding style and best practices.
- Be respectful, helpful, and open-minded.
- Respect the **CC BY-NC-SA license**.
-> Whether itβs writing docs, squashing bugs, or building new features, your contribution matters! π
+> Whether it's writing docs, squashing bugs, or building new features, your contribution matters! π
---
-## π Roadmap & Documentation
+## π Documentation & Roadmap
-- π **Roadmap:** Coming soon!
- π **Documentation:** [Dispatcharr Docs](https://dispatcharr.github.io/Dispatcharr-Docs/)
+**Upcoming Features (in no particular order):**
+
+- π¬ **VOD Management Enhancements** β Granular metadata control and cleanup of unwanted VOD content
+- π **Media Library** β Import local files and serve them over XC API
+- π₯ **Enhanced User Management** β Customizable XC API output per user account
+- π **Output Stream Profiles** β Different clients with different stream profiles (bandwidth control, quality tiers)
+- π **Fallback Videos** β Automatic fallback content when channels are unavailable
+- π‘ **Webhooks** β Event-driven integrations and automations
+
---
## β€οΈ Shoutouts
@@ -120,7 +154,7 @@ A huge thank you to all the incredible open-source projects and libraries that p
> Dispatcharr is licensed under **CC BY-NC-SA 4.0**:
-- **BY**: Give credit where creditβs due.
+- **BY**: Give credit where credit's due.
- **NC**: No commercial use.
- **SA**: Share alike if you remix.
@@ -131,8 +165,8 @@ For full license details, see [LICENSE](https://creativecommons.org/licenses/by-
## βοΈ Connect With Us
Have a question? Want to suggest a feature? Just want to say hi?\
-β‘οΈ **[Open an issue](https://github.com/Dispatcharr/Dispatcharr/issues)** or reach out on [Discord]( https://discord.gg/Sp45V5BcxU).
+β‘οΈ **[Open an issue](https://github.com/Dispatcharr/Dispatcharr/issues)** or reach out on [Discord](https://discord.gg/Sp45V5BcxU).
---
-### π *Happy Streaming! The Dispatcharr Team*
+### π _Happy Streaming! The Dispatcharr Team_
diff --git a/apps/proxy/ts_proxy/server.py b/apps/proxy/ts_proxy/server.py
index db5b3d57..df51744d 100644
--- a/apps/proxy/ts_proxy/server.py
+++ b/apps/proxy/ts_proxy/server.py
@@ -149,11 +149,15 @@ class ProxyServer:
redis_host = os.environ.get("REDIS_HOST", getattr(settings, 'REDIS_HOST', 'localhost'))
redis_port = int(os.environ.get("REDIS_PORT", getattr(settings, 'REDIS_PORT', 6379)))
redis_db = int(os.environ.get("REDIS_DB", getattr(settings, 'REDIS_DB', 0)))
+ redis_password = os.environ.get("REDIS_PASSWORD", getattr(settings, 'REDIS_PASSWORD', ''))
+ redis_user = os.environ.get("REDIS_USER", getattr(settings, 'REDIS_USER', ''))
pubsub_client = redis.Redis(
host=redis_host,
port=redis_port,
db=redis_db,
+ password=redis_password if redis_password else None,
+ username=redis_user if redis_user else None,
socket_timeout=60,
socket_connect_timeout=10,
socket_keepalive=True,
diff --git a/apps/proxy/vod_proxy/connection_manager.py b/apps/proxy/vod_proxy/connection_manager.py
index ec0bffa5..aafc75cd 100644
--- a/apps/proxy/vod_proxy/connection_manager.py
+++ b/apps/proxy/vod_proxy/connection_manager.py
@@ -101,7 +101,16 @@ class PersistentVODConnection:
redis_host = getattr(settings, 'REDIS_HOST', 'localhost')
redis_port = int(getattr(settings, 'REDIS_PORT', 6379))
redis_db = int(getattr(settings, 'REDIS_DB', 0))
- r = redis.StrictRedis(host=redis_host, port=redis_port, db=redis_db, decode_responses=True)
+ redis_password = getattr(settings, 'REDIS_PASSWORD', '')
+ redis_user = getattr(settings, 'REDIS_USER', '')
+ r = redis.StrictRedis(
+ host=redis_host,
+ port=redis_port,
+ db=redis_db,
+ password=redis_password if redis_password else None,
+ username=redis_user if redis_user else None,
+ decode_responses=True
+ )
content_length_key = f"vod_content_length:{self.session_id}"
stored_length = r.get(content_length_key)
if stored_length:
diff --git a/apps/proxy/vod_proxy/views.py b/apps/proxy/vod_proxy/views.py
index 2ec95cc3..948604d6 100644
--- a/apps/proxy/vod_proxy/views.py
+++ b/apps/proxy/vod_proxy/views.py
@@ -333,7 +333,16 @@ class VODStreamView(View):
redis_host = getattr(settings, 'REDIS_HOST', 'localhost')
redis_port = int(getattr(settings, 'REDIS_PORT', 6379))
redis_db = int(getattr(settings, 'REDIS_DB', 0))
- r = redis.StrictRedis(host=redis_host, port=redis_port, db=redis_db, decode_responses=True)
+ redis_password = getattr(settings, 'REDIS_PASSWORD', '')
+ redis_user = getattr(settings, 'REDIS_USER', '')
+ r = redis.StrictRedis(
+ host=redis_host,
+ port=redis_port,
+ db=redis_db,
+ password=redis_password if redis_password else None,
+ username=redis_user if redis_user else None,
+ decode_responses=True
+ )
content_length_key = f"vod_content_length:{session_id}"
r.set(content_length_key, total_size, ex=1800) # Store for 30 minutes
logger.info(f"[VOD-HEAD] Stored total content length {total_size} for session {session_id}")
diff --git a/core/utils.py b/core/utils.py
index 98e2ba78..bd80782e 100644
--- a/core/utils.py
+++ b/core/utils.py
@@ -55,6 +55,8 @@ class RedisClient:
redis_host = os.environ.get("REDIS_HOST", getattr(settings, 'REDIS_HOST', 'localhost'))
redis_port = int(os.environ.get("REDIS_PORT", getattr(settings, 'REDIS_PORT', 6379)))
redis_db = int(os.environ.get("REDIS_DB", getattr(settings, 'REDIS_DB', 0)))
+ redis_password = os.environ.get("REDIS_PASSWORD", getattr(settings, 'REDIS_PASSWORD', ''))
+ redis_user = os.environ.get("REDIS_USER", getattr(settings, 'REDIS_USER', ''))
# Use standardized settings
socket_timeout = getattr(settings, 'REDIS_SOCKET_TIMEOUT', 5)
@@ -68,6 +70,8 @@ class RedisClient:
host=redis_host,
port=redis_port,
db=redis_db,
+ password=redis_password if redis_password else None,
+ username=redis_user if redis_user else None,
socket_timeout=socket_timeout,
socket_connect_timeout=socket_connect_timeout,
socket_keepalive=socket_keepalive,
@@ -148,6 +152,8 @@ class RedisClient:
redis_host = os.environ.get("REDIS_HOST", getattr(settings, 'REDIS_HOST', 'localhost'))
redis_port = int(os.environ.get("REDIS_PORT", getattr(settings, 'REDIS_PORT', 6379)))
redis_db = int(os.environ.get("REDIS_DB", getattr(settings, 'REDIS_DB', 0)))
+ redis_password = os.environ.get("REDIS_PASSWORD", getattr(settings, 'REDIS_PASSWORD', ''))
+ redis_user = os.environ.get("REDIS_USER", getattr(settings, 'REDIS_USER', ''))
# Use standardized settings but without socket timeouts for PubSub
# Important: socket_timeout is None for PubSub operations
@@ -161,6 +167,8 @@ class RedisClient:
host=redis_host,
port=redis_port,
db=redis_db,
+ password=redis_password if redis_password else None,
+ username=redis_user if redis_user else None,
socket_timeout=None, # Critical: No timeout for PubSub operations
socket_connect_timeout=socket_connect_timeout,
socket_keepalive=socket_keepalive,
diff --git a/core/views.py b/core/views.py
index 5806d63c..082cccc3 100644
--- a/core/views.py
+++ b/core/views.py
@@ -40,7 +40,15 @@ def stream_view(request, channel_uuid):
redis_host = getattr(settings, "REDIS_HOST", "localhost")
redis_port = int(getattr(settings, "REDIS_PORT", 6379))
redis_db = int(getattr(settings, "REDIS_DB", "0"))
- redis_client = redis.Redis(host=redis_host, port=redis_port, db=redis_db)
+ redis_password = getattr(settings, "REDIS_PASSWORD", "")
+ redis_user = getattr(settings, "REDIS_USER", "")
+ redis_client = redis.Redis(
+ host=redis_host,
+ port=redis_port,
+ db=redis_db,
+ password=redis_password if redis_password else None,
+ username=redis_user if redis_user else None
+ )
# Retrieve the channel by the provided stream_id.
channel = Channel.objects.get(uuid=channel_uuid)
diff --git a/dispatcharr/persistent_lock.py b/dispatcharr/persistent_lock.py
index 27d480be..2ed72bf4 100644
--- a/dispatcharr/persistent_lock.py
+++ b/dispatcharr/persistent_lock.py
@@ -78,7 +78,15 @@ if __name__ == "__main__":
redis_host = os.environ.get("REDIS_HOST", "localhost")
redis_port = int(os.environ.get("REDIS_PORT", 6379))
redis_db = int(os.environ.get("REDIS_DB", 0))
- client = redis.Redis(host=redis_host, port=redis_port, db=redis_db)
+ redis_password = os.environ.get("REDIS_PASSWORD", "")
+ redis_user = os.environ.get("REDIS_USER", "")
+ client = redis.Redis(
+ host=redis_host,
+ port=redis_port,
+ db=redis_db,
+ password=redis_password if redis_password else None,
+ username=redis_user if redis_user else None
+ )
lock = PersistentLock(client, "lock:example_account", lock_timeout=120)
if lock.acquire():
diff --git a/dispatcharr/settings.py b/dispatcharr/settings.py
index 7afa75c5..8fba3143 100644
--- a/dispatcharr/settings.py
+++ b/dispatcharr/settings.py
@@ -1,6 +1,7 @@
import os
from pathlib import Path
from datetime import timedelta
+from urllib.parse import quote_plus
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -8,6 +9,8 @@ SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")
REDIS_HOST = os.environ.get("REDIS_HOST", "localhost")
REDIS_PORT = int(os.environ.get("REDIS_PORT", 6379))
REDIS_DB = os.environ.get("REDIS_DB", "0")
+REDIS_USER = os.environ.get("REDIS_USER", "")
+REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD", "")
# Set DEBUG to True for development, False for production
if os.environ.get("DISPATCHARR_DEBUG", "False").lower() == "true":
@@ -121,7 +124,12 @@ CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
- "hosts": [(REDIS_HOST, REDIS_PORT, REDIS_DB)], # Ensure Redis is running
+ "hosts": ["redis://{redis_auth}{host}:{port}/{db}".format(
+ redis_auth=f"{quote_plus(REDIS_USER)}:{quote_plus(REDIS_PASSWORD)}@" if REDIS_PASSWORD and REDIS_USER else f":{quote_plus(REDIS_PASSWORD)}@" if REDIS_PASSWORD else "",
+ host=REDIS_HOST,
+ port=REDIS_PORT,
+ db=REDIS_DB
+ )], # URL format supports authentication
},
},
}
@@ -199,8 +207,19 @@ STATICFILES_DIRS = [
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = "accounts.User"
-# Build default Redis URL from components for Celery
-_default_redis_url = f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}"
+# Build default Redis URL from components for Celery with optional authentication
+# Build auth string conditionally with URL encoding for special characters
+if REDIS_PASSWORD:
+ encoded_password = quote_plus(REDIS_PASSWORD)
+ if REDIS_USER:
+ encoded_user = quote_plus(REDIS_USER)
+ redis_auth = f"{encoded_user}:{encoded_password}@"
+ else:
+ redis_auth = f":{encoded_password}@"
+else:
+ redis_auth = ""
+
+_default_redis_url = f"redis://{redis_auth}{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}"
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", _default_redis_url)
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", CELERY_BROKER_URL)
@@ -271,7 +290,7 @@ SIMPLE_JWT = {
}
# Redis connection settings
-REDIS_URL = os.environ.get("REDIS_URL", f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}")
+REDIS_URL = os.environ.get("REDIS_URL", _default_redis_url)
REDIS_SOCKET_TIMEOUT = 60 # Socket timeout in seconds
REDIS_SOCKET_CONNECT_TIMEOUT = 5 # Connection timeout in seconds
REDIS_HEALTH_CHECK_INTERVAL = 15 # Health check every 15 seconds
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index c8e2d53e..519b288a 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,4 +1,11 @@
+# Dispatcharr - Modular Deployment Configuration
+# This compose file runs Dispatcharr in modular mode with separate containers
+# for web, celery workers, PostgreSQL database, and Redis cache.
+
services:
+ # ============================================================================
+ # Web Service
+ # ============================================================================
web:
image: ghcr.io/dispatcharr/dispatcharr:latest
container_name: dispatcharr_web
@@ -9,44 +16,67 @@ services:
depends_on:
- db
- redis
+
+ # --- Environment Configuration ---
environment:
+ # Deployment Mode
- DISPATCHARR_ENV=modular
+
+ # PostgreSQL Connection
- POSTGRES_HOST=db
- POSTGRES_PORT=5432
- POSTGRES_DB=dispatcharr
- POSTGRES_USER=dispatch
- POSTGRES_PASSWORD=secret
+
+ # Redis Connection
- REDIS_HOST=redis
- REDIS_PORT=6379
+
+ # Redis Authentication (Optional)
+ # Uncomment and set if your Redis requires authentication:
+ #- REDIS_PASSWORD=your_strong_redis_password
+ #- REDIS_USER=your_redis_username # For Redis 6+ ACL - see Redis service below
+
+ # Logging
- DISPATCHARR_LOG_LEVEL=info
+
# Legacy CPU Support (Optional)
# Uncomment to enable legacy NumPy build for older CPUs (circa 2009)
- # that lack support for newer baseline CPU features
+ # that lack support for newer baseline CPU features:
#- USE_LEGACY_NUMPY=true
+
# Process Priority Configuration (Optional)
# Lower values = higher priority. Range: -20 (highest) to 19 (lowest)
- # Negative values require cap_add: SYS_NICE (uncomment below)
+ # Negative values require cap_add: SYS_NICE (see below)
#- UWSGI_NICE_LEVEL=-5 # uWSGI/FFmpeg/Streaming (default: 0, recommended: -5 for high priority)
- #
+
+ # --- Advanced Configuration ---
# Uncomment to enable high priority for streaming (required if UWSGI_NICE_LEVEL < 0)
#cap_add:
# - SYS_NICE
- # Optional for hardware acceleration
+
+ # --- Hardware Acceleration (Optional) ---
+ # Uncomment for GPU access (transcoding acceleration)
#group_add:
# - video
- # #- render # Uncomment if your GPU requires it
+ # #- render # Uncomment if your GPU requires it
#devices:
# - /dev/dri:/dev/dri # For Intel/AMD GPU acceleration (VA-API)
+
+ # NVIDIA GPU Support (requires NVIDIA Container Toolkit)
# Uncomment the following lines for NVIDIA GPU support
- # NVidia GPU support (requires NVIDIA Container Toolkit)
#deploy:
# resources:
- # reservations:
- # devices:
- # - driver: nvidia
- # count: all
- # capabilities: [gpu]
+ # reservations:
+ # devices:
+ # - driver: nvidia
+ # count: all
+ # capabilities: [gpu]
+ # ============================================================================
+ # Celery Service - Background task worker
+ # ============================================================================
celery:
image: ghcr.io/dispatcharr/dispatcharr:latest
container_name: dispatcharr_celery
@@ -58,24 +88,47 @@ services:
- ./data:/data
extra_hosts:
- "host.docker.internal:host-gateway"
+ entrypoint: ["/app/docker/entrypoint.celery.sh"]
+
+ # --- Environment Configuration ---
environment:
+ # Deployment Mode
- DISPATCHARR_ENV=modular
+
+ # PostgreSQL Connection
- POSTGRES_HOST=db
- POSTGRES_PORT=5432
- POSTGRES_DB=dispatcharr
- POSTGRES_USER=dispatch
- POSTGRES_PASSWORD=secret
+
+ # Redis Connection
- REDIS_HOST=redis
- REDIS_PORT=6379
+
+ # Redis Authentication (Optional)
+ # Uncomment and set if your Redis requires authentication:
+ #- REDIS_PASSWORD=your_strong_redis_password
+ #- REDIS_USER=your_redis_username # For Redis 6+ ACL - see Redis service below
+
+ # Logging
- DISPATCHARR_LOG_LEVEL=info
- #- CELERY_NICE_LEVEL=5 #Celery/EPG/Background tasks (default:5, low priority; Range: -20 to 19)
+
+ # Process Priority Configuration (Optional)
+ #- CELERY_NICE_LEVEL=5 # Celery/EPG/Background tasks (default: 5, low priority; Range: -20 to 19)
+
+ # Django Configuration
- DJANGO_SETTINGS_MODULE=dispatcharr.settings
- PYTHONUNBUFFERED=1
+
+ # --- Advanced Configuration ---
# Uncomment to enable high priority for Celery (required if CELERY_NICE_LEVEL < 0)
#cap_add:
# - SYS_NICE
- entrypoint: ["/app/docker/entrypoint.celery.sh"]
+ # ============================================================================
+ # PostgreSQL
+ # ============================================================================
db:
image: postgres:17
container_name: dispatcharr_db
@@ -93,14 +146,42 @@ services:
timeout: 5s
retries: 5
+ # ============================================================================
+ # Redis
+ # ============================================================================
redis:
image: redis:latest
container_name: dispatcharr_redis
+
+ # --- Authentication Configuration (Optional) ---
+ # By default, Redis runs without authentication.
+ # Choose ONE of the following options if authentication is required:
+
+ # Option 1: Password-only authentication (Redis <6 or default user)
+ #command: ["redis-server", "--requirepass", "your_strong_redis_password"]
+
+ # Option 2: Redis 6+ ACL with username + password (requires custom config file - see Redis documentation for configuration)
+ #command: ["redis-server", "/etc/redis/redis.conf"]
+ #volumes:
+ # - ./redis.conf:/etc/redis/redis.conf:ro
+
+ # --- Health Check Configuration ---
healthcheck:
+ # Default: No authentication
test: ["CMD", "redis-cli", "ping"]
+
+ # If using Option 1 (password-only), uncomment this instead:
+ #test: ["CMD", "redis-cli", "-a", "your_strong_redis_password", "ping"]
+
+ # If using Option 2 (Redis 6+ ACL), uncomment this instead:
+ #test: ["CMD", "redis-cli", "--user", "your_redis_username", "-a", "your_strong_redis_password", "ping"]
+
interval: 5s
timeout: 5s
retries: 5
+# ==============================================================================
+# Volumes
+# ==============================================================================
volumes:
postgres_data:
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
index ead6aadd..b79af952 100755
--- a/docker/entrypoint.sh
+++ b/docker/entrypoint.sh
@@ -38,6 +38,8 @@ export PG_BINDIR="/usr/lib/postgresql/${PG_VERSION}/bin"
export REDIS_HOST=${REDIS_HOST:-localhost}
export REDIS_PORT=${REDIS_PORT:-6379}
export REDIS_DB=${REDIS_DB:-0}
+export REDIS_PASSWORD=${REDIS_PASSWORD:-}
+export REDIS_USER=${REDIS_USER:-}
export DISPATCHARR_PORT=${DISPATCHARR_PORT:-9191}
export LIBVA_DRIVERS_PATH='/usr/local/lib/x86_64-linux-gnu/dri'
export LD_LIBRARY_PATH='/usr/local/lib'
@@ -104,7 +106,7 @@ if [[ ! -f /etc/profile.d/dispatcharr.sh ]]; then
PATH VIRTUAL_ENV DJANGO_SETTINGS_MODULE PYTHONUNBUFFERED PYTHONDONTWRITEBYTECODE
POSTGRES_DB POSTGRES_USER POSTGRES_PASSWORD POSTGRES_HOST POSTGRES_PORT
DISPATCHARR_ENV DISPATCHARR_DEBUG DISPATCHARR_LOG_LEVEL
- REDIS_HOST REDIS_PORT REDIS_DB POSTGRES_DIR DISPATCHARR_PORT
+ REDIS_HOST REDIS_PORT REDIS_DB REDIS_PASSWORD REDIS_USER POSTGRES_DIR DISPATCHARR_PORT
DISPATCHARR_VERSION DISPATCHARR_TIMESTAMP LIBVA_DRIVERS_PATH LIBVA_DRIVER_NAME LD_LIBRARY_PATH
CELERY_NICE_LEVEL UWSGI_NICE_LEVEL DJANGO_SECRET_KEY
)
@@ -161,20 +163,9 @@ if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
pids+=("$postgres_pid")
else
echo "π Modular mode: Using external PostgreSQL at ${POSTGRES_HOST}:${POSTGRES_PORT}"
- # Wait for external PostgreSQL to be ready using Python (no pg_isready needed)
+ # Wait for external PostgreSQL to be ready using pg_isready (checks actual protocol readiness)
echo_with_timestamp "Waiting for external PostgreSQL to be ready..."
- until python3 -c "
-import socket
-import sys
-try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(2)
- s.connect(('${POSTGRES_HOST}', ${POSTGRES_PORT}))
- s.close()
- sys.exit(0)
-except Exception:
- sys.exit(1)
-" 2>/dev/null; do
+ until $PG_BINDIR/pg_isready -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -q >/dev/null 2>&1; do
echo_with_timestamp "Waiting for PostgreSQL at ${POSTGRES_HOST}:${POSTGRES_PORT}..."
sleep 1
done
@@ -189,12 +180,9 @@ if [[ "$DISPATCHARR_ENV" == "modular" ]]; then
echo "π Modular mode: Using external Redis at ${REDIS_HOST}:${REDIS_PORT}"
echo_with_timestamp "Waiting for external Redis to be ready..."
until python3 -c "
-import socket
-import sys
+import socket, sys
try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(2)
- s.connect(('${REDIS_HOST}', ${REDIS_PORT}))
+ s = socket.create_connection(('${REDIS_HOST}', ${REDIS_PORT}), timeout=2)
s.close()
sys.exit(0)
except Exception:
diff --git a/docs/images/channels.png b/docs/images/channels.png
new file mode 100644
index 00000000..87236f1f
Binary files /dev/null and b/docs/images/channels.png differ
diff --git a/docs/images/guide.png b/docs/images/guide.png
new file mode 100644
index 00000000..37cfa132
Binary files /dev/null and b/docs/images/guide.png differ
diff --git a/docs/images/m3u-epg-manager.png b/docs/images/m3u-epg-manager.png
new file mode 100644
index 00000000..34bea87d
Binary files /dev/null and b/docs/images/m3u-epg-manager.png differ
diff --git a/docs/images/settings.png b/docs/images/settings.png
new file mode 100644
index 00000000..9106fbe5
Binary files /dev/null and b/docs/images/settings.png differ
diff --git a/docs/images/stats.png b/docs/images/stats.png
new file mode 100644
index 00000000..a664bb7d
Binary files /dev/null and b/docs/images/stats.png differ
diff --git a/docs/images/vod-library.png b/docs/images/vod-library.png
new file mode 100644
index 00000000..6effdb70
Binary files /dev/null and b/docs/images/vod-library.png differ
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
index ec2b712d..b7837cf2 100644
--- a/frontend/eslint.config.js
+++ b/frontend/eslint.config.js
@@ -1,7 +1,7 @@
-import js from '@eslint/js'
-import globals from 'globals'
-import reactHooks from 'eslint-plugin-react-hooks'
-import reactRefresh from 'eslint-plugin-react-refresh'
+import js from '@eslint/js';
+import globals from 'globals';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
export default [
{ ignores: ['dist'] },
@@ -30,4 +30,4 @@ export default [
],
},
},
-]
+];
diff --git a/frontend/index.html b/frontend/index.html
index 86207098..d2f6a575 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,10 +4,24 @@
-
-
-
-
+
+
+
+
Dispatcharr
diff --git a/frontend/prettier.config.js b/frontend/prettier.config.js
index d2b86878..7787ae62 100644
--- a/frontend/prettier.config.js
+++ b/frontend/prettier.config.js
@@ -3,8 +3,9 @@ export default {
semi: true, // Add semicolons at the end of statements
singleQuote: true, // Use single quotes instead of double
tabWidth: 2, // Set the indentation width
- trailingComma: "es5", // Add trailing commas where valid in ES5
+ trailingComma: 'es5', // Add trailing commas where valid in ES5
printWidth: 80, // Wrap lines at 80 characters
bracketSpacing: true, // Add spaces inside object braces
- arrowParens: "always", // Always include parentheses around arrow function parameters
+ arrowParens: 'always', // Always include parentheses around arrow function parameters
+ endOfLine: 'lf', // Enforce LF for all files
};
diff --git a/frontend/public/site.webmanifest b/frontend/public/site.webmanifest
index 45dc8a20..fa99de77 100644
--- a/frontend/public/site.webmanifest
+++ b/frontend/public/site.webmanifest
@@ -1 +1,19 @@
-{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
\ No newline at end of file
+{
+ "name": "",
+ "short_name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/frontend/src/assets/site.webmanifest b/frontend/src/assets/site.webmanifest
index 45dc8a20..fa99de77 100644
--- a/frontend/src/assets/site.webmanifest
+++ b/frontend/src/assets/site.webmanifest
@@ -1 +1,19 @@
-{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
\ No newline at end of file
+{
+ "name": "",
+ "short_name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
diff --git a/frontend/src/components/ErrorBoundary.jsx b/frontend/src/components/ErrorBoundary.jsx
index 60c4ba38..83fa4de7 100644
--- a/frontend/src/components/ErrorBoundary.jsx
+++ b/frontend/src/components/ErrorBoundary.jsx
@@ -15,4 +15,4 @@ class ErrorBoundary extends React.Component {
}
}
-export default ErrorBoundary;
\ No newline at end of file
+export default ErrorBoundary;
diff --git a/frontend/src/components/Field.jsx b/frontend/src/components/Field.jsx
index 186ef7e0..6ee94243 100644
--- a/frontend/src/components/Field.jsx
+++ b/frontend/src/components/Field.jsx
@@ -1,4 +1,11 @@
-import { NumberInput, Select, Switch, Text, Textarea, TextInput } from '@mantine/core';
+import {
+ NumberInput,
+ Select,
+ Switch,
+ Text,
+ Textarea,
+ TextInput,
+} from '@mantine/core';
import React from 'react';
export const Field = ({ field, value, onChange }) => {
diff --git a/frontend/src/components/GuideRow.jsx b/frontend/src/components/GuideRow.jsx
index 7780ecff..28f41563 100644
--- a/frontend/src/components/GuideRow.jsx
+++ b/frontend/src/components/GuideRow.jsx
@@ -1,13 +1,13 @@
-import React from "react";
+import React from 'react';
import {
CHANNEL_WIDTH,
EXPANDED_PROGRAM_HEIGHT,
HOUR_WIDTH,
PROGRAM_HEIGHT,
} from '../pages/guideUtils.js';
-import {Box, Flex, Text} from "@mantine/core";
-import {Play} from "lucide-react";
-import logo from "../images/logo.png";
+import { Box, Flex, Text } from '@mantine/core';
+import { Play } from 'lucide-react';
+import logo from '../images/logo.png';
const GuideRow = React.memo(({ index, style, data }) => {
const {
@@ -36,31 +36,33 @@ const GuideRow = React.memo(({ index, style, data }) => {
: PROGRAM_HEIGHT);
const PlaceholderProgram = () => {
- return <>
- {Array.from({length: Math.ceil(24 / 2)}).map(
- (_, placeholderIndex) => (
-
- No program data
-
- )
- )}
- >;
- }
+ return (
+ <>
+ {Array.from({ length: Math.ceil(24 / 2) }).map(
+ (_, placeholderIndex) => (
+
+ No program data
+
+ )
+ )}
+ >
+ );
+ };
return (