Skip to content

Stop polling when backend state is unavailable - #470

Draft
aryan-j wants to merge 8 commits into
Appz4Fun:mainfrom
aryan-j:agent/stop-unobservable-queue-polls
Draft

Stop polling when backend state is unavailable#470
aryan-j wants to merge 8 commits into
Appz4Fun:mainfrom
aryan-j:agent/stop-unobservable-queue-polls

Conversation

@aryan-j

@aryan-j aryan-j commented Jul 29, 2026

Copy link
Copy Markdown

Summary

  • retain strict content identity filtering and sequential distinct-release retries from superseded draft Harden content selection and retry distinct releases #469
  • stop local polling after 60 seconds when job status, history, and WebDAV are all unobservable
  • surface a final connection error instead of leaving Kodi on the last-known Queued state
  • deliberately avoid cancelling the remote job because it may still be active and complete
  • redact nonstandard Newznab credentials embedded in GetNZB URL paths, scoped to URL spans

Evidence

A real S02E01 request had one terminal missing-article failure followed by a second job whose status/history/WebDAV calls all returned HTTP 500. Kodi retained Queued; the remote second job later completed. The configured one-hour download timeout did not bound this observation outage, and cancellation also returned HTTP 500.

Service-log follow-up identified the shared HTTP 500 source as intermittent NZBDav SQLite Error 14 (unable to open database file) affecting its API and WebDAV layers. That service-storage issue is operationally separate from this add-on-side bounded failure handling.

Codacy remediation

The original check reported six exact annotations: one ErrorProne observation-state comparison warning, four cyclomatic-complexity findings, and one method-length finding. The observation tracker now uses an explicit numeric sentinel and helper; new identity, retry-selection, and poll-state logic was decomposed below Codacy's thresholds without changing behavior.

Validation

  • focused identity/retry/observability tests: 71 passed
  • complete local suite: 2,596 passed, 2 skipped, 3 environment/platform failures
    • two pre-existing Windows timing-baseline failures, reproducible on untouched upstream
    • one Windows environment failure because WSL has no default /bin/bash
  • ruff and black: passed
  • pylint: 10.00/10
  • vermin: runtime floor remains Python 3.7
  • credential redaction coverage includes direct URLs, free-form errors, and non-URL short-parameter preservation

This draft is the canonical review target and supersedes #469.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Improved result matching to select only the requested movie or episode, including title, season, episode, and year.
    • Added automatic retries using distinct releases when a selected release is unavailable.
    • Added richer playback metadata and improved TV episode identity handling.
  • Bug Fixes
    • Prevented credentials in download links and error messages from being exposed.
    • Polling now stops with a clear notification when backend status cannot be observed for an extended period.
  • Chores
    • Updated episode playback configuration to support separate show and episode metadata.

Walkthrough

Strict content identity filtering, scoped URL credential redaction, bounded polling, distinct release retries, and playback identity metadata were added. Episode player templates now pass separate show and episode TMDB identifiers, with tests covering the new filtering, retry, observability, redaction, and metadata behavior.

Changes

Resolution reliability and playback identity

