From c472ec089cd20aea3367d0191bcf5ab8fa4465c4 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:29:33 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`fix?= =?UTF-8?q?-search-and-results`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @Zzackllack. * https://github.com/Zzackllack/AniBridge/pull/67#issuecomment-3885839535 The following files were modified: * `app/api/torznab/api.py` * `app/utils/title_resolver.py` * `tests/test_torznab.py` --- app/api/torznab/api.py | 50 +++++++++++++++++++++++------- app/utils/title_resolver.py | 62 ++++++++++++++++++++++++------------- tests/test_torznab.py | 15 ++++++++- 3 files changed, 93 insertions(+), 34 deletions(-) diff --git a/app/api/torznab/api.py b/app/api/torznab/api.py index 3fee32e..fbda8a7 100644 --- a/app/api/torznab/api.py +++ b/app/api/torznab/api.py @@ -65,9 +65,13 @@ def _default_languages_for_site(site: str) -> List[str]: def _coerce_positive_int(value: object) -> Optional[int]: """ - Parse an input value into a positive integer. - - Returns None when parsing fails or value is <= 0. + Coerce an arbitrary value into a positive integer. + + Parameters: + value (object): Value to convert to an integer. + + Returns: + The parsed positive integer if conversion succeeds and is greater than zero, `None` otherwise. """ try: parsed = int(value) # type: ignore[arg-type] @@ -83,11 +87,15 @@ def _resolve_tvsearch_query_from_ids( imdbid: Optional[str], ) -> Optional[str]: """ - Resolve a canonical series title from Torznab identifier parameters. - - Uses SkyHook in this order: - 1) direct show lookup by tvdbid - 2) tvdb resolution via tmdb/imdb id search, then show lookup + Resolve a canonical TV series title from provided Torznab identifiers. + + If a positive `tvdbid` is supplied, the function looks up the show title for that ID. + If not, it attempts to resolve a `tvdbid` by querying SkyHook using `tmdbid` and/or `imdbid`, + then looks up the show title for the resolved `tvdbid`. + + Returns: + title (str): The resolved show title when found. + None: If no title could be resolved. """ tvdb_id = _coerce_positive_int(tvdbid) if tvdb_id is None: @@ -150,9 +158,27 @@ def _try_mapped_special_probe( special_map, ) -> tuple[bool, Optional[int], Optional[str], Optional[str], int, int, int, int]: """ - Probe mapped AniWorld special coordinates using cache first, then live probe. - - Returns availability tuple together with resolved source/alias coordinates. + Probe availability and quality for an AniWorld special that maps to a different source episode, using cached availability when possible. + + Parameters: + tn_module: Provider module exposing `get_availability` and `probe_episode_quality` used to fetch cached availability or probe live quality. + session (Session): Database/session object used by `get_availability`. + slug (str): Show identifier used for probing. + lang (str): Language to probe (e.g., "German Dub"). + site_found (str): Catalogue site name where the source episode is hosted. + special_map: Mapping object containing `source_season`, `source_episode`, `alias_season`, and `alias_episode` that describe the source coordinates and their alias. + + Returns: + tuple: ( + available (bool): `True` if the source episode is available, `False` otherwise, + height (Optional[int]): video height in pixels if known, otherwise `None`, + vcodec (Optional[str]): video codec identifier if known, otherwise `None`, + provider (Optional[str]): provider name that supplied the quality info if known, otherwise `None`, + source_season (int): season number of the mapped source episode, + source_episode (int): episode number of the mapped source episode, + alias_season (int): alias season number requested, + alias_episode (int): alias episode number requested + ) """ source_season = special_map.source_season source_episode = special_map.source_episode @@ -1121,4 +1147,4 @@ def torznab_api( xml = ET.tostring(rss, encoding="utf-8", xml_declaration=True).decode("utf-8") logger.info(f"Returning RSS feed with {count} items.") - return Response(content=xml, media_type="application/rss+xml; charset=utf-8") + return Response(content=xml, media_type="application/rss+xml; charset=utf-8") \ No newline at end of file diff --git a/app/utils/title_resolver.py b/app/utils/title_resolver.py index 69aa0a2..ce574cf 100644 --- a/app/utils/title_resolver.py +++ b/app/utils/title_resolver.py @@ -531,16 +531,23 @@ def _normalize_tokens(s: str) -> Set[str]: def _normalize_alnum(s: str) -> str: - """Lowercase and filter to alphanumeric characters.""" + """ + Produce a lowercase string containing only the alphanumeric characters from the input. + + Returns: + str: The input lowercased with all non-alphanumeric characters removed. + """ return "".join(ch.lower() for ch in s if ch.isalnum()) def _match_tokens(s: str) -> Set[str]: """ - Build query/title tokens for scoring while ignoring common stop words. - - Falls back to the unfiltered token set when filtering would remove every - token, so very short titles/queries still remain matchable. + Produce a set of query/title tokens suitable for matching by removing common stopwords. + + If removing stopwords would remove every token, returns the original normalized token set. + + Returns: + Set[str]: Tokens lowercased and split on non-alphanumeric boundaries with common stopwords removed; if that yields an empty set, the unfiltered normalized tokens are returned. """ tokens = _normalize_tokens(s) if not tokens: @@ -553,10 +560,12 @@ def _score_title_candidate( query_tokens: Set[str], query_norm: str, candidate_title: str ) -> float: """ - Score how well a candidate title matches the query. - - Uses token overlap, precision/recall, normalized string similarity and - exact/substring checks. Higher is better. + Assigns a numeric relevance score indicating how well `candidate_title` matches the query. + + The score increases with token overlap and balance between precision and recall (F1), and is boosted for exact or substring matches and for higher normalized string similarity. + + Returns: + float: Relevance score (higher is better). Returns 0.0 when there is no meaningful match. """ title_tokens = _match_tokens(candidate_title) title_norm = _normalize_alnum(candidate_title) @@ -599,11 +608,11 @@ def _score_title_candidate( def _build_sto_search_terms(query: str) -> List[str]: - """Build ordered S.to search variants from a raw query. - - Returns the raw query, a compact alphanumeric-only variant, and a dashed - variant when the compact form is numeric with length >= 3. Empty values are - filtered and the list is de-duplicated while preserving order. + """ + Builds ordered search variants for S.to from a raw query. + + Returns: + terms (List[str]): Ordered, de-duplicated list of non-empty search variants including the original trimmed query, a compact alphanumeric-only variant when different, and a dashed numeric variant when the compact form is all digits of length >= 3. """ raw = (query or "").strip() if not raw: @@ -675,19 +684,30 @@ def _search_sto_slug(query: str) -> Optional[str]: def slug_from_query(q: str, site: Optional[str] = None) -> Optional[Tuple[str, str]]: """ - Find the best-matching site and slug for a free-text query by comparing token overlap with titles and alternative titles. - + Determine the best matching catalog site and slug for a free-text series query. + Parameters: - q (str): Free-text query used to match against series titles. - site (Optional[str]): If provided, restricts the search to this site; otherwise searches all configured sites. - + q (str): Free-text query to match against site indexes and alternative titles. + site (Optional[str]): If provided, restrict search to this site; otherwise searches configured catalog sites and applies site-specific fallbacks. + Returns: - Optional[Tuple[str, str]]: `(site, slug)` of the best match, `None` if the query is empty or no match is found. + Optional[Tuple[str, str]]: Tuple `(site, slug)` for the best match, or `None` if the query is empty or no acceptable match is found. """ if not q: return None def _search_sites(sites: List[str]) -> Optional[Tuple[str, str]]: + """ + Finds the best matching (site, slug) for the current free-text query across the given sites. + + Evaluates each site's cached index and alternative titles using the module's title-scoring logic, returning the site and slug with the highest score that meets the minimum match threshold. For sites without an index, attempts a search-only slug derivation; if no indexed match is found and "s.to" is among the sites, queries the S.to suggest API as a fallback. + + Parameters: + sites (List[str]): Ordered list of site identifiers to search. + + Returns: + Optional[Tuple[str, str]]: `(site, slug)` of the best match if a candidate meets the minimum score, otherwise `None`. + """ q_tokens = _match_tokens(q) q_norm = _normalize_alnum(q) best_slug: Optional[str] = None @@ -818,4 +838,4 @@ def _search_megakino_slug(query: str) -> Optional[str]: except Exception as exc: logger.debug("Megakino provider search failed: {}", exc) return None - return match.slug if match else None + return match.slug if match else None \ No newline at end of file diff --git a/tests/test_torznab.py b/tests/test_torznab.py index c675a30..43ca62a 100644 --- a/tests/test_torznab.py +++ b/tests/test_torznab.py @@ -96,6 +96,19 @@ class Rec: seen = {"query": None} def _slug_from_query(query, site=None): + """ + Record the provided query in the shared `seen` mapping and return a fixed (site, slug) pair. + + Parameters: + query (str): The query string to record. + site (str | None): Optional site hint (unused by this stub). + + Returns: + tuple: A two-element tuple (site, slug) where `site` is `"aniworld.to"` and `slug` is `"slug"`. + + Side effects: + Mutates the `seen` mapping by setting `seen["query"] = query`. + """ seen["query"] = query return ("aniworld.to", "slug") @@ -140,4 +153,4 @@ def _slug_from_query(query, site=None): assert resp.status_code == 200 root = ET.fromstring(resp.text) assert root.find("./channel/item") is not None - assert seen["query"] == "The Rookie" + assert seen["query"] == "The Rookie" \ No newline at end of file From d471721bf435918531b862fe7eb6c003e4a9b41a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Feb 2026 17:30:01 +0000 Subject: [PATCH 2/2] style: Format Python code with Ruff --- app/api/torznab/api.py | 14 +++++++------- app/utils/title_resolver.py | 24 ++++++++++++------------ tests/test_torznab.py | 8 ++++---- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/api/torznab/api.py b/app/api/torznab/api.py index fbda8a7..c4b4840 100644 --- a/app/api/torznab/api.py +++ b/app/api/torznab/api.py @@ -66,10 +66,10 @@ def _default_languages_for_site(site: str) -> List[str]: def _coerce_positive_int(value: object) -> Optional[int]: """ Coerce an arbitrary value into a positive integer. - + Parameters: value (object): Value to convert to an integer. - + Returns: The parsed positive integer if conversion succeeds and is greater than zero, `None` otherwise. """ @@ -88,11 +88,11 @@ def _resolve_tvsearch_query_from_ids( ) -> Optional[str]: """ Resolve a canonical TV series title from provided Torznab identifiers. - + If a positive `tvdbid` is supplied, the function looks up the show title for that ID. If not, it attempts to resolve a `tvdbid` by querying SkyHook using `tmdbid` and/or `imdbid`, then looks up the show title for the resolved `tvdbid`. - + Returns: title (str): The resolved show title when found. None: If no title could be resolved. @@ -159,7 +159,7 @@ def _try_mapped_special_probe( ) -> tuple[bool, Optional[int], Optional[str], Optional[str], int, int, int, int]: """ Probe availability and quality for an AniWorld special that maps to a different source episode, using cached availability when possible. - + Parameters: tn_module: Provider module exposing `get_availability` and `probe_episode_quality` used to fetch cached availability or probe live quality. session (Session): Database/session object used by `get_availability`. @@ -167,7 +167,7 @@ def _try_mapped_special_probe( lang (str): Language to probe (e.g., "German Dub"). site_found (str): Catalogue site name where the source episode is hosted. special_map: Mapping object containing `source_season`, `source_episode`, `alias_season`, and `alias_episode` that describe the source coordinates and their alias. - + Returns: tuple: ( available (bool): `True` if the source episode is available, `False` otherwise, @@ -1147,4 +1147,4 @@ def torznab_api( xml = ET.tostring(rss, encoding="utf-8", xml_declaration=True).decode("utf-8") logger.info(f"Returning RSS feed with {count} items.") - return Response(content=xml, media_type="application/rss+xml; charset=utf-8") \ No newline at end of file + return Response(content=xml, media_type="application/rss+xml; charset=utf-8") diff --git a/app/utils/title_resolver.py b/app/utils/title_resolver.py index ce574cf..9cd3105 100644 --- a/app/utils/title_resolver.py +++ b/app/utils/title_resolver.py @@ -533,7 +533,7 @@ def _normalize_tokens(s: str) -> Set[str]: def _normalize_alnum(s: str) -> str: """ Produce a lowercase string containing only the alphanumeric characters from the input. - + Returns: str: The input lowercased with all non-alphanumeric characters removed. """ @@ -543,9 +543,9 @@ def _normalize_alnum(s: str) -> str: def _match_tokens(s: str) -> Set[str]: """ Produce a set of query/title tokens suitable for matching by removing common stopwords. - + If removing stopwords would remove every token, returns the original normalized token set. - + Returns: Set[str]: Tokens lowercased and split on non-alphanumeric boundaries with common stopwords removed; if that yields an empty set, the unfiltered normalized tokens are returned. """ @@ -561,9 +561,9 @@ def _score_title_candidate( ) -> float: """ Assigns a numeric relevance score indicating how well `candidate_title` matches the query. - + The score increases with token overlap and balance between precision and recall (F1), and is boosted for exact or substring matches and for higher normalized string similarity. - + Returns: float: Relevance score (higher is better). Returns 0.0 when there is no meaningful match. """ @@ -610,7 +610,7 @@ def _score_title_candidate( def _build_sto_search_terms(query: str) -> List[str]: """ Builds ordered search variants for S.to from a raw query. - + Returns: terms (List[str]): Ordered, de-duplicated list of non-empty search variants including the original trimmed query, a compact alphanumeric-only variant when different, and a dashed numeric variant when the compact form is all digits of length >= 3. """ @@ -685,11 +685,11 @@ def _search_sto_slug(query: str) -> Optional[str]: def slug_from_query(q: str, site: Optional[str] = None) -> Optional[Tuple[str, str]]: """ Determine the best matching catalog site and slug for a free-text series query. - + Parameters: q (str): Free-text query to match against site indexes and alternative titles. site (Optional[str]): If provided, restrict search to this site; otherwise searches configured catalog sites and applies site-specific fallbacks. - + Returns: Optional[Tuple[str, str]]: Tuple `(site, slug)` for the best match, or `None` if the query is empty or no acceptable match is found. """ @@ -699,12 +699,12 @@ def slug_from_query(q: str, site: Optional[str] = None) -> Optional[Tuple[str, s def _search_sites(sites: List[str]) -> Optional[Tuple[str, str]]: """ Finds the best matching (site, slug) for the current free-text query across the given sites. - + Evaluates each site's cached index and alternative titles using the module's title-scoring logic, returning the site and slug with the highest score that meets the minimum match threshold. For sites without an index, attempts a search-only slug derivation; if no indexed match is found and "s.to" is among the sites, queries the S.to suggest API as a fallback. - + Parameters: sites (List[str]): Ordered list of site identifiers to search. - + Returns: Optional[Tuple[str, str]]: `(site, slug)` of the best match if a candidate meets the minimum score, otherwise `None`. """ @@ -838,4 +838,4 @@ def _search_megakino_slug(query: str) -> Optional[str]: except Exception as exc: logger.debug("Megakino provider search failed: {}", exc) return None - return match.slug if match else None \ No newline at end of file + return match.slug if match else None diff --git a/tests/test_torznab.py b/tests/test_torznab.py index 43ca62a..73a82d3 100644 --- a/tests/test_torznab.py +++ b/tests/test_torznab.py @@ -98,14 +98,14 @@ class Rec: def _slug_from_query(query, site=None): """ Record the provided query in the shared `seen` mapping and return a fixed (site, slug) pair. - + Parameters: query (str): The query string to record. site (str | None): Optional site hint (unused by this stub). - + Returns: tuple: A two-element tuple (site, slug) where `site` is `"aniworld.to"` and `slug` is `"slug"`. - + Side effects: Mutates the `seen` mapping by setting `seen["query"] = query`. """ @@ -153,4 +153,4 @@ def _slug_from_query(query, site=None): assert resp.status_code == 200 root = ET.fromstring(resp.text) assert root.find("./channel/item") is not None - assert seen["query"] == "The Rookie" \ No newline at end of file + assert seen["query"] == "The Rookie"