# 050 — Next episode, Continue logic, autoplay, collections

## 1. Episode order of a series

- Built from cached TMDb data (`tv:<id>` `seasons`, `season:<id>:<n>`
  `episodes`): regular seasons 1..N in order, episodes by `episode_number`.
- **Specials (season 0) are excluded** from the order (decision 9). They are
  listed under the series as their own season and can be played and marked
  manually.
- Aired = `air_date` ≤ today (local date); an episode without `air_date` is
  treated as not aired.
- Season data of returning series is at most 12 h old (030 §3); the service
  refresher also refreshes every series in Continue or in My series once a
  day and when a series' next episode's air date has passed.

## 2. Continue state of a series (pure function `next_up(show)`)

In this order:
1. the episode with the newest `last_played_at` that has `resume_pos > 0`
   and is not watched → **in progress** (resume);
2. else the first episode in order after the **latest watched episode by
   order** (not by time) → **next**: crossing a season boundary means
   S(n)E(last) → S(n+1)E1; gaps of unwatched earlier episodes are ignored
   (the user moves forward);
3. if that next episode is not aired → **upcoming** with its air date
   (decision 10), not playable; when its date passes it becomes **next**;
4. no next episode and the series status is Ended/Canceled → **finished**,
   not shown in Continue;
5. series with no watch activity at all are not in Continue.

A series counts as watched when every aired regular episode has
`playcount > 0`.

## 3. Continue rows

- Movies row: `movie_state.resume_pos > 0`, not watched, not hidden, newest
  `last_activity_at` first.
- Series row: every series with state in-progress / next / upcoming (§2),
  not hidden, by `last_activity_at`; upcoming items show
  "S03E01 · vyjde 12. 10." (label2 / plot line), clicking them shows a
  notification with the date instead of playing.
- Both rows exclude MyTV / MyCinema activity by construction (040 §1).

## 4. Autoplay of the next episode

- Applies to every episode played through `mplay` when the existing global
  autoplay setting (`autoplay_enabled_global`) is on; countdown length and
  trigger from the existing settings (`autoplay_countdown_seconds`,
  `autoplay_trigger_seconds`); dialog style from the existing four styles.
