Skip to content

fix anime split-cour stream matching#1022

Open
devils-shadow wants to merge 2 commits into
Viren070:mainfrom
devils-shadow:fix/anime-split-cour-stream-matching
Open

fix anime split-cour stream matching#1022
devils-shadow wants to merge 2 commits into
Viren070:mainfrom
devils-shadow:fix/anime-split-cour-stream-matching

Conversation

@devils-shadow

@devils-shadow devils-shadow commented Jun 16, 2026

Copy link
Copy Markdown

Summary

Fix anime stream matching for split-cour / sequel entries where the Stremio request ID uses one external season number, but anime databases and release titles identify the requested content as a later logical season or cour.

The motivating case is Bungo Stray Dogs: Stremio metadata can expose three seasons, while anime trackers commonly list the same later episodes as separate 4th and 5th season entries. Requests such as tt5679720:3:* can therefore need streams titled Bungo Stray Dogs 4th Season / Bungo Stray Dogs 5th Season, not generic season 3 releases.

Problem

AIOStreams currently treats season / episode as a single coordinate system. That works for most TV metadata, but it breaks when anime metadata has two valid coordinate systems:

  • External request coordinates from Stremio / IMDb / TMDB / TVDB, for example tt5679720:3:14.
  • Logical anime entry coordinates from anime mappings and release titles, for example Bungo Stray Dogs 4th Season, episode 1.

When those are collapsed into one value, several downstream stages can become incorrect:

  • Search queries may ask indexers for the external season instead of the anime sequel title or logical season.
  • Season/episode filtering may discard otherwise correct releases because parsed files contain logical season numbers.
  • Broad query fanout can over-query Torznab/Newznab endpoints when the first query form does not match.
  • Repeated 429 responses can continue to be retried immediately instead of backing off.

Approach

This PR preserves both coordinate systems instead of replacing one with the other:

  • logicalSeason / logicalEpisode: anime-entry numbering inferred from anime entry titles and episode offsets.
  • externalSeason / externalEpisode: original request coordinates from the parsed Stremio ID.
  • Stream selection and debrid metadata can use logical coordinates where that is necessary, while formatter/expression contexts expose logical and external values explicitly.

For ambiguous anime seasons, the search metadata now keeps sequel-specific anime titles and marks the request for query search. Nab-based addons then use bounded query waves:

  1. Base title + logical SxxExx / Sxx.
  2. Anime entry title + logical episode forms.

The second wave only runs if the first wave returns no results. This keeps the fix targeted without spraying every possible title/episode variant at indexers.

If query search is required but the selected Nab search function does not support q, the request now fails fast instead of issuing a broad unqualified search.

Nab rate-limit handling

Torznab/Newznab APIs now track endpoint-level 429 cooldowns. During cooldown:

  • cached search results can still be returned without refresh;
  • uncached searches fail fast with a rate-limit error;
  • query batches stop treating 429s as an invitation to continue fanout.

This is intentionally generic and applies to Nab endpoints broadly, not to one indexer-specific workaround.

Filtering and expressions

Season/episode filtering now prefers logical anime coordinates while still allowing an exact external season/episode tuple when a release is parsed that way.

Formatter and expression contexts also expose the explicit logical/external values, so advanced users can reason about either coordinate system without overloading season and episode.

Validation

Added deterministic unit coverage for the pure anime search helpers:

  • extracts logical seasons from sequel titles such as 4th Season, Season 2, and roman numeral forms;
  • converts external episode offsets into entry-relative logical episodes;
  • keeps sequel-specific titles while deriving base-title candidates for bounded query search.
  • verifies Bungo-style split-cour query waves such as Bungo Stray Dogs S04E01 and Bungo Stray Dogs 4th Season 01.

Validated locally with:

pnpm -F core test
pnpm -F core run build

No tests call live indexers or content endpoints.

Summary by CodeRabbit

  • New Features
    • Improved anime season/episode handling using logical vs external numbering to generate more accurate search queries for ambiguous releases.
    • Added configurable season/episode query variants and title limits for debrid lookups.
    • Enhanced NAB search to better manage q-based querying, including wave-based query progression.
    • Implemented NAB rate-limit handling with per-endpoint cooldown, cached results during cooldown, and richer retry timing.
  • Bug Fixes
    • Preserved valid Stremio coordinate value 0 and improved season/episode parameter derivation for searches.
  • Tests
    • Added/extended test coverage for anime search metadata helpers and Stremio coordinate parsing.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 60ed1614-fb16-4d83-a0ad-593504d89c5a

