0.7.1
## M3U Source Prioritization Stream-Mapparr allows you to prioritize streams from specific M3U providers, ensuring your preferred sources are always selected first, regardless of quality metrics. ### How It Works When you specify M3U sources in the **"📡 M3U Sources"** setting, streams are sorted using a multi-level hierarchy: 1. **M3U Priority** (0 = highest) - Based on order in your comma-separated list 2. **Quality Tier** - High (HD+FPS) > Medium > Low > Dead 3. **Resolution** - Higher resolution preferred within tier 4. **FPS** - Higher framerate preferred within tier ### Example Scenario **Settings:** ``` M3U Sources: Premium Sports, Backup Provider, Free Streams ``` **Matched Streams for "ESPN":** - ESPN HD (Premium Sports, 1920x1080, 60fps) → Priority 0, High Quality - ESPN UHD (Free Streams, 3840x2160, 60fps) → Priority 2, High Quality - ESPN SD (Premium Sports, 854x480, 30fps) → Priority 0, Low Quality - ESPN FHD (Backup Provider, 1920x1080, 30fps) → Priority 1, Medium Quality **Sorted Order:** 1. ESPN HD (Premium Sports) - Priority 0, High Quality ← Best overall 2. ESPN SD (Premium Sports) - Priority 0, Low Quality ← Same provider 3. ESPN FHD (Backup Provider) - Priority 1, Medium Quality 4. ESPN UHD (Free Streams) - Priority 2, High Quality ← Despite being 4K, sorted last ### Configuration 1. Navigate to the Stream-Mapparr plugin settings 2. Find the **"📡 M3U Sources (comma-separated, prioritized)"** field 3. Enter your M3U provider names in priority order, separated by commas: ``` Premium IPTV, Local M3U, Backup Provider ``` 4. Save settings and run "Load/Process Channels" or "Sort Alternate Streams" ### Use Cases - **Primary/Backup Providers**: Prioritize premium providers over backup sources for reliability - **Regional Preferences**: Prioritize local M3U sources for better geographic performance - **Quality Control**: Use specific providers known for better encoding/stability - **Cost Optimization**: Prioritize cheaper sources when quality differences are negligible ### Notes - Leave the M3U Sources field **empty** to disable prioritization (streams sort by quality only) - Order matters: the **first** M3U in the list gets **highest** priority - Prioritization applies to both "Match & Assign Streams" and "Sort Alternate Streams" actions - Streams from unspecified M3U sources receive lowest priority (999)
This commit is contained in:
@@ -63,7 +63,7 @@ class PluginConfig:
|
||||
"""
|
||||
|
||||
# === PLUGIN METADATA ===
|
||||
PLUGIN_VERSION = "0.7.0"
|
||||
PLUGIN_VERSION = "0.7.1"
|
||||
|
||||
# === MATCHING SETTINGS ===
|
||||
DEFAULT_FUZZY_MATCH_THRESHOLD = 85 # Minimum similarity score (0-100)
|
||||
@@ -415,11 +415,11 @@ class Plugin:
|
||||
},
|
||||
{
|
||||
"id": "selected_m3us",
|
||||
"label": "📡 M3U Sources (comma-separated)",
|
||||
"label": "📡 M3U Sources (comma-separated, prioritized)",
|
||||
"type": "string",
|
||||
"default": PluginConfig.DEFAULT_SELECTED_M3US,
|
||||
"placeholder": "IPTV Provider 1, Local M3U, Sports",
|
||||
"help_text": "Specific M3U sources to use when matching, or leave empty for all M3U sources. Multiple M3U sources can be specified separated by commas.",
|
||||
"help_text": "Specific M3U sources to use when matching, or leave empty for all M3U sources. Multiple M3U sources can be specified separated by commas. Order matters: streams from earlier M3U sources are prioritized over later ones when sorting by quality.",
|
||||
},
|
||||
{
|
||||
"id": "ignore_tags",
|
||||
@@ -1536,20 +1536,29 @@ class Plugin:
|
||||
return ""
|
||||
|
||||
def _sort_streams_by_quality(self, streams):
|
||||
"""Sort streams by quality using stream_stats (resolution + FPS).
|
||||
"""Sort streams by M3U priority first, then by quality using stream_stats (resolution + FPS).
|
||||
|
||||
Priority:
|
||||
1. Valid streams with good quality (>=1280x720 and >=30 FPS)
|
||||
2. Valid streams with lower quality
|
||||
3. Streams with no stats (unknown quality)
|
||||
4. Dead streams (0x0 resolution)
|
||||
1. M3U source priority (if specified - lower priority number = higher precedence)
|
||||
2. Quality tier (High > Medium > Low > Unknown > Dead)
|
||||
3. Resolution (higher = better)
|
||||
4. FPS (higher = better)
|
||||
|
||||
Quality tiers:
|
||||
- Tier 0: High quality (>=1280x720 and >=30 FPS)
|
||||
- Tier 1: Medium quality (either HD or good FPS)
|
||||
- Tier 2: Low quality (below HD and below 30 FPS)
|
||||
- Tier 3: Dead streams (0x0 resolution)
|
||||
"""
|
||||
def get_stream_quality_score(stream):
|
||||
"""Calculate quality score for sorting.
|
||||
Returns tuple: (tier, -resolution_pixels, -fps)
|
||||
Lower tier = higher priority
|
||||
Returns tuple: (m3u_priority, tier, -resolution_pixels, -fps)
|
||||
Lower values = higher priority
|
||||
Negative resolution/fps for descending sort
|
||||
"""
|
||||
# Get M3U priority (0 = highest, 999 = lowest/unspecified)
|
||||
m3u_priority = stream.get('_m3u_priority', 999)
|
||||
|
||||
stats = stream.get('stats', {})
|
||||
|
||||
# Check for dead stream (0x0)
|
||||
@@ -1558,7 +1567,7 @@ class Plugin:
|
||||
|
||||
if width == 0 or height == 0:
|
||||
# Tier 3: Dead streams (0x0) - lowest priority
|
||||
return (3, 0, 0)
|
||||
return (m3u_priority, 3, 0, 0)
|
||||
|
||||
# Calculate total pixels
|
||||
resolution_pixels = width * height
|
||||
@@ -1580,11 +1589,12 @@ class Plugin:
|
||||
# Tier 2: Low quality (below HD and below 30 FPS)
|
||||
tier = 2
|
||||
|
||||
# Return tuple for sorting: (tier, -pixels, -fps)
|
||||
# Return tuple for sorting: (m3u_priority, tier, -pixels, -fps)
|
||||
# M3U priority first, then quality tier, then resolution, then FPS
|
||||
# Negative values so higher resolution/fps sorts first within tier
|
||||
return (tier, -resolution_pixels, -fps)
|
||||
return (m3u_priority, tier, -resolution_pixels, -fps)
|
||||
|
||||
# Sort streams by quality score
|
||||
# Sort streams by M3U priority first, then quality score
|
||||
return sorted(streams, key=get_stream_quality_score)
|
||||
|
||||
def _filter_working_streams(self, streams, logger):
|
||||
@@ -3180,7 +3190,7 @@ class Plugin:
|
||||
selected_stream_groups = []
|
||||
stream_group_filter_info = " (all stream groups)"
|
||||
|
||||
# Filter streams by selected M3U sources
|
||||
# Filter streams by selected M3U sources and add priority metadata
|
||||
if selected_m3us_str:
|
||||
selected_m3us = [m.strip() for m in selected_m3us_str.split(',') if m.strip()]
|
||||
valid_m3u_ids = [m3u_name_to_id[name] for name in selected_m3us if name in m3u_name_to_id]
|
||||
@@ -3188,15 +3198,32 @@ class Plugin:
|
||||
logger.warning("[Stream-Mapparr] None of the specified M3U sources were found. Using all streams.")
|
||||
selected_m3us = []
|
||||
m3u_filter_info = " (all M3U sources - specified M3Us not found)"
|
||||
# Add default priority to all streams (no prioritization)
|
||||
for stream in all_streams_data:
|
||||
stream['_m3u_priority'] = 999 # Low priority for unspecified M3Us
|
||||
else:
|
||||
# Filter streams by m3u_account (which is the M3U account ID)
|
||||
filtered_streams = [s for s in all_streams_data if s.get('m3u_account') in valid_m3u_ids]
|
||||
# Create M3U ID to priority mapping (0 = highest priority)
|
||||
m3u_priority_map = {m3u_id: idx for idx, m3u_id in enumerate(valid_m3u_ids)}
|
||||
|
||||
# Filter streams by m3u_account (which is the M3U account ID) and add priority
|
||||
filtered_streams = []
|
||||
for s in all_streams_data:
|
||||
m3u_id = s.get('m3u_account')
|
||||
if m3u_id in valid_m3u_ids:
|
||||
# Add priority metadata based on order in selected_m3us list
|
||||
s['_m3u_priority'] = m3u_priority_map[m3u_id]
|
||||
filtered_streams.append(s)
|
||||
|
||||
logger.info(f"[Stream-Mapparr] Filtered streams from {len(all_streams_data)} to {len(filtered_streams)} based on M3U sources: {', '.join(selected_m3us)}")
|
||||
logger.info(f"[Stream-Mapparr] M3U priority order: {', '.join([f'{name} (priority {idx})' for idx, name in enumerate(selected_m3us)])}")
|
||||
all_streams_data = filtered_streams
|
||||
m3u_filter_info = f" in M3U sources: {', '.join(selected_m3us)}"
|
||||
else:
|
||||
selected_m3us = []
|
||||
m3u_filter_info = " (all M3U sources)"
|
||||
# Add default priority to all streams (no prioritization when no M3U filter)
|
||||
for stream in all_streams_data:
|
||||
stream['_m3u_priority'] = 999 # Low priority for unspecified M3Us
|
||||
|
||||
self.loaded_channels = channels_to_process
|
||||
self.loaded_streams = all_streams_data
|
||||
@@ -4485,6 +4512,25 @@ class Plugin:
|
||||
channels_in_profile = filtered_channels
|
||||
logger.info(f"[Stream-Mapparr] Filtered to {len(channels_in_profile)} channels in groups: {', '.join(selected_groups)}")
|
||||
|
||||
# Build M3U priority map if M3U sources are specified
|
||||
selected_m3us_str = settings.get('selected_m3us', '').strip()
|
||||
m3u_priority_map = {}
|
||||
if selected_m3us_str:
|
||||
# Fetch M3U sources
|
||||
try:
|
||||
all_m3us = self._get_api_data("/api/m3u/accounts/", token, settings, logger, None)
|
||||
m3u_name_to_id = {m['name']: m['id'] for m in all_m3us if 'name' in m and 'id' in m}
|
||||
|
||||
selected_m3us = [m.strip() for m in selected_m3us_str.split(',') if m.strip()]
|
||||
valid_m3u_ids = [m3u_name_to_id[name] for name in selected_m3us if name in m3u_name_to_id]
|
||||
|
||||
if valid_m3u_ids:
|
||||
# Create M3U ID to priority mapping (0 = highest priority)
|
||||
m3u_priority_map = {m3u_id: idx for idx, m3u_id in enumerate(valid_m3u_ids)}
|
||||
logger.info(f"[Stream-Mapparr] M3U priority order: {', '.join([f'{name} (priority {idx})' for idx, name in enumerate(selected_m3us)])}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[Stream-Mapparr] Could not fetch M3U sources for prioritization: {e}")
|
||||
|
||||
# Get channels with multiple streams using Django ORM
|
||||
channels_with_multiple_streams = []
|
||||
for channel in channels_in_profile:
|
||||
@@ -4493,15 +4539,25 @@ class Plugin:
|
||||
stream_ids = list(ChannelStream.objects.filter(channel_id=channel_id).order_by('order').values_list('stream_id', flat=True))
|
||||
|
||||
if len(stream_ids) > 1:
|
||||
# Fetch stream details including stats
|
||||
# Fetch stream details including stats and M3U account
|
||||
streams = []
|
||||
for stream_id in stream_ids:
|
||||
try:
|
||||
stream = Stream.objects.get(id=stream_id)
|
||||
|
||||
# Get M3U priority for this stream
|
||||
m3u_account_id = stream.m3u_account_id
|
||||
if m3u_account_id and m3u_account_id in m3u_priority_map:
|
||||
m3u_priority = m3u_priority_map[m3u_account_id]
|
||||
else:
|
||||
# Stream not from a prioritized M3U source
|
||||
m3u_priority = 999
|
||||
|
||||
streams.append({
|
||||
'id': stream.id,
|
||||
'name': stream.name,
|
||||
'stats': stream.stream_stats or {}
|
||||
'stats': stream.stream_stats or {},
|
||||
'_m3u_priority': m3u_priority
|
||||
})
|
||||
except Stream.DoesNotExist:
|
||||
logger.warning(f"[Stream-Mapparr] Stream {stream_id} no longer exists, skipping")
|
||||
|
||||
Reference in New Issue
Block a user