docs(stalker): record series position compatibility

This commit is contained in:
4gray committed 2026-07-30 11:10:24 +02:00
1 parent 7b6d576fef
commit 829a51d571
5 files changed
+51 -5

No files matched your search

@@ -0,0 +1,6 @@
---
type: fix
area: stalker
---
Stalker VOD series now report progress correctly when portals return boolean series flags, keep episode progress separate between shows, and resume positions saved by earlier versions.
+1 -1
View File
@@ -889,7 +889,7 @@ engine` (restart required) or
- 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()`)
- Xtream VOD treats metadata presentation and playability as separate contracts. Empty or sparse `get_vod_info` data keeps the curated fallback detail page, while Play/Resume, Favorite, and Download remain available whenever a positive stream id and non-empty container extension resolve from `movie_data` or the catalog fields. Playback fields are selected as one atomic pair in detail → recovered catalog → owner-valid cached catalog order; incomplete candidates never combine into a synthetic source. In-memory VOD categories/streams carry their owner playlist, and cross-portal Favorites/Recent details ignore arrays from another playlist so colliding Xtream ids cannot inject stale playback or presentation data. When Electron's normalized catalog cache lacks the extension, the detail loader immediately publishes the sparse fallback and ends its loading state, then performs a best-effort category-scoped raw catalog lookup and reactively upgrades the same item with actions on success. It maps the normal SQLite route category through all persisted categories, including hidden ones, while also accepting the provider `xtream_id` carried by cross-portal Similar links; ambiguous numeric matches keep local-id precedence, deduplicate provider candidates, and try the next candidate when the exact VOD is absent. PWA falls back to API categories. It skips that request when existing data is sufficient, never sends an unresolved database id as a provider id, preserves concurrent metadata enrichment, and drops late detail/recovery responses after replacement, playlist reset, or detail teardown. Inline playback moves either detail page into Watch; external MPV/VLC remains in Browse. Unresolvable items expose no actions, and playback/download titles and posters fall back through `info`, `movie_data`, then catalog fields.
- 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.
- Stalker preserves this contract for regular `/series`, embedded VOD `series[]`, and lazy Ministra VOD `is_series` items; `is_series` is normalized only from `true`, `1`, or `'1'`. 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. Lazy VOD episode tracking IDs scope the parent series, provider episode, season key, and episode number; the previous season/episode hash is only a compatibility alias. Exact scoped positions win, while compatible legacy rows are considered only for the current parent and must match any stored season/episode coordinates. The scoped row is persisted through the strict failure-propagating boundary before confirmed legacy cleanup, so a failed save keeps the old row; compatibility is lazy and performs no schema migration or bulk rewrite.
- 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.
+30 -3
View File
@@ -267,6 +267,10 @@ list:
Stalker has multiple real-world data shapes. The current implementation supports all three:
Across catalog, detail, and activity normalization, `is_series` is accepted
only as boolean `true`, numeric `1`, or string `'1'`. Unsupported
`is_series` values do not by themselves classify a VOD item as a series.
1. Regular Series (`/series`):
- Seasons come from API resource (`serialSeasonsResource`).
@@ -300,7 +304,11 @@ Stalker has multiple real-world data shapes. The current implementation supports
ordering cannot start the wrong episode.
- 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.
- Lazy VOD-series episodes use scoped tracking IDs derived from the parent
series ID, provider episode ID, season key, and episode number. The season
key follows the mapping fallback (`season_number`, then name, then ID).
- The previous season/episode hash remains available only as a compatibility
alias. New playback positions always use the scoped tracking ID.
- Quick-start actions preserve both their translation key and interpolation
parameters when adapted for the Stalker CTA. Dropping `labelParams` exposes
the raw `{{episode}}` placeholder.
@@ -323,6 +331,22 @@ Series inline playback behavior is shared across all three modes:
quick-start labels share the same naturally ordered season fallback so later
seasons are not persisted as season 1.
### Playback Position Identity and Compatibility
Legacy playback positions are reconciled lazily when the current parent
series' positions and mapped episodes are available:
- The lookup is scoped to the current parent series. A legacy row is eligible
only when its stored season and episode metadata, when present, match the
mapped episode.
- An exact scoped tracking-ID row always wins. The old tracking ID may supply
an in-memory compatibility alias only when no exact row exists.
- On the next position write, IPTVnator persists the scoped row through the
strict, failure-propagating persistence boundary before removing a confirmed
legacy row. If the scoped write fails, the legacy row remains intact.
- This is an on-read/on-write compatibility path, not a database schema
migration or a bulk rewrite of saved positions.
The VOD-series contract is cross-surface:
- Favorites and recently viewed records preserve the raw `is_series` flag and
@@ -467,10 +491,13 @@ This reduces duplicate UI logic across portal types and keeps compatibility beha
## Regression Coverage
Focused regression tests for Stalker VOD mode branching and the cross-surface
series contract live in:
The compatibility helper and focused regression coverage for Stalker VOD mode
branching and the cross-surface series contract live in:
- `libs/portal/stalker/data-access/src/lib/stalker-vod.utils.spec.ts`
- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.ts`
- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-position-compatibility.spec.ts`
- `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.position-compatibility.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`
@@ -23,6 +23,19 @@ describe('extractStalkerItemType', () => {
})
).toBe('live');
});
it.each([true, 1, '1'] as const)(
'treats is_series=%p as a series item',
(isSeries) => {
expect(
extractStalkerItemType({
id: '50001',
title: 'Portal Series',
is_series: isSeries,
})
).toBe('series');
}
);
});
describe('isStalkerRadioItem', () => {
@@ -50,7 +50,7 @@ export function extractStalkerItemPoster(
* Determine the normalised activity type of a Stalker item.
*
* - `itv` / `live` / radio → `'live'`
* - `series` or `is_series` truthy → `'series'`
* - `series` or `is_series` equal to `true`, `1`, or `'1'` → `'series'`
* - everything else → `'movie'`
*/
export function extractStalkerItemType(