📥 Commits

Reviewing files that changed from the base of the PR and between cc3256f and 0ea2f81.

📒 Files selected for processing (7)
  • packages/core/src/builtins/base/debrid.ts
  • packages/core/src/builtins/base/nab/addon.ts
  • packages/core/src/formatters/base.ts
  • packages/core/src/streams/context.ts
  • packages/core/src/streams/filterer.ts
  • packages/core/src/utils/id-parser.test.ts
  • packages/core/src/utils/id-parser.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/core/src/streams/filterer.ts
  • packages/core/src/builtins/base/nab/addon.ts
  • packages/core/src/streams/context.ts
  • packages/core/src/builtins/base/debrid.ts

Walkthrough

Introduces logical vs external season/episode disambiguation for anime across the full stack: new utility functions parse logical season/episode from titles, build ambiguous query waves for split-cour anime, and propagate these fields through stream context, filterer, formatter/expression context, debrid query building, and persisted title metadata. Separately, NAB API gains NabRateLimitError with per-endpoint cooldowns and cached fallback, integrated into NAB addon query execution with wave-based sequential processing and parallel query batching.

Changes

Anime Logical/External Season Disambiguation

Layer / File(s) Summary
Anime search utility library
packages/core/src/utils/anime-search.ts, packages/core/src/utils/anime-search.test.ts, packages/core/src/utils/index.ts
New file exports extractLogicalSeasonFromTitles, calculateLogicalEpisode, buildAnimeTitles, stripAnimeSeasonQualifier, selectAmbiguousAnimeBaseTitles, selectAmbiguousAnimeEntryTitles, buildAmbiguousAnimeQueryWaves, and related helpers; covered by a new Vitest suite; re-exported from utils index.
Stremio coordinate parser
packages/core/src/utils/id-parser.ts, packages/core/src/utils/id-parser.test.ts
New parseStremioCoordinate helper validates and normalises optional season/episode values, preserving 0 for Specials; test suite ensures correct handling of edge cases.
Shared logical/external season/episode contracts
packages/core/src/debrid/base.ts, packages/core/src/builtins/base/debrid.ts, packages/core/src/streams/context.ts, packages/core/src/formatters/base.ts
TitleMetadataSchema, SearchMetadata, ExtendedMetadata, ExpressionContext, and FormatterContext all gain optional logicalSeason, externalSeason, logicalEpisode, externalEpisode fields; ExtendedMetadata additionally gains isAmbiguousAnimeSeason; SearchMetadata gains externalTitle, externalTitles, forceQuerySearch.
Stream context derivation and propagation
packages/core/src/streams/context.ts
During metadata fetch, parses coordinates via parseStremioCoordinate, computes external and logical season/episode for anime, detects isAmbiguousAnimeSeason, adjusts title selection via buildAnimeTitles for ambiguous cases, refines anime "Not Found" error logging; toFormatterContext and toExpressionContext now include all four new fields plus ambiguity flag.
Expression engine constants
packages/core/src/parser/streamExpression.ts
Registers logicalSeason, externalSeason, logicalEpisode, externalEpisode as expr-eval constants (defaulting to -1).
Formatter context wiring
packages/core/src/formatters/base.ts
BaseFormatter.convertStreamToParseValue populates logical/external season/episode in ParseValue.metadata from formatter context with null fallbacks.
Stream filterer logical/external season matching
packages/core/src/streams/filterer.ts
performSeasonEpisodeMatch reads logical season/episode with parsedId fallback; adds early rejection when requestedSeason === 0 but stream has episodes and does not include 0; season and episode mismatches each have an additional external season/episode tuple check before the existing absolute/relative fallback path.
Debrid search metadata and query building
packages/core/src/builtins/base/debrid.ts
_getSearchMetadata parses coordinates, derives logical season/episode for anime, sets forceQuerySearch on ambiguous cases, rebuilds animeTitles via buildAnimeTitles when ambiguous; titleMetadata persisted to store gains four new fields; buildQueries gains maxTitles, includeSeasonOnly, includeAbsoluteEpisode, includeRelativeAbsoluteEpisode, includeEpisodeOnly, includeExternalQueries options and emits deduplicated queries using logical/external values.

NAB Rate Limiting and Query Wave Execution