Layer / File(s) Summary
Requested identity filtering
repo/plugin.video.nzbdav/resources/lib/filter.py, repo/plugin.video.nzbdav/resources/lib/router_play.py, tests/test_filter.py
Picker results are filtered by title, media type, season/episode, year, and extra-content markers before existing result filtering.
getnzb credential redaction
repo/plugin.video.nzbdav/resources/lib/http_util.py, tests/test_http_util.py
Credential-like i and r values in getnzb URL paths are redacted in URLs and embedded text, while unrelated parameters remain unchanged.
Bounded poll observation outages
repo/plugin.video.nzbdav/resources/lib/resolver.py, repo/plugin.video.nzbdav/resources/lib/resolver_pollloop.py, tests/test_poll_observability.py
Unavailable backend observations are tracked for a bounded period, after which polling stops and a notification or modal message is surfaced without remote cancellation.
Distinct release retry flow
repo/plugin.video.nzbdav/resources/lib/router_play.py, repo/plugin.video.nzbdav/resources/lib/resolver_flow.py, repo/plugin.video.nzbdav/resources/lib/resolver_retry.py, tests/test_release_retry.py
Picker candidates are deduplicated and capped, then polled sequentially only when the previous release is provably dead.
Playback identity and episode handoff
repo/plugin.video.nzbdav/resources/lib/resolver_metadata.py, repo/plugin.video.nzbdav/resources/lib/resolver_resume.py, repo/plugin.video.nzbdav/resources/lib/resolver.py, repo/plugin.video.nzbdav/resources/lib/resolver_flow.py, repo/plugin.video.nzbdav/resources/players/nzbdav.json, repo/plugin.video.nzbdav/resources/lib/player_installer.py, tests/test_resolver_metadata.py, tests/test_player_installer.py, tests/test_resolver.py
Playback parameters now populate Kodi metadata, artwork, cast, and stable IDs; episode player commands pass distinct show and episode TMDB identifiers.
Estimated code review effort: 4 (Complex) ~60 minutes

Suggested reviewers: xbmc4lyfe

Poem

A rabbit hops through releases bright,
Matching titles left and right.
Secrets hide where URLs wind,
Lost streams find a fallback kind.
Show and episode IDs align—
Metadata blooms in playback time.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: stopping polling when backend state is unavailable.
Description check ✅ Passed The description is directly related to the changeset and covers polling, retries, and redaction work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Not up to standards ⛔

🔴 Issues 5 medium

Alerts:
⚠ 5 issues (≤ 0 issues of at least minor severity)

Results:
5 new issues

Category Results
Complexity 5 medium

View in Codacy

🟢 Metrics 138 complexity · 0 duplication

Metric Results
Complexity 138
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai
coderabbitai Bot requested a review from xbmc4lyfe July 29, 2026 13:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_release_retry.py (1)

17-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: prefer unpacking over list concatenation.

♻️ Suggested fix
-    _attach_retry_candidates(params, selected, [selected, duplicate] + distinct)
+    _attach_retry_candidates(params, selected, [selected, duplicate, *distinct])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_release_retry.py` around lines 17 - 33, Update
test_retry_candidates_are_distinct_and_capped to pass the retry candidates by
unpacking the selected and duplicate rows alongside distinct, instead of
concatenating lists, while preserving the assertion that only the first five
distinct candidates are retained.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@repo/plugin.video.nzbdav/resources/lib/filter.py`:
- Around line 567-574: Update _media_type_matches and the filtering flow used by
_prepare_picker_rows so season-pack rows or season-only candidates bypass the
strict _episode_identity_matches check when both season and episode are set.
Preserve strict episode-marker validation for individual episode candidates,
while allowing whole-season titles such as S01.COMPLETE to remain available for
_prepend_pack.

In `@repo/plugin.video.nzbdav/resources/lib/http_util.py`:
- Around line 44-53: Update redact_url() to apply the scoped getnzb credential
redaction to query parameters as well as parts.path, covering getnzb URLs with
?i=...&r=... and the existing .nzb& form. Keep i/r redaction limited to
getnzb-looking URLs, while preserving ordinary application query parameters and
existing _REDACT_PARAM_NAMES behavior.

---

Nitpick comments:
In `@tests/test_release_retry.py`:
- Around line 17-33: Update test_retry_candidates_are_distinct_and_capped to
pass the retry candidates by unpacking the selected and duplicate rows alongside
distinct, instead of concatenating lists, while preserving the assertion that
only the first five distinct candidates are retained.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 34bfd276-da4f-4edf-998c-ea437039d2e6

📥 Commits

Reviewing files that changed from the base of the PR and between 000221a and ac0f313.

