Files
iptvnator/docs/architecture/stalker-store-api-baseline.md
T
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

7.9 KiB

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.