Layer / File(s) Summary
NabRateLimitError and cooldown infrastructure
packages/core/src/builtins/base/nab/api.ts
Exports NabRateLimitError with retryAfterMs; BaseNabApi maintains per-endpoint rateLimitCooldowns; search falls back to searchCache during cooldown with a warning; request throws immediately during cooldown, on HTTP 429 (parsing retry-after header), and on XML error code 429.
NAB addon query batch and preferQuerySearch
packages/core/src/builtins/base/nab/addon.ts
Adds fetchQueryBatch (parallel via Promise.allSettled, rate-limit capture, rethrow); performSearch derives preferQuerySearch from config+metadata, overrides chosen search function accordingly, uses externalSeason for the season parameter, builds and executes queryWaves sequentially and standard queries via fetchQueryBatch, propagating NabRateLimitError only when results are empty.

Sequence Diagram(s)

sequenceDiagram
  participant StreamContext
  participant AnimeUtils as anime-search utils
  participant ExtendedMetadata
  participant StreamFilterer
  participant BaseDebridAddon
  participant BaseNabAddon
  participant BaseNabApi

  StreamContext->>AnimeUtils: extractLogicalSeasonFromTitles(titles)
  AnimeUtils-->>StreamContext: logicalSeason
  StreamContext->>AnimeUtils: calculateLogicalEpisode(externalEpisode, animeEntry)
  AnimeUtils-->>StreamContext: logicalEpisode
  StreamContext->>ExtendedMetadata: set logicalSeason, externalSeason, logicalEpisode, externalEpisode, isAmbiguousAnimeSeason
  ExtendedMetadata-->>StreamFilterer: match uses logicalSeason/logicalEpisode + external fallback

  BaseDebridAddon->>AnimeUtils: buildAmbiguousAnimeQueryWaves(metadata)
  AnimeUtils-->>BaseDebridAddon: queryWaves[][]
  BaseDebridAddon->>BaseNabAddon: performSearch with forceQuerySearch + queryWaves
  loop each wave (sequential)
    BaseNabAddon->>BaseNabApi: fetchQueryBatch(wave queries)
    alt cooldown active
      BaseNabApi-->>BaseNabAddon: NabRateLimitError
    else HTTP 429
      BaseNabApi-->>BaseNabAddon: NabRateLimitError(retryAfterMs)
    else success
      BaseNabApi-->>BaseNabAddon: results
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~100 minutes

Possibly related PRs

  • Viren070/AIOStreams#539: Both PRs modify BaseDebridAddon.buildQueries() in packages/core/src/builtins/base/debrid.ts around how/when metadata.year is appended and season-year effects on year.

Suggested reviewers

  • Viren070

Poem

🐇 Hoppity-hop through split-cour despair,
Logical seasons now floating in air!
Wave one, wave two, the queries cascade,
Rate limits met with a cooldown grenade.
External and logical, matched to a T —
This bunny approves, with a hop and a wheee! 🎌

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix anime split-cour stream matching' directly reflects the main objective: fixing anime stream matching for split-cour and sequel entries with differing coordinate systems.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@devils-shadow devils-shadow marked this pull request as ready for review June 16, 2026 09:25
@devils-shadow

Copy link
Copy Markdown
Author

forgot to add, I've been using this fix on my instance for the past week without any obvious issues, tested mainly with anime-type media.

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

Caution

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

⚠️ Outside diff range comments (1)
packages/core/src/streams/filterer.ts (1)

1005-1062: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not treat 0 as “not requested” in season/episode matching.

The new logical/external matching uses truthy guards, so S00 specials skip the season checks; for example, S00E01 can match S01E01 when episode 1 is present. Compare against undefined instead.

