From 69e0cba86ef21d866bf255b046fc4c8756f79f36 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 21 Apr 2026 09:23:35 -0500 Subject: [PATCH] Bug Fix: EPG channel name vlaue too long for type character varying(255) (Fixes #1134) --- CHANGELOG.md | 4 ++++ ...35_alter_channel_name_alter_stream_name.py | 23 +++++++++++++++++++ apps/channels/models.py | 4 ++-- .../epg/migrations/0022_alter_epgdata_name.py | 18 +++++++++++++++ apps/epg/models.py | 2 +- apps/epg/tasks.py | 5 ++++ apps/m3u/tasks.py | 7 ++++++ 7 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 apps/channels/migrations/0035_alter_channel_name_alter_stream_name.py create mode 100644 apps/epg/migrations/0022_alter_epgdata_name.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 728d9108..6bfa2934 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 + +- **EPG channel name truncation**: EPG sources that include long `` values (e.g. event-based channels with descriptions appended to the name) would crash the channel-parse task with a `value too long for type character varying(255)` PostgreSQL error and silently discard the entire batch. The `EPGData.name`, `Stream.name`, and `Channel.name` fields have been widened to 512 characters, and names exceeding this limit are now truncated with a warning log rather than aborting the import. (Fixes #1134) + ### Added - **Plugin warning & disclaimer components**: extracted shared plugin warning UI into a new `PluginWarnings.jsx` component and normalized warning/disclaimer usage across all plugin action modals. New reusable components: `PluginSecurityWarning` (untrusted code), `PluginSupportDisclaimer` (community support scope), `PluginDowngradeWarning` (version downgrade), `PluginInfoNote` (informational), and `PluginRestartWarning` (backend restart on import). Updated `Plugins.jsx`, `AvailablePluginCard.jsx`, and `PluginCard.jsx` to use the shared components. — Thanks [@sethwv](https://github.com/sethwv) diff --git a/apps/channels/migrations/0035_alter_channel_name_alter_stream_name.py b/apps/channels/migrations/0035_alter_channel_name_alter_stream_name.py new file mode 100644 index 00000000..21906b87 --- /dev/null +++ b/apps/channels/migrations/0035_alter_channel_name_alter_stream_name.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.4 on 2026-04-21 14:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dispatcharr_channels', '0034_remove_stream_dispatcharr_stream_id_idx_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='channel', + name='name', + field=models.CharField(max_length=512), + ), + migrations.AlterField( + model_name='stream', + name='name', + field=models.CharField(default='Default Stream', max_length=512), + ), + ] diff --git a/apps/channels/models.py b/apps/channels/models.py index 51629db3..1c989855 100644 --- a/apps/channels/models.py +++ b/apps/channels/models.py @@ -56,7 +56,7 @@ class Stream(models.Model): Represents a single stream (e.g. from an M3U source or custom URL). """ - name = models.CharField(max_length=255, default="Default Stream") + name = models.CharField(max_length=512, default="Default Stream") url = models.URLField(max_length=4096, blank=True, null=True) m3u_account = models.ForeignKey( M3UAccount, @@ -294,7 +294,7 @@ class ChannelManager(models.Manager): class Channel(models.Model): channel_number = models.FloatField(db_index=True) - name = models.CharField(max_length=255) + name = models.CharField(max_length=512) logo = models.ForeignKey( "Logo", on_delete=models.SET_NULL, diff --git a/apps/epg/migrations/0022_alter_epgdata_name.py b/apps/epg/migrations/0022_alter_epgdata_name.py new file mode 100644 index 00000000..3da56637 --- /dev/null +++ b/apps/epg/migrations/0022_alter_epgdata_name.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.4 on 2026-04-21 14:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('epg', '0021_epgsource_priority'), + ] + + operations = [ + migrations.AlterField( + model_name='epgdata', + name='name', + field=models.CharField(max_length=512), + ), + ] diff --git a/apps/epg/models.py b/apps/epg/models.py index b3696edc..d5758ce2 100644 --- a/apps/epg/models.py +++ b/apps/epg/models.py @@ -137,7 +137,7 @@ class EPGData(models.Model): # Removed the Channel foreign key. We now just store the original tvg_id # and a name (which might simply be the tvg_id if no real channel exists). tvg_id = models.CharField(max_length=255, null=True, blank=True, db_index=True) - name = models.CharField(max_length=255) + name = models.CharField(max_length=512) icon_url = models.URLField(max_length=500, null=True, blank=True) epg_source = models.ForeignKey( EPGSource, diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py index 7700ad1b..8efe3618 100644 --- a/apps/epg/tasks.py +++ b/apps/epg/tasks.py @@ -966,6 +966,7 @@ def parse_channels_only(source): batch_size = 500 # Process in batches to limit memory usage progress = 0 # Initialize progress variable here icon_url_max_length = EPGData._meta.get_field('icon_url').max_length # Get max length for icon_url field + name_max_length = EPGData._meta.get_field('name').max_length # Get max length for name field # Track memory at key points if process: @@ -1022,6 +1023,10 @@ def parse_channels_only(source): if not display_name: display_name = tvg_id + if display_name and len(display_name) > name_max_length: + logger.warning(f"EPG display name too long ({len(display_name)} > {name_max_length}), truncating: {display_name[:80]}...") + display_name = display_name[:name_max_length] + # Use lazy loading approach to reduce memory usage if tvg_id in existing_tvg_ids: # Only fetch the object if we need to update it and it hasn't been loaded yet diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index 8f446b6e..e724e655 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -1087,6 +1087,8 @@ def process_m3u_batch_direct(account_id, batch, groups, hash_keys): streams_to_update = [] stream_hashes = {} + name_max_length = Stream._meta.get_field('name').max_length + logger.debug(f"Processing batch of {len(batch)} for M3U account {account_id}") if compiled_filters: logger.debug(f"Using compiled filters: {[f[1].regex_pattern for f in compiled_filters]}") @@ -1099,6 +1101,11 @@ def process_m3u_batch_direct(account_id, batch, groups, hash_keys): logger.warning(f"Skipping stream '{name}': URL too long ({len(url)} characters, max 4096)") continue + # Truncate name if it exceeds the model field limit + if name and len(name) > name_max_length: + logger.warning(f"Stream name too long ({len(name)} > {name_max_length}), truncating: {name[:80]}...") + name = name[:name_max_length] + tvg_id, tvg_logo = get_case_insensitive_attr( stream_info["attributes"], "tvg-id", "" ), get_case_insensitive_attr(stream_info["attributes"], "tvg-logo", "")