Integrate category-based pattern control in Stream-Mapparr plugin

Changes:
1. Add four new UI settings for granular pattern control:
   - ignore_quality_tags: Control quality pattern removal ([4K], HD, etc.)
   - ignore_regional_tags: Control regional pattern removal (East)
   - ignore_geographic_tags: Control geographic prefix removal (US:, USA:)
   - ignore_misc_tags: Control miscellaneous pattern removal ((CX), (Backup), etc.)

2. Update _clean_channel_name method:
   - Add parameters: ignore_quality, ignore_regional, ignore_geographic, ignore_misc
   - Pass these to fuzzy_matcher.normalize_name
   - Defaults to True for backward compatibility

3. Update _match_streams_to_channel method:
   - Accept category flag parameters
   - Pass them through to all _clean_channel_name calls

4. Store category settings in processed_data:
   - Save settings when loading/processing channels
   - Retrieve settings when executing actions
   - Apply settings consistently across all operations

5. Update all action methods:
   - preview_changes_action
   - add_streams_to_channels_action
   - manage_channel_visibility_action
   - All retrieve and use category settings from processed_data

This enables users to fine-tune pattern matching behavior through the UI
while maintaining full backward compatibility with existing configurations.
This commit is contained in:
Claude
2025-11-09 18:09:13 +00:00
parent 4e794879e5
commit 5e5d3ed49d

View File

