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.
This commit is contained in:
Claude
2025-11-06 13:11:31 +00:00
parent 8b75a7e296
commit 03b8bfc31e

View File

@@ -18,6 +18,9 @@ from apps.channels.models import Channel, Stream, ChannelStream, ChannelProfileM
# Import fuzzy matcher # Import fuzzy matcher
from .fuzzy_matcher import FuzzyMatcher from .fuzzy_matcher import FuzzyMatcher
# Import WebSocket update function
from core.utils import send_websocket_update
# Setup logging using Dispatcharr's format # Setup logging using Dispatcharr's format
LOGGER = logging.getLogger("plugins.stream_mapparr") LOGGER = logging.getLogger("plugins.stream_mapparr")
if not LOGGER.handlers: if not LOGGER.handlers:
@@ -32,27 +35,27 @@ class Plugin:
name = "Stream-Mapparr" name = "Stream-Mapparr"
version = "0.5.0a" 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 # Settings rendered by UI
fields = [ fields = [
{ {
"id": "overwrite_streams", "id": "overwrite_streams",
"label": "Overwrite Existing Streams", "label": "🔄 Overwrite Existing Streams",
"type": "boolean", "type": "boolean",
"default": True, "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).", "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", "id": "fuzzy_match_threshold",
"label": "Fuzzy Match Threshold", "label": "🎯 Fuzzy Match Threshold",
"type": "number", "type": "number",
"default": 85, "default": 85,
"help_text": "Minimum similarity score (0-100) for fuzzy matching. Higher values require closer matches. Default: 85", "help_text": "Minimum similarity score (0-100) for fuzzy matching. Higher values require closer matches. Default: 85",
}, },
{ {
"id": "dispatcharr_url", "id": "dispatcharr_url",
"label": "Dispatcharr URL", "label": "🌐 Dispatcharr URL",
"type": "string", "type": "string",
"default": "", "default": "",
"placeholder": "http://192.168.1.10:9191", "placeholder": "http://192.168.1.10:9191",
@@ -60,20 +63,20 @@ class Plugin:
}, },
{ {
"id": "dispatcharr_username", "id": "dispatcharr_username",
"label": "Dispatcharr Admin Username", "label": "👤 Dispatcharr Admin Username",
"type": "string", "type": "string",
"help_text": "Your admin username for the Dispatcharr UI. Required for API access.", "help_text": "Your admin username for the Dispatcharr UI. Required for API access.",
}, },
{ {
"id": "dispatcharr_password", "id": "dispatcharr_password",
"label": "Dispatcharr Admin Password", "label": "🔑 Dispatcharr Admin Password",
"type": "string", "type": "string",
"input_type": "password", "input_type": "password",
"help_text": "Your admin password for the Dispatcharr UI. Required for API access.", "help_text": "Your admin password for the Dispatcharr UI. Required for API access.",
}, },
{ {
"id": "profile_name", "id": "profile_name",
"label": "Profile Name", "label": "📋 Profile Name",
"type": "string", "type": "string",
"default": "", "default": "",
"placeholder": "Sports", "placeholder": "Sports",
@@ -81,7 +84,7 @@ class Plugin:
}, },
{ {
"id": "selected_groups", "id": "selected_groups",
"label": "Channel Groups (comma-separated)", "label": "📁 Channel Groups (comma-separated)",
"type": "string", "type": "string",
"default": "", "default": "",
"placeholder": "Sports, News, Entertainment", "placeholder": "Sports, News, Entertainment",
@@ -89,7 +92,7 @@ class Plugin:
}, },
{ {
"id": "ignore_tags", "id": "ignore_tags",
"label": "Ignore Tags (comma-separated)", "label": "🏷️ Ignore Tags (comma-separated)",
"type": "string", "type": "string",
"default": "", "default": "",
"placeholder": "4K, [4K], [Dead]", "placeholder": "4K, [4K], [Dead]",
@@ -97,7 +100,7 @@ class Plugin:
}, },
{ {
"id": "visible_channel_limit", "id": "visible_channel_limit",
"label": "Visible Channel Limit", "label": "👁️ Visible Channel Limit",
"type": "number", "type": "number",
"default": 1, "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.", "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 = [ actions = [
{ {
"id": "load_process_channels", "id": "load_process_channels",
"label": "Load/Process Channels", "label": "📥 Load/Process Channels",
"description": "Validate settings and load channels from the specified profile and groups", "description": "Validate settings and load channels from the specified profile and groups",
}, },
{ {
"id": "preview_changes", "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", "description": "Preview which streams will be added to channels without making changes",
}, },
{ {
"id": "add_streams_to_channels", "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", "description": "Add matching streams to channels and replace existing stream assignments",
"confirm": { "confirm": {
"required": True, "required": True,
@@ -128,17 +131,17 @@ class Plugin:
}, },
{ {
"id": "manage_channel_visibility", "id": "manage_channel_visibility",
"label": "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)", "description": "Disable all channels, then enable only channels with 1 or more streams (excluding channels attached to others)",
"confirm": { "confirm": {
"required": True, "required": True,
"title": "Manage Channel Visibility?", "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", "id": "clear_csv_exports",
"label": "Clear CSV Exports", "label": "🗑️ Clear CSV Exports",
"description": "Delete all CSV export files created by this plugin", "description": "Delete all CSV export files created by this plugin",
"confirm": { "confirm": {
"required": True, "required": True,
@@ -909,6 +912,16 @@ class Plugin:
logger.info(f"[Stream-Mapparr] Previewing changes for {len(channels)} channels with {len(streams)} available streams") 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}") 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 # Group channels by their cleaned name for matching
channel_groups = {} channel_groups = {}
ignore_tags = processed_data.get('ignore_tags', []) ignore_tags = processed_data.get('ignore_tags', [])
@@ -943,6 +956,18 @@ class Plugin:
logger.info(f"[Stream-Mapparr] [{progress_pct}%] Processing channel group: {group_key} ({len(group_channels)} channels)") 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 # Sort channels in this group by priority
sorted_channels = self._sort_channels_by_priority(group_channels) sorted_channels = self._sort_channels_by_priority(group_channels)
@@ -993,6 +1018,16 @@ class Plugin:
logger.info(f"[Stream-Mapparr] [100%] Preview processing complete") 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 # Export to CSV
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"stream_mapparr_preview_{timestamp}.csv" filename = f"stream_mapparr_preview_{timestamp}.csv"
@@ -1102,6 +1137,16 @@ class Plugin:
logger.info(f"[Stream-Mapparr] Visible channel limit: {visible_channel_limit}") logger.info(f"[Stream-Mapparr] Visible channel limit: {visible_channel_limit}")
logger.info(f"[Stream-Mapparr] Overwrite existing streams: {overwrite_streams}") 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 # Group channels by their cleaned name
channel_groups = {} channel_groups = {}
for channel in channels: for channel in channels:
@@ -1136,6 +1181,18 @@ class Plugin:
logger.info(f"[Stream-Mapparr] [{progress_pct}%] Processing channel group: {group_key} ({len(group_channels)} channels)") 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 # Sort channels in this group by priority
sorted_channels = self._sort_channels_by_priority(group_channels) sorted_channels = self._sort_channels_by_priority(group_channels)
@@ -1228,6 +1285,17 @@ class Plugin:
logger.info(f"[Stream-Mapparr] [100%] Processing complete") 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 # Trigger frontend refresh
self._trigger_frontend_refresh(settings, logger) self._trigger_frontend_refresh(settings, logger)
@@ -1311,8 +1379,24 @@ class Plugin:
logger.info(f"[Stream-Mapparr] Managing visibility for {len(channels)} channels") logger.info(f"[Stream-Mapparr] Managing visibility for {len(channels)} channels")
logger.info(f"[Stream-Mapparr] Visible channel limit: {visible_channel_limit}") 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 # Step 1: Get stream counts for all channels
logger.info("[Stream-Mapparr] Step 1: Counting streams for each channel...") 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 = {} channel_stream_counts = {}
for channel in channels: for channel in channels:
@@ -1326,6 +1410,13 @@ class Plugin:
# Step 2: Find channels that are attached to other channels # Step 2: Find channels that are attached to other channels
logger.info("[Stream-Mapparr] Step 2: Identifying attached 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() channels_attached_to_others = set()
for channel in channels: for channel in channels:
@@ -1336,6 +1427,13 @@ class Plugin:
# Step 3: Disable all channels first # Step 3: Disable all channels first
logger.info(f"[Stream-Mapparr] Step 3: Disabling all {len(channels)} channels...") 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: try:
bulk_disable_payload = [ bulk_disable_payload = [
{"channel_id": channel['id'], "enabled": False} {"channel_id": channel['id'], "enabled": False}
@@ -1370,6 +1468,13 @@ class Plugin:
# Step 3.5: Group channels and apply visible channel limit # Step 3.5: Group channels and apply visible channel limit
logger.info("[Stream-Mapparr] Step 3.5: Grouping channels and applying visibility 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 # Group channels by their cleaned name
channel_groups = {} channel_groups = {}
@@ -1408,7 +1513,7 @@ class Plugin:
# If there are eligible channels, enable only the highest priority one # If there are eligible channels, enable only the highest priority one
enabled_in_group = False enabled_in_group = False
for ch in group_channels: for ch in sorted_channels:
channel_id = ch['id'] channel_id = ch['id']
channel_name = ch['name'] channel_name = ch['name']
@@ -1426,17 +1531,18 @@ class Plugin:
if is_attached: if is_attached:
reason = 'Attached to another channel' reason = 'Attached to another channel'
should_enable = False should_enable = False
elif stream_count >= 2: elif not enabled_in_group and stream_count >= 1:
reason = f'{stream_count} streams (too many)' # This is the highest priority, non-attached channel WITH streams
should_enable = False reason = f'{stream_count} stream{"s" if stream_count != 1 else ""}'
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"}'
should_enable = True should_enable = True
enabled_in_group = True enabled_in_group = True
elif stream_count == 0:
# This channel has no streams
reason = 'No streams found'
should_enable = False
else: else:
# Another channel in this group is already enabled # This is a duplicate (a lower-priority channel in the group)
reason = 'Duplicate - higher priority channel in group already enabled' reason = 'Duplicate - higher priority channel enabled'
should_enable = False should_enable = False
channel_stream_counts[channel_id] = { channel_stream_counts[channel_id] = {
@@ -1453,6 +1559,14 @@ class Plugin:
# Step 4: Enable selected channels # Step 4: Enable selected channels
logger.info(f"[Stream-Mapparr] Step 4: Enabling {len(channels_to_enable)} 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 channels_enabled = 0
if channels_to_enable: if channels_to_enable:
@@ -1490,6 +1604,17 @@ class Plugin:
except Exception as e2: except Exception as e2:
logger.error(f"[Stream-Mapparr] Failed to enable channel {channel_id}: {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 # Trigger frontend refresh
self._trigger_frontend_refresh(settings, logger) self._trigger_frontend_refresh(settings, logger)