# 013 - Knihovna Milionáře (Filmy, Seriály, Pokračovat, další epizoda)

**Status:** COMPLETED (viz TODO)
**Updated:** 2026-10-01 (fáze L0-L6 otestované uživatelem, včetně reálného Webshare přehrávání)

---

## Účel

Addon sám drží to, co dřív dělala video knihovna Kodi: seznamy Moje filmy / Moje seriály, stav zhlédnutí a rozkoukání, Pokračovat, další epizodu přes hranici sezóny, autoplay s odpočtem, TMDb metadata a jejich cache. `skin.milionar` ukazuje jen tato data (nikdy ne data knihovny Kodi). Knihovna Kodi (`.strm` export, kontextové položky pro knihovní položky) zůstává pro jiné skiny.

Proč: dřív se stav ukládal jen u části přehrávání, seznamy byly JSON soubory, TMDb data se stahovala několikrát a Kodi knihovna s plugin položkami neumí „další epizodu" ani konzistentní resume. Plán a rozhodnutí: `docs/plan/milionar-library/` (`000-index.md`, `900-decisions.md`, `140-handoff.md`).

Pravidla, která drží celé řešení:
- Každé přehrání z addonu (kromě MyTV / MyCinema, ty do úložiště nikdy nepíšou) jde přes **kanonickou URL** a sleduje ho služba.
- Hotovo = stav je v `library.db`; Kodi dostává zrcadlo (`Files.SetFileDetails`) a změny z Kodi se přebírají zpět.
- Žádné čtení Kodi knihovny v `skin.milionar`; „Přidat do knihovny Kodi" je v tomto skinu všude skryté.
- Seznamy a řádky Home jdou rychlou cestou `main.py` (bez importu routeru, 50-180 ms); Kodi 21.3 padá, když se při Zpět zastaví běžící skript seznamu (viz TODO).

---

## Architektura

```
plugin proces (krátký)                         služba (po celou dobu Kodi)
main.py rychlá cesta:                          service.py -> wcs/library2/tracker.run()
  mplay        -> resolver.run                   Tracker(xbmc.Player) jen řadí události do fronty
  mplay_req    -> play.request                   Engine.tick(): pozice, ukládání (10 s), prahy,
  mlist        -> listings.run                    autoplay, adopce změn z Kodi (5 s),
  widget_row   -> rows.run                        publish_focus (Home widget pro kontextové menu)
  ctx_*        -> ctx.run                      wcs/library2/refresher.py (TMDb seriálů, 30 min)
  skin_slot*   -> wcs/skin/slots.run
  mupcoming    -> listings.upcoming
        \                                       /
         library.db (SQLite, WAL)  ← store.py + meta.py (TMDb cache)
```

Tok přehrání: položka s URL `plugin://plugin.video.milionar/?action=mplay&type=movie&tmdb=ID` (nebo `type=episode&tmdb=ID&s=N&e=N`, pevné pořadí parametrů) → Kodi zavolá `resolver.run` → ten přečte volitelný požadavek (`Window(10000).Property(milionar.play_req)`: `src` dialog|auto|last, `from` resume|start), vybere zdroj (dialog / automaticky / stejný soubor jako minule), `setResolvedUrl` a zapíše kontrakt `milionar.np` → `Tracker.onAVStarted` ho spáruje se streamem a vznikne `Session`.

Stav a prahy (nastavení addonu): rozkoukáno po `lib_resume_min_seconds` (180 s), zhlédnuto při `lib_watched_percent` (90 %) nebo když začne další epizoda přes odpočet; přirozený konec se vždy počítá jako zhlédnutí (pozice se nastaví na celek). Autosave se přeskočí za hranicí zhlédnutí, aby pád nenechal resume na 99 %.

