From 6c8270d0e582ea7667db75036b354de72e972673 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 4 Dec 2025 15:28:21 -0600 Subject: [PATCH 01/20] Enhancement: Add support for 'extracting' status and display additional progress information in EPGsTable --- CHANGELOG.md | 1 + frontend/src/components/tables/EPGsTable.jsx | 63 +++++++++++++++++--- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e46bffe1..0de26314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **Performance**: EPG program parsing optimized for sources with many channels but only a fraction mapped. Now parses XML file once per source instead of once per channel, dramatically reducing I/O and CPU overhead. For sources with 10,000 channels and 100 mapped, this results in ~99x fewer file opens and ~100x fewer full file scans. Orphaned programs for unmapped channels are also cleaned up during refresh to prevent database bloat. Database updates are now atomic to prevent clients from seeing empty/partial EPG data during refresh. +- EPG table now displays detailed status messages including refresh progress, success messages, and last message for idle sources (matching M3U table behavior) - IPv6 access now allowed by default with all IPv6 CIDRs accepted - Thanks [@adrianmace](https://github.com/adrianmace) - nginx.conf updated to bind to both IPv4 and IPv6 ports - Thanks [@jordandalley](https://github.com/jordandalley) diff --git a/frontend/src/components/tables/EPGsTable.jsx b/frontend/src/components/tables/EPGsTable.jsx index 71e920e0..b8dfeb6d 100644 --- a/frontend/src/components/tables/EPGsTable.jsx +++ b/frontend/src/components/tables/EPGsTable.jsx @@ -160,6 +160,9 @@ const EPGsTable = () => { case 'downloading': label = 'Downloading'; break; + case 'extracting': + label = 'Extracting'; + break; case 'parsing_channels': label = 'Parsing Channels'; break; @@ -170,6 +173,22 @@ const EPGsTable = () => { return null; } + // Build additional info string from progress data + let additionalInfo = ''; + if (progress.message) { + additionalInfo = progress.message; + } else if ( + progress.processed !== undefined && + progress.channels !== undefined + ) { + additionalInfo = `${progress.processed.toLocaleString()} programs for ${progress.channels} channels`; + } else if ( + progress.processed !== undefined && + progress.total !== undefined + ) { + additionalInfo = `${progress.processed.toLocaleString()} / ${progress.total.toLocaleString()}`; + } + return ( @@ -181,7 +200,14 @@ const EPGsTable = () => { style={{ margin: '2px 0' }} /> {progress.speed && ( - Speed: {parseInt(progress.speed)} KB/s + + Speed: {parseInt(progress.speed)} KB/s + + )} + {additionalInfo && ( + + {additionalInfo} + )} ); @@ -286,14 +312,35 @@ const EPGsTable = () => { // Show success message for successful sources if (data.status === 'success') { + const successMessage = + data.last_message || 'EPG data refreshed successfully'; return ( - - EPG data refreshed successfully - + + + {successMessage} + + + ); + } + + // Show last_message for idle sources (from previous refresh) + if (data.status === 'idle' && data.last_message) { + return ( + + + {data.last_message} + + ); } From 3b34fb11ef0d401c07be61ec8f0c76228fdf3485 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 4 Dec 2025 15:43:33 -0600 Subject: [PATCH 02/20] Fix: Fixes bug where Updated column wouldn't update in the EPG table without a webui refresh. --- CHANGELOG.md | 3 ++- apps/epg/tasks.py | 3 ++- frontend/src/WebSocket.jsx | 10 ++++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0de26314..8a77b7b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **Performance**: EPG program parsing optimized for sources with many channels but only a fraction mapped. Now parses XML file once per source instead of once per channel, dramatically reducing I/O and CPU overhead. For sources with 10,000 channels and 100 mapped, this results in ~99x fewer file opens and ~100x fewer full file scans. Orphaned programs for unmapped channels are also cleaned up during refresh to prevent database bloat. Database updates are now atomic to prevent clients from seeing empty/partial EPG data during refresh. -- EPG table now displays detailed status messages including refresh progress, success messages, and last message for idle sources (matching M3U table behavior) +- EPG table now displays detailed status messages including refresh progress, success messages, and last message for idle sources (matching M3U table behavior) (Closes #214) +- EPG table "Updated" column now updates in real-time via WebSocket using the actual backend timestamp instead of requiring a page refresh - IPv6 access now allowed by default with all IPv6 CIDRs accepted - Thanks [@adrianmace](https://github.com/adrianmace) - nginx.conf updated to bind to both IPv4 and IPv6 ports - Thanks [@jordandalley](https://github.com/jordandalley) diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py index 9fa999cd..c565dbf5 100644 --- a/apps/epg/tasks.py +++ b/apps/epg/tasks.py @@ -1668,7 +1668,8 @@ def parse_programs_for_source(epg_source, tvg_id=None): # Send completion notification with status send_epg_update(epg_source.id, "parsing_programs", 100, status="success", - message=epg_source.last_message) + message=epg_source.last_message, + updated_at=epg_source.updated_at.isoformat()) logger.info(f"Completed parsing programs for source: {epg_source.name} - " f"{total_programs:,} programs for {channels_with_programs} channels, " diff --git a/frontend/src/WebSocket.jsx b/frontend/src/WebSocket.jsx index f2e28ae9..40035d33 100644 --- a/frontend/src/WebSocket.jsx +++ b/frontend/src/WebSocket.jsx @@ -574,7 +574,7 @@ export const WebsocketProvider = ({ children }) => { const sourceId = parsedEvent.data.source || parsedEvent.data.account; const epg = epgs[sourceId]; - + // Only update progress if the EPG still exists in the store // This prevents crashes when receiving updates for deleted EPGs if (epg) { @@ -582,7 +582,9 @@ export const WebsocketProvider = ({ children }) => { updateEPGProgress(parsedEvent.data); } else { // EPG was deleted, ignore this update - console.debug(`Ignoring EPG refresh update for deleted EPG ${sourceId}`); + console.debug( + `Ignoring EPG refresh update for deleted EPG ${sourceId}` + ); break; } @@ -621,6 +623,10 @@ export const WebsocketProvider = ({ children }) => { status: parsedEvent.data.status || 'success', last_message: parsedEvent.data.message || epg.last_message, + // Use the timestamp from the backend if provided + ...(parsedEvent.data.updated_at && { + updated_at: parsedEvent.data.updated_at, + }), }); // Only show success notification if we've finished parsing programs and had no errors From 0d177e44f8ce8cdb2e7dba10bc9266f2b973849e Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 4 Dec 2025 15:45:09 -0600 Subject: [PATCH 03/20] changelog: Change updated change to bug fix instead of change. --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a77b7b7..0b95749e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Performance**: EPG program parsing optimized for sources with many channels but only a fraction mapped. Now parses XML file once per source instead of once per channel, dramatically reducing I/O and CPU overhead. For sources with 10,000 channels and 100 mapped, this results in ~99x fewer file opens and ~100x fewer full file scans. Orphaned programs for unmapped channels are also cleaned up during refresh to prevent database bloat. Database updates are now atomic to prevent clients from seeing empty/partial EPG data during refresh. - EPG table now displays detailed status messages including refresh progress, success messages, and last message for idle sources (matching M3U table behavior) (Closes #214) -- EPG table "Updated" column now updates in real-time via WebSocket using the actual backend timestamp instead of requiring a page refresh - IPv6 access now allowed by default with all IPv6 CIDRs accepted - Thanks [@adrianmace](https://github.com/adrianmace) - nginx.conf updated to bind to both IPv4 and IPv6 ports - Thanks [@jordandalley](https://github.com/jordandalley) +### Fixed + +- EPG table "Updated" column now updates in real-time via WebSocket using the actual backend timestamp instead of requiring a page refresh + ## [0.13.0] - 2025-12-02 ### Added From c1d960138e4f455543caac34e0b6ef8ca16911e8 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 5 Dec 2025 09:02:03 -0600 Subject: [PATCH 04/20] Fix: Bulk channel editor confirmation dialog now shows the correct stream profile that will be set. --- CHANGELOG.md | 1 + frontend/src/components/forms/ChannelBatch.jsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b95749e..0b0223f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - EPG table "Updated" column now updates in real-time via WebSocket using the actual backend timestamp instead of requiring a page refresh +- Bulk channel editor confirmation dialog now displays the correct stream profile name that will be applied to the selected channels. ## [0.13.0] - 2025-12-02 diff --git a/frontend/src/components/forms/ChannelBatch.jsx b/frontend/src/components/forms/ChannelBatch.jsx index e42d418c..a1cebe54 100644 --- a/frontend/src/components/forms/ChannelBatch.jsx +++ b/frontend/src/components/forms/ChannelBatch.jsx @@ -135,8 +135,10 @@ const ChannelBatchForm = ({ channelIds, isOpen, onClose }) => { if (values.stream_profile_id === '0') { changes.push(`• Stream Profile: Use Default`); } else { - const profileName = - streamProfiles[values.stream_profile_id]?.name || 'Selected Profile'; + const profile = streamProfiles.find( + (p) => `${p.id}` === `${values.stream_profile_id}` + ); + const profileName = profile?.name || 'Selected Profile'; changes.push(`• Stream Profile: ${profileName}`); } } From 759569b871973253c89dd7b625e1272fe5e9c7eb Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 5 Dec 2025 09:54:11 -0600 Subject: [PATCH 05/20] Enhancement: Add a priority field to EPGSource and prefer higher-priority sources when matching channels. Also ignore EPG sources where is_active is false during matching, and update serializers/forms/frontend accordingly.(Closes #603, #672) --- CHANGELOG.md | 3 ++ apps/channels/tasks.py | 37 ++++++++++++++----- .../epg/migrations/0021_epgsource_priority.py | 18 +++++++++ apps/epg/models.py | 4 ++ apps/epg/serializers.py | 1 + frontend/src/components/forms/EPG.jsx | 28 ++++++++++---- 6 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 apps/epg/migrations/0021_epgsource_priority.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0223f9..d58c0ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Sort buttons for 'Group' and 'M3U' columns in Streams table for improved stream organization and filtering - Thanks [@bobey6](https://github.com/bobey6) +- EPG source priority field for controlling which EPG source is preferred when multiple sources have matching entries for a channel (higher numbers = higher priority) (Closes #603) ### Changed @@ -17,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - EPG table now displays detailed status messages including refresh progress, success messages, and last message for idle sources (matching M3U table behavior) (Closes #214) - IPv6 access now allowed by default with all IPv6 CIDRs accepted - Thanks [@adrianmace](https://github.com/adrianmace) - nginx.conf updated to bind to both IPv4 and IPv6 ports - Thanks [@jordandalley](https://github.com/jordandalley) +- EPG matching now respects source priority and only uses active (enabled) EPG sources (Closes #672) +- EPG form API Key field now only visible when Schedules Direct source type is selected ### Fixed diff --git a/apps/channels/tasks.py b/apps/channels/tasks.py index 5a9528a7..7ca73ac2 100755 --- a/apps/channels/tasks.py +++ b/apps/channels/tasks.py @@ -295,7 +295,11 @@ def match_channels_to_epg(channels_data, epg_data, region_code=None, use_ml=True if score > 50: # Only show decent matches logger.debug(f" EPG '{row['name']}' (norm: '{row['norm_name']}') => score: {score} (base: {base_score}, bonus: {bonus})") - if score > best_score: + # When scores are equal, prefer higher priority EPG source + row_priority = row.get('epg_source_priority', 0) + best_priority = best_epg.get('epg_source_priority', 0) if best_epg else -1 + + if score > best_score or (score == best_score and row_priority > best_priority): best_score = score best_epg = row @@ -471,9 +475,9 @@ def match_epg_channels(): "norm_chan": normalize_name(channel.name) # Always use channel name for fuzzy matching! }) - # Get all EPG data + # Get all EPG data from active sources, ordered by source priority (highest first) so we prefer higher priority matches epg_data = [] - for epg in EPGData.objects.all(): + for epg in EPGData.objects.select_related('epg_source').filter(epg_source__is_active=True): normalized_tvg_id = epg.tvg_id.strip().lower() if epg.tvg_id else "" epg_data.append({ 'id': epg.id, @@ -482,9 +486,13 @@ def match_epg_channels(): 'name': epg.name, 'norm_name': normalize_name(epg.name), 'epg_source_id': epg.epg_source.id if epg.epg_source else None, + 'epg_source_priority': epg.epg_source.priority if epg.epg_source else 0, }) - logger.info(f"Processing {len(channels_data)} channels against {len(epg_data)} EPG entries") + # Sort EPG data by source priority (highest first) so we prefer higher priority matches + epg_data.sort(key=lambda x: x['epg_source_priority'], reverse=True) + + logger.info(f"Processing {len(channels_data)} channels against {len(epg_data)} EPG entries (from active sources only)") # Run EPG matching with progress updates - automatically uses conservative thresholds for bulk operations result = match_channels_to_epg(channels_data, epg_data, region_code, use_ml=True, send_progress=True) @@ -618,9 +626,9 @@ def match_selected_channels_epg(channel_ids): "norm_chan": normalize_name(channel.name) }) - # Get all EPG data + # Get all EPG data from active sources, ordered by source priority (highest first) so we prefer higher priority matches epg_data = [] - for epg in EPGData.objects.all(): + for epg in EPGData.objects.select_related('epg_source').filter(epg_source__is_active=True): normalized_tvg_id = epg.tvg_id.strip().lower() if epg.tvg_id else "" epg_data.append({ 'id': epg.id, @@ -629,9 +637,13 @@ def match_selected_channels_epg(channel_ids): 'name': epg.name, 'norm_name': normalize_name(epg.name), 'epg_source_id': epg.epg_source.id if epg.epg_source else None, + 'epg_source_priority': epg.epg_source.priority if epg.epg_source else 0, }) - logger.info(f"Processing {len(channels_data)} selected channels against {len(epg_data)} EPG entries") + # Sort EPG data by source priority (highest first) so we prefer higher priority matches + epg_data.sort(key=lambda x: x['epg_source_priority'], reverse=True) + + logger.info(f"Processing {len(channels_data)} selected channels against {len(epg_data)} EPG entries (from active sources only)") # Run EPG matching with progress updates - automatically uses appropriate thresholds result = match_channels_to_epg(channels_data, epg_data, region_code, use_ml=True, send_progress=True) @@ -749,9 +761,10 @@ def match_single_channel_epg(channel_id): test_normalized = normalize_name(test_name) logger.debug(f"DEBUG normalization example: '{test_name}' → '{test_normalized}' (call sign preserved)") - # Get all EPG data for matching - must include norm_name field + # Get all EPG data for matching from active sources - must include norm_name field + # Ordered by source priority (highest first) so we prefer higher priority matches epg_data_list = [] - for epg in EPGData.objects.filter(name__isnull=False).exclude(name=''): + for epg in EPGData.objects.select_related('epg_source').filter(epg_source__is_active=True, name__isnull=False).exclude(name=''): normalized_epg_tvg_id = epg.tvg_id.strip().lower() if epg.tvg_id else "" epg_data_list.append({ 'id': epg.id, @@ -760,10 +773,14 @@ def match_single_channel_epg(channel_id): 'name': epg.name, 'norm_name': normalize_name(epg.name), 'epg_source_id': epg.epg_source.id if epg.epg_source else None, + 'epg_source_priority': epg.epg_source.priority if epg.epg_source else 0, }) + # Sort EPG data by source priority (highest first) so we prefer higher priority matches + epg_data_list.sort(key=lambda x: x['epg_source_priority'], reverse=True) + if not epg_data_list: - return {"matched": False, "message": "No EPG data available for matching"} + return {"matched": False, "message": "No EPG data available for matching (from active sources)"} logger.info(f"Matching single channel '{channel.name}' against {len(epg_data_list)} EPG entries") diff --git a/apps/epg/migrations/0021_epgsource_priority.py b/apps/epg/migrations/0021_epgsource_priority.py new file mode 100644 index 00000000..f2696d67 --- /dev/null +++ b/apps/epg/migrations/0021_epgsource_priority.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-12-05 15:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('epg', '0020_migrate_time_to_starttime_placeholders'), + ] + + operations = [ + migrations.AddField( + model_name='epgsource', + name='priority', + field=models.PositiveIntegerField(default=0, help_text='Priority for EPG matching (higher numbers = higher priority). Used when multiple EPG sources have matching entries for a channel.'), + ), + ] diff --git a/apps/epg/models.py b/apps/epg/models.py index e5f3847b..b3696edc 100644 --- a/apps/epg/models.py +++ b/apps/epg/models.py @@ -45,6 +45,10 @@ class EPGSource(models.Model): null=True, help_text="Custom properties for dummy EPG configuration (regex patterns, timezone, duration, etc.)" ) + priority = models.PositiveIntegerField( + default=0, + help_text="Priority for EPG matching (higher numbers = higher priority). Used when multiple EPG sources have matching entries for a channel." + ) status = models.CharField( max_length=20, choices=STATUS_CHOICES, diff --git a/apps/epg/serializers.py b/apps/epg/serializers.py index bfb750fc..e4d5f466 100644 --- a/apps/epg/serializers.py +++ b/apps/epg/serializers.py @@ -24,6 +24,7 @@ class EPGSourceSerializer(serializers.ModelSerializer): 'is_active', 'file_path', 'refresh_interval', + 'priority', 'status', 'last_message', 'created_at', diff --git a/frontend/src/components/forms/EPG.jsx b/frontend/src/components/forms/EPG.jsx index db4f8310..50c8553c 100644 --- a/frontend/src/components/forms/EPG.jsx +++ b/frontend/src/components/forms/EPG.jsx @@ -29,6 +29,7 @@ const EPG = ({ epg = null, isOpen, onClose }) => { api_key: '', is_active: true, refresh_interval: 24, + priority: 0, }, validate: { @@ -69,6 +70,7 @@ const EPG = ({ epg = null, isOpen, onClose }) => { api_key: epg.api_key, is_active: epg.is_active, refresh_interval: epg.refresh_interval, + priority: epg.priority ?? 0, }; form.setValues(values); setSourceType(epg.source_type); @@ -148,14 +150,24 @@ const EPG = ({ epg = null, isOpen, onClose }) => { key={form.key('url')} /> - + )} + + {/* Put checkbox at the same level as Refresh Interval */} From f3a901cb3a50f16f104598ec615cd6bd1a2ffc35 Mon Sep 17 00:00:00 2001 From: dekzter Date: Sat, 6 Dec 2025 13:40:10 -0500 Subject: [PATCH 06/20] Security Fix - generate JWT on application init --- dispatcharr/settings.py | 2 +- docker/entrypoint.sh | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/dispatcharr/settings.py b/dispatcharr/settings.py index d6c29dd9..5f8c23e2 100644 --- a/dispatcharr/settings.py +++ b/dispatcharr/settings.py @@ -4,7 +4,7 @@ from datetime import timedelta BASE_DIR = Path(__file__).resolve().parent.parent -SECRET_KEY = "REPLACE_ME_WITH_A_REAL_SECRET" +SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_DB = os.environ.get("REDIS_DB", "0") diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index fa0eea01..9c3ec88c 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -40,6 +40,22 @@ export REDIS_DB=${REDIS_DB:-0} 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' +export SECRET_FILE="/data/jwt" + +if [ ! -f "$SECRET_FILE" ]; then + umask 077 + tmpfile="$(mktemp "${SECRET_FILE}.XXXXXX")" || { echo "mktemp failed"; exit 1; } + python3 - <<'PY' >"$tmpfile" || { echo "secret generation failed"; rm -f "$tmpfile"; exit 1; } +import secrets +print(secrets.token_urlsafe(64)) +PY + mv -f "$tmpfile" "$SECRET_FILE" || { echo "move failed"; rm -f "$tmpfile"; exit 1; } +fi + +chown $PUID:$PGID "$SECRET_FILE" || true +chmod 600 "$SECRET_FILE" || true + +export DJANGO_SECRET_KEY="$(cat "$SECRET_FILE")" # Process priority configuration # UWSGI_NICE_LEVEL: Absolute nice value for uWSGI/streaming (default: 0 = normal priority) @@ -90,7 +106,7 @@ if [[ ! -f /etc/profile.d/dispatcharr.sh ]]; then DISPATCHARR_ENV DISPATCHARR_DEBUG DISPATCHARR_LOG_LEVEL REDIS_HOST REDIS_DB POSTGRES_DIR DISPATCHARR_PORT DISPATCHARR_VERSION DISPATCHARR_TIMESTAMP LIBVA_DRIVERS_PATH LIBVA_DRIVER_NAME LD_LIBRARY_PATH - CELERY_NICE_LEVEL UWSGI_NICE_LEVEL + CELERY_NICE_LEVEL UWSGI_NICE_LEVEL DJANGO_SECRET_KEY ) # Process each variable for both profile.d and environment From 10f329d67380eca9a619d1208d51ab88920c9601 Mon Sep 17 00:00:00 2001 From: dekzter Date: Sat, 6 Dec 2025 13:42:48 -0500 Subject: [PATCH 07/20] release notes for built --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f082eb8..f9b7b450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- JWT token generated so is unique for each deployment + ## [0.13.0] - 2025-12-02 ### Added From a9120552551860b40ecff123fc68fc3a803234cc Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Sat, 6 Dec 2025 18:43:16 +0000 Subject: [PATCH 08/20] Release v0.13.1 --- CHANGELOG.md | 2 ++ version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9b7b450..bf381879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.13.1] - 2025-12-06 + ### Fixed - JWT token generated so is unique for each deployment diff --git a/version.py b/version.py index b27fed86..f017df85 100644 --- a/version.py +++ b/version.py @@ -1,5 +1,5 @@ """ Dispatcharr version information. """ -__version__ = '0.13.0' # Follow semantic versioning (MAJOR.MINOR.PATCH) +__version__ = '0.13.1' # Follow semantic versioning (MAJOR.MINOR.PATCH) __timestamp__ = None # Set during CI/CD build process From d0edc3fa072f726b3f6a6117a1ea16b38f8eeda3 Mon Sep 17 00:00:00 2001 From: dekzter Date: Sun, 7 Dec 2025 07:54:30 -0500 Subject: [PATCH 09/20] remove permission lines to see if this resolves lack of django secret key in environment profile.d --- docker/entrypoint.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 9c3ec88c..df1584b0 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -52,9 +52,6 @@ PY mv -f "$tmpfile" "$SECRET_FILE" || { echo "move failed"; rm -f "$tmpfile"; exit 1; } fi -chown $PUID:$PGID "$SECRET_FILE" || true -chmod 600 "$SECRET_FILE" || true - export DJANGO_SECRET_KEY="$(cat "$SECRET_FILE")" # Process priority configuration From 3512c3a6233844b41ed2c8132bb0fdcfdf3f3740 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sun, 7 Dec 2025 19:05:31 -0600 Subject: [PATCH 10/20] Add DJANGO_SECRET_KEY environment variable to uwsgi configuration files --- docker/uwsgi.debug.ini | 2 +- docker/uwsgi.dev.ini | 1 + docker/uwsgi.ini | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/uwsgi.debug.ini b/docker/uwsgi.debug.ini index 3de890a5..1d7cca93 100644 --- a/docker/uwsgi.debug.ini +++ b/docker/uwsgi.debug.ini @@ -20,7 +20,7 @@ module = scripts.debug_wrapper:application virtualenv = /dispatcharrpy master = true env = DJANGO_SETTINGS_MODULE=dispatcharr.settings - +env = DJANGO_SECRET_KEY=$(DJANGO_SECRET_KEY) socket = /app/uwsgi.sock chmod-socket = 777 vacuum = true diff --git a/docker/uwsgi.dev.ini b/docker/uwsgi.dev.ini index e476e216..1ef9709e 100644 --- a/docker/uwsgi.dev.ini +++ b/docker/uwsgi.dev.ini @@ -22,6 +22,7 @@ module = dispatcharr.wsgi:application virtualenv = /dispatcharrpy master = true env = DJANGO_SETTINGS_MODULE=dispatcharr.settings +env = DJANGO_SECRET_KEY=$(DJANGO_SECRET_KEY) socket = /app/uwsgi.sock chmod-socket = 777 vacuum = true diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini index f8fe8ab7..bb359b06 100644 --- a/docker/uwsgi.ini +++ b/docker/uwsgi.ini @@ -21,6 +21,7 @@ module = dispatcharr.wsgi:application virtualenv = /dispatcharrpy master = true env = DJANGO_SETTINGS_MODULE=dispatcharr.settings +env = DJANGO_SECRET_KEY=$(DJANGO_SECRET_KEY) socket = /app/uwsgi.sock chmod-socket = 777 vacuum = true From cf37c6fd9869e8210589a8a99331da702030c2db Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sun, 7 Dec 2025 19:06:45 -0600 Subject: [PATCH 11/20] changelog: Updated changelog for 0.13.1 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb8324f1..90db90c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- JWT token generated so is unique for each deployment + ## [0.13.0] - 2025-12-02 ### Added From 2155229d7f0ce6ad079e3b802588822f1ef1b6b4 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sun, 7 Dec 2025 19:40:32 -0600 Subject: [PATCH 12/20] Fix uwsgi command path in entrypoint script --- docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index df1584b0..088bcd1e 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -200,7 +200,7 @@ fi # Users can override via UWSGI_NICE_LEVEL environment variable in docker-compose # Start with nice as root, then use setpriv to drop privileges to dispatch user # This preserves both the nice value and environment variables -nice -n $UWSGI_NICE_LEVEL su -p - "$POSTGRES_USER" -c "cd /app && exec uwsgi $uwsgi_args" & uwsgi_pid=$! +nice -n $UWSGI_NICE_LEVEL su - "$POSTGRES_USER" -c "cd /app && exec /dispatcharrpy/bin/uwsgi $uwsgi_args" & uwsgi_pid=$! echo "✅ uwsgi started with PID $uwsgi_pid (nice $UWSGI_NICE_LEVEL)" pids+=("$uwsgi_pid") From e2736babaae4db7393560f7c1ae50d49a611baf9 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sun, 7 Dec 2025 20:04:58 -0600 Subject: [PATCH 13/20] Reset umask after creating secret file. --- docker/entrypoint.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 088bcd1e..72eb5928 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -41,8 +41,10 @@ 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' export SECRET_FILE="/data/jwt" - +# Ensure Django secret key exists or generate a new one if [ ! -f "$SECRET_FILE" ]; then + echo "Generating new Django secret key..." + old_umask=$(umask) umask 077 tmpfile="$(mktemp "${SECRET_FILE}.XXXXXX")" || { echo "mktemp failed"; exit 1; } python3 - <<'PY' >"$tmpfile" || { echo "secret generation failed"; rm -f "$tmpfile"; exit 1; } @@ -50,8 +52,8 @@ import secrets print(secrets.token_urlsafe(64)) PY mv -f "$tmpfile" "$SECRET_FILE" || { echo "move failed"; rm -f "$tmpfile"; exit 1; } + umask $old_umask fi - export DJANGO_SECRET_KEY="$(cat "$SECRET_FILE")" # Process priority configuration From ce70b04097cb5fa0e52f500035fad4f7dcab73f5 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sun, 7 Dec 2025 20:56:59 -0600 Subject: [PATCH 14/20] changelog: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90db90c6..8efdc30c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - EPG table "Updated" column now updates in real-time via WebSocket using the actual backend timestamp instead of requiring a page refresh - Bulk channel editor confirmation dialog now displays the correct stream profile name that will be applied to the selected channels. +- uWSGI not found and 502 bad gateway on first startup ## [0.13.1] - 2025-12-06 From c03ddf60a09175631e868bd5d647ba1484426ad2 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sun, 7 Dec 2025 21:28:04 -0600 Subject: [PATCH 15/20] Fixed verbiage for epg parsing status. --- apps/epg/tasks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py index c565dbf5..bd78c6a3 100644 --- a/apps/epg/tasks.py +++ b/apps/epg/tasks.py @@ -1650,7 +1650,7 @@ def parse_programs_for_source(epg_source, tvg_id=None): epg_source.status = EPGSource.STATUS_SUCCESS epg_source.last_message = ( f"Parsed {total_programs:,} programs for {channels_with_programs} channels " - f"(skipped {skipped_programs:,} programmes for {total_epg_count - mapped_count} unmapped channels)" + f"(skipped {skipped_programs:,} programs for {total_epg_count - mapped_count} unmapped channels)" ) epg_source.updated_at = timezone.now() epg_source.save(update_fields=['status', 'last_message', 'updated_at']) @@ -1672,8 +1672,8 @@ def parse_programs_for_source(epg_source, tvg_id=None): updated_at=epg_source.updated_at.isoformat()) logger.info(f"Completed parsing programs for source: {epg_source.name} - " - f"{total_programs:,} programs for {channels_with_programs} channels, " - f"skipped {skipped_programs:,} programmes for unmapped channels") + f"{total_programs:,} programs for {channels_with_programs} channels, " + f"skipped {skipped_programs:,} programs for unmapped channels") return True except Exception as e: From 62f5c32609eb0c082c72897a9871559e69831d78 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Mon, 8 Dec 2025 17:27:07 -0600 Subject: [PATCH 16/20] Remove DJANGO_SECRET_KEY environment variable from uwsgi configuration files --- docker/uwsgi.debug.ini | 1 - docker/uwsgi.dev.ini | 1 - docker/uwsgi.ini | 1 - 3 files changed, 3 deletions(-) diff --git a/docker/uwsgi.debug.ini b/docker/uwsgi.debug.ini index 1d7cca93..69c040f2 100644 --- a/docker/uwsgi.debug.ini +++ b/docker/uwsgi.debug.ini @@ -20,7 +20,6 @@ module = scripts.debug_wrapper:application virtualenv = /dispatcharrpy master = true env = DJANGO_SETTINGS_MODULE=dispatcharr.settings -env = DJANGO_SECRET_KEY=$(DJANGO_SECRET_KEY) socket = /app/uwsgi.sock chmod-socket = 777 vacuum = true diff --git a/docker/uwsgi.dev.ini b/docker/uwsgi.dev.ini index 1ef9709e..e476e216 100644 --- a/docker/uwsgi.dev.ini +++ b/docker/uwsgi.dev.ini @@ -22,7 +22,6 @@ module = dispatcharr.wsgi:application virtualenv = /dispatcharrpy master = true env = DJANGO_SETTINGS_MODULE=dispatcharr.settings -env = DJANGO_SECRET_KEY=$(DJANGO_SECRET_KEY) socket = /app/uwsgi.sock chmod-socket = 777 vacuum = true diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini index bb359b06..f8fe8ab7 100644 --- a/docker/uwsgi.ini +++ b/docker/uwsgi.ini @@ -21,7 +21,6 @@ module = dispatcharr.wsgi:application virtualenv = /dispatcharrpy master = true env = DJANGO_SETTINGS_MODULE=dispatcharr.settings -env = DJANGO_SECRET_KEY=$(DJANGO_SECRET_KEY) socket = /app/uwsgi.sock chmod-socket = 777 vacuum = true From 98b29f97a1df25395cf72cbb9612c54c06367870 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Mon, 8 Dec 2025 17:49:40 -0600 Subject: [PATCH 17/20] changelog: Update verbiage --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8efdc30c..347bb22a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **Performance**: EPG program parsing optimized for sources with many channels but only a fraction mapped. Now parses XML file once per source instead of once per channel, dramatically reducing I/O and CPU overhead. For sources with 10,000 channels and 100 mapped, this results in ~99x fewer file opens and ~100x fewer full file scans. Orphaned programs for unmapped channels are also cleaned up during refresh to prevent database bloat. Database updates are now atomic to prevent clients from seeing empty/partial EPG data during refresh. +- EPG program parsing optimized for sources with many channels but only a fraction mapped. Now parses XML file once per source instead of once per channel, dramatically reducing I/O and CPU overhead. For sources with 10,000 channels and 100 mapped, this results in ~99x fewer file opens and ~100x fewer full file scans. Orphaned programs for unmapped channels are also cleaned up during refresh to prevent database bloat. Database updates are now atomic to prevent clients from seeing empty/partial EPG data during refresh. - EPG table now displays detailed status messages including refresh progress, success messages, and last message for idle sources (matching M3U table behavior) (Closes #214) - IPv6 access now allowed by default with all IPv6 CIDRs accepted - Thanks [@adrianmace](https://github.com/adrianmace) - nginx.conf updated to bind to both IPv4 and IPv6 ports - Thanks [@jordandalley](https://github.com/jordandalley) From 4df4e5f963606c66c137cc1b969355cdcf47e2bd Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Tue, 9 Dec 2025 00:01:50 +0000 Subject: [PATCH 18/20] Release v0.14.0 --- CHANGELOG.md | 2 ++ version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 347bb22a..4716c250 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.14.0] - 2025-12-09 + ### Added - Sort buttons for 'Group' and 'M3U' columns in Streams table for improved stream organization and filtering - Thanks [@bobey6](https://github.com/bobey6) diff --git a/version.py b/version.py index f017df85..807fc629 100644 --- a/version.py +++ b/version.py @@ -1,5 +1,5 @@ """ Dispatcharr version information. """ -__version__ = '0.13.1' # Follow semantic versioning (MAJOR.MINOR.PATCH) +__version__ = '0.14.0' # Follow semantic versioning (MAJOR.MINOR.PATCH) __timestamp__ = None # Set during CI/CD build process From 69f9ecd93c7868f57fd9e58c8339cc7adef969b9 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Mon, 8 Dec 2025 20:12:44 -0600 Subject: [PATCH 19/20] Bug Fix: Remove ipv6 binding from nginx config if ipv6 is not available. --- CHANGELOG.md | 4 ++++ docker/init/03-init-dispatcharr.sh | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4716c250..389bb8ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- nginx now gracefully handles hosts without IPv6 support by automatically disabling IPv6 binding at startup + ## [0.14.0] - 2025-12-09 ### Added diff --git a/docker/init/03-init-dispatcharr.sh b/docker/init/03-init-dispatcharr.sh index 5fbef23d..da7d4484 100644 --- a/docker/init/03-init-dispatcharr.sh +++ b/docker/init/03-init-dispatcharr.sh @@ -29,9 +29,17 @@ if [ "$(id -u)" = "0" ] && [ -d "/app" ]; then chown $PUID:$PGID /app fi fi - +# Configure nginx port sed -i "s/NGINX_PORT/${DISPATCHARR_PORT}/g" /etc/nginx/sites-enabled/default +# Configure nginx based on IPv6 availability +if ip -6 addr show | grep -q "inet6"; then + echo "✅ IPv6 is available, enabling IPv6 in nginx" +else + echo "⚠️ IPv6 not available, disabling IPv6 in nginx" + sed -i '/listen \[::\]:/d' /etc/nginx/sites-enabled/default +fi + # NOTE: mac doesn't run as root, so only manage permissions # if this script is running as root if [ "$(id -u)" = "0" ]; then From 514e7e06e4dfcdb8d24ed0eddfd3cf67cc2a7a49 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Mon, 8 Dec 2025 20:50:50 -0600 Subject: [PATCH 20/20] Bug fix: EPG API now returns correct date/time format for start/end fields and proper string types for timestamps and channel_id --- CHANGELOG.md | 1 + apps/output/views.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 389bb8ad..e363135f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - nginx now gracefully handles hosts without IPv6 support by automatically disabling IPv6 binding at startup +- XtreamCodes EPG API now returns correct date/time format for start/end fields and proper string types for timestamps and channel_id ## [0.14.0] - 2025-12-09 diff --git a/apps/output/views.py b/apps/output/views.py index bc2bace5..3a8406cb 100644 --- a/apps/output/views.py +++ b/apps/output/views.py @@ -2316,18 +2316,18 @@ def xc_get_epg(request, user, short=False): "epg_id": f"{epg_id}", "title": base64.b64encode(title.encode()).decode(), "lang": "", - "start": start.strftime("%Y%m%d%H%M%S"), - "end": end.strftime("%Y%m%d%H%M%S"), + "start": start.strftime("%Y-%m-%d %H:%M:%S"), + "end": end.strftime("%Y-%m-%d %H:%M:%S"), "description": base64.b64encode(description.encode()).decode(), - "channel_id": channel_num_int, - "start_timestamp": int(start.timestamp()), - "stop_timestamp": int(end.timestamp()), + "channel_id": str(channel_num_int), + "start_timestamp": str(int(start.timestamp())), + "stop_timestamp": str(int(end.timestamp())), "stream_id": f"{channel_id}", } if short == False: program_output["now_playing"] = 1 if start <= django_timezone.now() <= end else 0 - program_output["has_archive"] = "0" + program_output["has_archive"] = 0 output['epg_listings'].append(program_output)