Files
iptvnator/docs/architecture/stalker-store-api-baseline.md
4grayandClaude Opus 5.5 e1d4f00d93 fix(i18n): follow runtime language switches in stored and memoized labels (#1872)
* fix(i18n): follow runtime language switches in stored and memoized labels

A label translated once and kept kept its language after Settings ->
Language until the data reloaded or the app restarted. The Stalker store
baked `translate.instant('PORTALS.ALL_CATEGORIES' | 'PORTALS.ALL_RADIO')`
into its category list, so the every-item genre stayed English in the
rail, the live header, the fullscreen panel title, the catalog title and
the search scope.

Stalker: the every-item genre now carries `labelKey` and an empty
`category_name`; the category views render the key through the translate
pipe. `getSelectedCategoryName` becomes `getSelectedCategoryLabel`
({ name, labelKey }) and every text consumer goes through
`stalkerCategoryLabelText()` inside a computed that reads a language
signal, so no consumer can show the empty name or a stale translation.

Same class elsewhere (computed or stored `instant` text without a language
signal): the Xtream import overlay title and progress (shell-provided),
the Settings About version note (stored in a signal on the page where the
language changes), the remove-all-playlists progress, the context panel's
status/error text and category search, and the movie/series heroes
(`createVodDetailsHeroState`, `createSeriesHeroState`, the Xtream movie
presenter), cast & crew "Director", the M3U sidebar count, the M3U movie
hero and the collection panel title. These read
`toSignal(onLangChange.pipe(startWith(null)))` and also recover when the
translation file lands after the first render.

Documents the rule in the UI guidelines and the Stalker store contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(stalker): record getSelectedCategoryLabel in the store API baseline

getSelectedCategoryName is gone without an alias: a name-only selector is
empty for the every-item genre, whose label is a translation key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(i18n): one translation tick that also follows late dictionaries

Greptile asked for coverage of a dictionary that lands after the first
render. That exposed a gap: every hand-written tick listened to
`onLangChange` only, but a start-up without a saved language never calls
`use()` - the default dictionary landing fires `onDefaultLangChange` alone,
so computeds that ran before it kept raw keys. Dictionary updates
(`onTranslationChange`) were missed the same way.

`injectTranslationTick()` in `@iptvnator/pipes` merges the three events the
translate pipe re-renders on (the embedded MPV player already did). All 37
ticks use it now, including the hero factories' `language` dependency. New
specs cover the delayed default-language load, a late `use()` dictionary
and a dictionary update; the hero specs add the start-up case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(i18n): stub the Xtream selection the merged search scope reads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(i18n): series view reads the shared translation tick

#1871 added an onLangChange-only tick for the synthetic episode titles; a
start-up dictionary that lands without use() would leave raw keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(i18n): move injectTranslationTick to @iptvnator/services

The tick injects TranslateService and subscribes, so it is injectable
runtime state; nx-workspace-boundaries.md reserves type:util for pure
helpers and contracts. @iptvnator/services is the shared type:data-access
project every consumer domain may depend on, and it imports none of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:31:38 +02:00

174 lines
7.9 KiB
Markdown

# Stalker Store API Baseline
This is the compatibility baseline for refactoring `libs/portal/stalker/data-access/src/lib/stalker.store.ts`.
Goal: keep this public surface stable while splitting to feature stores.
## Source of Truth
- Store implementation: `libs/portal/stalker/data-access/src/lib/stalker.store.ts`
- Baseline created on current branch state before feature-store extraction.
## Public State Signals
Direct signal properties currently exposed by `signalStore`:
- `selectedContentType: 'vod' | 'itv' | 'radio' | 'series'`
- `selectedCategoryId: string | null | undefined`
- `selectedVodId: string | undefined`
- `selectedSerialId: string | undefined`
- `selectedItvId: string | undefined`
- `page: number`
- `searchPhrase: string`
- `currentPlaylist: PlaylistMeta | undefined`
- `totalCount: number`
- `selectedItem: StalkerVodSource | null | undefined`
- `vodCategories: StalkerCategoryItem[]`
- `seriesCategories: StalkerCategoryItem[]`
- `itvCategories: StalkerCategoryItem[]`
- `radioCategories: StalkerCategoryItem[]`
- `hasMoreChannels: boolean`
- `itvChannels: StalkerItvChannel[]`
- `radioChannels: StalkerItvChannel[]`
- `vodSeriesSeasons: StalkerVodSeriesSeason[]`
- `vodSeriesEpisodes: StalkerVodSeriesEpisode[]`
- `selectedVodSeriesSeasonId: string | undefined`
## Public Computed Selectors
- `getPaginatedContent: StalkerContentItem[]`
- `isPaginatedContentLoading: boolean`
- `isPaginatedContentFailed: unknown`
- `getSerialSeasonsResource: StalkerSeason[]`
- `isSerialSeasonsLoading: boolean`
- `getVodSeriesSeasonsResource: StalkerVodSeriesSeason[]`
- `isVodSeriesSeasonsLoading: boolean`
- `getCategoryResource: StalkerCategoryItem[]`
- `isCategoryResourceLoading: boolean`
- `isCategoryResourceFailed: unknown`
- `getSelectedCategoryLabel: StalkerCategoryLabel` (`{ name, labelKey }`;
render with `stalkerCategoryLabelText()` inside a computed that reads
`injectTranslationTick()`)
## Exposed Resources/Props
These are currently reachable on the store object and used internally by computed selectors:
- `getCategoryResource` (computed selector with stable array output)
- `categoryResource` (internal resource)
- `getContentResource` (resource)
- `serialSeasonsResource` (resource)
- `vodSeriesSeasonsResource` (resource)
Removed:
- `makeStalkerRequest(...)` — deleted with the endpoint-discovery work. It
was production-dead (no caller outside its own spec) and carried a fourth
private copy of the portal-mode branch. Every Stalker request goes through
`executeStalkerRequest()` (`stores/utils/stalker-request.utils.ts`), which
owns mode routing plus the lazy portal repair; no facade alias is provided
because reinstating one would reintroduce the drift the shared predicate
exists to prevent.
- `limit`, `setLimit(...)` and `getTotalPages` — deleted with the catalog
pagination removal (#1392, #1395). Catalogs now accumulate server-paged
results into one deduplicated list, so `hasMoreContent` (accumulated length
vs `total_items`) answers what `getTotalPages` used to, and no caller reads a
page size: the portal decides how large a page is. No facade alias is
provided, because a surviving `limit` would advertise a client-side window
that the append path does not honour.
- `getSelectedCategoryName` — replaced by `getSelectedCategoryLabel`. The
every-item genre the store adds (`category_id: '*'`) has no portal name: it
carries a `labelKey` that the caller translates, so the label follows a
runtime language switch. No alias is provided, because a name-only
selector is empty for that entry, and translating in the root store would
keep the language the list was loaded in.
During refactor:
- Keep compatibility for external callers that may read these directly.
- If moved/renamed internally, provide facade aliases.
## Public Methods (Compatibility Contract)
- `setSelectedContentType(type: 'vod' | 'itv' | 'radio' | 'series'): void`
- `setSelectedCategory(id: string | number | null): void`
- `setSelectedSerialId(id: string): void`
- `setSelectedVodId(id: string): void`
- `setSelectedItvId(id: string): void`
- `setPage(page: number): void`
- `setCurrentPlaylist(playlist: PlaylistMeta | undefined): Promise<void>`
- `setSelectedItem(selectedItem: StalkerVodSource | null | undefined): void`
- `clearSelectedItem(): void`
- `setCategories(type: 'vod' | 'series' | 'itv' | 'radio', categories: StalkerCategoryItem[]): void`
- `resetCategories(): void`
Clears only; it starts no request (called on portal switches and route teardown).
- `reloadCategories(): void`
Refetches the current portal's categories after a reset that keeps the owner.
- `setItvChannels(channels: StalkerItvChannel[]): void`
- `setRadioChannels(channels: StalkerItvChannel[]): void`
- `setSearchPhrase(phrase: string): void`
- `fetchVodSeriesEpisodes(videoId: string, seasonId: string): Promise<StalkerVodSeriesEpisode[]>`
- `getSelectedCategory(): { id: string | number; name: string; type: 'vod' | 'itv' | 'radio' | 'series' }`
Backed by `withComputed` for compatibility, not by `withMethods`.
- `fetchLinkToPlay(portalUrl: string, macAddress: string, cmd: string, series?: number): Promise<string>`
- `getExpireDate(): Promise<string>`
- `resolveItvPlayback(item: StalkerPortalItem): Promise<ResolvedPortalPlayback>`
- `resolveRadioPlayback(item: StalkerPortalItem): Promise<ResolvedPortalPlayback>`
- `addToFavorites(item: any, onDone?: () => void): void`
- `removeFromFavorites(favoriteId: string, onDone?: () => void): void`
- `fetchMovieFileId(movieId: string): Promise<string | null>`
- `createLinkToPlayVod(cmd?: string, title?: string, thumbnail?: string, episodeNum?: number, episodeId?: number, startTime?: number): Promise<void>`
- `addToRecentlyViewed(item: any): void`
- `removeFromRecentlyViewed(itemId: number, onComplete?: () => void): void`
- `fetchChannelEpg(channelId: number | string, size?: number): Promise<EpgItem[]>`
## Current Consumers (Observed)
Top observed store API usage in app code:
- `currentPlaylist` (16 references)
- `setSelectedContentType` (9)
- `selectedItem` (9)
- `setSelectedItem` (7)
- `setSelectedCategory` (6)
- `createLinkToPlayVod` (6)
- `removeFromFavorites` (5)
- `setPage` (4)
- `addToFavorites` (4)
- `fetchChannelEpg` (3)
- plus lower-frequency calls for paging/resources/series/recent.
Consumer directories sampled:
- `libs/portal/stalker/**`
- `libs/portal/xtream/feature/**`
- `libs/portal/catalog/feature/**`
- `libs/ui/components/**`
## Invariants to Preserve During Refactor
- Selection IDs (`selectedVodId`, `selectedItvId`) are synchronized in `setSelectedItem`.
- `selectedSerialId` is set only when `selectedContentType` is `series`, and is
cleared for every other content type. `serialSeasonsResource` fires a
`get_ordered_list&type=series` portal request on each change of that id, and
it is the only episode source for a `series` selection — while VOD-context
shapes (embedded `series[]`, Ministra `is_series`) resolve their episodes
elsewhere, so carrying the id there only wastes a request. The gate must stay
on content type alone: gating it on item shape would leave a series-section
item that happens to carry `is_series`/`series[]` with an empty episode list.
- `setSelectedCategory(...)` resets `page` to `0`.
- `getPaginatedContent()` and `getCategoryResource()` always return arrays,
even when the underlying request fails.
- Radio stores stations separately in `radioChannels` and falls back to a
synthetic all-radio category when a portal does not support radio category
responses.
- Request failures must surface through `isPaginatedContentFailed()` and
`isCategoryResourceFailed()` rather than resource reads that throw.
- `createLinkToPlayVod(...)` continues to:
- support episode playback metadata
- append recently viewed
- preserve external player payload shape
- Full-portal auth path continues through `StalkerSessionService`.
- Non-auth/simple path continues through `DataService.sendIpcEvent(STALKER_REQUEST, ...)`.
- Resource-driven loading signals preserve existing names.