Skip to content

Add Support for Requested Seasons for Seer Webhook#1392

Open
spelech wants to merge 3 commits intorivenmedia:mainfrom
spelech:feature/seerWebhookTVSeasons
Open

Add Support for Requested Seasons for Seer Webhook#1392
spelech wants to merge 3 commits intorivenmedia:mainfrom
spelech:feature/seerWebhookTVSeasons

Conversation

@spelech
Copy link
Copy Markdown

@spelech spelech commented Mar 14, 2026

Pull Request Check List

Resolves: #issue-number-here

  • Added tests for changed code.
  • Updated documentation for changed code.

Description:

Finishes piping through the optional requested seasons field for Seer Webhook notifications. Initial implementation looked to be there but not fully integrated

Summary by CodeRabbit

  • New Features

    • Support for requesting specific TV seasons via Overseerr webhooks; requested seasons propagate to shows and new seasons not requested are left paused.
    • Re-requesting can promote previously paused seasons to requested state.
  • Tests

    • Added unit tests validating parsing of requested seasons from Overseerr webhooks.
  • Documentation

    • Added Overseerr webhook configuration guidance to the README.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 14, 2026

Walkthrough

The changes add propagation of a requested_seasons field through MediaItem syncing, TVDB indexing, and Overseerr webhook handling, enabling requested-season-aware creation and state transitions for seasons and emitting events when paused seasons are promoted.

Changes

Cohort / File(s) Summary
Metadata Attribute Synchronization
src/program/services/indexers/base.py
Added requested_seasons to attributes copied between MediaItem instances.
TVDB Indexing / Season Creation
src/program/services/indexers/tvdb_indexer.py
Pass original item into show creation; propagate requested_seasons to created shows; when creating new seasons, set last_state to Paused if season number not in requested_seasons.
Overseerr Webhook Handling
src/routers/secure/webhooks.py
On TV webhook: lookup existing show by external IDs; if found, promote matching Paused seasons to Requested and emit events; otherwise create new show and store requested_seasons on the created item.
Tests
src/tests/test_overseerr_webhook.py
Added unit tests for parsing requested_seasons from Overseerr webhook extras (comma-separated values and empty case).
Docs
README.md
Added Overseerr webhook configuration guidance describing the "Requested Seasons" extra and payload format (duplicated insertion).

Sequence Diagram

sequenceDiagram
    participant Overseerr as Overseerr
    participant Webhook as Webhook Handler
    participant DB as Database
    participant Show as Show Item
    participant Season as Season Item
    participant Events as Event System

    Overseerr->>Webhook: POST TV item (includes requested_seasons)
    Webhook->>DB: get_item_by_external_id(tvdbId/tmdbId)
    alt show found
        DB-->>Webhook: return existing show
        Webhook->>Show: read requested_seasons
        loop for each season in Show
            alt season.number in requested_seasons and season.last_state == Paused
                Webhook->>Season: set last_state = Requested
                Webhook->>Events: emit season requested event
            end
        end
        Webhook->>DB: commit
        Webhook-->>Overseerr: 200 OK
    else show not found
        DB-->>Webhook: no show
        Webhook->>Show: create new show (tvdb_id) and attach requested_seasons
        Webhook->>DB: persist new show
        Webhook-->>Overseerr: 200 OK
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • iPromKnight

Poem

A rabbit peeks at seasons queued,
Numbers whispered, neatly hewed.
Paused to requested, one by one,
Hops of code beneath the sun. 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: adding support for requested seasons in the Overseerr webhook flow, which is reflected across all modified files.
Description check ✅ Passed The description follows the template structure with completed checklist items and a clear explanation of the changes, though the issue number placeholder was not filled in.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

CodeRabbit can use your project's `pylint` configuration to improve the quality of Python code reviews.

Add a pylint configuration file to your project to customize how CodeRabbit runs pylint.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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)
src/tests/test_overseerr_webhook.py (1)

4-39: These tests never hit the new behavior.

They only validate OverseerrWebhook parsing. The new control flow lives in src/routers/secure/webhooks.py:77-115 and src/program/services/indexers/tvdb_indexer.py:402-483, so regressions in the actual requested-seasons path can slip through. Please add one handler-level test for promoting paused seasons on an existing show and one indexer test for a fresh show request carrying requested_seasons.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tests/test_overseerr_webhook.py` around lines 4 - 39, Add two tests
exercising the actual control flow: (1) a handler-level test that sends an
OverseerrWebhook-like payload to the webhooks handler in
src/routers/secure/webhooks.py (the function that consumes OverseerrWebhook and
promotes paused seasons) for an existing show with paused seasons and assert the
handler promotes the requested seasons (e.g., season 1/2/5 become active); (2)
an indexer unit test in src/program/services/indexers/tvdb_indexer.py that
simulates a fresh show request carrying requested_seasons and invokes the
TvdbIndexer method that handles new requests (the indexer function that
currently processes requested_seasons) asserting the indexer persists/applies
the requested_seasons for the new show; mock external dependencies (DB/indexer
clients) as needed and reuse the existing OverseerrWebhook payload structure
from src/tests/test_overseerr_webhook.py for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/program/services/indexers/base.py`:
- Line 39: The mapped Show isn’t getting requested_seasons when creating a fresh
show, so ensure requested_seasons is set on the mapped Show before seasons are
added: in TVDBIndexer._create_show_from_id (the branch that builds a new Show)
assign mapped_show.requested_seasons = source_show.requested_seasons (or
metadata.get('requested_seasons')) immediately after constructing the mapped
Show and before calling _add_seasons_to_show(); additionally, update
copy_items()’s Show branch to include copying show-level attrs
(requested_seasons) so subsequent metadata-copy paths remain consistent.

