From be5579500da75a8485e8aea436a154c6ca295f12 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Nov 2025 18:15:40 +0000 Subject: [PATCH 1/2] Add stream group filter feature - Add new 'selected_stream_groups' setting field to filter streams by group - Fetch stream groups from API endpoint /api/channels/stream-groups/ - Filter streams based on selected stream groups (comma-separated or blank for ALL) - Update CSV header comments to display selected stream groups - Store selected_stream_groups in processed_data for persistence - Apply filter in both Preview Changes and Add Streams actions This allows users to limit which streams are considered during matching by specifying stream group names (e.g., "TVE, Cable") or leaving blank to use all available stream groups. --- Stream-Mapparr/plugin.py | 73 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/Stream-Mapparr/plugin.py b/Stream-Mapparr/plugin.py index 05583f1..746af33 100644 --- a/Stream-Mapparr/plugin.py +++ b/Stream-Mapparr/plugin.py @@ -158,6 +158,14 @@ class Plugin: "placeholder": "Sports, News, Entertainment", "help_text": "Specific channel groups to process, or leave empty for all groups.", }, + { + "id": "selected_stream_groups", + "label": "📺 Stream Groups (comma-separated)", + "type": "string", + "default": "", + "placeholder": "TVE, Cable, Satellite", + "help_text": "Specific stream groups to use when matching, or leave empty for all stream groups. Multiple groups can be specified separated by commas.", + }, { "id": "ignore_tags", "label": "🏷️ Ignore Tags (comma-separated)", @@ -1975,6 +1983,8 @@ class Plugin: profile_names_str = profile_names_str.strip() if profile_names_str else "" selected_groups_str = settings.get("selected_groups") or "" selected_groups_str = selected_groups_str.strip() if selected_groups_str else "" + selected_stream_groups_str = settings.get("selected_stream_groups") or "" + selected_stream_groups_str = selected_stream_groups_str.strip() if selected_stream_groups_str else "" ignore_tags_str = settings.get("ignore_tags") or "" ignore_tags_str = ignore_tags_str.strip() if ignore_tags_str else "" visible_channel_limit_str = settings.get("visible_channel_limit", "1") @@ -2049,6 +2059,44 @@ class Plugin: group_name_to_id = {g['name']: g['id'] for g in all_groups if 'name' in g and 'id' in g} + # Fetch stream groups with rate limiting + self._send_progress_update("load_process_channels", 'running', 35, 'Fetching stream groups...', context) + all_stream_groups = [] + page = 1 + while True: + try: + api_stream_groups = self._get_api_data(f"/api/channels/stream-groups/?page={page}", token, settings, logger, limiter=limiter) + except Exception as e: + # If we get an error (e.g., 404 for non-existent page), we've reached the end + if page > 1: + logger.debug(f"[Stream-Mapparr] No more stream group pages available (attempted page {page})") + break + else: + # If error on first page, stream groups might not be available in this API version + logger.warning(f"[Stream-Mapparr] Could not fetch stream groups (API may not support this endpoint): {e}") + break + + if isinstance(api_stream_groups, dict) and 'results' in api_stream_groups: + results = api_stream_groups['results'] + if not results: + logger.debug("[Stream-Mapparr] Reached last page of stream groups (empty results)") + break + all_stream_groups.extend(results) + if not api_stream_groups.get('next'): + break + page += 1 + elif isinstance(api_stream_groups, list): + if not api_stream_groups: + logger.debug("[Stream-Mapparr] Reached last page of stream groups (empty results)") + break + all_stream_groups.extend(api_stream_groups) + break + else: + break + + stream_group_name_to_id = {g['name']: g['id'] for g in all_stream_groups if 'name' in g and 'id' in g} + logger.info(f"[Stream-Mapparr] Found {len(all_stream_groups)} stream groups") + # Fetch channels with rate limiting self._send_progress_update("load_process_channels", 'running', 40, 'Fetching channels...', context) all_channels = self._get_api_data("/api/channels/channels/", token, settings, logger, limiter=limiter) @@ -2139,6 +2187,24 @@ class Plugin: logger.warning("[Stream-Mapparr] Unexpected streams response format") break + # Filter streams by selected stream groups + if selected_stream_groups_str: + selected_stream_groups = [g.strip() for g in selected_stream_groups_str.split(',') if g.strip()] + valid_stream_group_ids = [stream_group_name_to_id[name] for name in selected_stream_groups if name in stream_group_name_to_id] + if not valid_stream_group_ids: + logger.warning("[Stream-Mapparr] None of the specified stream groups were found. Using all streams.") + selected_stream_groups = [] + stream_group_filter_info = " (all stream groups - specified groups not found)" + else: + # Filter streams by stream_group_id + filtered_streams = [s for s in all_streams_data if s.get('stream_group_id') in valid_stream_group_ids] + logger.info(f"[Stream-Mapparr] Filtered streams from {len(all_streams_data)} to {len(filtered_streams)} based on stream groups: {', '.join(selected_stream_groups)}") + all_streams_data = filtered_streams + stream_group_filter_info = f" in stream groups: {', '.join(selected_stream_groups)}" + else: + selected_stream_groups = [] + stream_group_filter_info = " (all stream groups)" + self.loaded_channels = channels_to_process self.loaded_streams = all_streams_data @@ -2149,6 +2215,7 @@ class Plugin: "profile_id": profile_id, "profile_ids": profile_ids, "selected_groups": selected_groups, + "selected_stream_groups": selected_stream_groups, "ignore_tags": ignore_tags, "visible_channel_limit": visible_channel_limit, "ignore_quality": ignore_quality, @@ -2182,8 +2249,9 @@ class Plugin: profile_name = processed_data.get('profile_name', 'N/A') selected_groups = processed_data.get('selected_groups', []) + selected_stream_groups = processed_data.get('selected_stream_groups', []) current_threshold = settings.get('fuzzy_match_threshold', 85) - + # Build header with all settings except login credentials header_lines = [ f"# Stream-Mapparr Export v{self.version}", @@ -2191,7 +2259,8 @@ class Plugin: "#", "# === Profile & Group Settings ===", f"# Profile Name(s): {profile_name}", - f"# Selected Groups: {', '.join(selected_groups) if selected_groups else '(all groups)'}", + f"# Selected Channel Groups: {', '.join(selected_groups) if selected_groups else '(all groups)'}", + f"# Selected Stream Groups: {', '.join(selected_stream_groups) if selected_stream_groups else '(all stream groups)'}", "#", "# === Matching Settings ===", f"# Fuzzy Match Threshold: {current_threshold}", From 05a89bfb27f965fbfd52432f0e6179e5fdf0d37d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Nov 2025 18:32:38 +0000 Subject: [PATCH 2/2] Add M3U source filter feature - Add new 'selected_m3us' setting field to filter streams by M3U source - Fetch M3U sources from API endpoint /api/channels/m3us/ - Filter streams based on selected M3U sources (comma-separated or blank for ALL) - Update CSV header comments to display selected M3U sources - Store selected_m3us in processed_data for persistence - Apply filter in both Preview Changes and Add Streams actions This allows users to limit which streams are considered during matching by specifying M3U source names (e.g., "IPTV Provider 1, Local M3U") or leaving blank to use all available M3U sources. Works in conjunction with the stream group filter for fine-grained stream selection. --- Stream-Mapparr/plugin.py | 69 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/Stream-Mapparr/plugin.py b/Stream-Mapparr/plugin.py index 746af33..a5b4e5c 100644 --- a/Stream-Mapparr/plugin.py +++ b/Stream-Mapparr/plugin.py @@ -166,6 +166,14 @@ class Plugin: "placeholder": "TVE, Cable, Satellite", "help_text": "Specific stream groups to use when matching, or leave empty for all stream groups. Multiple groups can be specified separated by commas.", }, + { + "id": "selected_m3us", + "label": "📡 M3U Sources (comma-separated)", + "type": "string", + "default": "", + "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.", + }, { "id": "ignore_tags", "label": "🏷️ Ignore Tags (comma-separated)", @@ -1985,6 +1993,8 @@ class Plugin: selected_groups_str = selected_groups_str.strip() if selected_groups_str else "" selected_stream_groups_str = settings.get("selected_stream_groups") or "" selected_stream_groups_str = selected_stream_groups_str.strip() if selected_stream_groups_str else "" + selected_m3us_str = settings.get("selected_m3us") or "" + selected_m3us_str = selected_m3us_str.strip() if selected_m3us_str else "" ignore_tags_str = settings.get("ignore_tags") or "" ignore_tags_str = ignore_tags_str.strip() if ignore_tags_str else "" visible_channel_limit_str = settings.get("visible_channel_limit", "1") @@ -2097,6 +2107,44 @@ class Plugin: stream_group_name_to_id = {g['name']: g['id'] for g in all_stream_groups if 'name' in g and 'id' in g} logger.info(f"[Stream-Mapparr] Found {len(all_stream_groups)} stream groups") + # Fetch M3U sources with rate limiting + self._send_progress_update("load_process_channels", 'running', 37, 'Fetching M3U sources...', context) + all_m3us = [] + page = 1 + while True: + try: + api_m3us = self._get_api_data(f"/api/channels/m3us/?page={page}", token, settings, logger, limiter=limiter) + except Exception as e: + # If we get an error (e.g., 404 for non-existent page), we've reached the end + if page > 1: + logger.debug(f"[Stream-Mapparr] No more M3U pages available (attempted page {page})") + break + else: + # If error on first page, M3Us might not be available in this API version + logger.warning(f"[Stream-Mapparr] Could not fetch M3U sources (API may not support this endpoint): {e}") + break + + if isinstance(api_m3us, dict) and 'results' in api_m3us: + results = api_m3us['results'] + if not results: + logger.debug("[Stream-Mapparr] Reached last page of M3Us (empty results)") + break + all_m3us.extend(results) + if not api_m3us.get('next'): + break + page += 1 + elif isinstance(api_m3us, list): + if not api_m3us: + logger.debug("[Stream-Mapparr] Reached last page of M3Us (empty results)") + break + all_m3us.extend(api_m3us) + break + else: + break + + m3u_name_to_id = {m['name']: m['id'] for m in all_m3us if 'name' in m and 'id' in m} + logger.info(f"[Stream-Mapparr] Found {len(all_m3us)} M3U sources") + # Fetch channels with rate limiting self._send_progress_update("load_process_channels", 'running', 40, 'Fetching channels...', context) all_channels = self._get_api_data("/api/channels/channels/", token, settings, logger, limiter=limiter) @@ -2205,6 +2253,24 @@ class Plugin: selected_stream_groups = [] stream_group_filter_info = " (all stream groups)" + # Filter streams by selected M3U sources + 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] + if not valid_m3u_ids: + 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)" + else: + # Filter streams by m3u_id + filtered_streams = [s for s in all_streams_data if s.get('m3u_id') in valid_m3u_ids] + logger.info(f"[Stream-Mapparr] Filtered streams from {len(all_streams_data)} to {len(filtered_streams)} based on M3U sources: {', '.join(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)" + self.loaded_channels = channels_to_process self.loaded_streams = all_streams_data @@ -2216,6 +2282,7 @@ class Plugin: "profile_ids": profile_ids, "selected_groups": selected_groups, "selected_stream_groups": selected_stream_groups, + "selected_m3us": selected_m3us, "ignore_tags": ignore_tags, "visible_channel_limit": visible_channel_limit, "ignore_quality": ignore_quality, @@ -2250,6 +2317,7 @@ class Plugin: profile_name = processed_data.get('profile_name', 'N/A') selected_groups = processed_data.get('selected_groups', []) selected_stream_groups = processed_data.get('selected_stream_groups', []) + selected_m3us = processed_data.get('selected_m3us', []) current_threshold = settings.get('fuzzy_match_threshold', 85) # Build header with all settings except login credentials @@ -2261,6 +2329,7 @@ class Plugin: f"# Profile Name(s): {profile_name}", f"# Selected Channel Groups: {', '.join(selected_groups) if selected_groups else '(all groups)'}", f"# Selected Stream Groups: {', '.join(selected_stream_groups) if selected_stream_groups else '(all stream groups)'}", + f"# Selected M3U Sources: {', '.join(selected_m3us) if selected_m3us else '(all M3U sources)'}", "#", "# === Matching Settings ===", f"# Fuzzy Match Threshold: {current_threshold}",