From 03b8bfc31ed7178e40d30114b16476b87fc25170 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 13:11:31 +0000 Subject: [PATCH 1/3] Add emojis, fix visibility logic, and add WebSocket notifications Changes: - Add tasteful emojis to plugin GUI fields and action labels - Fix bug: iterate sorted_channels instead of group_channels (line 1411) - Fix visibility logic: enable channels with >= 1 streams instead of 0-1 * Channels with >= 1 streams are now enabled (highest priority only) * Channels with 0 streams are disabled * Duplicate channels (lower priority in group) are disabled * Attached channels remain disabled - Add real-time WebSocket progress notifications for: * Preview Changes action (stream_mapparr_preview) * Add Streams to Channels action (stream_mapparr_add) * Manage Channel Visibility action (stream_mapparr_visibility) - Update action descriptions to reflect corrected behavior This resolves the contradiction where add_streams_to_channels would create channels with multiple streams, and manage_channel_visibility would immediately disable them. --- Stream-Mapparr/plugin.py | 207 +++++++++++++++++++++++++++++++-------- 1 file changed, 166 insertions(+), 41 deletions(-) diff --git a/Stream-Mapparr/plugin.py b/Stream-Mapparr/plugin.py index c7806f2..9d84aa0 100644 --- a/Stream-Mapparr/plugin.py +++ b/Stream-Mapparr/plugin.py @@ -18,6 +18,9 @@ from apps.channels.models import Channel, Stream, ChannelStream, ChannelProfileM # Import fuzzy matcher from .fuzzy_matcher import FuzzyMatcher +# Import WebSocket update function +from core.utils import send_websocket_update + # Setup logging using Dispatcharr's format LOGGER = logging.getLogger("plugins.stream_mapparr") if not LOGGER.handlers: @@ -32,27 +35,27 @@ class Plugin: name = "Stream-Mapparr" version = "0.5.0a" - description = "Automatically add matching streams to channels based on name similarity and quality precedence with enhanced fuzzy matching" + description = "🎯 Automatically add matching streams to channels based on name similarity and quality precedence with enhanced fuzzy matching" # Settings rendered by UI fields = [ { "id": "overwrite_streams", - "label": "Overwrite Existing Streams", + "label": "🔄 Overwrite Existing Streams", "type": "boolean", "default": True, "help_text": "If enabled, all existing streams will be removed and replaced with matched streams. If disabled, only new streams will be added (existing streams preserved).", }, { "id": "fuzzy_match_threshold", - "label": "Fuzzy Match Threshold", + "label": "🎯 Fuzzy Match Threshold", "type": "number", "default": 85, "help_text": "Minimum similarity score (0-100) for fuzzy matching. Higher values require closer matches. Default: 85", }, { "id": "dispatcharr_url", - "label": "Dispatcharr URL", + "label": "🌐 Dispatcharr URL", "type": "string", "default": "", "placeholder": "http://192.168.1.10:9191", @@ -60,20 +63,20 @@ class Plugin: }, { "id": "dispatcharr_username", - "label": "Dispatcharr Admin Username", + "label": "👤 Dispatcharr Admin Username", "type": "string", "help_text": "Your admin username for the Dispatcharr UI. Required for API access.", }, { "id": "dispatcharr_password", - "label": "Dispatcharr Admin Password", + "label": "🔑 Dispatcharr Admin Password", "type": "string", "input_type": "password", "help_text": "Your admin password for the Dispatcharr UI. Required for API access.", }, { "id": "profile_name", - "label": "Profile Name", + "label": "📋 Profile Name", "type": "string", "default": "", "placeholder": "Sports", @@ -81,7 +84,7 @@ class Plugin: }, { "id": "selected_groups", - "label": "Channel Groups (comma-separated)", + "label": "📁 Channel Groups (comma-separated)", "type": "string", "default": "", "placeholder": "Sports, News, Entertainment", @@ -89,7 +92,7 @@ class Plugin: }, { "id": "ignore_tags", - "label": "Ignore Tags (comma-separated)", + "label": "🏷️ Ignore Tags (comma-separated)", "type": "string", "default": "", "placeholder": "4K, [4K], [Dead]", @@ -97,7 +100,7 @@ class Plugin: }, { "id": "visible_channel_limit", - "label": "Visible Channel Limit", + "label": "👁️ Visible Channel Limit", "type": "number", "default": 1, "help_text": "Number of channels that will be visible and have streams added. Channels are prioritized by quality tags, then by channel number.", @@ -108,17 +111,17 @@ class Plugin: actions = [ { "id": "load_process_channels", - "label": "Load/Process Channels", + "label": "📥 Load/Process Channels", "description": "Validate settings and load channels from the specified profile and groups", }, { "id": "preview_changes", - "label": "Preview Changes (Dry Run)", + "label": "👀 Preview Changes (Dry Run)", "description": "Preview which streams will be added to channels without making changes", }, { "id": "add_streams_to_channels", - "label": "Add Stream(s) to Channels", + "label": "✅ Add Stream(s) to Channels", "description": "Add matching streams to channels and replace existing stream assignments", "confirm": { "required": True, @@ -128,17 +131,17 @@ class Plugin: }, { "id": "manage_channel_visibility", - "label": "Manage Channel Visibility", - "description": "Disable all channels, then enable only channels with 0 or 1 stream (excluding channels attached to others)", + "label": "👁️ Manage Channel Visibility", + "description": "Disable all channels, then enable only channels with 1 or more streams (excluding channels attached to others)", "confirm": { "required": True, "title": "Manage Channel Visibility?", - "message": "This will disable ALL channels in the profile, then enable only channels with 0 or 1 stream that are not attached to other channels. Continue?" + "message": "This will disable ALL channels in the profile, then enable only channels with 1 or more streams that are not attached to other channels. Continue?" } }, { "id": "clear_csv_exports", - "label": "Clear CSV Exports", + "label": "🗑️ Clear CSV Exports", "description": "Delete all CSV export files created by this plugin", "confirm": { "required": True, @@ -908,7 +911,17 @@ class Plugin: logger.info(f"[Stream-Mapparr] Previewing changes for {len(channels)} channels with {len(streams)} available streams") logger.info(f"[Stream-Mapparr] Visible channel limit: {visible_channel_limit}") - + + # Send initial WebSocket notification + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_preview', + 'stage': 'starting', + 'total': len(channels), + 'matched': 0, + 'progress_percent': 0, + 'message': f'Starting preview for {len(channels)} channels...' + }) + # Group channels by their cleaned name for matching channel_groups = {} ignore_tags = processed_data.get('ignore_tags', []) @@ -936,16 +949,28 @@ class Plugin: total_groups = len(channel_groups) current_group = 0 - + for group_key, group_channels in channel_groups.items(): current_group += 1 progress_pct = int((current_group / total_groups) * 100) - + logger.info(f"[Stream-Mapparr] [{progress_pct}%] Processing channel group: {group_key} ({len(group_channels)} channels)") - + + # Send progress WebSocket notification + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_preview', + 'stage': 'matching', + 'total': total_groups, + 'current': current_group, + 'matched': total_channels_with_matches, + 'progress_percent': progress_pct, + 'current_group': group_key, + 'message': f'Processing {current_group}/{total_groups} channel groups ({progress_pct}%)...' + }) + # Sort channels in this group by priority sorted_channels = self._sort_channels_by_priority(group_channels) - + # Match streams for this channel group (using first channel as representative) matched_streams, cleaned_channel_name, cleaned_stream_names, match_reason = self._match_streams_to_channel( sorted_channels[0], streams, logger, ignore_tags, channels_data @@ -992,7 +1017,17 @@ class Plugin: all_matches.append(match_info) logger.info(f"[Stream-Mapparr] [100%] Preview processing complete") - + + # Send completion WebSocket notification + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_preview', + 'stage': 'completed', + 'total': len(channels), + 'matched': total_channels_with_matches, + 'progress_percent': 100, + 'message': f'Preview complete: {total_channels_with_matches} channels matched' + }) + # Export to CSV timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"stream_mapparr_preview_{timestamp}.csv" @@ -1101,7 +1136,17 @@ class Plugin: logger.info(f"[Stream-Mapparr] Adding streams to {len(channels)} channels") logger.info(f"[Stream-Mapparr] Visible channel limit: {visible_channel_limit}") logger.info(f"[Stream-Mapparr] Overwrite existing streams: {overwrite_streams}") - + + # Send initial WebSocket notification + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_add', + 'stage': 'starting', + 'total': len(channels), + 'updated': 0, + 'progress_percent': 0, + 'message': f'Starting to add streams to {len(channels)} channels...' + }) + # Group channels by their cleaned name channel_groups = {} for channel in channels: @@ -1133,12 +1178,24 @@ class Plugin: for group_key, group_channels in channel_groups.items(): current_group += 1 progress_pct = int((current_group / total_groups) * 100) - + logger.info(f"[Stream-Mapparr] [{progress_pct}%] Processing channel group: {group_key} ({len(group_channels)} channels)") - + + # Send progress WebSocket notification + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_add', + 'stage': 'processing', + 'total': total_groups, + 'current': current_group, + 'updated': channels_updated, + 'progress_percent': progress_pct, + 'current_group': group_key, + 'message': f'Processing {current_group}/{total_groups} channel groups ({progress_pct}%)...' + }) + # Sort channels in this group by priority sorted_channels = self._sort_channels_by_priority(group_channels) - + # Match streams for this channel group matched_streams, cleaned_channel_name, cleaned_stream_names, match_reason = self._match_streams_to_channel( sorted_channels[0], streams, logger, ignore_tags, channels_data @@ -1227,7 +1284,18 @@ class Plugin: channels_skipped += 1 logger.info(f"[Stream-Mapparr] [100%] Processing complete") - + + # Send completion WebSocket notification + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_add', + 'stage': 'completed', + 'total': len(channels), + 'updated': channels_updated, + 'streams_added': total_streams_added, + 'progress_percent': 100, + 'message': f'Complete: {channels_updated} channels updated with {total_streams_added} streams' + }) + # Trigger frontend refresh self._trigger_frontend_refresh(settings, logger) @@ -1310,9 +1378,25 @@ class Plugin: logger.info(f"[Stream-Mapparr] Managing visibility for {len(channels)} channels") logger.info(f"[Stream-Mapparr] Visible channel limit: {visible_channel_limit}") - + + # Send initial WebSocket notification + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_visibility', + 'stage': 'starting', + 'total': len(channels), + 'progress_percent': 0, + 'message': f'Starting visibility management for {len(channels)} channels...' + }) + # Step 1: Get stream counts for all channels logger.info("[Stream-Mapparr] Step 1: Counting streams for each channel...") + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_visibility', + 'stage': 'counting', + 'total': len(channels), + 'progress_percent': 10, + 'message': 'Counting streams for each channel...' + }) channel_stream_counts = {} for channel in channels: @@ -1326,6 +1410,13 @@ class Plugin: # Step 2: Find channels that are attached to other channels logger.info("[Stream-Mapparr] Step 2: Identifying attached channels...") + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_visibility', + 'stage': 'identifying', + 'total': len(channels), + 'progress_percent': 25, + 'message': 'Identifying attached channels...' + }) channels_attached_to_others = set() for channel in channels: @@ -1336,6 +1427,13 @@ class Plugin: # Step 3: Disable all channels first logger.info(f"[Stream-Mapparr] Step 3: Disabling all {len(channels)} channels...") + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_visibility', + 'stage': 'disabling', + 'total': len(channels), + 'progress_percent': 40, + 'message': f'Disabling all {len(channels)} channels...' + }) try: bulk_disable_payload = [ {"channel_id": channel['id'], "enabled": False} @@ -1370,7 +1468,14 @@ class Plugin: # Step 3.5: Group channels and apply visible channel limit logger.info("[Stream-Mapparr] Step 3.5: Grouping channels and applying visibility limit...") - + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_visibility', + 'stage': 'grouping', + 'total': len(channels), + 'progress_percent': 60, + 'message': 'Grouping channels and applying visibility rules...' + }) + # Group channels by their cleaned name channel_groups = {} for channel in channels: @@ -1407,8 +1512,8 @@ class Plugin: # If there are eligible channels, enable only the highest priority one enabled_in_group = False - - for ch in group_channels: + + for ch in sorted_channels: channel_id = ch['id'] channel_name = ch['name'] @@ -1426,17 +1531,18 @@ class Plugin: if is_attached: reason = 'Attached to another channel' should_enable = False - elif stream_count >= 2: - reason = f'{stream_count} streams (too many)' - should_enable = False - elif not enabled_in_group and (stream_count == 0 or stream_count == 1): - # This is the highest priority channel with 0-1 streams - reason = f'{stream_count} stream{"" if stream_count == 1 else "s"}' + elif not enabled_in_group and stream_count >= 1: + # This is the highest priority, non-attached channel WITH streams + reason = f'{stream_count} stream{"s" if stream_count != 1 else ""}' should_enable = True enabled_in_group = True + elif stream_count == 0: + # This channel has no streams + reason = 'No streams found' + should_enable = False else: - # Another channel in this group is already enabled - reason = 'Duplicate - higher priority channel in group already enabled' + # This is a duplicate (a lower-priority channel in the group) + reason = 'Duplicate - higher priority channel enabled' should_enable = False channel_stream_counts[channel_id] = { @@ -1453,6 +1559,14 @@ class Plugin: # Step 4: Enable selected channels logger.info(f"[Stream-Mapparr] Step 4: Enabling {len(channels_to_enable)} channels...") + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_visibility', + 'stage': 'enabling', + 'total': len(channels), + 'to_enable': len(channels_to_enable), + 'progress_percent': 80, + 'message': f'Enabling {len(channels_to_enable)} channels...' + }) channels_enabled = 0 if channels_to_enable: @@ -1490,9 +1604,20 @@ class Plugin: except Exception as e2: logger.error(f"[Stream-Mapparr] Failed to enable channel {channel_id}: {e2}") + # Send completion WebSocket notification + send_websocket_update('updates', 'update', { + 'type': 'stream_mapparr_visibility', + 'stage': 'completed', + 'total': len(channels), + 'enabled': channels_enabled, + 'disabled': len(channels) - channels_enabled, + 'progress_percent': 100, + 'message': f'Complete: {channels_enabled} channels enabled, {len(channels) - channels_enabled} disabled' + }) + # Trigger frontend refresh self._trigger_frontend_refresh(settings, logger) - + # Generate visibility report CSV timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"stream_mapparr_visibility_{timestamp}.csv" From 05860d3a2fd90c0e226143700278b389fa9afdcb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 13:26:15 +0000 Subject: [PATCH 2/3] Add Cinemax handling for channels containing 'max' When a channel name contains "max" (case insensitive), the plugin now removes "Cinemax" from stream names during matching. This allows channels like "5StarMax" to properly match streams like: - US: Cinemax 5Starmax - US 5STARMAX (East) (H) - US: 5 STARMAX Changes: - Add remove_cinemax parameter to normalize_name() in fuzzy_matcher.py - Add remove_cinemax parameter to _clean_channel_name() in plugin.py - Detect if channel name contains "max" in _match_streams_to_channel() - Pass remove_cinemax=True to all stream name cleaning when applicable - Update fuzzy_match() and find_best_match() to support Cinemax removal This improves matching accuracy for Cinemax channels that include the network name in stream names but not in channel names. --- Stream-Mapparr/fuzzy_matcher.py | 79 +++++++++++++++++++-------------- Stream-Mapparr/plugin.py | 66 +++++++++++++++------------ 2 files changed, 83 insertions(+), 62 deletions(-) diff --git a/Stream-Mapparr/fuzzy_matcher.py b/Stream-Mapparr/fuzzy_matcher.py index 27b49bd..b365393 100644 --- a/Stream-Mapparr/fuzzy_matcher.py +++ b/Stream-Mapparr/fuzzy_matcher.py @@ -158,29 +158,34 @@ class FuzzyMatcher: callsign = re.sub(r'-(?:TV|CD|LP|DT|LD)$', '', callsign) return callsign - def normalize_name(self, name, user_ignored_tags=None, remove_quality_tags=True): + def normalize_name(self, name, user_ignored_tags=None, remove_quality_tags=True, remove_cinemax=False): """ Normalize channel or stream name for matching by removing tags, prefixes, and other noise. - + Args: name: Name to normalize user_ignored_tags: Additional user-configured tags to ignore (list of strings) remove_quality_tags: If True, remove hardcoded quality patterns (for matching only, not display) - + remove_cinemax: If True, remove "Cinemax" prefix (useful when channel name contains "max") + Returns: Normalized name """ if user_ignored_tags is None: user_ignored_tags = [] - + # Remove leading parenthetical prefixes like (SP2), (D1), etc. name = re.sub(r'^\([^\)]+\)\s*', '', name) - + + # Remove "Cinemax" prefix if requested (for channels containing "max") + if remove_cinemax: + name = re.sub(r'\bCinemax\s+', '', name, flags=re.IGNORECASE) + # Apply hardcoded ignore patterns only if remove_quality_tags is True if remove_quality_tags: for pattern in HARDCODED_IGNORE_PATTERNS: name = re.sub(pattern, '', name, flags=re.IGNORECASE) - + # Apply user-configured ignored tags for tag in user_ignored_tags: escaped_tag = re.escape(tag) @@ -315,25 +320,26 @@ class FuzzyMatcher: tokens = sorted([token for token in cleaned_s.split() if token]) return " ".join(tokens) - def find_best_match(self, query_name, candidate_names, user_ignored_tags=None): + def find_best_match(self, query_name, candidate_names, user_ignored_tags=None, remove_cinemax=False): """ Find the best fuzzy match for a name among a list of candidate names. - + Args: query_name: Name to match candidate_names: List of candidate names to match against user_ignored_tags: User-configured tags to ignore - + remove_cinemax: If True, remove "Cinemax" from candidate names + Returns: Tuple of (matched_name, score) or (None, 0) if no match found """ if not candidate_names: return None, 0 - + if user_ignored_tags is None: user_ignored_tags = [] - - # Normalize the query + + # Normalize the query (channel name - don't remove Cinemax from it) normalized_query = self.normalize_name(query_name, user_ignored_tags) if not normalized_query: @@ -341,12 +347,14 @@ class FuzzyMatcher: # Process query for token-sort matching processed_query = self.process_string_for_matching(normalized_query) - + best_score = -1.0 best_match = None - + for candidate in candidate_names: - processed_candidate = self.process_string_for_matching(candidate) + # Normalize candidate (stream name) with Cinemax removal if requested + candidate_normalized = self.normalize_name(candidate, user_ignored_tags, remove_cinemax=remove_cinemax) + processed_candidate = self.process_string_for_matching(candidate_normalized) score = self.calculate_similarity(processed_query, processed_candidate) if score > best_score: @@ -361,26 +369,27 @@ class FuzzyMatcher: return None, 0 - def fuzzy_match(self, query_name, candidate_names, user_ignored_tags=None): + def fuzzy_match(self, query_name, candidate_names, user_ignored_tags=None, remove_cinemax=False): """ Generic fuzzy matching function that can match any name against a list of candidates. This is the main entry point for fuzzy matching. - + Args: - query_name: Name to match - candidate_names: List of candidate names to match against + query_name: Name to match (channel name) + candidate_names: List of candidate names to match against (stream names) user_ignored_tags: User-configured tags to ignore - + remove_cinemax: If True, remove "Cinemax" from candidate names (for channels with "max") + Returns: Tuple of (matched_name, score, match_type) or (None, 0, None) if no match found """ if not candidate_names: return None, 0, None - + if user_ignored_tags is None: user_ignored_tags = [] - - # Normalize for matching + + # Normalize query (channel name - don't remove Cinemax from it) normalized_query = self.normalize_name(query_name, user_ignored_tags) if not normalized_query: @@ -393,31 +402,33 @@ class FuzzyMatcher: # Stage 1: Exact match (after normalization) normalized_query_lower = normalized_query.lower() normalized_query_nospace = re.sub(r'[\s&\-]+', '', normalized_query_lower) - + for candidate in candidate_names: - candidate_normalized = self.normalize_name(candidate, user_ignored_tags) + # Normalize candidate (stream name) with Cinemax removal if requested + candidate_normalized = self.normalize_name(candidate, user_ignored_tags, remove_cinemax=remove_cinemax) candidate_lower = candidate_normalized.lower() candidate_nospace = re.sub(r'[\s&\-]+', '', candidate_lower) - + # Exact match if normalized_query_nospace == candidate_nospace: return candidate, 100, "exact" - + # Very high similarity (97%+) ratio = self.calculate_similarity(normalized_query_lower, candidate_lower) if ratio >= 0.97 and ratio > best_ratio: best_match = candidate best_ratio = ratio match_type = "exact" - + if best_match: return best_match, int(best_ratio * 100), match_type - + # Stage 2: Substring matching for candidate in candidate_names: - candidate_normalized = self.normalize_name(candidate, user_ignored_tags) + # Normalize candidate (stream name) with Cinemax removal if requested + candidate_normalized = self.normalize_name(candidate, user_ignored_tags, remove_cinemax=remove_cinemax) candidate_lower = candidate_normalized.lower() - + # Check if one is a substring of the other if normalized_query_lower in candidate_lower or candidate_lower in normalized_query_lower: # Calculate similarity score @@ -426,12 +437,12 @@ class FuzzyMatcher: best_match = candidate best_ratio = ratio match_type = "substring" - + if best_match and int(best_ratio * 100) >= self.match_threshold: return best_match, int(best_ratio * 100), match_type - + # Stage 3: Fuzzy matching with token sorting - fuzzy_match, score = self.find_best_match(query_name, candidate_names, user_ignored_tags) + fuzzy_match, score = self.find_best_match(query_name, candidate_names, user_ignored_tags, remove_cinemax=remove_cinemax) if fuzzy_match: return fuzzy_match, score, f"fuzzy ({score})" diff --git a/Stream-Mapparr/plugin.py b/Stream-Mapparr/plugin.py index 9d84aa0..f335e9b 100644 --- a/Stream-Mapparr/plugin.py +++ b/Stream-Mapparr/plugin.py @@ -351,14 +351,19 @@ class Plugin: logger.warning(f"[Stream-Mapparr] Could not trigger frontend refresh: {e}") return False - def _clean_channel_name(self, name, ignore_tags=None): + def _clean_channel_name(self, name, ignore_tags=None, remove_cinemax=False): """ Remove brackets and their contents from channel name for matching, and remove ignore tags. Uses fuzzy matcher's normalization if available, otherwise falls back to basic cleaning. + + Args: + name: Channel or stream name to clean + ignore_tags: List of tags to ignore + remove_cinemax: If True, remove "Cinemax" prefix (for streams when channel contains "max") """ if self.fuzzy_matcher: # Use fuzzy matcher's normalization - return self.fuzzy_matcher.normalize_name(name, ignore_tags, remove_quality_tags=True) + return self.fuzzy_matcher.normalize_name(name, ignore_tags, remove_quality_tags=True, remove_cinemax=remove_cinemax) # Fallback to basic cleaning if ignore_tags is None: @@ -488,12 +493,15 @@ class Plugin: ignore_tags = [] if channels_data is None: channels_data = [] - + channel_name = channel['name'] - + # Get channel info from JSON channel_info = self._get_channel_info_from_json(channel_name, channels_data, logger) - + + # Check if channel name contains "max" (case insensitive) - used for Cinemax handling + channel_has_max = 'max' in channel_name.lower() + cleaned_channel_name = self._clean_channel_name(channel_name, ignore_tags) if "24/7" in channel_name.lower(): @@ -520,10 +528,10 @@ class Plugin: if matching_streams: sorted_streams = self._sort_streams_by_quality(matching_streams) logger.info(f"[Stream-Mapparr] Sorted {len(sorted_streams)} streams by quality (callsign matching)") - - cleaned_stream_names = [self._clean_channel_name(s['name'], ignore_tags) for s in sorted_streams] + + cleaned_stream_names = [self._clean_channel_name(s['name'], ignore_tags, remove_cinemax=channel_has_max) for s in sorted_streams] match_reason = "Callsign match" - + return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason else: logger.info(f"[Stream-Mapparr] No callsign matches found for {callsign}") @@ -532,33 +540,35 @@ class Plugin: # Use fuzzy matching if available if self.fuzzy_matcher: logger.info(f"[Stream-Mapparr] Using fuzzy matcher for channel: {channel_name}") - + # Get all stream names stream_names = [stream['name'] for stream in all_streams] - + # Use fuzzy matcher to find best match + # Pass remove_cinemax flag if channel contains "max" matched_stream_name, score, match_type = self.fuzzy_matcher.fuzzy_match( channel_name, stream_names, - ignore_tags + ignore_tags, + remove_cinemax=channel_has_max ) if matched_stream_name: # Find all streams that match this name (different qualities) matching_streams = [] - cleaned_matched = self._clean_channel_name(matched_stream_name, ignore_tags) - + cleaned_matched = self._clean_channel_name(matched_stream_name, ignore_tags, remove_cinemax=channel_has_max) + for stream in all_streams: - cleaned_stream = self._clean_channel_name(stream['name'], ignore_tags) - + cleaned_stream = self._clean_channel_name(stream['name'], ignore_tags, remove_cinemax=channel_has_max) + if cleaned_stream.lower() == cleaned_matched.lower(): matching_streams.append(stream) - + if matching_streams: sorted_streams = self._sort_streams_by_quality(matching_streams) logger.info(f"[Stream-Mapparr] Found {len(sorted_streams)} streams via fuzzy match (score: {score}, type: {match_type})") - - cleaned_stream_names = [self._clean_channel_name(s['name'], ignore_tags) for s in sorted_streams] + + cleaned_stream_names = [self._clean_channel_name(s['name'], ignore_tags, remove_cinemax=channel_has_max) for s in sorted_streams] match_reason = f"Fuzzy match ({match_type}, score: {score})" return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason @@ -582,33 +592,33 @@ class Plugin: # Look for streams that match this channel name exactly for stream in all_streams: - cleaned_stream_name = self._clean_channel_name(stream['name'], ignore_tags) - + cleaned_stream_name = self._clean_channel_name(stream['name'], ignore_tags, remove_cinemax=channel_has_max) + if cleaned_stream_name.lower() == cleaned_channel_name.lower(): matching_streams.append(stream) - + if matching_streams: sorted_streams = self._sort_streams_by_quality(matching_streams) logger.info(f"[Stream-Mapparr] Found {len(sorted_streams)} streams matching exact channel name") - - cleaned_stream_names = [self._clean_channel_name(s['name'], ignore_tags) for s in sorted_streams] + + cleaned_stream_names = [self._clean_channel_name(s['name'], ignore_tags, remove_cinemax=channel_has_max) for s in sorted_streams] match_reason = "Exact match (channels.json)" return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason # Fallback to basic substring matching for stream in all_streams: - cleaned_stream_name = self._clean_channel_name(stream['name'], ignore_tags) - + cleaned_stream_name = self._clean_channel_name(stream['name'], ignore_tags, remove_cinemax=channel_has_max) + # Simple case-insensitive substring matching if cleaned_channel_name.lower() in cleaned_stream_name.lower() or cleaned_stream_name.lower() in cleaned_channel_name.lower(): matching_streams.append(stream) - + if matching_streams: sorted_streams = self._sort_streams_by_quality(matching_streams) logger.info(f"[Stream-Mapparr] Found {len(sorted_streams)} streams matching via basic substring match") - - cleaned_stream_names = [self._clean_channel_name(s['name'], ignore_tags) for s in sorted_streams] + + cleaned_stream_names = [self._clean_channel_name(s['name'], ignore_tags, remove_cinemax=channel_has_max) for s in sorted_streams] match_reason = "Basic substring match" return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason From ec65d904a1fcad856c1aec0196117f255af14ebc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 13:34:42 +0000 Subject: [PATCH 3/3] Remove WebSocket notifications from plugin The WebSocket notifications require frontend handlers in Dispatcharr's WebSocket.jsx that cannot be added from the plugin code. Removed all send_websocket_update() calls to avoid sending notifications that won't be displayed. The plugin still logs progress to the console for debugging. Changes: - Remove import of send_websocket_update - Remove all WebSocket notification calls from: * preview_changes_action * add_streams_to_channels_action * manage_channel_visibility_action - Progress is still logged via standard logging --- Stream-Mapparr/plugin.py | 124 --------------------------------------- 1 file changed, 124 deletions(-) diff --git a/Stream-Mapparr/plugin.py b/Stream-Mapparr/plugin.py index f335e9b..543006e 100644 --- a/Stream-Mapparr/plugin.py +++ b/Stream-Mapparr/plugin.py @@ -18,9 +18,6 @@ from apps.channels.models import Channel, Stream, ChannelStream, ChannelProfileM # Import fuzzy matcher from .fuzzy_matcher import FuzzyMatcher -# Import WebSocket update function -from core.utils import send_websocket_update - # Setup logging using Dispatcharr's format LOGGER = logging.getLogger("plugins.stream_mapparr") if not LOGGER.handlers: @@ -922,16 +919,6 @@ class Plugin: logger.info(f"[Stream-Mapparr] Previewing changes for {len(channels)} channels with {len(streams)} available streams") logger.info(f"[Stream-Mapparr] Visible channel limit: {visible_channel_limit}") - # Send initial WebSocket notification - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_preview', - 'stage': 'starting', - 'total': len(channels), - 'matched': 0, - 'progress_percent': 0, - 'message': f'Starting preview for {len(channels)} channels...' - }) - # Group channels by their cleaned name for matching channel_groups = {} ignore_tags = processed_data.get('ignore_tags', []) @@ -966,18 +953,6 @@ class Plugin: logger.info(f"[Stream-Mapparr] [{progress_pct}%] Processing channel group: {group_key} ({len(group_channels)} channels)") - # Send progress WebSocket notification - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_preview', - 'stage': 'matching', - 'total': total_groups, - 'current': current_group, - 'matched': total_channels_with_matches, - 'progress_percent': progress_pct, - 'current_group': group_key, - 'message': f'Processing {current_group}/{total_groups} channel groups ({progress_pct}%)...' - }) - # Sort channels in this group by priority sorted_channels = self._sort_channels_by_priority(group_channels) @@ -1028,16 +1003,6 @@ class Plugin: logger.info(f"[Stream-Mapparr] [100%] Preview processing complete") - # Send completion WebSocket notification - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_preview', - 'stage': 'completed', - 'total': len(channels), - 'matched': total_channels_with_matches, - 'progress_percent': 100, - 'message': f'Preview complete: {total_channels_with_matches} channels matched' - }) - # Export to CSV timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"stream_mapparr_preview_{timestamp}.csv" @@ -1147,16 +1112,6 @@ class Plugin: logger.info(f"[Stream-Mapparr] Visible channel limit: {visible_channel_limit}") logger.info(f"[Stream-Mapparr] Overwrite existing streams: {overwrite_streams}") - # Send initial WebSocket notification - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_add', - 'stage': 'starting', - 'total': len(channels), - 'updated': 0, - 'progress_percent': 0, - 'message': f'Starting to add streams to {len(channels)} channels...' - }) - # Group channels by their cleaned name channel_groups = {} for channel in channels: @@ -1191,18 +1146,6 @@ class Plugin: logger.info(f"[Stream-Mapparr] [{progress_pct}%] Processing channel group: {group_key} ({len(group_channels)} channels)") - # Send progress WebSocket notification - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_add', - 'stage': 'processing', - 'total': total_groups, - 'current': current_group, - 'updated': channels_updated, - 'progress_percent': progress_pct, - 'current_group': group_key, - 'message': f'Processing {current_group}/{total_groups} channel groups ({progress_pct}%)...' - }) - # Sort channels in this group by priority sorted_channels = self._sort_channels_by_priority(group_channels) @@ -1295,17 +1238,6 @@ class Plugin: logger.info(f"[Stream-Mapparr] [100%] Processing complete") - # Send completion WebSocket notification - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_add', - 'stage': 'completed', - 'total': len(channels), - 'updated': channels_updated, - 'streams_added': total_streams_added, - 'progress_percent': 100, - 'message': f'Complete: {channels_updated} channels updated with {total_streams_added} streams' - }) - # Trigger frontend refresh self._trigger_frontend_refresh(settings, logger) @@ -1389,24 +1321,8 @@ class Plugin: logger.info(f"[Stream-Mapparr] Managing visibility for {len(channels)} channels") logger.info(f"[Stream-Mapparr] Visible channel limit: {visible_channel_limit}") - # Send initial WebSocket notification - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_visibility', - 'stage': 'starting', - 'total': len(channels), - 'progress_percent': 0, - 'message': f'Starting visibility management for {len(channels)} channels...' - }) - # Step 1: Get stream counts for all channels logger.info("[Stream-Mapparr] Step 1: Counting streams for each channel...") - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_visibility', - 'stage': 'counting', - 'total': len(channels), - 'progress_percent': 10, - 'message': 'Counting streams for each channel...' - }) channel_stream_counts = {} for channel in channels: @@ -1420,13 +1336,6 @@ class Plugin: # Step 2: Find channels that are attached to other channels logger.info("[Stream-Mapparr] Step 2: Identifying attached channels...") - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_visibility', - 'stage': 'identifying', - 'total': len(channels), - 'progress_percent': 25, - 'message': 'Identifying attached channels...' - }) channels_attached_to_others = set() for channel in channels: @@ -1437,13 +1346,6 @@ class Plugin: # Step 3: Disable all channels first logger.info(f"[Stream-Mapparr] Step 3: Disabling all {len(channels)} channels...") - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_visibility', - 'stage': 'disabling', - 'total': len(channels), - 'progress_percent': 40, - 'message': f'Disabling all {len(channels)} channels...' - }) try: bulk_disable_payload = [ {"channel_id": channel['id'], "enabled": False} @@ -1478,13 +1380,6 @@ class Plugin: # Step 3.5: Group channels and apply visible channel limit logger.info("[Stream-Mapparr] Step 3.5: Grouping channels and applying visibility limit...") - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_visibility', - 'stage': 'grouping', - 'total': len(channels), - 'progress_percent': 60, - 'message': 'Grouping channels and applying visibility rules...' - }) # Group channels by their cleaned name channel_groups = {} @@ -1569,14 +1464,6 @@ class Plugin: # Step 4: Enable selected channels logger.info(f"[Stream-Mapparr] Step 4: Enabling {len(channels_to_enable)} channels...") - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_visibility', - 'stage': 'enabling', - 'total': len(channels), - 'to_enable': len(channels_to_enable), - 'progress_percent': 80, - 'message': f'Enabling {len(channels_to_enable)} channels...' - }) channels_enabled = 0 if channels_to_enable: @@ -1614,17 +1501,6 @@ class Plugin: except Exception as e2: logger.error(f"[Stream-Mapparr] Failed to enable channel {channel_id}: {e2}") - # Send completion WebSocket notification - send_websocket_update('updates', 'update', { - 'type': 'stream_mapparr_visibility', - 'stage': 'completed', - 'total': len(channels), - 'enabled': channels_enabled, - 'disabled': len(channels) - channels_enabled, - 'progress_percent': 100, - 'message': f'Complete: {channels_enabled} channels enabled, {len(channels) - channels_enabled} disabled' - }) - # Trigger frontend refresh self._trigger_frontend_refresh(settings, logger)