diff --git a/.codex/skills/stalker-portal/SKILL.md b/.codex/skills/stalker-portal/SKILL.md new file mode 100644 index 000000000..1976a934d --- /dev/null +++ b/.codex/skills/stalker-portal/SKILL.md @@ -0,0 +1,64 @@ +--- +name: stalker-portal +description: Repository guidance for Stalker/Ministra portal catalogs, VOD/series shapes, playback metadata, collections, EPG, and remote control. +--- + +# Stalker Portal + +Use this skill when changing Stalker/Ministra routes, stores, catalog/detail +views, playback, favorites/recent activity, EPG, or remote control. + +## Read First + +- `docs/architecture/stalker-portal.md` +- `docs/architecture/stalker-epg.md` for ITV EPG work +- `docs/architecture/remote-control.md` for live remote-control work + +## Ownership + +- Feature UI: `libs/portal/stalker/feature/src/lib/` +- Store/API data access: `libs/portal/stalker/data-access/src/lib/` +- Electron requests: `apps/electron-backend/src/app/events/stalker.events.ts` +- Shared Stalker item normalization: + `libs/shared/interfaces/src/lib/stalker-item.normalizer.ts` +- Dashboard aggregation: `libs/workspace/dashboard/data-access/src/lib/` + +Keep provider-specific API and normalization behavior in Stalker data access. +Keep shared portal layouts/utilities provider-neutral. Preserve full-portal +session auth and simple IPC request paths. + +## `is_series` Cross-Surface Checklist + +Treat VOD items with `is_series` as series across every downstream surface. +Do not stop after making the detail view render. + +1. Accept portal flags `true`, `1`, and `'1'` through the existing normalizers. + Preserve all three modes: regular `/series`, embedded VOD `series[]`, and + lazy Ministra VOD `is_series`. +2. Build quick-start state through the shared series utility. Preserve + `labelKey`, `labelParams`, and `episodeLabel` when adapting it for Stalker; + translation parameters must reach the template. +3. Preserve `is_series` and the VOD origin in favorites/recent activity. + `extractStalkerItemType()` must normalize that activity to dashboard type + `series`. +4. Before either inline or external episode playback, persist the parent + `seriesXtreamId` plus resolved `seasonNumber` and `episodeNumber`. Keep + generated episode tracking IDs stable for lazy `is_series` episodes. +5. The dashboard reads saved playback positions; it must not infer episode + numbers from provider payloads. Legacy rows without season/episode metadata + remain badge-less until that episode is played again. + +## Regression Coverage + +- Series view/UI and playback handoff: + `pnpm nx test portal-stalker-feature` +- Stalker shape/store behavior: + `pnpm nx test portal-stalker-data-access` +- Dashboard classification and position lookup: + `pnpm nx test workspace-dashboard-data-access` +- Dashboard badge rendering when changed: + `pnpm nx test workspace-dashboard-feature` + +For user-visible workflow changes, run the closest available E2E target. If no +fixture covers the affected portal shape, record that gap and perform the +strongest targeted unit/build validation available. diff --git a/AGENTS.md b/AGENTS.md index fb252fee7..01f664e84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -383,6 +383,11 @@ Key files: Use when moving heavy database work off the main thread, adding worker-backed SQLite operations, or wiring loading/progress UI for Xtream and playlist DB flows. File: `.codex/skills/iptvnator-sqlite-db-worker/SKILL.md` +- `stalker-portal` + Repository-specific guidance for Stalker/Ministra catalogs, all three VOD/series modes, cross-surface `is_series` behavior, playback metadata, collections, EPG, and remote control. + Use when changing Stalker routes, stores, detail views, playback, favorites/recent activity, EPG, or remote control. + File: `.codex/skills/stalker-portal/SKILL.md` + - `xtream-electron` Repository-specific guidance for IPTVnator's Electron-first Xtream implementation, including feature/data-access boundaries, worker-backed DB flows, and Xtream loading/progress UX expectations. Use when working on Xtream routes, store/data-source logic, or Electron-backed Xtream import/search/delete behavior. diff --git a/CLAUDE.md b/CLAUDE.md index 47bbfd6d2..d8c1f2c9a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -780,6 +780,7 @@ engine` (restart required) or - Xtream and Stalker detail pages use the shared `PortalDetailShellComponent` (`libs/ui/components/src/lib/portal-detail-shell/`) with two states: **Browse** (hero with poster/metadata/actions, episodes below) and **Watch** (hero collapses with a ~300ms morph, the inline player takes the full content width, metadata moves to an About block below the episodes) - Watch state derives from `inlinePlayback() !== null` only; external MPV/VLC playback keeps the browse layout. Esc and "Close player" exit to browse without navigation; the now-playing back arrow is route-level back (straight to the list via the host's `goBack()`) - A successful external MPV/VLC episode launch immediately persists the selected episode as the latest playback-position entry and retargets the series CTA to `Play episode N`; real player telemetry overwrites that marker when available, so episode identity is reliable while exact external timestamps remain best-effort. +- Stalker preserves this contract for regular `/series`, embedded VOD `series[]`, and lazy Ministra VOD `is_series` items: quick-start translation parameters must reach the CTA, and inline/external episode handoffs must include the parent series id plus resolved season and episode numbers. This metadata lets the dashboard render the tracked S/E badge for VOD-backed series. Existing playback rows without it remain badge-less until the episode is played again. - Hosts pass hero chips/meta/actions as `*appDetailTags`/`*appDetailMeta`/`*appDetailActions` templates; the shell stamps them into both the hero and the About block - Seasons are tabs (`SeasonTabsComponent`, dropdown beyond 6 seasons) with auto-selection (playing episode's season → resume season → first) that fires the same `seasonSelected` lazy-load/enrichment hooks as manual clicks; grid/list episode view toggle persists to localStorage; season descriptions come from `get_series_info` (Xtream) or TMDB (Stalker) - Dashboard hero/Continue Watching clicks for an Xtream series carry a one-shot resume target through the global-recent inline-detail handoff; after series metadata and playback positions load, the exact saved episode starts at its stored position. A failed positions load leaves the target unconsumed and the handoff detail-only, so a transient storage error never starts the episode from the beginning. Ordinary global-recent grid clicks remain detail-only. diff --git a/docs/architecture/stalker-portal.md b/docs/architecture/stalker-portal.md index 42b3f4b20..e2dfb6502 100644 --- a/docs/architecture/stalker-portal.md +++ b/docs/architecture/stalker-portal.md @@ -29,7 +29,8 @@ Stalker support covers: ## Routing Structure -Primary route tree lives in `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts`. +Primary route tree lives in +`libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts`. - `/stalker/:id/vod` - `/stalker/:id/series` @@ -45,30 +46,31 @@ Primary route tree lives in `/Users/4gray/Code/iptvnator/libs/portal/stalker/fea 1. Angular Stalker screens call methods/resources in `StalkerStore`. 2. `StalkerStore` builds request params based on selected content type and current view state. 3. Requests go through `DataService.sendIpcEvent(STALKER_REQUEST, ...)` or `StalkerSessionService` (full portal auth). -4. Electron main process handles `STALKER_REQUEST` in `/Users/4gray/Code/iptvnator/apps/electron-backend/src/app/events/stalker.events.ts`. +4. Electron main process handles `STALKER_REQUEST` in + `apps/electron-backend/src/app/events/stalker.events.ts`. 5. Axios calls Stalker `load.php` API with required headers/cookies and returns normalized payloads to renderer. ## Main UI Components -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-main-container.component.ts` +- `libs/portal/stalker/feature/src/lib/stalker-main-container.component.ts` - Category + content layout for `vod` and `series` -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` +- `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` - ITV live playback, radio playback, channel/station navigation, EPG panel integration -- `/Users/4gray/Code/iptvnator/libs/ui/playback/src/lib/audio-player/audio-player.component.ts` +- `libs/ui/playback/src/lib/audio-player/audio-player.component.ts` - Shared inline audio player used by M3U radio channels and Stalker radio stations -- `/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/stalker-series-view/stalker-series-view.component.ts` +- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts` - Season/episode UI for all Stalker series modes -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts` -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts` -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts` +- `libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts` +- `libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts` +- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts` ## Store and Data Flow Stalker store is now feature-composed: -- Facade: `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stalker.store.ts` -- Feature slices: `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stores/features/*` -- Shared helpers: `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/*` +- Facade: `libs/portal/stalker/data-access/src/lib/stalker.store.ts` +- Feature slices: `libs/portal/stalker/data-access/src/lib/stores/features/*` +- Shared helpers: `libs/portal/stalker/data-access/src/lib/*` Important store responsibilities: @@ -185,6 +187,9 @@ Stalker has multiple real-world data shapes. The current implementation supports - For unloaded VOD-series seasons, the CTA target label is derived from season metadata and rendered as `SxxE01` until episode details are loaded. - Uses unique generated tracking IDs for episode playback position compatibility. +- Quick-start actions preserve both their translation key and interpolation + parameters when adapted for the Stalker CTA. Dropping `labelParams` exposes + the raw `{{episode}}` placeholder. Series inline playback behavior is shared across all three modes: @@ -194,11 +199,27 @@ Series inline playback behavior is shared across all three modes: - Inline series autoplay is enabled by default. On player EOF (`ended`), Stalker starts the next episode only when it already exists in the current season's mapped episode list. - Autoplay and Next stop at the last episode of the current season. They do not jump to the next season and do not lazy-load an unloaded `is_series=1` season. Quick start remains the only flow that may load another VOD-series season before playback. - Previous is disabled on the first episode of the current season and otherwise switches directly to the previous episode. +- Before either inline or external playback starts, the resolved content info + includes the parent `seriesXtreamId` and the mapped `seasonNumber` / + `episodeNumber`. Future playback-position rows therefore carry enough + metadata for workspace surfaces to render an episode badge. Existing rows + without those fields are intentionally not migrated and remain badge-less + until the episode is played again. + +The VOD-series contract is cross-surface: + +- Favorites and recently viewed records preserve the raw `is_series` flag and + VOD origin so reopening still uses the lazy Ministra resources. +- `extractStalkerItemType()` normalizes those activity records to dashboard + type `series`. +- The dashboard resolves episode progress by the parent `seriesXtreamId` and + renders the saved season/episode metadata. It does not infer episode numbers + from provider payloads. Core decision logic and normalization are centralized in: -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts` -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/models/*.ts` +- `libs/portal/stalker/data-access/src/lib/stalker-vod.utils.ts` +- `libs/portal/stalker/data-access/src/lib/models/*.ts` ## Favorites and Recently Viewed @@ -213,10 +234,10 @@ Current implementation is shared via Stalker-specific helpers: Where this is used: -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts` -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts` -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts` -- `/Users/4gray/Code/iptvnator/libs/ui/components/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts` +- `libs/portal/stalker/feature/src/lib/stalker-favorites/stalker-favorites.component.ts` +- `libs/portal/stalker/feature/src/lib/recently-viewed/recently-viewed.component.ts` +- `libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts` +- `libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts` Navigation rule to preserve: @@ -264,7 +285,7 @@ Import rule: Stalker live remote control is implemented in: -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` +- `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` Supported today: @@ -299,9 +320,12 @@ This reduces duplicate UI logic across portal types and keeps compatibility beha ## Regression Coverage -Focused regression tests for Stalker VOD mode branching live in: +Focused regression tests for Stalker VOD mode branching and the cross-surface +series contract live in: -- `/Users/4gray/Code/iptvnator/libs/portal/stalker/data-access/src/lib/stalker-vod.utils.spec.ts` +- `libs/portal/stalker/data-access/src/lib/stalker-vod.utils.spec.ts` +- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.spec.ts` +- `libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.spec.ts` Covered scenarios include: @@ -310,3 +334,7 @@ Covered scenarios include: - VOD-backed series favorites keep VOD-series loading semantics when opened from favorites/global favorites - Favorite toggle helper path invokes the expected add/remove flow +- Quick-start episode labels interpolate their episode number +- Inline and external episode handoffs carry resolved season/episode metadata +- Dashboard activity classifies `is_series` VOD as series and resolves its + saved episode position diff --git a/docs/architecture/workspace-dashboard.md b/docs/architecture/workspace-dashboard.md index 913421a86..7e0cb4ee7 100644 --- a/docs/architecture/workspace-dashboard.md +++ b/docs/architecture/workspace-dashboard.md @@ -93,10 +93,14 @@ Render rules: 2. It derives the dashboard surface via `computed()`: 1. `hero` — first item of `globalRecentItems()`. 2. `continueWatchingCards` — maps `globalRecentVodItems()` to movie/series - cover cards. Xtream playback positions are bulk-loaded per playlist so + cover cards. Portal playback positions are bulk-loaded per playlist so hero and cards can show progress, remaining time, and series season/ - episode badges. Series lookup uses keyed maps for both direct episode ids - and series ids; card renders must not scan the full playback-position map. + episode badges. This includes Stalker VOD activity normalized to series + through `is_series`. Series lookup uses keyed maps for both direct + episode ids and parent series ids; card renders must not scan the full + playback-position map. The badge uses saved `seasonNumber` / + `episodeNumber` metadata and does not infer it from provider payloads; + legacy rows without that metadata remain badge-less until replay. Dashboard-originated Xtream series clicks also carry that exact episode target through the global-recent inline-detail handoff. Once the series metadata and playback positions load, the detail player consumes the