Proposed fix
       const matchesLogicalSeason = !!(
-        requestedSeason &&
+        requestedSeason !== undefined &&
         seasons &&
         seasons.length > 0 &&
         seasons.includes(requestedSeason)
       );
       const matchesExternalSeason = !!(
-        externalSeason &&
+        externalSeason !== undefined &&
         seasons &&
         seasons.length > 0 &&
         seasons.includes(externalSeason)
       );
 
       if (
-        requestedSeason &&
+        requestedSeason !== undefined &&
         seasons &&
         seasons.length > 0 &&
         !matchesLogicalSeason
       ) {
         if (
           matchesExternalSeason &&
-          externalEpisode &&
+          externalEpisode !== undefined &&
           stream.parsedFile?.episodes?.length &&
           stream.parsedFile.episodes.includes(externalEpisode)
         ) {
@@
       if (
-        requestedEpisode &&
+        requestedEpisode !== undefined &&
         stream.parsedFile?.episodes?.length &&
         !stream.parsedFile?.episodes?.includes(requestedEpisode)
       ) {
         if (
           matchesExternalSeason &&
-          externalEpisode &&
+          externalEpisode !== undefined &&
           stream.parsedFile.episodes.includes(externalEpisode)
         ) {
🤖 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 `@packages/core/src/streams/filterer.ts` around lines 1005 - 1062, The season
and episode matching logic uses truthy guards that incorrectly treat numeric
zero values (like season 0 for specials) as "not requested". Replace the truthy
checks on requestedSeason and requestedEpisode variables with explicit undefined
comparisons. Specifically, change the conditions that use `if (requestedSeason
&&` and `if (requestedEpisode &&` to instead use `if (requestedSeason !==
undefined &&` and `if (requestedEpisode !== undefined &&` respectively, so that
S00 specials and other zero-based values are properly evaluated against the
seasons and episodes arrays.
🤖 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 `@packages/core/src/builtins/base/debrid.ts`:
- Around line 551-555: The querySeason and queryEpisode variables initialized
from parsedId can be strings, while externalSeason and externalEpisode are
numbers. When these are compared using strict inequality operators around line
621, string and number values that are semantically identical will be treated as
different, causing unnecessary external query fanout. Normalize the types of
querySeason and queryEpisode to numbers before they are used in comparisons with
the external season and episode values, ensuring that the strict comparison at
the point where external-tuple comparison occurs will accurately detect when
values are equal.
- Around line 682-697: The variables externalSeason and externalEpisode are
being assigned numeric conversions without validating that the parsed values are
actually numeric strings. Currently the code checks if parsedId.season and
parsedId.episode are truthy, but truthy non-numeric strings like "*" will
convert to NaN via Number(), which then propagates into logicalEpisode and
contaminates metadata. Add validation to ensure that parsedId.season and
parsedId.episode contain valid numeric content before calling Number() on them,
falling back to undefined if the values are non-numeric or malformed.

In `@packages/core/src/builtins/base/nab/addon.ts`:
- Around line 74-77: Import NabRateLimitError from ./api.js at line 16 with the
other imports. Then update the isRateLimitError function to check if the error
is an instance of NabRateLimitError directly using the instanceof operator
instead of using a regex pattern on the error message. This eliminates the
fragility of pattern matching and provides a more reliable type check for rate
limit errors.

In `@packages/core/src/formatters/base.ts`:
- Around line 197-200: The four new fields (logicalSeason, externalSeason,
logicalEpisode, externalEpisode) are defined in FormatterContext but are not
being wired into the metadata object of ParseValue. To fix this, locate the
convertStreamToParseValue function where the metadata object is constructed and
add these four fields to it, mapping their values from the FormatterContext so
that formatter templates can access them via {metadata.logicalSeason},
{metadata.externalSeason}, {metadata.logicalEpisode}, and
{metadata.externalEpisode}.

---

Outside diff comments:
In `@packages/core/src/streams/filterer.ts`:
- Around line 1005-1062: The season and episode matching logic uses truthy
guards that incorrectly treat numeric zero values (like season 0 for specials)
as "not requested". Replace the truthy checks on requestedSeason and
requestedEpisode variables with explicit undefined comparisons. Specifically,
change the conditions that use `if (requestedSeason &&` and `if
(requestedEpisode &&` to instead use `if (requestedSeason !== undefined &&` and
`if (requestedEpisode !== undefined &&` respectively, so that S00 specials and
other zero-based values are properly evaluated against the seasons and episodes
arrays.
🪄 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 UI

Review profile: CHILL

Plan: Pro

Run ID: d7ba81c1-4f6d-4266-bf1b-c5b5a14fbbe3

📥 Commits

Reviewing files that changed from the base of the PR and between c30c3a3 and cc3256f.

📒 Files selected for processing (11)
  • packages/core/src/builtins/base/debrid.ts
  • packages/core/src/builtins/base/nab/addon.ts
  • packages/core/src/builtins/base/nab/api.ts
  • packages/core/src/debrid/base.ts
  • packages/core/src/formatters/base.ts
  • packages/core/src/parser/streamExpression.ts
  • packages/core/src/streams/context.ts
  • packages/core/src/streams/filterer.ts
  • packages/core/src/utils/anime-search.test.ts
  • packages/core/src/utils/anime-search.ts
  • packages/core/src/utils/index.ts

Comment thread packages/core/src/builtins/base/debrid.ts Outdated
Comment thread packages/core/src/builtins/base/debrid.ts Outdated
Comment thread packages/core/src/builtins/base/nab/addon.ts
Comment thread packages/core/src/formatters/base.ts
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.

2 participants