#!/usr/bin/env python3 """Helpers for resolving watchlist identities from queued download jobs.""" import re def _resolution_result(show_id, title, reason, return_reason): if return_reason: return show_id, title, reason return show_id, title def _normalized_variants(title, normalize_title_key_fn, base_title_variants_fn): del base_title_variants_fn variants = set() normalized_title = normalize_title_key_fn(title) if normalized_title: variants.add(normalized_title) stripped = re.sub(r"\b(19|20)\d{2}\b", " ", normalized_title) stripped = re.sub(r"\s+", " ", stripped).strip() if stripped: variants.add(stripped) return variants def titles_confidently_match(expected_title, candidate_title, normalize_title_key_fn, base_title_variants_fn): expected_variants = _normalized_variants(expected_title, normalize_title_key_fn, base_title_variants_fn) candidate_variants = _normalized_variants(candidate_title, normalize_title_key_fn, base_title_variants_fn) if not expected_variants or not candidate_variants: return False return bool(expected_variants & candidate_variants) def resolve_job_watchlist_identity( job, *, search_fn, normalize_title_key_fn, base_title_variants_fn, default_mode, return_reason=False, ): show_id = str(job.get("show_id") or "").strip() title = str(job.get("title") or job.get("anime_name") or "").strip() if show_id: return _resolution_result(show_id, title, "", return_reason) raw_index = job.get("result_index") if raw_index in (None, ""): index = None else: try: index = max(1, int(raw_index)) except (TypeError, ValueError): return _resolution_result("", title, "invalid queued result index", return_reason) query = str(job.get("query") or title).strip() if not query or not title: return _resolution_result("", title, "missing queued search query or title", return_reason) try: results = search_fn(query, job.get("mode") or default_mode) except Exception as exc: return _resolution_result("", title, f"search fallback failed: {exc}", return_reason) matches = [] for position, result in enumerate(results, start=1): candidate_title = str(result.get("title") or "").strip() if not titles_confidently_match(title, candidate_title, normalize_title_key_fn, base_title_variants_fn): continue resolved_show_id = str(result.get("id") or "").strip() if not resolved_show_id: continue matches.append((position, result, candidate_title, resolved_show_id)) if not matches: candidate_title = str((results[0].get("title") if results else "") or "").strip() return _resolution_result("", title, f"title mismatch with fallback search result: {candidate_title or 'unknown'}", return_reason) if index is not None: indexed = next((match for match in matches if match[0] == index), None) if indexed is not None: _position, result, candidate_title, resolved_show_id = indexed return _resolution_result(resolved_show_id, candidate_title or title, "", return_reason) if len(matches) != 1: return _resolution_result("", title, "ambiguous fallback search results", return_reason) _position, result, candidate_title, resolved_show_id = matches[0] if not resolved_show_id: return _resolution_result("", title, "fallback search result did not include a show_id", return_reason) return _resolution_result(resolved_show_id, candidate_title or title, "", return_reason)