📒 Files selected for processing (11)
  • repo/plugin.video.nzbdav/resources/lib/filter.py
  • repo/plugin.video.nzbdav/resources/lib/http_util.py
  • repo/plugin.video.nzbdav/resources/lib/resolver.py
  • repo/plugin.video.nzbdav/resources/lib/resolver_flow.py
  • repo/plugin.video.nzbdav/resources/lib/resolver_pollloop.py
  • repo/plugin.video.nzbdav/resources/lib/resolver_retry.py
  • repo/plugin.video.nzbdav/resources/lib/router_play.py
  • tests/test_filter.py
  • tests/test_http_util.py
  • tests/test_poll_observability.py
  • tests/test_release_retry.py
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Codacy Static Code Analysis
🧰 Additional context used
🪛 ast-grep (0.45.0)
repo/plugin.video.nzbdav/resources/lib/filter.py

[warning] 579-579: XPath query is request-/variable-derived; use parameterized XPath to prevent injection.
Context: _YEAR_RE.findall(candidate_title)
Note: [CWE-643] Improper Neutralization of Data within XPath Expressions ('XPath Injection').

(xpath-injection-python)


[warning] 535-540: Regex pattern passed to re is built from a non-literal (variable, call, concatenation, or f-string) value. If that value is attacker-controlled it can introduce a malicious pattern with catastrophic backtracking (ReDoS). Use a hardcoded literal pattern, or validate/escape untrusted input with re.escape() and bound the regex complexity before compiling.
Context: re.compile(
r"(?<![A-Za-z0-9])"
+ r"[\W_]+".join(re.escape(word) for word in words)
+ r"(?![A-Za-z0-9])",
re.I,
)
Note: [CWE-1333] Inefficient Regular Expression Complexity.

(redos-non-literal-regex-python)

🪛 GitHub Check: Codacy Static Code Analysis
repo/plugin.video.nzbdav/resources/lib/filter.py

[warning] 544-544: repo/plugin.video.nzbdav/resources/lib/filter.py#L544
Method _episode_identity_matches has a cyclomatic complexity of 9 (limit is 8)

repo/plugin.video.nzbdav/resources/lib/router_play.py

[warning] 491-491: repo/plugin.video.nzbdav/resources/lib/router_play.py#L491
Method _attach_retry_candidates has a cyclomatic complexity of 10 (limit is 8)

🪛 Ruff (0.16.0)
tests/test_release_retry.py

[warning] 31-31: Consider [selected, duplicate, *distinct] instead of concatenation

Replace with [selected, duplicate, *distinct]

(RUF005)

🔇 Additional comments (11)
repo/plugin.video.nzbdav/resources/lib/filter.py (1)

518-621: LGTM! Strict identity filter logic and its rejection/acceptance paths match the provided test cases (test_strict_identity_filter_rejects_wrong_title_episode_and_extras, test_strict_identity_filter_rejects_episode_and_wrong_year_for_movie).

repo/plugin.video.nzbdav/resources/lib/router_play.py (1)

331-345: LGTM! Retry-candidate attachment and identity-based picker filtering are wired consistently across all play/search selection paths, and the dedupe/cap logic matches test_retry_candidates_are_distinct_and_capped.

Also applies to: 441-441, 472-514, 827-827, 853-856, 911-911, 933-933

tests/test_filter.py (1)

11-11: LGTM!

Also applies to: 38-71

repo/plugin.video.nzbdav/resources/lib/http_util.py (1)

127-134: LGTM! _redact_url_span correctly scopes full redact_url() redaction (userinfo + getnzb path credentials) to just the matched embedded URL span, confirmed by test_redact_text_scopes_getnzb_path_credentials_to_url.

Also applies to: 152-152

tests/test_http_util.py (1)

232-248: LGTM!

Also applies to: 306-333

repo/plugin.video.nzbdav/resources/lib/resolver.py (1)

96-97: LGTM!

Also applies to: 444-448, 494-497

repo/plugin.video.nzbdav/resources/lib/resolver_pollloop.py (1)

84-135: LGTM! Bounded observation-outage tracking correctly requires a continuous 60s streak of total backend-observation failure, surfaces a local-only timeout without cancelling the remote job, and matches the accompanying test_poll_observability.py coverage.

Also applies to: 241-254, 271-271, 286-297, 321-321

tests/test_poll_observability.py (1)

15-125: LGTM!

repo/plugin.video.nzbdav/resources/lib/resolver_retry.py (1)

1-73: LGTM! The dead-gated retry sequencing correctly avoids triggering a fallback release attempt on an ambiguous bounded-observation timeout (since that path never marks the URL dead), only on a provably terminal failure — matches test_release_retry_runs_only_after_provably_dead_attempt / test_release_retry_stops_after_nonterminal_failure.

repo/plugin.video.nzbdav/resources/lib/resolver_flow.py (1)

201-218: LGTM!

tests/test_release_retry.py (1)

35-81: LGTM!

Comment thread repo/plugin.video.nzbdav/resources/lib/filter.py
Comment on lines +44 to +53
# Some Newznab indexers return download URLs shaped like
# ``/getnzb/id.nzb&i=ACCOUNT&r=APIKEY``. Because there is no ``?``, both
# credentials are parsed as part of the URL path rather than query params.
# Restrict the short ``i``/``r`` names to getnzb-looking paths so ordinary
# application URLs using those names are not over-redacted.
_GETNZB_PATH_CRED_RE = re.compile(
r"([&;](?:i|r))=([^&\s\"'<>]+)",
re.IGNORECASE,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "_REDACT_PARAM_NAMES" repo/plugin.video.nzbdav/resources/lib/http_util.py -B2 -A10

Repository: Appz4Fun/nzbdavkodi

Length of output: 1244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,130p' repo/plugin.video.nzbdav/resources/lib/http_util.py | cat -n
printf '\n--- occurrences ---\n'
rg -n "_GETNZB_PATH_CRED_RE|redact_url|_REDACT_PARAM_NAMES|getnzb" repo/plugin.video.nzbdav/resources/lib
printf '\n--- read-only parser probe ---\n'
python3 - <<'PY'
import urllib.parse as up
url = "https://example.com/getnzb/id.nzb?i=ACCOUNT&r=APIKEY"
parts = up.urlparse(url)
print("scheme", parts.scheme)
print("netloc", parts.netloc)
print("path", parts.path)
print("query", parts.query)
print("parsed qsl keys:", [k for k, v in up.parse_qsl(parts.query, keep_blank_values=True)])
PY

Repository: Appz4Fun/nzbdavkodi

Length of output: 11415


Redact i/r when they appear as getnzb query parameters.

redact_url() only applies _GETNZB_PATH_CRED_RE to parts.path; a ?-delimited getnzb URL such as .../getnzb/id.nzb?i=ACCOUNT&r=APIKEY keeps those values in parts.query, and i/r are not included in _REDACT_PARAM_NAMES, so they would round-trip unredacted. Handle the same scoped getnzb/.nzb& case for query params as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@repo/plugin.video.nzbdav/resources/lib/http_util.py` around lines 44 - 53,
Update redact_url() to apply the scoped getnzb credential redaction to query
parameters as well as parts.path, covering getnzb URLs with ?i=...&r=... and the
existing .nzb& form. Keep i/r redaction limited to getnzb-looking URLs, while
preserving ordinary application query parameters and existing
_REDACT_PARAM_NAMES behavior.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
repo/plugin.video.nzbdav/resources/lib/resolver_resume.py (1)

168-180: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply playback identity exactly once in each no-proxy path.

_resolve_direct_no_proxy receives params but never uses them, so handle-based direct playback loses the new metadata/unique-ID handoff. Conversely, the player path invokes the metadata lookup twice.

Proposed fix
 def _resolve_direct_no_proxy(
     handle, stream_url, stream_headers, monitor_key, resume_seconds, params=None
 ):
@@
     li = _resolver._make_playable_listitem(bust_url, stream_headers)
+    _resolver._apply_playback_identity(li, params)
     _apply_resume_start_offset(li, resume_seconds)
@@
     li = _resolver._make_playable_listitem(bust_url, stream_headers)
     _resolver._apply_playback_identity(li, params)
-    _resolver._apply_playback_identity(li, params)
     _apply_resume_start_offset(li, resume_seconds)

Also applies to: 306-307

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@repo/plugin.video.nzbdav/resources/lib/resolver_resume.py` around lines 168 -
180, Update _resolve_direct_no_proxy to use its params argument when applying
playback identity, ensuring handle-based direct playback receives the metadata
and unique-ID handoff. Inspect the corresponding player path around the
additionally flagged lines and remove the duplicate metadata lookup so identity
is applied exactly once per no-proxy path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@repo/plugin.video.nzbdav/resources/lib/resolver_resume.py`:
- Around line 168-180: Update _resolve_direct_no_proxy to use its params
argument when applying playback identity, ensuring handle-based direct playback
receives the metadata and unique-ID handoff. Inspect the corresponding player
path around the additionally flagged lines and remove the duplicate metadata
lookup so identity is applied exactly once per no-proxy path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: eb2ead97-aa71-474c-a4c3-a365a213d1c1

📥 Commits

Reviewing files that changed from the base of the PR and between ac0f313 and 966d965.

📒 Files selected for processing (11)
  • repo/plugin.video.nzbdav/resources/lib/filter.py
  • repo/plugin.video.nzbdav/resources/lib/player_installer.py
  • repo/plugin.video.nzbdav/resources/lib/resolver.py
  • repo/plugin.video.nzbdav/resources/lib/resolver_flow.py
  • repo/plugin.video.nzbdav/resources/lib/resolver_metadata.py
  • repo/plugin.video.nzbdav/resources/lib/resolver_resume.py
  • repo/plugin.video.nzbdav/resources/lib/router_play.py
  • repo/plugin.video.nzbdav/resources/players/nzbdav.json
  • tests/test_player_installer.py
  • tests/test_resolver.py
  • tests/test_resolver_metadata.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • repo/plugin.video.nzbdav/resources/lib/resolver_flow.py
  • repo/plugin.video.nzbdav/resources/lib/filter.py
  • repo/plugin.video.nzbdav/resources/lib/router_play.py
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Codacy Static Code Analysis
🧰 Additional context used
🪛 ast-grep (0.45.0)
repo/plugin.video.nzbdav/resources/lib/resolver_metadata.py

[info] 90-90: use jsonify instead of json.dumps for JSON output
Context: json.dumps(request)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

tests/test_player_installer.py

[info] 137-137: use jsonify instead of json.dumps for JSON output
Context: json.dumps(PLAYER_JSON)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

tests/test_resolver_metadata.py

[info] 27-27: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"result": {"files": []}})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🔇 Additional comments (10)
repo/plugin.video.nzbdav/resources/lib/resolver.py (4)

96-97: LGTM!


380-386: LGTM!


451-455: LGTM!


501-504: LGTM!

repo/plugin.video.nzbdav/resources/lib/resolver_metadata.py (1)

12-188: LGTM!

repo/plugin.video.nzbdav/resources/lib/player_installer.py (1)

35-54: LGTM!

repo/plugin.video.nzbdav/resources/players/nzbdav.json (1)

6-8: LGTM!

tests/test_player_installer.py (1)

135-165: LGTM!

tests/test_resolver.py (1)

2650-2654: LGTM!

Also applies to: 2736-2736, 2794-2794, 3274-3274, 3548-3548, 3614-3617, 3693-3693

tests/test_resolver_metadata.py (1)

13-91: LGTM!

@xbmc4lyfe

Copy link
Copy Markdown
Collaborator

@aryan-j Let me know when this fixes your issue, and if you can give me a nzb and I can test it locally and will merge. it in.

Thanks for getting into the source code and helping with this more advanced type of issue -- its some of the hardest to code for!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants