Merge pull request #4 from PiratesIRC/claude/fix-visibility-add-emojis-011CUrgQDHW2FZ25e9XdPjNi
Claude/fix visibility add emojis 011 c urg qdhw2 fz25e9 xd pj ni
This commit is contained in:
@@ -158,29 +158,34 @@ class FuzzyMatcher:
|
|||||||
callsign = re.sub(r'-(?:TV|CD|LP|DT|LD)$', '', callsign)
|
callsign = re.sub(r'-(?:TV|CD|LP|DT|LD)$', '', callsign)
|
||||||
return 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.
|
Normalize channel or stream name for matching by removing tags, prefixes, and other noise.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: Name to normalize
|
name: Name to normalize
|
||||||
user_ignored_tags: Additional user-configured tags to ignore (list of strings)
|
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_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:
|
Returns:
|
||||||
Normalized name
|
Normalized name
|
||||||
"""
|
"""
|
||||||
if user_ignored_tags is None:
|
if user_ignored_tags is None:
|
||||||
user_ignored_tags = []
|
user_ignored_tags = []
|
||||||
|
|
||||||
# Remove leading parenthetical prefixes like (SP2), (D1), etc.
|
# Remove leading parenthetical prefixes like (SP2), (D1), etc.
|
||||||
name = re.sub(r'^\([^\)]+\)\s*', '', name)
|
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
|
# Apply hardcoded ignore patterns only if remove_quality_tags is True
|
||||||
if remove_quality_tags:
|
if remove_quality_tags:
|
||||||
for pattern in HARDCODED_IGNORE_PATTERNS:
|
for pattern in HARDCODED_IGNORE_PATTERNS:
|
||||||
name = re.sub(pattern, '', name, flags=re.IGNORECASE)
|
name = re.sub(pattern, '', name, flags=re.IGNORECASE)
|
||||||
|
|
||||||
# Apply user-configured ignored tags
|
# Apply user-configured ignored tags
|
||||||
for tag in user_ignored_tags:
|
for tag in user_ignored_tags:
|
||||||
escaped_tag = re.escape(tag)
|
escaped_tag = re.escape(tag)
|
||||||
@@ -315,25 +320,26 @@ class FuzzyMatcher:
|
|||||||
tokens = sorted([token for token in cleaned_s.split() if token])
|
tokens = sorted([token for token in cleaned_s.split() if token])
|
||||||
return " ".join(tokens)
|
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.
|
Find the best fuzzy match for a name among a list of candidate names.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_name: Name to match
|
query_name: Name to match
|
||||||
candidate_names: List of candidate names to match against
|
candidate_names: List of candidate names to match against
|
||||||
user_ignored_tags: User-configured tags to ignore
|
user_ignored_tags: User-configured tags to ignore
|
||||||
|
remove_cinemax: If True, remove "Cinemax" from candidate names
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (matched_name, score) or (None, 0) if no match found
|
Tuple of (matched_name, score) or (None, 0) if no match found
|
||||||
"""
|
"""
|
||||||
if not candidate_names:
|
if not candidate_names:
|
||||||
return None, 0
|
return None, 0
|
||||||
|
|
||||||
if user_ignored_tags is None:
|
if user_ignored_tags is None:
|
||||||
user_ignored_tags = []
|
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)
|
normalized_query = self.normalize_name(query_name, user_ignored_tags)
|
||||||
|
|
||||||
if not normalized_query:
|
if not normalized_query:
|
||||||
@@ -341,12 +347,14 @@ class FuzzyMatcher:
|
|||||||
|
|
||||||
# Process query for token-sort matching
|
# Process query for token-sort matching
|
||||||
processed_query = self.process_string_for_matching(normalized_query)
|
processed_query = self.process_string_for_matching(normalized_query)
|
||||||
|
|
||||||
best_score = -1.0
|
best_score = -1.0
|
||||||
best_match = None
|
best_match = None
|
||||||
|
|
||||||
for candidate in candidate_names:
|
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)
|
score = self.calculate_similarity(processed_query, processed_candidate)
|
||||||
|
|
||||||
if score > best_score:
|
if score > best_score:
|
||||||
@@ -361,26 +369,27 @@ class FuzzyMatcher:
|
|||||||
|
|
||||||
return None, 0
|
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.
|
Generic fuzzy matching function that can match any name against a list of candidates.
|
||||||
This is the main entry point for fuzzy matching.
|
This is the main entry point for fuzzy matching.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query_name: Name to match
|
query_name: Name to match (channel name)
|
||||||
candidate_names: List of candidate names to match against
|
candidate_names: List of candidate names to match against (stream names)
|
||||||
user_ignored_tags: User-configured tags to ignore
|
user_ignored_tags: User-configured tags to ignore
|
||||||
|
remove_cinemax: If True, remove "Cinemax" from candidate names (for channels with "max")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (matched_name, score, match_type) or (None, 0, None) if no match found
|
Tuple of (matched_name, score, match_type) or (None, 0, None) if no match found
|
||||||
"""
|
"""
|
||||||
if not candidate_names:
|
if not candidate_names:
|
||||||
return None, 0, None
|
return None, 0, None
|
||||||
|
|
||||||
if user_ignored_tags is None:
|
if user_ignored_tags is None:
|
||||||
user_ignored_tags = []
|
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)
|
normalized_query = self.normalize_name(query_name, user_ignored_tags)
|
||||||
|
|
||||||
if not normalized_query:
|
if not normalized_query:
|
||||||
@@ -393,31 +402,33 @@ class FuzzyMatcher:
|
|||||||
# Stage 1: Exact match (after normalization)
|
# Stage 1: Exact match (after normalization)
|
||||||
normalized_query_lower = normalized_query.lower()
|
normalized_query_lower = normalized_query.lower()
|
||||||
normalized_query_nospace = re.sub(r'[\s&\-]+', '', normalized_query_lower)
|
normalized_query_nospace = re.sub(r'[\s&\-]+', '', normalized_query_lower)
|
||||||
|
|
||||||
for candidate in candidate_names:
|
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_lower = candidate_normalized.lower()
|
||||||
candidate_nospace = re.sub(r'[\s&\-]+', '', candidate_lower)
|
candidate_nospace = re.sub(r'[\s&\-]+', '', candidate_lower)
|
||||||
|
|
||||||
# Exact match
|
# Exact match
|
||||||
if normalized_query_nospace == candidate_nospace:
|
if normalized_query_nospace == candidate_nospace:
|
||||||
return candidate, 100, "exact"
|
return candidate, 100, "exact"
|
||||||
|
|
||||||
# Very high similarity (97%+)
|
# Very high similarity (97%+)
|
||||||
ratio = self.calculate_similarity(normalized_query_lower, candidate_lower)
|
ratio = self.calculate_similarity(normalized_query_lower, candidate_lower)
|
||||||
if ratio >= 0.97 and ratio > best_ratio:
|
if ratio >= 0.97 and ratio > best_ratio:
|
||||||
best_match = candidate
|
best_match = candidate
|
||||||
best_ratio = ratio
|
best_ratio = ratio
|
||||||
match_type = "exact"
|
match_type = "exact"
|
||||||
|
|
||||||
if best_match:
|
if best_match:
|
||||||
return best_match, int(best_ratio * 100), match_type
|
return best_match, int(best_ratio * 100), match_type
|
||||||
|
|
||||||
# Stage 2: Substring matching
|
# Stage 2: Substring matching
|
||||||
for candidate in candidate_names:
|
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_lower = candidate_normalized.lower()
|
||||||
|
|
||||||
# Check if one is a substring of the other
|
# Check if one is a substring of the other
|
||||||
if normalized_query_lower in candidate_lower or candidate_lower in normalized_query_lower:
|
if normalized_query_lower in candidate_lower or candidate_lower in normalized_query_lower:
|
||||||
# Calculate similarity score
|
# Calculate similarity score
|
||||||
@@ -426,12 +437,12 @@ class FuzzyMatcher:
|
|||||||
best_match = candidate
|
best_match = candidate
|
||||||
best_ratio = ratio
|
best_ratio = ratio
|
||||||
match_type = "substring"
|
match_type = "substring"
|
||||||
|
|
||||||
if best_match and int(best_ratio * 100) >= self.match_threshold:
|
if best_match and int(best_ratio * 100) >= self.match_threshold:
|
||||||
return best_match, int(best_ratio * 100), match_type
|
return best_match, int(best_ratio * 100), match_type
|
||||||
|
|
||||||
# Stage 3: Fuzzy matching with token sorting
|
# 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:
|
if fuzzy_match:
|
||||||
return fuzzy_match, score, f"fuzzy ({score})"
|
return fuzzy_match, score, f"fuzzy ({score})"
|
||||||
|
|
||||||
|
|||||||
@@ -32,27 +32,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 +60,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 +81,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 +89,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 +97,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 +108,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 +128,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,
|
||||||
@@ -348,14 +348,19 @@ 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):
|
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.
|
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.
|
||||||
|
|
||||||
|
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:
|
if self.fuzzy_matcher:
|
||||||
# Use fuzzy matcher's normalization
|
# 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
|
# Fallback to basic cleaning
|
||||||
if ignore_tags is None:
|
if ignore_tags is None:
|
||||||
@@ -485,12 +490,15 @@ class Plugin:
|
|||||||
ignore_tags = []
|
ignore_tags = []
|
||||||
if channels_data is None:
|
if channels_data is None:
|
||||||
channels_data = []
|
channels_data = []
|
||||||
|
|
||||||
channel_name = channel['name']
|
channel_name = channel['name']
|
||||||
|
|
||||||
# Get channel info from JSON
|
# Get channel info from JSON
|
||||||
channel_info = self._get_channel_info_from_json(channel_name, channels_data, logger)
|
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)
|
cleaned_channel_name = self._clean_channel_name(channel_name, ignore_tags)
|
||||||
|
|
||||||
if "24/7" in channel_name.lower():
|
if "24/7" in channel_name.lower():
|
||||||
@@ -517,10 +525,10 @@ class Plugin:
|
|||||||
if matching_streams:
|
if matching_streams:
|
||||||
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) 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"
|
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
|
||||||
else:
|
else:
|
||||||
logger.info(f"[Stream-Mapparr] No callsign matches found for {callsign}")
|
logger.info(f"[Stream-Mapparr] No callsign matches found for {callsign}")
|
||||||
@@ -529,33 +537,35 @@ class Plugin:
|
|||||||
# Use fuzzy matching if available
|
# Use fuzzy matching if available
|
||||||
if self.fuzzy_matcher:
|
if self.fuzzy_matcher:
|
||||||
logger.info(f"[Stream-Mapparr] Using fuzzy matcher for channel: {channel_name}")
|
logger.info(f"[Stream-Mapparr] Using fuzzy matcher for channel: {channel_name}")
|
||||||
|
|
||||||
# Get all stream names
|
# Get all stream names
|
||||||
stream_names = [stream['name'] for stream in all_streams]
|
stream_names = [stream['name'] for stream in all_streams]
|
||||||
|
|
||||||
# Use fuzzy matcher to find best match
|
# 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(
|
matched_stream_name, score, match_type = self.fuzzy_matcher.fuzzy_match(
|
||||||
channel_name,
|
channel_name,
|
||||||
stream_names,
|
stream_names,
|
||||||
ignore_tags
|
ignore_tags,
|
||||||
|
remove_cinemax=channel_has_max
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
cleaned_matched = self._clean_channel_name(matched_stream_name, ignore_tags, 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)
|
cleaned_stream = self._clean_channel_name(stream['name'], ignore_tags, 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)
|
||||||
|
|
||||||
if matching_streams:
|
if matching_streams:
|
||||||
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) 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})"
|
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
|
||||||
@@ -579,33 +589,33 @@ 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)
|
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():
|
if cleaned_stream_name.lower() == cleaned_channel_name.lower():
|
||||||
matching_streams.append(stream)
|
matching_streams.append(stream)
|
||||||
|
|
||||||
if matching_streams:
|
if matching_streams:
|
||||||
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) 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)"
|
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)
|
cleaned_stream_name = self._clean_channel_name(stream['name'], ignore_tags, 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():
|
||||||
matching_streams.append(stream)
|
matching_streams.append(stream)
|
||||||
|
|
||||||
if matching_streams:
|
if matching_streams:
|
||||||
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) 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"
|
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
|
||||||
@@ -908,7 +918,7 @@ 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}")
|
||||||
|
|
||||||
# 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', [])
|
||||||
@@ -936,16 +946,16 @@ class Plugin:
|
|||||||
|
|
||||||
total_groups = len(channel_groups)
|
total_groups = len(channel_groups)
|
||||||
current_group = 0
|
current_group = 0
|
||||||
|
|
||||||
for group_key, group_channels in channel_groups.items():
|
for group_key, group_channels in channel_groups.items():
|
||||||
current_group += 1
|
current_group += 1
|
||||||
progress_pct = int((current_group / total_groups) * 100)
|
progress_pct = int((current_group / total_groups) * 100)
|
||||||
|
|
||||||
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)")
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
# 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, channels_data
|
||||||
@@ -992,7 +1002,7 @@ class Plugin:
|
|||||||
all_matches.append(match_info)
|
all_matches.append(match_info)
|
||||||
|
|
||||||
logger.info(f"[Stream-Mapparr] [100%] Preview processing complete")
|
logger.info(f"[Stream-Mapparr] [100%] Preview processing complete")
|
||||||
|
|
||||||
# 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"
|
||||||
@@ -1101,7 +1111,7 @@ class Plugin:
|
|||||||
logger.info(f"[Stream-Mapparr] Adding streams to {len(channels)} channels")
|
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] 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}")
|
||||||
|
|
||||||
# Group channels by their cleaned name
|
# Group channels by their cleaned name
|
||||||
channel_groups = {}
|
channel_groups = {}
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
@@ -1133,12 +1143,12 @@ class Plugin:
|
|||||||
for group_key, group_channels in channel_groups.items():
|
for group_key, group_channels in channel_groups.items():
|
||||||
current_group += 1
|
current_group += 1
|
||||||
progress_pct = int((current_group / total_groups) * 100)
|
progress_pct = int((current_group / total_groups) * 100)
|
||||||
|
|
||||||
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)")
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
# 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, channels_data
|
||||||
@@ -1227,7 +1237,7 @@ class Plugin:
|
|||||||
channels_skipped += 1
|
channels_skipped += 1
|
||||||
|
|
||||||
logger.info(f"[Stream-Mapparr] [100%] Processing complete")
|
logger.info(f"[Stream-Mapparr] [100%] Processing complete")
|
||||||
|
|
||||||
# Trigger frontend refresh
|
# Trigger frontend refresh
|
||||||
self._trigger_frontend_refresh(settings, logger)
|
self._trigger_frontend_refresh(settings, logger)
|
||||||
|
|
||||||
@@ -1310,7 +1320,7 @@ 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}")
|
||||||
|
|
||||||
# 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...")
|
||||||
channel_stream_counts = {}
|
channel_stream_counts = {}
|
||||||
@@ -1370,7 +1380,7 @@ 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...")
|
||||||
|
|
||||||
# Group channels by their cleaned name
|
# Group channels by their cleaned name
|
||||||
channel_groups = {}
|
channel_groups = {}
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
@@ -1407,8 +1417,8 @@ 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 +1436,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] = {
|
||||||
@@ -1492,7 +1503,7 @@ class Plugin:
|
|||||||
|
|
||||||
# Trigger frontend refresh
|
# Trigger frontend refresh
|
||||||
self._trigger_frontend_refresh(settings, logger)
|
self._trigger_frontend_refresh(settings, logger)
|
||||||
|
|
||||||
# Generate visibility report CSV
|
# Generate visibility report 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_visibility_{timestamp}.csv"
|
filename = f"stream_mapparr_visibility_{timestamp}.csv"
|
||||||
|
|||||||
Reference in New Issue
Block a user