In `@src/routers/secure/webhooks.py`:
- Around line 92-103: The current loop calls season.store_state() and
Program.em.add_event() before session.commit(), causing notifications and
event-queue reads to see transient/rolled-back data; change the flow to first
collect the target season IDs/objects (from existing_show.seasons where
season.number in requested_seasons and season.last_state == States.Paused),
perform the database state update in a non-notifying way (e.g., set season.state
or use a low-level update) and call session.commit(), then after a successful
commit iterate the collected seasons and call
season.store_state(States.Requested) and di[Program].em.add_event(Event(...)) to
emit events; reference season.store_state, Program.em.add_event,
existing_show.seasons, States.Requested, States.Paused, and
OverseerrWebhookResponse when locating the code to change.

---

Nitpick comments:
In `@src/tests/test_overseerr_webhook.py`:
- Around line 4-39: Add two tests exercising the actual control flow: (1) a
handler-level test that sends an OverseerrWebhook-like payload to the webhooks
handler in src/routers/secure/webhooks.py (the function that consumes
OverseerrWebhook and promotes paused seasons) for an existing show with paused
seasons and assert the handler promotes the requested seasons (e.g., season
1/2/5 become active); (2) an indexer unit test in
src/program/services/indexers/tvdb_indexer.py that simulates a fresh show
request carrying requested_seasons and invokes the TvdbIndexer method that
handles new requests (the indexer function that currently processes
requested_seasons) asserting the indexer persists/applies the requested_seasons
for the new show; mock external dependencies (DB/indexer clients) as needed and
reuse the existing OverseerrWebhook payload structure from
src/tests/test_overseerr_webhook.py for consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a54c85b5-e6f9-4eb0-9652-069a5fc9bea7

📥 Commits

Reviewing files that changed from the base of the PR and between 3a6af54 and a0b9f00.

📒 Files selected for processing (4)
  • src/program/services/indexers/base.py
  • src/program/services/indexers/tvdb_indexer.py
  • src/routers/secure/webhooks.py
  • src/tests/test_overseerr_webhook.py

Comment on lines +92 to +103
if requested_seasons:
for season in existing_show.seasons: # type: ignore
if getattr(season, "number", None) in requested_seasons and season.last_state == States.Paused:
season.store_state(States.Requested)

di[Program].em.add_event(Event(
emitted_by=Overseerr.__class__.__name__,
item_id=season.id
))

session.commit()
return OverseerrWebhookResponse(success=True)
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.

⚠️ Potential issue | 🔴 Critical

Defer state-change side effects until after commit.

season.store_state() notifies immediately (src/program/media/item.py:202-230), and Program.em.add_event() queues by re-reading the item from the DB (src/program/managers/event_manager.py:173-219). Because both happen before Line 102 commits, a failed commit can leak a rolled-back transition, and the queue can still see the old Paused row. At minimum, move event emission behind a successful commit; ideally this whole path should use an after-commit/outbox pattern.

Minimum safer ordering
                 if existing_show:
                     logger.info(f"Show {existing_show.log_string} already exists, handling requested seasons from overseerr")
                     
                     if requested_seasons:
+                        season_ids_to_queue = []
                         for season in existing_show.seasons: # type: ignore
                             if getattr(season, "number", None) in requested_seasons and season.last_state == States.Paused:
                                 season.store_state(States.Requested)
-                                
-                                di[Program].em.add_event(Event(
-                                    emitted_by=Overseerr.__class__.__name__,
-                                    item_id=season.id
-                                ))
+                                if season.id is not None:
+                                    season_ids_to_queue.append(season.id)
                                 
                         session.commit()
