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
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,
@@ -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] 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', [])
@@ -943,6 +956,18 @@ 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)
@@ -993,6 +1018,16 @@ 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"
@@ -1102,6 +1137,16 @@ 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:
@@ -1136,6 +1181,18 @@ 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)
@@ -1228,6 +1285,17 @@ 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)
@@ -1311,8 +1379,24 @@ 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,6 +1468,13 @@ 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 = {}
@@ -1408,7 +1513,7 @@ 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,6 +1604,17 @@ 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)