feat(anime): episode navigation and autoplay in the stream player #92

Merged
Graphicscore merged 7 commits from feat/anime-episode-navigation into master 2026-07-18 15:48:50 +02:00
Graphicscore commented 2026-07-18 15:06:09 +02:00 (Migrated from github.com)

Adds next/previous episode controls to the anime player and continues automatically into the next episode when one finishes.

What it does

  • Next/previous buttons in the player's overlay toolbar, shown only when that neighbour actually exists.
  • Autoplay on playback end, announced by a cancellable 5-second countdown, behind a new Nächste Episode automatisch abspielen setting (default on).
  • Advancing forwards respects areBookmarksAutomatic, exactly as the episode list already does. Going backwards does not bookmark.

How it works

StreamActivity is launched with an already-resolved video URL, so it previously had no way to reach a neighbouring episode. A new standalone StreamEpisodeViewModel lists the hosters for the target episode, prefers the one already playing, and walks candidates until one resolves to a playable Video — resolvers can also return Link/App/Message, which the internal player can't use. The activity then swaps the media source in place rather than restarting itself.

Two things fall out of that worth calling out for review:

  • No extra network call to render the controls. episodeAmount and hosterName ride along as intent extras; AnimeScreen already has both before any stream can play, and StreamActivity isn't exported. When episodeAmount is absent the controls hide and autoplay is off.
  • makeIntent now copies its template intent. StreamActivity assigns the returned intent to itself and StreamPlayerManager writes playback bookkeeping onto it, which would otherwise mutate the shared instance across episodes.

Bugs caught in review and fixed here

  • Next episode started at the previous episode's position. retry() prepares the local player with setMediaSource(source, resetPosition = false), which deliberately keeps getCurrentPosition(). reset() only seeked when startPosition > 0, but the ordinary autoplay case is <= 0 — a never-watched episode has no stored position. Episode 2 therefore started at episode 1's runtime, ended instantly, and chain-loaded the rest of the series. Now seeks unconditionally, coerced to 0 (which also stops -1 reaching CastPlayer.seekTo).
  • Stale request context after a swap. The Referer interceptor and ad tag were baked in at construction, so an episode resolved from a different hoster was fetched with the wrong referer (403 on hosts that check it). Both are now rebuilt on reset, and the IMA loader is created lazily so an episode that gains an ad tag still gets one.
  • Resolved hoster wasn't carried forward, so every later navigation re-tried a hoster that had already fallen through.

Verification

  • assembleDebug, compileDebugAndroidTestKotlin: pass.
  • testDebugUnitTest: 381 tests, 0 failures (16 new, covering the candidate walk, hoster preference and fallback, Link/App/Message and resolver-error skipping, bookmark gating, and the new preference).
  • detekt: no new findings. It is red on ChatActivity.kt, IndustryActivity.kt and ErrorUtils.kt, but those files are byte-identical to master here — pre-existing.
  • Not verified on a device: the two androidTest files covering the swap have never been executed (no device attached; they need API 31+ per CLAUDE.md). The swap behaviour is covered by unit tests and reasoning, not by a real player run.

The TV frontend (tv/stream/) opts out via episodeAmount = -1 and deserves its own pass.

Design: docs/superpowers/specs/2026-07-18-anime-episode-navigation-design.md
Plan: docs/superpowers/plans/2026-07-18-anime-episode-navigation.md

🤖 Generated with Claude Code

Adds next/previous episode controls to the anime player and continues automatically into the next episode when one finishes. ## What it does - **Next/previous buttons** in the player's overlay toolbar, shown only when that neighbour actually exists. - **Autoplay** on playback end, announced by a cancellable 5-second countdown, behind a new **Nächste Episode automatisch abspielen** setting (default on). - Advancing forwards respects `areBookmarksAutomatic`, exactly as the episode list already does. Going backwards does not bookmark. ## How it works `StreamActivity` is launched with an already-resolved video URL, so it previously had no way to reach a neighbouring episode. A new standalone `StreamEpisodeViewModel` lists the hosters for the target episode, prefers the one already playing, and walks candidates until one resolves to a playable `Video` — resolvers can also return `Link`/`App`/`Message`, which the internal player can't use. The activity then swaps the media source in place rather than restarting itself. Two things fall out of that worth calling out for review: - **No extra network call to render the controls.** `episodeAmount` and `hosterName` ride along as intent extras; `AnimeScreen` already has both before any stream can play, and `StreamActivity` isn't exported. When `episodeAmount` is absent the controls hide and autoplay is off. - **`makeIntent` now copies its template intent.** `StreamActivity` assigns the returned intent to itself and `StreamPlayerManager` writes playback bookkeeping onto it, which would otherwise mutate the shared instance across episodes. ## Bugs caught in review and fixed here - **Next episode started at the previous episode's position.** `retry()` prepares the local player with `setMediaSource(source, resetPosition = false)`, which deliberately keeps `getCurrentPosition()`. `reset()` only seeked when `startPosition > 0`, but the ordinary autoplay case is `<= 0` — a never-watched episode has no stored position. Episode 2 therefore started at episode 1's runtime, ended instantly, and chain-loaded the rest of the series. Now seeks unconditionally, coerced to 0 (which also stops `-1` reaching `CastPlayer.seekTo`). - **Stale request context after a swap.** The `Referer` interceptor and ad tag were baked in at construction, so an episode resolved from a different hoster was fetched with the wrong referer (403 on hosts that check it). Both are now rebuilt on reset, and the IMA loader is created lazily so an episode that gains an ad tag still gets one. - **Resolved hoster wasn't carried forward**, so every later navigation re-tried a hoster that had already fallen through. ## Verification - `assembleDebug`, `compileDebugAndroidTestKotlin`: pass. - `testDebugUnitTest`: **381 tests, 0 failures** (16 new, covering the candidate walk, hoster preference and fallback, `Link`/`App`/`Message` and resolver-error skipping, bookmark gating, and the new preference). - `detekt`: no new findings. It is red on `ChatActivity.kt`, `IndustryActivity.kt` and `ErrorUtils.kt`, but those files are byte-identical to `master` here — pre-existing. - **Not verified on a device:** the two `androidTest` files covering the swap have never been executed (no device attached; they need API 31+ per CLAUDE.md). The swap behaviour is covered by unit tests and reasoning, not by a real player run. The TV frontend (`tv/stream/`) opts out via `episodeAmount = -1` and deserves its own pass. Design: `docs/superpowers/specs/2026-07-18-anime-episode-navigation-design.md` Plan: `docs/superpowers/plans/2026-07-18-anime-episode-navigation.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No description provided.