+                        for season_id in season_ids_to_queue:
+                            di[Program].em.add_event(
+                                Event(
+                                    emitted_by=Overseerr.__class__.__name__,
+                                    item_id=season_id,
+                                )
+                            )
                         return OverseerrWebhookResponse(success=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routers/secure/webhooks.py` around lines 92 - 103, The current loop calls
season.store_state() and Program.em.add_event() before session.commit(), causing
notifications and event-queue reads to see transient/rolled-back data; change
the flow to first collect the target season IDs/objects (from
existing_show.seasons where season.number in requested_seasons and
season.last_state == States.Paused), perform the database state update in a
non-notifying way (e.g., set season.state or use a low-level update) and call
session.commit(), then after a successful commit iterate the collected seasons
and call season.store_state(States.Requested) and
di[Program].em.add_event(Event(...)) to emit events; reference
season.store_state, Program.em.add_event, existing_show.seasons,
States.Requested, States.Paused, and OverseerrWebhookResponse when locating the
code to change.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/program/services/indexers/tvdb_indexer.py (3)

448-452: Logic is correct; consider hoisting the import.

The season-pausing logic correctly handles the case when requested_seasons is specified but doesn't include the current season. However, the inline import of States at line 450 is unusual. Since States is a stable enum with no circular dependency risk, consider moving it to the top-level imports for consistency.

♻️ Proposed refactor

Add to top-level imports:

from program.media.state import States

Then simplify:

                         if requested_seasons and season_item.number not in requested_seasons:
-                            from program.media.state import States
                             season_item.last_state = States.Paused
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/program/services/indexers/tvdb_indexer.py` around lines 448 - 452, Hoist
the inline import of States out of the conditional in tvdb_indexer.py: add "from
program.media.state import States" to the module top-level imports and remove
the inline "from program.media.state import States" inside the if block; keep
the existing logic that sets season_item.last_state = States.Paused when
requested_seasons is set and season_item.number is not in requested_seasons
(refer to the conditional using requested_seasons and season_item.last_state).

273-275: Same issue: replace setattr with direct assignment.

Consistent with the fix at lines 246-248.

♻️ Proposed fix
                             if show_item:
                                 if item:
-                                    setattr(show_item, "requested_seasons", getattr(item, "requested_seasons", None))
+                                    show_item.requested_seasons = getattr(item, "requested_seasons", None)
                                 self._add_seasons_to_show(show_item, show_details)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/program/services/indexers/tvdb_indexer.py` around lines 273 - 275, The
code uses setattr(show_item, "requested_seasons", getattr(item,
"requested_seasons", None)) which should be replaced with a direct assignment
for clarity and consistency with the earlier fix; change this to
show_item.requested_seasons = getattr(item, "requested_seasons", None) (keep the
getattr to safely fetch from item), leaving the subsequent call to
self._add_seasons_to_show(show_item, show_details) unchanged so
_add_seasons_to_show continues to operate on the updated show_item.

246-248: Replace setattr with direct attribute assignment.

Per static analysis (Ruff B010), using setattr with a constant attribute name is unnecessary. Direct assignment is clearer and equally safe.

♻️ Proposed fix
                     if show_item:
                         if item:
-                            setattr(show_item, "requested_seasons", getattr(item, "requested_seasons", None))
+                            show_item.requested_seasons = getattr(item, "requested_seasons", None)
                         self._add_seasons_to_show(show_item, show_details)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/program/services/indexers/tvdb_indexer.py` around lines 246 - 248,
Replace the dynamic setattr call on show_item with a direct attribute
assignment: when item exists assign show_item.requested_seasons = getattr(item,
"requested_seasons", None) instead of using setattr(show_item,
"requested_seasons", ...); keep the surrounding logic (the if item check and the
subsequent call to self._add_seasons_to_show(show_item, show_details))
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/program/services/indexers/tvdb_indexer.py`:
- Around line 448-452: Hoist the inline import of States out of the conditional
in tvdb_indexer.py: add "from program.media.state import States" to the module
top-level imports and remove the inline "from program.media.state import States"
inside the if block; keep the existing logic that sets season_item.last_state =
States.Paused when requested_seasons is set and season_item.number is not in
requested_seasons (refer to the conditional using requested_seasons and
season_item.last_state).
- Around line 273-275: The code uses setattr(show_item, "requested_seasons",
getattr(item, "requested_seasons", None)) which should be replaced with a direct
assignment for clarity and consistency with the earlier fix; change this to
show_item.requested_seasons = getattr(item, "requested_seasons", None) (keep the
getattr to safely fetch from item), leaving the subsequent call to
self._add_seasons_to_show(show_item, show_details) unchanged so
_add_seasons_to_show continues to operate on the updated show_item.
- Around line 246-248: Replace the dynamic setattr call on show_item with a
direct attribute assignment: when item exists assign show_item.requested_seasons
= getattr(item, "requested_seasons", None) instead of using setattr(show_item,
"requested_seasons", ...); keep the surrounding logic (the if item check and the
subsequent call to self._add_seasons_to_show(show_item, show_details))
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 040a730d-090d-49ae-a238-2ffd1bec8b18

📥 Commits

Reviewing files that changed from the base of the PR and between a0b9f00 and 9c8381b.

📒 Files selected for processing (3)
  • README.md
  • src/program/services/indexers/base.py
  • src/program/services/indexers/tvdb_indexer.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/program/services/indexers/base.py

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant