This commit is contained in:
Admin9705
2026-01-31 17:50:44 -05:00
parent 0b6703ff46
commit df8a58617f
9 changed files with 89 additions and 49 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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,

View File

@@ -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

View File

@@ -1 +1 @@
9.1.4
9.1.5