Autoplay: při startu epizody se na pozadí spočítá plán (`autoplay.plan`); na bodu odpočtu (`autoplay_countdown_seconds` od konce, spuštění `autoplay_trigger_seconds` před koncem) se použije hned. Další epizoda se spouští `Player.play(kanonická URL)` ze smyčky služby (jednou za přehrání), žádost `src=auto` ji vyřeší bez dialogu zdroje. Cancel zahodí frontu, Dismiss nechá epizodu doběhnout a další pak naváže, uživatelské Stop nic nespustí. Nevyšlá další epizoda / konec seriálu: jen oznámení na konci. Filmová kolekce: nabídka „Další v kolekci" bez automatického startu.

TMDb data: **jediná cache** je tabulka `meta` (`movie:<id>`, `tv:<id>`, `season:<id>:<n>`, `collection:<id>`, obecné `http:<sha>`). Staré procházení sezón / epizod, dialogy Moje seznamy, popisy kolekcí i autoplay čtou přes `wcs/library2/tmdb.py`; `wcs/metadata/TMDbCache.py` je jen fasáda nad `meta` pro logo, aliasy a ČSFD. `tmdb_cache.json` neexistuje.

---

## Mapa souborů

| Soubor | Role |
|--------|------|
| `wcs/library2/store.py` | SQLite úložiště: `titles`, `movie_state`, `episode_state`, `play_log`, `meta`, `flags`; seznamy, stav, zhlédnuto/nezhlédnuto, log přehrání |
| `wcs/library2/meta.py` | TTL cache TMDb (12 h běžící seriál, 30 dní ostatní), `get_many`, obecný klíčový prostor |
| `wcs/library2/tmdb.py` | `movie/show/season/collection` přes cache (`strict=True` pro seznamy otevřené uživatelem), `refresh_show` |
| `wcs/library2/urls.py`, `play.py` | kanonické URL; požadavek na přehrání a jeho spotřeba |
| `wcs/library2/resolver.py` | `action=mplay`: výběr zdroje, `setResolvedUrl`, kontrakt pro službu |
| `wcs/library2/tracker.py` | `Engine` ve službě: pozice, prahy, autoplay, odpočet, adopce z Kodi, `publish_focus` |
| `wcs/library2/policy.py`, `next_up.py`, `autoplay.py` | čistá pravidla: výsledek přehrání, `reconcile`, pořadí epizod, Pokračovat, plán autoplaye |
| `wcs/library2/mirror.py` | zrcadlení stavu do Kodi a přebírání změn (`push`, `check`, `check_paths`) |
| `wcs/library2/refresher.py` | služba: obnova TMDb sledovaných seriálů |
| `wcs/library2/listings.py` | Filmy / Seriály / sezóny / epizody jako nativní seznamy (`action=mlist`), tlačítka nahoře, žánrový filtr |
| `wcs/library2/rows.py` | řádky Home (`action=widget_row`), vyhledávací řádky, `Data` (jedno načtení za volání) |
| `wcs/library2/menu.py`, `ctx.py` | jedno kontextové menu (`build_parts`: nahoře ≤ 6, zbytek v „Další…"), akce `ctx_*` |
| `wcs/library2/compat.py` | v `skin.milionar` skrývá nabídky „knihovna Kodi" v dialozích |
| `wcs/skin/slots.py` | sloty widgetů: výchozí hodnoty, správce widgetů (kožní řetězce `mil.<sec>.<n>.*`) |
| `scripts/skin_harness/gen_slots.py` | generuje `resources/skin.milionar/xml/Includes_MilionarSlots.xml` |
| `resources/skin.milionar/xml/Home.xml`, `Custom_1123_MilionarRows.xml`, `DialogContextMenu.xml`, `DialogVideoInfo.xml`, `Variables.xml`, `Includes*.xml` | menu, skupiny widgetů, správce widgetů, skrytí menu Kodi, tlačítko Milionář v info dialogu, počty zhlédnutých |
| `scripts/library_harness/` | unit testy (158), `live.py` pro živé scénáře, fixture provider (lokální videa) |

---

## Klíčová rozhraní

- `urls.movie_url(id)`, `urls.episode_url(show, s, e)`, `urls.parse(url)` - kanonické URL.
- `play.request(canonical, src, start)` - požadavek + `PlayMedia`; `play.prepare(...)` pro frontu autoplaye.
- `store.save_resume / mark_watched / mark_unwatched / set_in_list / get_state / get_all_episode_states / recent_logs` - zápis a čtení stavu.
- `tmdb.show(id, strict=False)` / `season(id, n)` / `movie(id)` / `collection(id)` - jediná cesta k TMDb datům (neúplná položka v cache se stáhne znovu).
- `next_up.next_up(order, states, status, today)` → `resume | next | upcoming | finished | caught_up`; `autoplay.plan(np)` → `series_next | upcoming | finished | collection | none`.
- `rows.build(row, params)` - řádek katalogu; `menu.Menu().build_parts(kind, tmdb, s, e)` - (nahoře, „Další…").
- `slots.apply_defaults(sec=None)`, `slots.write_slot(...)`, `slots.edit_slot(sec, n)` - sloty Home.

Akce pluginu (rychlá cesta): `mplay`, `mplay_req`, `mlist` (`kind=movies|series|seasons|episodes`, `genre`), `mupcoming`, `widget_row` (`row,limit,sort,style,hw,up,q`), `ctx_mark`, `ctx_hide_continue`, `ctx_list`, `ctx_open`, `skin_slots`, `skin_slot_edit`, `skin_slots_defaults`. Přes router: `ctx_channel`. Staré položky menu doplňku `tmdb_added_movies` / `tmdb_added_series` jsou přesměrované na `mlist`.

---

## UI

- **Home:** menu Pokračovat, **Filmy**, **Seriály**, MyTV, MyCinema, Milionář, AI doporučení, (Kraska), TMDb / ČSFD, Videa, Doplňky; položky Kodi Filmy / Seriály jsou ve výchozím stavu vypnuté. Každá sekce má 6 slotů; výchozí stav max 3 řádky (`MilionarSlotsV2`). Slot = tři Estuary widgety (plakát / epizody / kategorie), aktivní je právě jeden podle `mil.<sec>.<n>.type|style`. Styl „Široký obrázek" používá široké karty epizod. Kliknutí: Pokračovat a rozkoukané řádky navazují od pozice, ostatní filmové řádky otevřou náš detail, seriály sezóny.
- **Seznamy (Videa okno):** stejné vzhledem jako knihovna Kodi: počty zhlédnutých/celkem u seriálů a sezón, „* Všechny sezóny", epizody `01. Název` se hodnocením a náhledem, bez kvalitativních značek. Nahoře tlačítka Přidat / Smazat všechny / AI (`SpecialSort=top`). Kliknutí na film se řídí nastavením „Výchozí akce filmu".
- **MyTV widgety v Pokračovat:** správce widgetů sekce Pokračovat nabízí navíc „Další epizody“ a „Moje kanály“ z MyTV (druhá linie pokračování); výchozí stav se nemění.
- **Správce widgetů:** okno 1123 – vlevo seznam aktivních widgetů („počet · řazení · styl“) a „+ Přidat widget“ (max 6), vpravo tlačítka Nahoru, Dolů, Upravit, Odebrat, přepínače sekce, Obnovit výchozí widgety, Uložit, Zrušit. Změny jsou v návrhu; Uložit je zapíše a jednou přenačte skin, Zrušit / Zpět se při změnách zeptá. „Upravit“ otevře jedno okno s hodnotami Obsah / Počet / Řazení / Styl / Hotovo. Vstup: Nastavení vzhledu → Milionář → „- Widgety“, položka „Upravit widgety“ na konci podmenu „Další…“ kontextového menu položky widgetu na Home, nebo klávesa Info na položce hlavního menu Home (Kodi nemá kontextové menu na statickém seznamu).
- **Kontextové menu:** u položek s `milionar.type` skin skryje a zavře menu Kodi a otevře standardní dialog jen s našimi položkami (`ctx.open_menu`, ochrana `Window(Home).Property(milionar.ctx.active)`); na Home čte zaostřenou položku z `milionar.focus.*`. Info dialog: tlačítko 4501 „Milionář", skryté Verze / Bonusová videa / Zvolte obrázek / Stejný režisér.
- Texty: addon `32290-32420` (cs, sk, en), skin `31916-31917`, `31954-31959`.

---

## Závislosti

- Používá: `wcs/utils.py` (výběr zdroje, `play_text_game` a dialogy zdrojů), `wcs/ai/MediaChannelManager.py` (MyTV / MyCinema), `wcs/playback/dialogs/countdown.py` (odpočet), TMDb API klíč v nastavení.
- Používají ho: `service.py` (tracker, refresher), `main.py`, `wcs/router.py`, `wcs/skin/*` (hledání, widgety, hra), `wcs/ui/ContextMenu.py` (v `skin.milionar` vrací jednotné menu), starý TMDbClient (sezóny / epizody z `meta`).
- MyTV / MyCinema jsou oddělené světy: do úložiště nepíšou a tracker jejich přehrávání ignoruje (bez kontraktu `milionar.np`).

---

## Jak rozšířit

- **Nový řádek Home:** přidat id do `rows.CATALOGUE` + builder v `rows._build`, titulek a styl do `wcs/skin/slots.py` (`SECTIONS`, `TITLES`, `styles_of`), texty (cs/sk/en), případně do výchozích slotů; test v `test_rows.py` / `test_slots.py`. Změna počtu sekcí / slotů vyžaduje `python3 scripts/skin_harness/gen_slots.py`.
- **Nová položka kontextového menu:** do `Menu._play_items/_state_items/_list_items/_discovery_items` a do rozdělení v `build_parts` (nahoře jen časté věci), akci, která mění úložiště, do `ctx.py`.
- **Nový zdroj přehrávání:** vrátit stream z `resolver._stream_for`; kontrakt a tracker se nemění.
- **Po přidání textů do addonu** je nutný restart Kodi (načítají se při startu); služba (`service.py` a co importuje) také jen po restartu: `python3 scripts/dialog_harness/kodi_live.py restart`.
- **Testy:** `python3 -m unittest discover -s scripts/library_harness -p 'test_*.py'`; živě `scripts/library_harness/live.py` (setup / play / stop / state, fixture provider `dev_fixture_provider`), po testech smazat řádky `>= 900000`, vrátit nastavení (odpočet 45 / 15, `lib_resume_min_seconds` 180).

---

## TODO / známá omezení

- Kodi 21.3 občas spadne (SIGSEGV v `CPythonInvoker::stop`), když se při rychlém Zpět zastaví skript seznamu právě ve chvíli, kdy končí. Chyba je v Kodi; seznamy jsou proto rychlé, ale riziko to nezruší.
- Prázdný řádek na Home se zaplní až při dalším vstupu na Home (Estuary schová prázdné panely); změna slotu vyžaduje `ReloadSkin()` (podmínky includů se vyhodnocují při načtení skinu).
- Adopce změn z Kodi se kontroluje jen u položek, které jsou zrovna vidět v seznamu (5 s).
- Kontextové menu Kodi pro naše položky nejde potlačit API (`replaceItems` se ignoruje), proto zásah ve `DialogContextMenu.xml` a `publish_focus` ve službě.
- ČSFD žebříčky vrací prázdno, pokud csfd.cz zobrazí kontrolu proti robotům (nesouvisí s knihovnou).
- Staré dialogy (`DialogWcsMyList`) zůstávají v kódu, ale v novém skinu se nepoužívají a nemají se otevírat při testech.