- 60 s before the countdown point (`autoplay_countdown_seconds` before the
  end) the tracker computes `next_up` as if the current episode were watched
  (rollover included). If the next episode is aired:
  1. the tracker adds its canonical URL to the Kodi video playlist after the
     current item (on the main loop), with a play request `src=auto`,
     `from=start`, so Kodi resolves it with the automatic file pick;
  2. shows the countdown dialog with the next episode's TMDb data (title,
     still, S/E, rating);
  3. **Play now** → mark current watched, `playnext()`; **timeout** (the
     existing countdown runs down to `autoplay_trigger_seconds` before the
     end, as today) → mark watched, `playnext()`; exactly one `playnext()`
     per playback (guard like today's `_handling_next`), never
     `stop()` + `PlayMedia()`; **Cancel** → remove the queued item, the
     current episode plays to its end and autoplay is not offered again for
     this playback.
- Next episode not aired → the countdown is not shown; at the end a
  notification "Další díl S03E01 vyjde 12. 10.".
- Last episode of an ended series → no countdown; notification "Seriál
  dokoukán".
- The automatic pick fails (no file) → notification "Další díl se nepodařilo
  najít" and the series stays in Continue with that episode as **next**.
- Playback started from a playlist whose items came from MyTV is never
  touched (no now-playing contract).

## 5. Movie collections (decision 14)

At the countdown point of a movie with `belongs_to_collection`: the next film
of the collection by release date that is not watched and is released →
countdown-style dialog "Další v kolekci: <title>" with **Přehrát** and
**Zavřít**, **no automatic start** (timeout = close). Přehrát → current movie
marked watched, next film queued as in §4 with `src=auto`.

## 6. Result (L2, 2026-10-01)

Built under `wcs/library2/`:

- `next_up.py` (pure): `episode_order` (specials excluded, `aired` from the
  given day, missing air date = not aired), `next_up` (resume > next by the
  latest watched *by order* > upcoming > finished / caught_up), `next_after`
  (autoplay: what follows an episode as if watched, rollover included),
  `next_in_collection`, `series_watched`, `air_label` ("8. 10.").
- `autoplay.py`: `settings()` (existing `autoplay_enabled_global`,
  `autoplay_countdown_seconds`, `autoplay_trigger_seconds`, clamped like the
  MyTV monitor), `dialog_duration`, `plan(np)` -> `series_next | upcoming |
  finished | collection | none` from cached TMDb data (missing season /
  collection data is fetched once).
- `tracker.Engine` (service): at `remaining <= countdown` one plan worker per
  playback; **series_next** -> a long-lived auto-pick request is left for the
  next canonical URL (`play.prepare`) and the existing countdown dialog (the
  user's chosen style) runs in a worker thread; its result comes back as an
  event. Play now / timeout -> current episode marked watched (flag
  `marked_watched`, no second count at the natural end), next item started
  **once** with `Player.play(<canonical>)` from the loop. Cancel -> request
  dropped, the episode plays on, autoplay not offered again. Dismiss -> the
  episode ends, then the next one starts. User stop -> queue dropped.
  **upcoming** -> no dialog; at the end the notification "Další díl S02E07
  vyjde 8. 10."; **finished** -> "Seriál dokoukán". **collection** -> the same
  dialog with "Další v kolekci", never starts by itself (only *Přehrát*).
  A failed automatic pick shows "Další díl se nepodařilo najít".
- **Design change found live:** `PlayMedia(url)` does not put the playing
  item into Kodi's video playlist (`Player.position` = -1), so `playnext()`
  has nothing to play (first live run: the file closed, nothing followed).
  The plan's "one `playnext()`" became one `Player.play()` of the next
  canonical URL from the service loop - still no `stop()` + `PlayMedia()`,
  still exactly one start per playback.
- `refresher.py` (service thread, first run 60 s after start, then every
  30 min, never while video plays): tracked series (My series or with
  activity) whose cached `tv:` data is stale get `tmdb.refresh_show` (show +
  regular seasons).
- Fixture provider: `seed_meta(relative_to_today=True)` moves the fixture
  dates so live tests see unaired episodes relative to the real day.
- **Tests: 92 green** (`test_next_up.py`: order / aired flags, mid-season,
  season rollover, gaps, latest by order vs by time, specials, in-progress
  beats next, newest in-progress, unaired -> upcoming with date, new episode
  aired -> next again, ended -> finished, caught up, `next_after`, collection
  cases, refresher; `test_autoplay.py`: nothing before the countdown point,
  timeout / play now / cancel / dismiss / user stop, no double start, no
  double count, unaired / finished / special, disabled setting, collection
  offer and play, movie without collection, dialog duration).
- **Live (fixture provider, countdown 15 s / trigger 5 s):** scenario 4:
  S01E10 (40 s) -> the premium countdown dialog "DALŠÍ EPIZODA S02E01" at
  25 s -> after 10 s S02E01 started by itself (auto pick, no source dialog),
  S01E10 watched once, a new session tracked S02E01. Scenario 5: S02E06 with
  S02E07 unaired -> no countdown, notification "Další díl S02E07 vyjde 8. 10."
  at the end, nothing started. (An earlier run with a frozen June date
  correctly autoplayed S02E07 because it had aired by the real date.)
- Not run live: the dismiss / cancel / collection paths (covered by unit
  tests), the real Webshare automatic pick for the next episode and the
  refresher against real TMDb (the user's real test, L6).
- After the tests the fixture rows and settings were restored (autoplay
  countdown / trigger back to the defaults 45 / 15 s; if the user had custom
  values there before, they need to be set again).

### 6a. Real test failed (2026-10-01)

See `140-handoff.md` §2: duplicated TMDb data (old listings uncached, new `meta` empty), partial cache entries, plan computed too late.

### 6b. Fix (2026-10-01), awaiting the user's "L2 OK"

- Plan prefetched at playback start (`tracker.Engine._prefetch_worker`); the countdown point uses it with no lookup; `_follow_up()` starts the next episode / notifies at the natural end even when the dialog never ran; respects `autoplay_enabled_global`.
- A natural end (`ended`) counts the position as the total, so a seek to the very end is watched exactly once even when the last poll saw an early position (found live: 5.6 s of 40 s -> 'short').
- One TMDb cache: old `display_series_seasons` / `display_season_episodes` (one season call, one show call per list instead of one show call per episode), the My list dialog, collection overviews read `meta` through `wcs.library2.tmdb`; `TMDbCache.py` is a facade over `meta` (030 §3). Live: opening Breaking Bad seasons / season 1 wrote `tv:1396` and `season:1396:1` once, repeat opens read them; logos, aliases, ČSFD reset and `display_my_movie_collections` verified; ČSFD charts return nothing only because csfd.cz answers a bot check (unrelated).
- **Tests: 107 green** (new: prefetch at start, no new lookup at the countdown, seek to the very end counts once and plays next once, stale position at the end, missing plan computed at the end, autoplay off, foreign prefetch ignored, strict listings, one download for listings + autoplay, generic cache facade).
- **Live (fixture provider, 15 s / 5 s):** scenario 4 (S01E10 -> countdown -> S02E01 by itself, E10 watched once); seek to 99 % of S01E10 -> S02E01 started once, playcount +1; scenario 5 (S02E06, S02E07 unaired) -> notification "Další díl S02E07 vyjde 8. 10." with a seek to the end, watched. Fixture rows removed, Kodi records reset, settings restored (autoplay 45 / 15, resume 180, fixture off).