@@ -95,6 +95,34 @@ class Plugin:
"placeholder": "4K, [4K], [Dead]", "placeholder": "4K, [4K], [Dead]",
"help_text": "Tags to ignore when matching streams. Space-separated in channel names unless they contain brackets/parentheses.", "help_text": "Tags to ignore when matching streams. Space-separated in channel names unless they contain brackets/parentheses.",
}, },
{
"id": "ignore_quality_tags",
"label": "🎬 Ignore Quality Tags",
"type": "boolean",
"default": True,
"help_text": "If enabled, hardcoded quality tags like [4K], [HD], (UHD), etc., will be ignored during matching.",
},
{
"id": "ignore_regional_tags",
"label": "🌍 Ignore Regional Tags",
"type": "boolean",
"default": True,
"help_text": "If enabled, hardcoded regional tags like 'East' will be ignored during matching.",
},
{
"id": "ignore_geographic_tags",
"label": "🗺️ Ignore Geographic Tags",
"type": "boolean",
"default": True,
"help_text": "If enabled, hardcoded geographic prefixes like 'US:', 'USA:' will be ignored during matching.",
},
{
"id": "ignore_misc_tags",
"label": "🏷️ Ignore Miscellaneous Tags",
"type": "boolean",
"default": True,
"help_text": "If enabled, miscellaneous tags like (CX), (Backup), and single-letter tags will be ignored during matching.",
},
{ {
"id": "visible_channel_limit", "id": "visible_channel_limit",
"label": "👁️ Visible Channel Limit", "label": "👁️ Visible Channel Limit",
@@ -353,7 +381,8 @@ class Plugin:
logger.warning(f"[Stream-Mapparr] Could not trigger frontend refresh: {e}") logger.warning(f"[Stream-Mapparr] Could not trigger frontend refresh: {e}")
return False return False
def _clean_channel_name(self, name, ignore_tags=None, remove_cinemax=False): def _clean_channel_name(self, name, ignore_tags=None, ignore_quality=True, ignore_regional=True,
ignore_geographic=True, ignore_misc=True, remove_cinemax=False):
""" """
Remove brackets and their contents from channel name for matching, and remove ignore tags. 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. Uses fuzzy matcher's normalization if available, otherwise falls back to basic cleaning.
@@ -361,11 +390,22 @@ class Plugin:
Args: Args:
name: Channel or stream name to clean name: Channel or stream name to clean
ignore_tags: List of tags to ignore ignore_tags: List of tags to ignore
ignore_quality: If True, remove quality-related patterns (e.g., [4K], HD, (SD))
ignore_regional: If True, remove regional indicator patterns (e.g., East)
ignore_geographic: If True, remove geographic prefix patterns (e.g., US:, USA)
ignore_misc: If True, remove miscellaneous patterns (e.g., (CX), (Backup), single-letter tags)
remove_cinemax: If True, remove "Cinemax" prefix (for streams when channel contains "max") remove_cinemax: If True, remove "Cinemax" prefix (for streams when channel contains "max")
""" """
if self.fuzzy_matcher: if self.fuzzy_matcher:
# Use fuzzy matcher's normalization # Use fuzzy matcher's normalization
return self.fuzzy_matcher.normalize_name(name, ignore_tags, remove_cinemax=remove_cinemax) return self.fuzzy_matcher.normalize_name(
name, ignore_tags,
ignore_quality=ignore_quality,
ignore_regional=ignore_regional,
ignore_geographic=ignore_geographic,
ignore_misc=ignore_misc,
remove_cinemax=remove_cinemax
)
# Fallback to basic cleaning # Fallback to basic cleaning
if ignore_tags is None: if ignore_tags is None:
@@ -489,7 +529,9 @@ class Plugin:
return callsign.upper() return callsign.upper()
def _match_streams_to_channel(self, channel, all_streams, logger, ignore_tags=None, channels_data=None): def _match_streams_to_channel(self, channel, all_streams, logger, ignore_tags=None,
ignore_quality=True, ignore_regional=True, ignore_geographic=True,
ignore_misc=True, channels_data=None):
"""Find matching streams for a channel using fuzzy matching when available.""" """Find matching streams for a channel using fuzzy matching when available."""
if ignore_tags is None: if ignore_tags is None:
ignore_tags = [] ignore_tags = []
@@ -504,7 +546,10 @@ class Plugin:
# Check if channel name contains "max" (case insensitive) - used for Cinemax handling # Check if channel name contains "max" (case insensitive) - used for Cinemax handling
channel_has_max = 'max' in channel_name.lower() channel_has_max = 'max' in channel_name.lower()
cleaned_channel_name = self._clean_channel_name(channel_name, ignore_tags) cleaned_channel_name = self._clean_channel_name(
channel_name, ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc
)
if "24/7" in channel_name.lower(): if "24/7" in channel_name.lower():
logger.info(f"[Stream-Mapparr] Cleaned channel name for matching: {cleaned_channel_name}") logger.info(f"[Stream-Mapparr] Cleaned channel name for matching: {cleaned_channel_name}")
@@ -531,7 +576,10 @@ class Plugin:
sorted_streams = self._sort_streams_by_quality(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)") 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, remove_cinemax=channel_has_max) for s in sorted_streams] cleaned_stream_names = [self._clean_channel_name(
s['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc, remove_cinemax=channel_has_max
) for s in sorted_streams]
match_reason = "Callsign match" match_reason = "Callsign match"
return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason
@@ -558,10 +606,16 @@ class Plugin:
if matched_stream_name: if matched_stream_name:
# Find all streams that match this name (different qualities) # Find all streams that match this name (different qualities)
matching_streams = [] matching_streams = []
cleaned_matched = self._clean_channel_name(matched_stream_name, ignore_tags, remove_cinemax=channel_has_max) cleaned_matched = self._clean_channel_name(
matched_stream_name, ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc, remove_cinemax=channel_has_max
)
for stream in all_streams: for stream in all_streams:
cleaned_stream = self._clean_channel_name(stream['name'], ignore_tags, remove_cinemax=channel_has_max) cleaned_stream = self._clean_channel_name(
stream['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc, remove_cinemax=channel_has_max
)
if cleaned_stream.lower() == cleaned_matched.lower(): if cleaned_stream.lower() == cleaned_matched.lower():
matching_streams.append(stream) matching_streams.append(stream)
@@ -570,7 +624,10 @@ class Plugin:
sorted_streams = self._sort_streams_by_quality(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})") 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, remove_cinemax=channel_has_max) for s in sorted_streams] cleaned_stream_names = [self._clean_channel_name(
s['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc, remove_cinemax=channel_has_max
) for s in sorted_streams]
match_reason = f"Fuzzy match ({match_type}, score: {score})" match_reason = f"Fuzzy match ({match_type}, score: {score})"
return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason
@@ -594,7 +651,10 @@ class Plugin:
# Look for streams that match this channel name exactly # Look for streams that match this channel name exactly
for stream in all_streams: for stream in all_streams:
cleaned_stream_name = self._clean_channel_name(stream['name'], ignore_tags, remove_cinemax=channel_has_max) cleaned_stream_name = self._clean_channel_name(
stream['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc, remove_cinemax=channel_has_max
)
if cleaned_stream_name.lower() == cleaned_channel_name.lower(): if cleaned_stream_name.lower() == cleaned_channel_name.lower():
matching_streams.append(stream) matching_streams.append(stream)
@@ -603,14 +663,20 @@ class Plugin:
sorted_streams = self._sort_streams_by_quality(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") 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, remove_cinemax=channel_has_max) for s in sorted_streams] cleaned_stream_names = [self._clean_channel_name(
s['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc, remove_cinemax=channel_has_max
) for s in sorted_streams]
match_reason = "Exact match (channels.json)" match_reason = "Exact match (channels.json)"
return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason
# Fallback to basic substring matching # Fallback to basic substring matching
for stream in all_streams: for stream in all_streams:
cleaned_stream_name = self._clean_channel_name(stream['name'], ignore_tags, remove_cinemax=channel_has_max) cleaned_stream_name = self._clean_channel_name(
stream['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc, remove_cinemax=channel_has_max
)
# Simple case-insensitive substring matching # 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(): if cleaned_channel_name.lower() in cleaned_stream_name.lower() or cleaned_stream_name.lower() in cleaned_channel_name.lower():
@@ -620,7 +686,10 @@ class Plugin:
sorted_streams = self._sort_streams_by_quality(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") 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, remove_cinemax=channel_has_max) for s in sorted_streams] cleaned_stream_names = [self._clean_channel_name(
s['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc, remove_cinemax=channel_has_max
) for s in sorted_streams]
match_reason = "Basic substring match" match_reason = "Basic substring match"
return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason return sorted_streams, cleaned_channel_name, cleaned_stream_names, match_reason
@@ -869,6 +938,22 @@ class Plugin:
visible_channel_limit_str = settings.get("visible_channel_limit", "1") visible_channel_limit_str = settings.get("visible_channel_limit", "1")
visible_channel_limit = int(visible_channel_limit_str) if visible_channel_limit_str else 1 visible_channel_limit = int(visible_channel_limit_str) if visible_channel_limit_str else 1
# Get category ignore settings
ignore_quality = settings.get("ignore_quality_tags", True)
ignore_regional = settings.get("ignore_regional_tags", True)
ignore_geographic = settings.get("ignore_geographic_tags", True)
ignore_misc = settings.get("ignore_misc_tags", True)
# Convert string values to boolean if needed
if isinstance(ignore_quality, str):
ignore_quality = ignore_quality.lower() in ('true', 'yes', '1')
if isinstance(ignore_regional, str):
ignore_regional = ignore_regional.lower() in ('true', 'yes', '1')
if isinstance(ignore_geographic, str):
ignore_geographic = ignore_geographic.lower() in ('true', 'yes', '1')
if isinstance(ignore_misc, str):
ignore_misc = ignore_misc.lower() in ('true', 'yes', '1')
if not profile_names_str: if not profile_names_str:
return {"status": "error", "message": "Profile Name must be configured in the plugin settings."} return {"status": "error", "message": "Profile Name must be configured in the plugin settings."}
@@ -885,6 +970,9 @@ class Plugin:
ignore_tags = [tag.strip() for tag in ignore_tags_str.split(',') if tag.strip()] ignore_tags = [tag.strip() for tag in ignore_tags_str.split(',') if tag.strip()]
logger.info(f"[Stream-Mapparr] Ignore tags configured: {ignore_tags}") logger.info(f"[Stream-Mapparr] Ignore tags configured: {ignore_tags}")
# Log category settings
logger.info(f"[Stream-Mapparr] Pattern categories - Quality: {ignore_quality}, Regional: {ignore_regional}, Geographic: {ignore_geographic}, Misc: {ignore_misc}")
# Get all profiles to find the specified ones # Get all profiles to find the specified ones
logger.info("[Stream-Mapparr] Fetching channel profiles...") logger.info("[Stream-Mapparr] Fetching channel profiles...")
profiles = self._get_api_data("/api/channels/profiles/", token, settings, logger) profiles = self._get_api_data("/api/channels/profiles/", token, settings, logger)
@@ -1049,6 +1137,10 @@ class Plugin:
"selected_groups": selected_groups, "selected_groups": selected_groups,
"ignore_tags": ignore_tags, "ignore_tags": ignore_tags,
"visible_channel_limit": visible_channel_limit, "visible_channel_limit": visible_channel_limit,
"ignore_quality": ignore_quality,
"ignore_regional": ignore_regional,
"ignore_geographic": ignore_geographic,
"ignore_misc": ignore_misc,
"channels": channels_to_process, "channels": channels_to_process,
"streams": all_streams_data "streams": all_streams_data
} }
@@ -1152,6 +1244,10 @@ class Plugin:
# 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', [])
ignore_quality = processed_data.get('ignore_quality', True)
ignore_regional = processed_data.get('ignore_regional', True)
ignore_geographic = processed_data.get('ignore_geographic', True)
ignore_misc = processed_data.get('ignore_misc', True)
for channel in channels: for channel in channels:
# Get channel info from JSON to determine if it has a callsign # Get channel info from JSON to determine if it has a callsign
@@ -1160,9 +1256,15 @@ class Plugin:
if self._is_ota_channel(channel_info): if self._is_ota_channel(channel_info):
# For OTA channels, group by callsign # For OTA channels, group by callsign
callsign = channel_info.get('callsign', '') callsign = channel_info.get('callsign', '')
group_key = f"OTA_{callsign}" if callsign else self._clean_channel_name(channel['name'], ignore_tags) group_key = f"OTA_{callsign}" if callsign else self._clean_channel_name(
channel['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc
)
else: else:
group_key = self._clean_channel_name(channel['name'], ignore_tags) group_key = self._clean_channel_name(
channel['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc
)
if group_key not in channel_groups: if group_key not in channel_groups:
channel_groups[group_key] = [] channel_groups[group_key] = []
@@ -1188,7 +1290,9 @@ class Plugin:
# Match streams for this channel group (using first channel as representative) # 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( matched_streams, cleaned_channel_name, cleaned_stream_names, match_reason = self._match_streams_to_channel(
sorted_channels[0], streams, logger, ignore_tags, channels_data sorted_channels[0], streams, logger, ignore_tags,
ignore_quality, ignore_regional, ignore_geographic, ignore_misc,
channels_data
) )
# Determine which channels will be updated based on limit # Determine which channels will be updated based on limit
@@ -1343,6 +1447,10 @@ class Plugin:
streams = processed_data.get('streams', []) streams = processed_data.get('streams', [])
ignore_tags = processed_data.get('ignore_tags', []) ignore_tags = processed_data.get('ignore_tags', [])
visible_channel_limit = processed_data.get('visible_channel_limit', 1) visible_channel_limit = processed_data.get('visible_channel_limit', 1)
ignore_quality = processed_data.get('ignore_quality', True)
ignore_regional = processed_data.get('ignore_regional', True)
ignore_geographic = processed_data.get('ignore_geographic', True)
ignore_misc = processed_data.get('ignore_misc', True)
# Get overwrite_streams setting # Get overwrite_streams setting
overwrite_streams = settings.get('overwrite_streams', True) overwrite_streams = settings.get('overwrite_streams', True)
@@ -1365,9 +1473,15 @@ class Plugin:
if self._is_ota_channel(channel_info): if self._is_ota_channel(channel_info):
# For OTA channels, group by callsign # For OTA channels, group by callsign
callsign = channel_info.get('callsign', '') callsign = channel_info.get('callsign', '')
group_key = f"OTA_{callsign}" if callsign else self._clean_channel_name(channel['name'], ignore_tags) group_key = f"OTA_{callsign}" if callsign else self._clean_channel_name(
channel['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc
)
else: else:
group_key = self._clean_channel_name(channel['name'], ignore_tags) group_key = self._clean_channel_name(
channel['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc
)
if group_key not in channel_groups: if group_key not in channel_groups:
channel_groups[group_key] = [] channel_groups[group_key] = []
@@ -1395,7 +1509,9 @@ class Plugin:
# Match streams for this channel group # Match streams for this channel group
matched_streams, cleaned_channel_name, cleaned_stream_names, match_reason = self._match_streams_to_channel( matched_streams, cleaned_channel_name, cleaned_stream_names, match_reason = self._match_streams_to_channel(
sorted_channels[0], streams, logger, ignore_tags, channels_data sorted_channels[0], streams, logger, ignore_tags,
ignore_quality, ignore_regional, ignore_geographic, ignore_misc,
channels_data
) )
# Determine which channels to update based on limit # Determine which channels to update based on limit
@@ -1572,6 +1688,10 @@ class Plugin:
channels = processed_data.get('channels', []) channels = processed_data.get('channels', [])
ignore_tags = processed_data.get('ignore_tags', []) ignore_tags = processed_data.get('ignore_tags', [])
visible_channel_limit = processed_data.get('visible_channel_limit', 1) visible_channel_limit = processed_data.get('visible_channel_limit', 1)
ignore_quality = processed_data.get('ignore_quality', True)
ignore_regional = processed_data.get('ignore_regional', True)
ignore_geographic = processed_data.get('ignore_geographic', True)
ignore_misc = processed_data.get('ignore_misc', True)
if not channels: if not channels:
return {"status": "error", "message": "No channels found in processed data."} return {"status": "error", "message": "No channels found in processed data."}
@@ -1649,9 +1769,15 @@ class Plugin:
if ota_info: if ota_info:
group_key = f"OTA_{ota_info['callsign']}" group_key = f"OTA_{ota_info['callsign']}"
else: else:
group_key = self._clean_channel_name(channel['name'], ignore_tags) group_key = self._clean_channel_name(
channel['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc
)
else: else:
group_key = self._clean_channel_name(channel['name'], ignore_tags) group_key = self._clean_channel_name(
channel['name'], ignore_tags, ignore_quality, ignore_regional,
ignore_geographic, ignore_misc
)
if group_key not in channel_groups: if group_key not in channel_groups:
channel_groups[group_key] = [] channel_groups[group_key] = []