diff --git a/src/primary/apps/eros/api.py b/src/primary/apps/eros/api.py index e37a93b8..78bdd251 100644 --- a/src/primary/apps/eros/api.py +++ b/src/primary/apps/eros/api.py @@ -500,14 +500,18 @@ def get_tag_id_by_label(api_url: str, api_key: str, api_timeout: int, tag_label: def get_exempt_tag_ids(api_url: str, api_key: str, api_timeout: int, exempt_tag_labels: list) -> dict: - """Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label. Exact match. Issue #676.""" - if not exempt_tag_labels: + """Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label. Exact match. Issue #676. + Only counts tags that are actually entered (non-empty); empty list or all-whitespace = no exclusions. + """ + if exempt_tag_labels is None: + return {} + if isinstance(exempt_tag_labels, str): + exempt_tag_labels = [exempt_tag_labels] + labels = [str(l).strip() for l in (exempt_tag_labels or []) if l is not None and str(l).strip()] + if not labels: return {} result = {} - for label in exempt_tag_labels: - label = (label or "").strip() - if not label: - continue + for label in labels: tid = get_tag_id_by_label(api_url, api_key, api_timeout, label) if tid is not None: result[tid] = label diff --git a/src/primary/apps/lidarr/api.py b/src/primary/apps/lidarr/api.py index 0a80c2e9..fbbf03c3 100644 --- a/src/primary/apps/lidarr/api.py +++ b/src/primary/apps/lidarr/api.py @@ -516,14 +516,18 @@ def get_tag_id_by_label(api_url: str, api_key: str, api_timeout: int, tag_label: def get_exempt_tag_ids(api_url: str, api_key: str, api_timeout: int, exempt_tag_labels: list) -> dict: - """Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label. Exact match. Issue #676.""" - if not exempt_tag_labels: + """Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label. Exact match. Issue #676. + Only counts tags that are actually entered (non-empty); empty list or all-whitespace = no exclusions. + """ + if exempt_tag_labels is None: + return {} + if isinstance(exempt_tag_labels, str): + exempt_tag_labels = [exempt_tag_labels] + labels = [str(l).strip() for l in (exempt_tag_labels or []) if l is not None and str(l).strip()] + if not labels: return {} result = {} - for label in exempt_tag_labels: - label = (label or "").strip() - if not label: - continue + for label in labels: tid = get_tag_id_by_label(api_url, api_key, api_timeout, label) if tid is not None: result[tid] = label diff --git a/src/primary/apps/radarr/api.py b/src/primary/apps/radarr/api.py index 79448aa4..b5054126 100644 --- a/src/primary/apps/radarr/api.py +++ b/src/primary/apps/radarr/api.py @@ -479,14 +479,17 @@ def get_exempt_tag_ids(api_url: str, api_key: str, api_timeout: int, exempt_tag_ """ Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label for tags that exist. Exact match on label. Used to filter out movies with exempt tags (issue #676). + Only counts tags that are actually entered (non-empty); empty list or all-whitespace = no exclusions. """ - if not exempt_tag_labels: + if exempt_tag_labels is None: + return {} + if isinstance(exempt_tag_labels, str): + exempt_tag_labels = [exempt_tag_labels] + labels = [str(l).strip() for l in (exempt_tag_labels or []) if l is not None and str(l).strip()] + if not labels: return {} result = {} - for label in exempt_tag_labels: - label = (label or "").strip() - if not label: - continue + for label in labels: tid = get_tag_id_by_label(api_url, api_key, api_timeout, label) if tid is not None: result[tid] = label diff --git a/src/primary/apps/readarr/api.py b/src/primary/apps/readarr/api.py index c5e4f6b5..9c435200 100644 --- a/src/primary/apps/readarr/api.py +++ b/src/primary/apps/readarr/api.py @@ -599,14 +599,18 @@ def get_tag_id_by_label(api_url: str, api_key: str, api_timeout: int, tag_label: def get_exempt_tag_ids(api_url: str, api_key: str, api_timeout: int, exempt_tag_labels: list) -> dict: - """Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label. Exact match. Issue #676.""" - if not exempt_tag_labels: + """Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label. Exact match. Issue #676. + Only counts tags that are actually entered (non-empty); empty list or all-whitespace = no exclusions. + """ + if exempt_tag_labels is None: + return {} + if isinstance(exempt_tag_labels, str): + exempt_tag_labels = [exempt_tag_labels] + labels = [str(l).strip() for l in (exempt_tag_labels or []) if l is not None and str(l).strip()] + if not labels: return {} result = {} - for label in exempt_tag_labels: - label = (label or "").strip() - if not label: - continue + for label in labels: tid = get_tag_id_by_label(api_url, api_key, api_timeout, label) if tid is not None: result[tid] = label diff --git a/src/primary/apps/sonarr/api.py b/src/primary/apps/sonarr/api.py index 7d9a9035..caf99d39 100644 --- a/src/primary/apps/sonarr/api.py +++ b/src/primary/apps/sonarr/api.py @@ -281,6 +281,7 @@ def get_missing_episodes(api_url: str, api_key: str, api_timeout: int, monitored while True: retry_count = 0 success = False + records = [] # Ensure defined for "if not records" after inner loop (e.g. on request exception) while retry_count <= retries_per_page and not success: # Parameters for the request @@ -288,7 +289,7 @@ def get_missing_episodes(api_url: str, api_key: str, api_timeout: int, monitored "page": page, "pageSize": page_size, "includeSeries": "true", - "monitored": monitored_only + "monitored": str(monitored_only).lower() # Sonarr API expects "true"/"false" } # Add series ID filter if provided @@ -650,9 +651,10 @@ def get_missing_episodes_random_page(api_url: str, api_key: str, api_timeout: in "page": 1, "pageSize": 1, "includeSeries": "true", # Include series info for filtering - "monitored": monitored_only + "monitored": str(monitored_only).lower() # Sonarr API expects "true"/"false" } - url = f"{api_url}/api/v3/{endpoint}" + base_url = api_url.rstrip('/') + url = f"{base_url}/api/v3/{endpoint.lstrip('/')}" for attempt in range(retries + 1): try: @@ -670,14 +672,17 @@ def get_missing_episodes_random_page(api_url: str, api_key: str, api_timeout: in try: data = response.json() - total_records = data.get('totalRecords', 0) + # Support both totalRecords (Sonarr v3) and total (some versions) + total_records = data.get('totalRecords', data.get('total', 0)) + if isinstance(total_records, dict): + total_records = 0 if total_records == 0: sonarr_logger.info("No missing episodes found in Sonarr.") return [] # Calculate total pages with our desired page size - total_pages = (total_records + page_size - 1) // page_size + total_pages = max(1, (total_records + page_size - 1) // page_size) sonarr_logger.info(f"Found {total_records} total missing episodes across {total_pages} pages") if total_pages == 0: @@ -693,7 +698,7 @@ def get_missing_episodes_random_page(api_url: str, api_key: str, api_timeout: in "page": random_page, "pageSize": page_size, "includeSeries": "true", - "monitored": monitored_only + "monitored": str(monitored_only).lower() # Sonarr API expects "true"/"false" } if series_id is not None: @@ -1180,14 +1185,19 @@ def get_tag_id_by_label(api_url: str, api_key: str, api_timeout: int, tag_label: def get_exempt_tag_ids(api_url: str, api_key: str, api_timeout: int, exempt_tag_labels: list) -> dict: - """Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label. Exact match. Issue #676.""" - if not exempt_tag_labels: + """Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label. Exact match. Issue #676. + Only counts tags that are actually entered (non-empty); empty list or all-whitespace = no exclusions. + """ + if exempt_tag_labels is None: + return {} + # Normalize: accept list or single string; only consider non-empty stripped labels + if isinstance(exempt_tag_labels, str): + exempt_tag_labels = [exempt_tag_labels] + labels = [str(l).strip() for l in (exempt_tag_labels or []) if l is not None and str(l).strip()] + if not labels: return {} result = {} - for label in exempt_tag_labels: - label = (label or "").strip() - if not label: - continue + for label in labels: tid = get_tag_id_by_label(api_url, api_key, api_timeout, label) if tid is not None: result[tid] = label diff --git a/src/primary/apps/sonarr/missing.py b/src/primary/apps/sonarr/missing.py index 336c3b97..122f84c2 100644 --- a/src/primary/apps/sonarr/missing.py +++ b/src/primary/apps/sonarr/missing.py @@ -51,9 +51,19 @@ def should_delay_episode_search(air_date_str: str, delay_days: int) -> bool: # Get logger for the Sonarr app sonarr_logger = get_logger("sonarr") + +def _normalize_exempt_tags(exempt_tags: list) -> list: + """Only count tags that are entered (non-empty). Empty/whitespace = no exclusions. Issue #805.""" + if exempt_tags is None: + return [] + return [t for t in (exempt_tags if isinstance(exempt_tags, list) else [exempt_tags]) + if isinstance(t, str) and (t or "").strip()] + + def _get_exempt_series_ids(api_url: str, api_key: str, api_timeout: int, exempt_tags: list) -> set: - """Return set of series IDs that have any exempt tag.""" + """Return set of series IDs that have any exempt tag. Empty exempt list = no series excluded.""" exempt_series_ids = set() + exempt_tags = _normalize_exempt_tags(exempt_tags) if not exempt_tags: return exempt_series_ids exempt_id_to_label = sonarr_api.get_exempt_tag_ids(api_url, api_key, api_timeout, exempt_tags) @@ -105,7 +115,8 @@ def process_missing_episodes( "shows_missing": "huntarr-shows-missing" } - exempt_tags = exempt_tags or [] + # Only count tags that are entered; empty = process every item (Issue #805) + exempt_tags = _normalize_exempt_tags(exempt_tags or []) # Handle different modes if hunt_missing_mode == "seasons_packs": @@ -163,7 +174,7 @@ def process_missing_seasons_packs_mode( Uses a direct episode lookup approach which is much more efficient """ processed_any = False - exempt_tags = exempt_tags or [] + exempt_tags = _normalize_exempt_tags(exempt_tags or []) # Use custom tags if provided, otherwise use defaults if custom_tags is None: @@ -392,7 +403,7 @@ def process_missing_shows_mode( ) -> bool: """Process missing episodes in show mode - gets all missing episodes for entire shows.""" processed_any = False - exempt_tags = exempt_tags or [] + exempt_tags = _normalize_exempt_tags(exempt_tags or []) # Use custom tags if provided, otherwise use defaults if custom_tags is None: @@ -617,7 +628,7 @@ def process_missing_episodes_mode( which can be useful for targeting specific episodes but is not recommended for most users. """ processed_any = False - exempt_tags = exempt_tags or [] + exempt_tags = _normalize_exempt_tags(exempt_tags or []) # Use custom tags if provided, otherwise use defaults if custom_tags is None: diff --git a/src/primary/apps/sonarr/upgrade.py b/src/primary/apps/sonarr/upgrade.py index 0ce78d81..58b91024 100644 --- a/src/primary/apps/sonarr/upgrade.py +++ b/src/primary/apps/sonarr/upgrade.py @@ -8,6 +8,7 @@ import random from typing import List, Dict, Any, Set, Callable, Union, Optional, Optional from src.primary.utils.logger import get_logger from src.primary.apps.sonarr import api as sonarr_api +from src.primary.apps.sonarr.missing import _normalize_exempt_tags from src.primary.stats_manager import increment_stat, check_hourly_cap_exceeded from src.primary.stateful_manager import is_processed, add_processed_id from src.primary.utils.history_utils import log_processed_media @@ -135,8 +136,7 @@ def process_cutoff_upgrades( sonarr_logger.info(f"Using {upgrade_mode.upper()} mode for quality upgrades") - # Use seasons_packs mode or episodes mode - exempt_tags = exempt_tags or [] + # Use seasons_packs mode or episodes mode (exempt_tags already normalized above) if upgrade_mode == "seasons_packs": return process_upgrade_seasons_mode( api_url, api_key, instance_name, api_timeout, monitored_only, diff --git a/src/primary/apps/whisparr/api.py b/src/primary/apps/whisparr/api.py index f94f9749..90b11c4c 100644 --- a/src/primary/apps/whisparr/api.py +++ b/src/primary/apps/whisparr/api.py @@ -455,14 +455,18 @@ def get_tag_id_by_label(api_url: str, api_key: str, api_timeout: int, tag_label: def get_exempt_tag_ids(api_url: str, api_key: str, api_timeout: int, exempt_tag_labels: list) -> dict: - """Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label. Exact match. Issue #676.""" - if not exempt_tag_labels: + """Resolve exempt tag labels to tag IDs. Returns dict tag_id -> label. Exact match. Issue #676. + Only counts tags that are actually entered (non-empty); empty list or all-whitespace = no exclusions. + """ + if exempt_tag_labels is None: + return {} + if isinstance(exempt_tag_labels, str): + exempt_tag_labels = [exempt_tag_labels] + labels = [str(l).strip() for l in (exempt_tag_labels or []) if l is not None and str(l).strip()] + if not labels: return {} result = {} - for label in exempt_tag_labels: - label = (label or "").strip() - if not label: - continue + for label in labels: tid = get_tag_id_by_label(api_url, api_key, api_timeout, label) if tid is not None: result[tid] = label diff --git a/version.txt b/version.txt index 03ea4a83..18f12509 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -9.1.4 +9.1.5