mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
docs(stalker): reconcile the Stalker docs after the API-compatibility series (#1375)
Nine PRs landed between 2026-08-01 and 2026-08-04 in parallel worktrees, each
editing its own section of docs/architecture/stalker-portal.md and CLAUDE.md.
Sections that were correct when written disagreed with each other, or with
master, afterwards. Every claim here was verified against the code.
Corrected in stalker-portal.md: routes listed without the /workspace prefix;
"simple portals carry only the mac= cookie" (every request goes through the
shared identity builder — but the direct branch forwards no serial, so no
SN/__cfduid either, while playback headers are NOT mode-gated); a facade
introduced as "three modules" above a list of five; the pre-#1370 "blank
fields are not generated" opening; an ambiguous stalker-identity.utils.ts
citation (two files share the name); two of the three surfaces that apply the
scoped header override; a bare {status: 1} now being a refusal; and the
session-state fields #1354 added to the backup exclusion list (mirrored in
playlist-backup-restore.md).
CLAUDE.md had no entry at all for portal mode / endpoint discovery / lazy
repair — the largest change of the series; added one. Its session-facade list
was missing two modules and status 1 still read as plain "blocked".
Mock server: documented the /stalker, /stream/gated and marketing-poster
routes and the HOST variable; replaced the global POST /reset guidance with
the real per-MAC isolation contract (OWNED_MACS, the sibling 00:1A:79:5F:*
range, mode: 'serial'); added get_main_info; refreshed the project tree; fixed
a broken anchor; and corrected MOCK_PORT, which moves the client side only —
nothing maps it to the server's PORT.
The repo skill's "keep Stalker request rules in Stalker data access" no longer
holds: the wire-format, identity, portal-mode and auth-failure contracts live
in shared/interfaces because the Electron main process cannot import renderer
libs.
Also fixes four stale code comments carrying the same claims, including
"Single choke point for Stalker API calls" — four callers deliberately go
direct, and only fetchViaProfile() wires repair itself.
Docs and comments only; no executable change. No release note (no user-visible
behavior); no-release-note label applied for the libs/** paths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
53c318bac7
commit
5e4f2ca3dd
11 files changed
+319
-75
No files matched your search
@@ -14,14 +14,30 @@ description: Use when changing Stalker or Ministra routes, stores, catalog or se
|
||||
## Ownership
|
||||
|
||||
- Routed UI: `libs/portal/stalker/feature/src/lib/`
|
||||
- API, session, store, and normalization:
|
||||
- Session, store, and normalization:
|
||||
`libs/portal/stalker/data-access/src/lib/`
|
||||
- Wire-format and identity contracts: `libs/shared/interfaces/src/lib/` —
|
||||
portal-mode predicate, MAC/device-ID utils, auth-failure classifier, `cmd`
|
||||
encoder, URL/identity builders. There because Electron main cannot import
|
||||
renderer libs; never fork them.
|
||||
- Electron transport: `apps/electron-backend/src/app/events/stalker.events.ts`
|
||||
- Provider-neutral collections: `libs/portal/shared/data-access/src/lib/`
|
||||
|
||||
Keep Stalker request and shape rules in Stalker data access. Shared portal UI
|
||||
Keep Stalker shape and store rules in Stalker data access. Shared portal UI
|
||||
must remain provider-neutral.
|
||||
|
||||
## Portal Mode And Session
|
||||
|
||||
Full vs. simple mode is decided by observed behavior, not URL shape, and read
|
||||
only through `isFullStalkerPortalPlaylist()`. Route playlist-backed catalog,
|
||||
content and playback calls through `executeStalkerRequest()`. Auth, discovery,
|
||||
account-profile refresh and row-less collection resolution go direct; read
|
||||
`stalker-request.utils.ts` before adding a fifth. Repair is lazy per session,
|
||||
never eager.
|
||||
Full portals reuse the persisted idempotent handshake token while its session
|
||||
fingerprint matches, and ping `get_events` at the profile cadence (default
|
||||
120 s). Auth failures are HTTP 200 plus plain text.
|
||||
|
||||
## Series Contract
|
||||
|
||||
Inside Stalker portal code, `isStalkerSeriesFlag()` is the canonical predicate
|
||||
@@ -69,5 +85,5 @@ Run:
|
||||
`workspace-dashboard-feature` test targets
|
||||
|
||||
For the user workflow, run
|
||||
`pnpm nx run web-e2e:e2e-ci--src/stalker.e2e.ts` or document the missing
|
||||
fixture and strongest focused coverage.
|
||||
`pnpm nx run web-e2e:e2e-ci--src/stalker.e2e.ts` or document the strongest
|
||||
focused coverage available.
|
||||
@@ -1111,10 +1111,19 @@ engine` (restart required) or
|
||||
- Stalker: `StalkerAccountInfoComponent` (`libs/portal/stalker/feature/src/lib/stalker-account-info/`), cached-first — renders the import-time `stalkerAccountInfo` snapshot instantly, then `StalkerAccountInfoService` refreshes, routing by the observed portal MODE rather than the URL shape (full mode: handshake+`get_profile`; simple mode: best-effort `account_info/get_main_info`, nested `js.account_info` envelope or flat fields), and re-routing when a lazy repair changes the mode mid-request. Details: `docs/architecture/stalker-portal.md` ("Account Info Dialog").
|
||||
- Dashboard source cards carry a passive subscription-expiry chip (amber within 7 days, error-toned once expired); account details remain behind ⋮ → Account info. `DashboardSourceExpiryService` (`libs/workspace/dashboard/data-access/`) gathers the facts: Xtream from `PortalStatusService.checkPortalStatusDetails()` (the switcher's cached status check, now carrying `exp_date`), Stalker from the persisted `stalkerAccountInfo` snapshot — it lives in the playlist payload, not on meta rows, so each Stalker source costs one memoized full-playlist read.
|
||||
|
||||
**Stalker Portal Mode and Endpoint Discovery**:
|
||||
|
||||
- Portal mode (full vs. simple) follows OBSERVED behavior, never a URL substring. The single predicate is `isFullStalkerPortalPlaylist()` / `isFullStalkerPortalUrl()` in `@iptvnator/shared/interfaces` (`stalker-portal-mode.util.ts`): the persisted `Playlist.isFullStalkerPortal` flag is authoritative and the URL shape is a fallback for legacy rows only. Three diverging copies of this rule used to exist and shipped broken configurations (#850/#686/#755) — never re-implement it. A token-enforcing `portal.php` panel is a full portal; a `server/load.php` endpoint that answers without a token is a simple one.
|
||||
- Import probes candidates in order (a pasted `.php` endpoint first, then `<base>/portal.php` → `<base>/server/load.php` → `<base>/stalker_portal/server/load.php`) and classifies each by behavior — a token-less `itv/get_genres` returning data proves a token-free panel; the plain-text auth failure proves a full portal, confirmed by a real handshake + `get_profile`. `StalkerPortalDiscoveryService` (`libs/portal/stalker/data-access`) persists the proven endpoint and mode.
|
||||
- `executeStalkerRequest()` (`stores/utils/stalker-request.utils.ts`) is the choke point for catalog, content and playback requests: mode routing, the in-session repair override, and retry-once all live there. Four callers are deliberately outside it because they run below or before the thing it routes on — `StalkerAuthApi` (handshake/`get_profile`/`do_auth`, which the full-portal branch is built from; routing them back would recurse), `StalkerPortalDiscoveryService` (probes precede the mode they determine), `StalkerAccountInfoService.fetchViaProfile()`, and `StreamResolverService` for a collection item with no playlist row. They are exempt from the routing, not from the repair it hooks, but only `fetchViaProfile()` wires `StalkerPortalRepairService` itself: discovery is what repair *drives*, the row-less resolver branch has no playlist to repair, and the auth layer needs nothing — a terminal handshake failure propagates out of the full-portal branch into whichever `executeStalkerRequest()` call triggered the authentication, which is why terminal handshake failures are a repair trigger. Anything new that is not auth or discovery belongs on `executeStalkerRequest()`. Existing playlists are repaired LAZILY (`StalkerPortalRepairService`) — only after a request fails with a shape a wrong endpoint/mode produces, at most once per source configuration per playlist per session, persisted through the atomic `PlaylistsService.transformPlaylistMeta`. There is deliberately **no eager one-shot migration**: a portal that works is never re-probed.
|
||||
- Both transports build the wire format from the same shared builders in `@iptvnator/shared/interfaces` — `buildStalkerRequestUrl()`, `buildStalkerIdentityRequestContext()`, `encodeStalkerCmdValue()` — so the Electron and PWA legs cannot drift. The mock's `/stalker` mirror shares the identity builder only — it dispatches in-process, so there is no portal URL to build and it mirrors the `JsHttpRequest` default by hand. Never fork any of them.
|
||||
- Simple portals skip the auth lifecycle (no handshake, token or watchdog) but their requests are not stripped to a bare cookie: they still carry everything the shared builder derives from a MAC alone (`mac`/`stb_lang`/`timezone` cookie, MAG `User-Agent`/`X-User-Agent`, `Accept` set). They do NOT carry the serial — `dispatchStalkerRequest()`'s direct branch forwards only `url`/`macAddress`/`params`, so no `SN` header and no serial-derived `__cfduid`, whatever the playlist stores. That gate is on API requests only: `buildStalkerExternalPlaybackHeaders()` reads the serial off the playlist row with no mode check, so the same simple-mode playlist does send `SN`/`__cfduid` with a portal-owned stream.
|
||||
- Contract: `docs/architecture/stalker-portal.md` ("Portal Mode and Endpoint Discovery", "Request Transport and `cmd` Encoding").
|
||||
|
||||
**Stalker Session Authentication**:
|
||||
|
||||
- Full portals authenticate through `StalkerSessionService` (`libs/portal/stalker/data-access/src/lib/stalker-session.service.ts`), a facade over `stalker-auth.api.ts` (handshake / `get_profile` / `do_auth` + the `authenticate()` orchestration), `stalker-watchdog.controller.ts`, `stalker-portal-error.ts` and `stalker-response-classification.ts`.
|
||||
- `get_profile`'s `js.status` decodes as: full profile/`0` = OK, `1` = blocked, `2` = login/password required → `do_auth` then `get_profile` with `auth_second_step=1` (only that retry sets it). Credentials come from the import dialog's username/password fields and are persisted so runtime re-auth can repeat `do_auth`. Status is read through a numeric coercion — portals stringify it.
|
||||
- Full portals authenticate through `StalkerSessionService` (`libs/portal/stalker/data-access/src/lib/stalker-session.service.ts`), a thin facade over `stalker-auth.api.ts` (handshake / `get_profile` / `do_auth` + the `authenticate()` orchestration), `stalker-watchdog.controller.ts`, `stalker-token-cache.ts` (in-run token + pending-auth state, tagged with the identity fingerprint), `stalker-session-store.ts` (the session persisted on the playlist row), `stalker-portal-error.ts` and `stalker-response-classification.ts`.
|
||||
- `get_profile`'s `js.status` decodes as: full profile/`0` = OK, `1` = refused (`device-conflict` when the message says so, otherwise `blocked`), `2` = login/password required → `do_auth` then `get_profile` with `auth_second_step=1` (only that retry sets it). A bare `{status: 1}` with no message is a refusal, not a success. Credentials come from the import dialog's username/password fields and are persisted so runtime re-auth can repeat `do_auth`. Status is read through a numeric coercion — portals stringify it.
|
||||
- Refusals throw `StalkerPortalError` (`login-required` / `login-rejected` / `device-conflict` / `blocked` / `auth-failed`) carrying the portal's markup-stripped `msg`/`block_msg` in `portalText`; the import dialog and the workspace context panel render it. Read it with `asStalkerPortalError()`, never `instanceof` in lazy-loaded code. `device-conflict` splits off `blocked` via `isStalkerDeviceConflictMessage` (narrow phrase set, structured `msg` only): it is the one refusal with a remedy, and the portal's own "Your STB is damaged" wording points away from it, so both surfaces lead with their own headline and append the portal text.
|
||||
- Auth failures are HTTP 200 + plain text (`Authorization failed.` / `Access denied.` / `Unauthorized request.`), classified at the transport boundary by `libs/shared/interfaces/src/lib/stalker-auth-failure.util.ts`; the Electron handler **returns** a `{stalkerAuthFailure}` marker rather than throwing, because `ipcRenderer.invoke` strips custom properties off rejections.
|
||||
- The handshake is idempotent, so `Playlist.stalkerToken` is re-presented and `get_profile` is skipped when it comes back unchanged (unless `not_valid` is set, or the persisted `stalkerSessionIdentity` no longer matches `stalkerSessionFingerprint(playlist)` — portal endpoint (origin **and** path) + identity + credentials; an edited endpoint, MAC or login must never inherit the previous session, and a token with no recorded fingerprint counts as unverified. The path is deliberate: discovery preserves tenant base paths, so `/tenant-a/server/load.php` and `/tenant-b/server/load.php` are different portals on one host and must not share a session). The advertised watchdog cadence is persisted alongside it (`stalkerWatchdogTimeout`/`stalkerTimeslot`) precisely because that reuse skips the response carrying it — and the skip only applies once the cadence is known, so a legacy token-only playlist profiles once instead of being stranded on the default. The *effective* cadence is stored, so stored absence means "never profiled" and nothing re-profiles on every start.
|
||||
|
||||
@@ -59,7 +59,10 @@ actually get wrong:
|
||||
accepted, so a client with a broken token pipeline fails loudly.
|
||||
- Auth failures come back as **HTTP 200 with a plain-text body**
|
||||
(`Authorization failed.`, `Unauthorized request.`), never a 401/403. Clients
|
||||
that only check status codes will silently render nothing.
|
||||
that only check status codes will silently render nothing. The stock server
|
||||
has a third body, `Access denied.` (blocked account), which the mock does not
|
||||
produce — the app classifies all three
|
||||
(`libs/shared/interfaces/src/lib/stalker-auth-failure.util.ts`).
|
||||
- The handshake is **idempotent**: presenting the MAC's current token returns
|
||||
that same token instead of rotating it.
|
||||
- `device_id`/`device_id2` are pinned to the MAC on first non-empty value; any
|
||||
@@ -98,19 +101,27 @@ flags a real portal sends. Outside the `static-channel-cmd` scenario they are
|
||||
| Environment Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `PORT` | `3210` | HTTP port the server listens on |
|
||||
| `HOST` | `127.0.0.1` | Bind address. Loopback by default — the fixture serves fabricated, unauthenticated content, so set `HOST=0.0.0.0` only to deliberately point a phone or STB at it |
|
||||
| `NODE_ENV` | `development` | Node environment |
|
||||
|
||||
## Utility Endpoints
|
||||
## Non-Portal Endpoints
|
||||
|
||||
Besides the portal paths above, `src/main.ts` mounts:
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|---|---|---|
|
||||
| `/stalker?url=<portal_url>&macAddress=<mac>&action=<action>&…` | `GET` | CORS-proxy mirror of the IPTVnator web-backend `/stalker` endpoint, used by PWA/Playwright runs. `macAddress`, `token` and `serialNumber` are control params turned into headers and stripped from the portal query (except `handshake`'s candidate token); the response is wrapped as `{ payload }`. Strictness follows the proxied `url`'s shape |
|
||||
| `/stream/gated/:file` | `GET` | `video.mp4` / `audio.mp4` fixtures for the `gated-stream` scenario — 403 without the mac cookie **and** the MAC's current Bearer token |
|
||||
| `/assets/marketing/poster/<slug>.png` | `GET` | The committed screenshot-safe poster catalog shared with the Xtream mock, served from this process so `marketing-demo` needs no second server |
|
||||
| `/health` | `GET` | Health check — returns `{ status: "ok" }` |
|
||||
| `/reset` | `POST` | Clear all in-memory data, favorites, sessions and watchdog counters (useful between test runs) |
|
||||
| `/reset[?macAddress=<mac>&macAddress=…]` | `POST` | Clear generated data, favorites, session/auth state (including pinned device IDs) and watchdog counters. **Pass the MACs you own**: state is per-MAC and parallel spec files share this process, so a bare `/reset` wipes their state mid-test. Repeated params clear several MACs in one request |
|
||||
| `/invalidate-session?macAddress=<mac>` | `POST` | Drop that MAC's tokens so the next portal call fails with `Authorization failed.` — lets tests assert the client re-handshakes and retries. Pinned device identity survives, as on a real portal |
|
||||
|
||||
## API Coverage
|
||||
|
||||
All endpoints are served at `GET /portal.php?action=<action>&...` matching the real Stalker protocol:
|
||||
Every action is served by the same dispatcher (`src/app/routes/dispatch.ts`) at
|
||||
every portal path — `GET /portal.php?action=<action>&...` and the strict
|
||||
`server/load.php` shapes alike — matching the real Stalker protocol:
|
||||
|
||||
| Action | Description |
|
||||
|---|---|
|
||||
@@ -118,11 +129,12 @@ All endpoints are served at `GET /portal.php?action=<action>&...` matching the r
|
||||
| `get_profile` | Turns the handshake token into a session; enforces device-id pinning, and on the strict endpoint the MAC format |
|
||||
| `get_events` | Watchdog ping; records the call and returns an empty event set (never affects authorization, as on a real portal) |
|
||||
| `do_auth` | Boolean login step: `{js:true}` for non-empty credentials (recorded for the login-required scenario), `{js:false}` otherwise |
|
||||
| `get_categories` | Category list filtered by `type` (itv/vod/series) |
|
||||
| `get_main_info` | `account_info/get_main_info` — simple-mode subscription facts for the Stalker account-info dialog, in the nested `js.account_info` envelope |
|
||||
| `get_categories` | Category list filtered by `type` (itv/vod/series); `get_genres_itv` / `get_genres_vod` are handled by the same handler |
|
||||
| `get_genres` | Genre list (mirrors categories) |
|
||||
| `get_ordered_list` | Paginated content list; if `movie_id` is present → returns seasons |
|
||||
| `get_all_channels` | Complete ITV channel list in one response (`type=itv` only); excludes censored (adult) genres; disabled in the `legacy-pagination` scenario |
|
||||
| `create_link` | Returns a real public HLS stream URL for playback |
|
||||
| `create_link` | Returns a playable stream URL — a real public HLS stream, or the credential-gated local URL in the `gated-stream` scenario. Also echoes the mock-only `cmd_received` / `query_keys_received` diagnostics that pin the client's `cmd` wire format |
|
||||
| `favorites` | Add / remove / get favorites (in-memory, resets on restart) |
|
||||
| `get_short_epg` | Current-and-upcoming EPG window for a channel (`ch_id`, `size`) |
|
||||
| `get_epg_info` | Bulk EPG keyed by channel id for a requested `period` window |
|
||||
@@ -159,7 +171,15 @@ nx e2e web-e2e
|
||||
nx e2e web-e2e --grep "@stalker"
|
||||
```
|
||||
|
||||
The test suite uses `00:1A:79:00:00:01` (default scenario) for most tests, and calls `POST /reset` in `beforeEach` to ensure a clean state between tests.
|
||||
The test suite uses `00:1A:79:00:00:01` (default scenario) for most tests. State
|
||||
is per-MAC and several spec files share this one server process, so `beforeEach`
|
||||
resets **only the MACs the file owns** (`OWNED_MACS` in `stalker.e2e.ts`, sent as
|
||||
repeated `macAddress` params in a single request) rather than clearing
|
||||
everything. The sibling specs that talk to this server (`self-hosted.e2e.ts`,
|
||||
the `sources-pwa` helpers) own a disjoint `00:1A:79:5F:*` range for the same
|
||||
reason, and `stalker.e2e.ts` runs `mode: 'serial'` because its tests
|
||||
deliberately share scenario MACs. Full contract:
|
||||
[`docs/architecture/stalker-mock-server.md`](../../docs/architecture/stalker-mock-server.md#test-isolation).
|
||||
|
||||
## EPG Behavior
|
||||
|
||||
@@ -182,25 +202,32 @@ See [`docs/architecture/stalker-mock-server.md`](../../docs/architecture/stalker
|
||||
```
|
||||
apps/stalker-mock-server/
|
||||
├── src/
|
||||
│ ├── main.ts # Express bootstrap
|
||||
│ ├── main.ts # Express bootstrap + non-portal routes
|
||||
│ └── app/
|
||||
│ ├── scenarios.ts # MAC → scenario config mapping
|
||||
│ ├── data-generator.ts # Seeded faker data generation
|
||||
│ ├── data-store.ts # Lazy per-MAC in-memory cache
|
||||
│ ├── auth-store.ts # Tokens, device pinning, do_auth state
|
||||
│ ├── request-mac.ts # MAC read from the `mac=` cookie
|
||||
│ ├── marketing-poster-url.ts # Origin-resolved poster paths
|
||||
│ ├── routes/
|
||||
│ │ ├── portal.route.ts # /portal.php route
|
||||
│ │ ├── portal.route.ts # Portal router (tolerant + strict)
|
||||
│ │ └── dispatch.ts # Shared Stalker action dispatcher
|
||||
│ └── handlers/
|
||||
│ ├── handshake.handler.ts
|
||||
│ ├── get-profile.handler.ts
|
||||
│ ├── get-events.handler.ts
|
||||
│ ├── do-auth.handler.ts
|
||||
│ ├── get-main-info.handler.ts
|
||||
│ ├── get-categories.handler.ts
|
||||
│ ├── get-genres.handler.ts
|
||||
│ ├── get-ordered-list.handler.ts
|
||||
│ ├── get-all-channels.handler.ts
|
||||
│ ├── get-seasons.handler.ts
|
||||
│ ├── create-link.handler.ts
|
||||
│ ├── favorites.handler.ts
|
||||
│ ├── get-epg-info.handler.ts
|
||||
│ ├── get-short-epg.handler.ts
|
||||
│ └── get-genres.handler.ts
|
||||
│ └── get-short-epg.handler.ts
|
||||
├── project.json
|
||||
├── tsconfig.json
|
||||
└── README.md
|
||||
|
||||
@@ -105,11 +105,16 @@ app.use('/server/load.php', createPortalRouter(true));
|
||||
app.use('/ministra/server/load.php', createPortalRouter(true));
|
||||
|
||||
/**
|
||||
* Mirror of the app's full-portal predicates (`isFullStalkerPortal` checks
|
||||
* `/stalker_portal/` or `/server/load.php`; import-time normalization checks
|
||||
* `/stalker_portal`). Any URL shape the client would authenticate against must
|
||||
* be enforced by the proxy too, or tests would silently exercise the tolerant
|
||||
* branch.
|
||||
* Which proxied portal URLs this mock enforces the token on. It mirrors
|
||||
* `isFullStalkerPortalUrl()` in `@iptvnator/shared/interfaces` — the union of
|
||||
* the three predicates that used to diverge in the app before endpoint
|
||||
* discovery unified them.
|
||||
*
|
||||
* The app itself no longer classifies by URL shape (mode is an observed,
|
||||
* persisted fact), but a fixture has to decide strictness from the path
|
||||
* alone: it IS the behavior being observed. Every URL shape the client would
|
||||
* authenticate against must be enforced here, or tests silently exercise the
|
||||
* tolerant branch.
|
||||
*/
|
||||
function isFullPortalUrlShape(url: string): boolean {
|
||||
return url.includes('/stalker_portal') || url.includes('/server/load.php');
|
||||
|
||||
@@ -51,7 +51,12 @@ export default defineConfig({
|
||||
},
|
||||
/* Run local dev servers before starting the tests.
|
||||
* Both the Angular app and the Stalker mock server start in parallel.
|
||||
* Set MOCK_PORT to override the default mock server port (3210).
|
||||
*
|
||||
* MOCK_PORT only moves where the CLIENT looks — this health check and the
|
||||
* specs' MOCK_SERVER constants. The mock reads PORT, which
|
||||
* stalker-mock-server's serve target pins to 3210, and nothing maps one to
|
||||
* the other, so MOCK_PORT alone makes the wait below time out. Use it to
|
||||
* point at a mock you started yourself on that port.
|
||||
*/
|
||||
webServer: [
|
||||
{
|
||||
|
||||
@@ -89,9 +89,11 @@ state.
|
||||
- favorites snapshots
|
||||
- recently viewed snapshots
|
||||
|
||||
Explicitly excluded:
|
||||
Explicitly excluded — session state, as opposed to the connection definition:
|
||||
|
||||
- `stalkerToken`
|
||||
- `stalkerSessionIdentity` (the fingerprint the token was negotiated for)
|
||||
- `stalkerWatchdogTimeout` / `stalkerTimeslot` (the profile-advertised cadence)
|
||||
- `stalkerAccountInfo`
|
||||
- playback positions in v1
|
||||
|
||||
|
||||
@@ -102,6 +102,13 @@ tests can assert the client re-handshakes and retries instead of surfacing an
|
||||
error; pinned device identity survives invalidation, as it does on a real
|
||||
portal.
|
||||
|
||||
Beside the portal endpoints, `main.ts` mounts a handful of non-portal routes:
|
||||
`GET /stalker` (the PWA CORS-proxy mirror), `GET /stream/gated/:file` (the
|
||||
credential-gated media fixtures for the `gated-stream` scenario),
|
||||
`GET /assets/marketing/poster/*` (the committed screenshot-safe posters) and
|
||||
the `/health` + `/reset` + `/invalidate-session` utilities. The full list with
|
||||
request shapes is in `apps/stalker-mock-server/README.md`.
|
||||
|
||||
## Data Generation Pipeline
|
||||
|
||||
```
|
||||
@@ -340,7 +347,9 @@ that no `create_link` request reaches the portal. See
|
||||
|
||||
Favorites are stored in a `Map<mac, Set<itemId>>` in `src/app/data-store.ts`. They persist for the lifetime of the server process and are shared across all requests for the same MAC.
|
||||
|
||||
Call `POST /reset` to clear all favorites (and regenerated data) between test runs.
|
||||
Call `POST /reset?macAddress=<mac>` to clear a MAC's favorites (and its cached
|
||||
generated data) between test runs — see "Test Isolation" for why the scoped
|
||||
form is the one specs should use.
|
||||
|
||||
## Playwright Integration
|
||||
|
||||
@@ -348,26 +357,53 @@ Call `POST /reset` to clear all favorites (and regenerated data) between test ru
|
||||
|
||||
```typescript
|
||||
webServer: [
|
||||
{
|
||||
command: 'pnpm nx run web:serve',
|
||||
url: 'http://localhost:4200',
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
},
|
||||
{ command: webServerCommand /* web:serve */, url: baseURL },
|
||||
{
|
||||
command: 'pnpm nx run stalker-mock-server:serve',
|
||||
url: 'http://localhost:3210/health',
|
||||
url: `http://localhost:${process.env['MOCK_PORT'] ?? '3210'}/health`,
|
||||
reuseExistingServer: !process.env['CI'],
|
||||
},
|
||||
// plus the xtream mock (3211) and web-backend (3333) entries
|
||||
]
|
||||
```
|
||||
|
||||
Playwright waits for both servers to be healthy before starting tests. If either is already running (e.g. in local dev), it reuses the existing instance.
|
||||
Playwright waits for every server to be healthy before starting tests. If one is already running (e.g. in local dev), it reuses the existing instance.
|
||||
|
||||
**`MOCK_PORT` moves the CLIENT side only** — Playwright's health-check URL and
|
||||
the `MOCK_SERVER` constants in the specs. The server's own port comes from
|
||||
`PORT` (`main.ts`), which the `serve` and `serve-with-watch` targets pin to
|
||||
`3210` in `project.json`, and nothing maps one variable to the other. Setting
|
||||
`MOCK_PORT` alone therefore points Playwright at a port nothing is listening
|
||||
on and the run times out waiting for `/health`. It is only useful against a
|
||||
mock you started yourself on that port (`reuseExistingServer` is on outside
|
||||
CI); relocating the Nx-managed one would need `MOCK_PORT` passed through as
|
||||
`PORT`.
|
||||
|
||||
### Test Isolation
|
||||
|
||||
Each stalker e2e test calls `POST http://localhost:3210/reset` in `beforeEach` to clear in-memory state. This ensures tests don't bleed favorites or other mutable state into each other.
|
||||
Mock state is keyed by MAC and one mock-server process is shared by every spec
|
||||
file running in parallel workers, so isolation is per-MAC rather than global:
|
||||
|
||||
`resetAll()` clears both the generated-content cache and in-memory favorites (`data-store.ts`). Because generation is seed-deterministic, the next request regenerates identical content, so the observable data does not change across resets.
|
||||
- `POST /reset` accepts one or more `?macAddress=` params and clears only those
|
||||
MACs. A bare `POST /reset` clears everything and is only safe when nothing
|
||||
else is talking to the server — a spec that used it would wipe a sibling
|
||||
spec's session mid-test.
|
||||
- `apps/web-e2e/src/stalker.e2e.ts` declares the MACs it owns in `OWNED_MACS`
|
||||
and clears exactly those in one batched request. The sibling specs that reach
|
||||
this server (`self-hosted.e2e.ts`, the `sources-pwa` helpers) own a disjoint
|
||||
`00:1A:79:5F:*` range, so neither file can clear the other's state.
|
||||
- Within the file, tests deliberately share scenario MACs (their fixture shapes
|
||||
are what the assertions are written against), so it pins itself to one worker
|
||||
with `test.describe.configure({ mode: 'serial' })`.
|
||||
- A few MACs are deliberately kept OUT of `OWNED_MACS`: the token-reuse test
|
||||
asserts that a session SURVIVES, so nothing may reset it, and it uses one MAC
|
||||
per browser project.
|
||||
|
||||
A reset drops the generated content cache, favorites (`data-store.ts`), the
|
||||
auth/session record (`auth-store.ts`, including any pinned device identity) and
|
||||
the watchdog ping counters. Because generation is seed-deterministic, the next
|
||||
request regenerates identical content, so the observable data does not change
|
||||
across resets.
|
||||
|
||||
### Recommended Test Structure
|
||||
|
||||
@@ -376,9 +412,16 @@ import { test, expect } from '@playwright/test';
|
||||
|
||||
const MOCK_URL = 'http://localhost:3210/portal.php';
|
||||
const MOCK_MAC = '00:1A:79:00:00:01'; // default scenario
|
||||
const OWNED_MACS = [MOCK_MAC];
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await request.post('http://localhost:3210/reset');
|
||||
// Scoped reset: only the MACs this file owns.
|
||||
const query = OWNED_MACS.map(
|
||||
(mac) => `macAddress=${encodeURIComponent(mac)}`
|
||||
).join('&');
|
||||
await request.post(`http://localhost:3210/reset?${query}`);
|
||||
});
|
||||
|
||||
test('browse VOD categories', async ({ page }) => {
|
||||
@@ -393,5 +436,5 @@ test('browse VOD categories', async ({ page }) => {
|
||||
- **New content types**: Add a new generator function in `data-generator.ts` and a new handler in `handlers/`.
|
||||
- **New scenarios**: Add to `SCENARIOS` in `scenarios.ts`.
|
||||
- **Session behaviour**: `auth-store.ts` owns tokens and device pinning. Add TTLs or a "token replaced by another device" mode there rather than in the handlers.
|
||||
- **Error simulation**: Add a special MAC or query param to trigger error responses for testing error handling in the Stalker store. Note that portal-level auth errors are *not* HTTP errors — see [Two Endpoints With Different Strictness](#two-endpoints-with-different-strictness).
|
||||
- **Error simulation**: Add a special MAC or query param to trigger error responses for testing error handling in the Stalker store. Note that portal-level auth errors are *not* HTTP errors — see [Endpoints With Different Strictness](#endpoints-with-different-strictness).
|
||||
- **Slow responses**: Add a `MOCK_DELAY_MS` env var and apply it in middleware for testing loading states.
|
||||
@@ -30,31 +30,71 @@ Stalker support covers:
|
||||
## Routing Structure
|
||||
|
||||
Primary route tree lives in
|
||||
`libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts`.
|
||||
`libs/portal/stalker/feature/src/lib/stalker-feature.routes.ts`
|
||||
(`createStalkerRoutes()`), mounted under the workspace shell, so every path
|
||||
below is reached as `/workspace/stalker/:id/…`.
|
||||
|
||||
- `/stalker/:id/vod` (plus `vod/:categoryId` child)
|
||||
- `/stalker/:id/series` (plus `series/:categoryId` child)
|
||||
- `/stalker/:id/itv`
|
||||
- `/stalker/:id/radio`
|
||||
- `/stalker/:id/favorites`
|
||||
- `/stalker/:id/recent`
|
||||
- `/stalker/:id/search`
|
||||
- `/stalker/:id/actor/:personId`
|
||||
- `/stalker/:id/downloads` (shared `DownloadsComponent` from `@iptvnator/portal/downloads/feature`)
|
||||
- `/stalker/:id/downloads/:downloadId` (focused local movie/series detail with
|
||||
no category context panel)
|
||||
- `/workspace/stalker/:id/vod` (plus `vod/:categoryId` child)
|
||||
- `/workspace/stalker/:id/series` (plus `series/:categoryId` child)
|
||||
- `/workspace/stalker/:id/itv`
|
||||
- `/workspace/stalker/:id/radio`
|
||||
- `/workspace/stalker/:id/favorites`
|
||||
- `/workspace/stalker/:id/recent`
|
||||
- `/workspace/stalker/:id/search`
|
||||
- `/workspace/stalker/:id/actor/:personId`
|
||||
- `/workspace/stalker/:id/downloads` (shared `DownloadsComponent` from `@iptvnator/portal/downloads/feature`)
|
||||
- `/workspace/stalker/:id/downloads/:downloadId` (focused local movie/series
|
||||
detail with no category context panel)
|
||||
|
||||
`/workspace/stalker/:id` itself redirects to `vod`.
|
||||
|
||||
## Runtime Architecture
|
||||
|
||||
1. Angular Stalker screens call methods/resources in `StalkerStore`.
|
||||
2. `StalkerStore` builds request params based on selected content type and current view state.
|
||||
3. Every portal API call funnels through `executeStalkerRequest()`
|
||||
3. Catalog, content and playback calls funnel through `executeStalkerRequest()`
|
||||
(`libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.ts`),
|
||||
the single choke point that decides the transport per portal mode:
|
||||
the choke point that decides the transport per portal mode:
|
||||
full portals go through `StalkerSessionService` (handshake + Bearer token +
|
||||
retry), token-free panels call
|
||||
`DataService.sendIpcEvent(STALKER_REQUEST, ...)` directly. It also hooks
|
||||
the lazy portal repair (see "Portal Mode and Endpoint Discovery").
|
||||
|
||||
Four callers deliberately sit outside it and issue `STALKER_REQUEST`
|
||||
themselves, because each one runs *below* or *before* what it routes on:
|
||||
|
||||
- `StalkerAuthApi` — `handshake` / `get_profile` / `do_auth` are what the
|
||||
full-portal branch is implemented in terms of, so routing them back
|
||||
through it would recurse.
|
||||
- `StalkerPortalDiscoveryService` — probes run before a mode exists; the
|
||||
mode is what they are determining.
|
||||
- `StalkerAccountInfoService.fetchViaProfile()` — the full-mode refresh is
|
||||
a profile request, so it takes the same exemption as the auth layer.
|
||||
- `StreamResolverService`, for a collection item carrying its own portal
|
||||
coordinates with **no playlist row** — there is no meta to route or
|
||||
repair with. The playlist-backed branch beside it does use
|
||||
`executeStalkerRequest()`, and wins when a row exists, so a repaired
|
||||
endpoint beats a stale favorite's snapshot.
|
||||
|
||||
The exemption is from the routing, not from the repair it hooks — but only
|
||||
**one** of the four wires `StalkerPortalRepairService` itself, and the
|
||||
asymmetry is worth knowing before adding a fifth:
|
||||
|
||||
- `StalkerAccountInfoService.fetchViaProfile()` wires it explicitly,
|
||||
because opening the account dialog on a playlist with a stale endpoint
|
||||
must be able to fix it instead of waiting for an unrelated catalog
|
||||
request.
|
||||
- `StalkerPortalDiscoveryService` is what repair *drives*, so it cannot
|
||||
repair itself.
|
||||
- The auth layer wires nothing. It does not need to: a terminal handshake
|
||||
failure propagates out of the full-portal branch and is caught by
|
||||
whichever `executeStalkerRequest()` call triggered the authentication,
|
||||
which is exactly why "terminal handshake failures" is one of the repair
|
||||
triggers listed above.
|
||||
- `StreamResolverService`'s row-less branch has no playlist to repair.
|
||||
|
||||
Anything new that is not auth or discovery belongs on
|
||||
`executeStalkerRequest()`.
|
||||
4. Electron main process handles `STALKER_REQUEST` in
|
||||
`apps/electron-backend/src/app/events/stalker.events.ts`.
|
||||
5. Axios calls the portal's persisted API endpoint (`portal.php` on
|
||||
@@ -86,8 +126,34 @@ Two portal modes exist, persisted per playlist as
|
||||
periodic authenticated `watchdog/get_events` pings at the cadence the
|
||||
portal advertises (`watchdog_timeout`, default 120 s — see "Watchdog"
|
||||
below) whose failures are non-fatal.
|
||||
- **Simple portal** (reseller-style `portal.php` panels): no auth lifecycle
|
||||
at all — requests carry only the `mac=` cookie.
|
||||
- **Simple portal** (typically a reseller-style `portal.php` panel): no auth
|
||||
lifecycle at all — no handshake, no Bearer token, no watchdog. The requests
|
||||
are not stripped down to a bare cookie either: every Stalker request goes
|
||||
through the shared identity builder, so a simple portal receives everything
|
||||
that builder can derive from a MAC alone — the `mac`/`stb_lang`/`timezone`
|
||||
cookie, the MAG `User-Agent` / `X-User-Agent` pair and the
|
||||
`Accept`/`Accept-Language`/`Connection` set (see "Request Transport and
|
||||
`cmd` Encoding").
|
||||
|
||||
What it does **not** get is anything carried by the session. The direct
|
||||
branch of `dispatchStalkerRequest()` forwards only `url`, `macAddress` and
|
||||
`params`, so the builder never sees a token *or* a serial: no
|
||||
`Authorization: Bearer` (there is none to send), and no `SN` header or
|
||||
serial-derived `__cfduid` cookie **even when the playlist stores a serial**.
|
||||
The full-portal branch forwards both, because `makeAuthenticatedRequest()`
|
||||
passes `token` and `serialNumber`. This covers portal API requests only —
|
||||
playback headers are built from the playlist row by a different helper and
|
||||
are NOT mode-gated, so the same simple-mode playlist does send its serial
|
||||
with a portal-owned stream (see "Stalker Identity Policy").
|
||||
Whether a simple panel ought to receive
|
||||
the serial is unproven: no reference portal is known to require it, and the
|
||||
`sn` *parameter* only ever travels on `get_profile`, which a simple portal
|
||||
never calls. Treat it as an open question rather than a bug to fix blind.
|
||||
|
||||
Neither label is tied to a URL shape: mode follows OBSERVED behavior, so a
|
||||
`portal.php` panel that enforces the token is classified — and treated
|
||||
everywhere — as a full portal, and a canonical `server/load.php` endpoint that
|
||||
answers without one is a simple portal.
|
||||
|
||||
The single predicate lives in `@iptvnator/shared/interfaces`
|
||||
(`stalker-portal-mode.util.ts`): `isFullStalkerPortalPlaylist()` treats the
|
||||
@@ -209,13 +275,33 @@ Failure-handling rule:
|
||||
## Stalker Identity Policy
|
||||
|
||||
Full Stalker/Ministra portal authentication defaults to MAC-only identity. The
|
||||
import UI can capture optional serial number, device IDs, and signatures, but
|
||||
blank fields are not generated or forwarded to `get_profile`.
|
||||
import UI can capture optional serial number, device IDs, and signatures, but a
|
||||
field the user leaves blank stays blank — nothing is invented for it, and
|
||||
nothing empty is forwarded to `get_profile`. The one way a value appears
|
||||
without being typed is the explicit import-time opt-in described under
|
||||
"Deriving device IDs from the MAC" below, which writes into the visible fields
|
||||
first. (The fixed MAG250 description `get_profile` reports is separate: it
|
||||
describes the emulated box, not the account — see "Reported device profile".)
|
||||
|
||||
- User-provided `sn`, `device_id`, `device_id2`, `signature`, and `signature2`
|
||||
values are trimmed, persisted under the canonical `stalker*` playlist fields,
|
||||
and reused for initial auth, token refresh, retry auth, normal API requests,
|
||||
and same-origin playback headers.
|
||||
|
||||
Those last two reach the wire by different routes, and **only one is
|
||||
mode-gated** — the asymmetry is easy to get backwards:
|
||||
|
||||
- **Portal API requests** receive the serial through
|
||||
`makeAuthenticatedRequest()`, which only the full-portal branch of
|
||||
`dispatchStalkerRequest()` calls. A simple-mode playlist therefore sends
|
||||
no serial on any API call, whatever it has stored (see "Portal Mode and
|
||||
Endpoint Discovery").
|
||||
- **Same-origin playback headers** are built by
|
||||
`buildStalkerExternalPlaybackHeaders()`, which reads
|
||||
`playlist.stalkerSerialNumber` straight off the row with no mode check
|
||||
at all. So a simple-mode playlist holding a real serial *does* send `SN`
|
||||
and the serial-derived `__cfduid` on portal-owned streams — while its
|
||||
API calls do not.
|
||||
- Empty optional identity fields remain absent. IPTVnator must never generate a
|
||||
device ID behind the user's back, and must never duplicate `device_id2` from
|
||||
`device_id1` on its own.
|
||||
@@ -296,7 +382,10 @@ dialog. Newly typed values are still held to the format.
|
||||
|
||||
### Deriving device IDs from the MAC
|
||||
|
||||
`deriveStalkerDeviceIdsFromMac` (`stalker-identity.utils.ts`) returns the pair
|
||||
`deriveStalkerDeviceIdsFromMac`
|
||||
(`libs/shared/interfaces/src/lib/stalker-identity.utils.ts` — note the
|
||||
same-named file in `libs/portal/stalker/data-access` is a different module)
|
||||
returns the pair
|
||||
StbEmu and `stalker-to-m3u` generate: uppercase hex `SHA256` of the canonical
|
||||
MAC for `device_id`, and of that MAC plus a `stalker` salt for `device_id2`.
|
||||
The import dialog offers it behind an opt-in checkbox that fills both fields.
|
||||
@@ -407,7 +496,8 @@ nothing to them.
|
||||
|
||||
Full portals authenticate through `StalkerSessionService`
|
||||
(`libs/portal/stalker/data-access/src/lib/stalker-session.service.ts`), which
|
||||
is a facade over three focused modules:
|
||||
is a thin facade over focused modules (it was split when the single file
|
||||
outgrew the `max-lines` budget; never re-add it to the baseline):
|
||||
|
||||
- `stalker-auth.api.ts` — the raw `handshake` / `get_profile` / `do_auth`
|
||||
requests and the `authenticate()` orchestration.
|
||||
@@ -478,7 +568,11 @@ every start.
|
||||
`msg`/`block_msg` carry the portal's own explanation; they are
|
||||
markup-stripped, combined, and thrown as `StalkerPortalError`. The kind is
|
||||
`device-conflict` when `isStalkerDeviceConflictMessage` matches the combined
|
||||
text, otherwise `blocked` — see "Device conflicts" below.
|
||||
text, otherwise `blocked` — see "Device conflicts" below. A **bare**
|
||||
`{status: 1}` with no message is a refusal too: it used to be read as success
|
||||
whenever the portal sent no `msg`, which imported dead sources (the stock
|
||||
MAC-format rejection is exactly that shape). A profile that carries refusal
|
||||
text without setting the status is likewise refused.
|
||||
- `status: 2` — login/password required. The client runs `do_auth`
|
||||
(`login`, `password`, plus `device_id`/`device_id2` when configured) and
|
||||
retries `get_profile` with `auth_second_step=1`. Only that retry claims the
|
||||
@@ -908,11 +1002,18 @@ streams it resolves, so a channel opened from a collection carries the same
|
||||
credentials as one opened from the portal.
|
||||
The resolved `ResolvedPortalPlayback.headers` feed both the external players
|
||||
(MPV/VLC/Embedded MPV via the launch IPC) and the built-in players via the
|
||||
scoped Electron request-header override (`ElectronStreamHeadersService`,
|
||||
applied by `WebPlayerViewComponent` for the video players and by the Stalker
|
||||
live layout for the radio audio player, which renders outside
|
||||
`WebPlayerViewComponent` — see `docs/architecture/electron-security.md`,
|
||||
"Scoped Request Header Overrides").
|
||||
scoped Electron request-header override (`ElectronStreamHeadersService` — see
|
||||
`docs/architecture/electron-security.md`, "Scoped Request Header Overrides").
|
||||
Three surfaces apply that override, because `WebPlayerViewComponent` owns it
|
||||
only for the video players and radio renders `AudioPlayerComponent` outside it:
|
||||
|
||||
- `WebPlayerViewComponent` — every built-in video player, on every route.
|
||||
- `StalkerLiveStreamLayoutComponent` — the Stalker radio route's audio player.
|
||||
- `UnifiedLiveTabComponent` (`libs/portal/shared/ui`) — the radio audio player
|
||||
of the Favorites / Recently Viewed collection routes.
|
||||
|
||||
Each owns a single scope slot and clears it only while it still owns it, so a
|
||||
handover between them cannot drop the other's credentials.
|
||||
|
||||
Two stream profiles exist, selected by one shared predicate:
|
||||
|
||||
@@ -1303,9 +1404,13 @@ Exported fields:
|
||||
- full-portal serial/device/signature fields when present
|
||||
- favorites and recently viewed collections
|
||||
|
||||
Excluded fields:
|
||||
Excluded fields — everything that describes a negotiated session rather than
|
||||
the connection:
|
||||
|
||||
- `stalkerToken`
|
||||
- `stalkerSessionIdentity` (the fingerprint the token was negotiated for)
|
||||
- `stalkerWatchdogTimeout` / `stalkerTimeslot` (the cadence the profile
|
||||
advertised)
|
||||
- `stalkerAccountInfo`
|
||||
- playback positions in backup v1
|
||||
|
||||
|
||||
@@ -52,8 +52,16 @@ export const STALKER_SERIAL_NUMBER = LEGACY_DEFAULT_STALKER_SERIAL;
|
||||
|
||||
/**
|
||||
* Service to manage Stalker portal session tokens.
|
||||
* Handles handshake authentication for full stalker portals (/stalker_portal/c URLs).
|
||||
* Persists tokens during session and handles re-authentication on auth failures.
|
||||
*
|
||||
* Handles handshake authentication for playlists in FULL portal mode. Mode is
|
||||
* a persisted, behavior-observed fact read through
|
||||
* `isFullStalkerPortalPlaylist()` — never a URL substring: since endpoint
|
||||
* discovery, a token-enforcing `portal.php` panel is a full portal and a
|
||||
* `server/load.php` endpoint that answers without a token is a simple one.
|
||||
*
|
||||
* Tokens are cached in-run and written back to the playlist row, so a session
|
||||
* survives a restart; both are tagged with the identity fingerprint they were
|
||||
* negotiated for. Re-authenticates on auth failures.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
||||
@@ -123,13 +123,36 @@ async function dispatchStalkerRequest<T>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Single choke point for Stalker API calls. On top of the mode routing it
|
||||
* hooks the lazy portal repair: when a request fails in a way only a wrong
|
||||
* persisted endpoint/mode produces (plain-text `Authorization failed.`
|
||||
* bodies on token-less requests, HTTP 404 on a vanished endpoint, terminal
|
||||
* handshake failures), the repair service re-probes the portal once per
|
||||
* session and — only when a different configuration is PROVEN to work —
|
||||
* the request is retried against it. Healthy portals never probe.
|
||||
* Choke point for Stalker catalog, content and playback calls. On top of the
|
||||
* mode routing it hooks the lazy portal repair: when a request fails in a way
|
||||
* only a wrong persisted endpoint/mode produces (plain-text
|
||||
* `Authorization failed.` bodies on token-less requests, HTTP 404 on a
|
||||
* vanished endpoint, terminal handshake failures), the repair service
|
||||
* re-probes the portal once per session and — only when a different
|
||||
* configuration is PROVEN to work — the request is retried against it.
|
||||
* Healthy portals never probe.
|
||||
*
|
||||
* Four callers deliberately issue `STALKER_REQUEST` themselves, because each
|
||||
* runs BELOW or BEFORE what this routes on:
|
||||
*
|
||||
* - `StalkerAuthApi` — `handshake`/`get_profile`/`do_auth` are what the
|
||||
* full-portal branch here is implemented in terms of, so routing them back
|
||||
* through it would recurse.
|
||||
* - `StalkerPortalDiscoveryService` — probes precede the mode they determine.
|
||||
* - `StalkerAccountInfoService.fetchViaProfile()` — a profile request, so it
|
||||
* takes the same exemption as the auth layer.
|
||||
* - `StreamResolverService`, for a collection item carrying its own portal
|
||||
* coordinates with no playlist row — there is no meta to route or repair
|
||||
* with. Its row-backed branch does come through here.
|
||||
*
|
||||
* The exemption is from the ROUTING, not from the repair hooked above — but
|
||||
* only `fetchViaProfile()` wires `StalkerPortalRepairService` itself.
|
||||
* Discovery is what repair drives, the row-less branch has no playlist to
|
||||
* repair, and the auth layer needs nothing: a terminal handshake failure
|
||||
* propagates out of the full-portal branch below and is caught here, which is
|
||||
* why it is one of the triggers listed above.
|
||||
*
|
||||
* Anything new that is not auth or discovery belongs here.
|
||||
*/
|
||||
export async function executeStalkerRequest<T>(
|
||||
deps: StalkerRequestDeps,
|
||||
|
||||
@@ -19,9 +19,10 @@ function getHeaderValue(
|
||||
* Single owner of the scoped Electron request-header override for built-in
|
||||
* playback. There is exactly one scoped override slot in the main process, so
|
||||
* every surface that plays a stream inline goes through this service:
|
||||
* `WebPlayerViewComponent` for the web video players, and the Stalker live
|
||||
* layout for the dedicated radio audio player, which never mounts a
|
||||
* `WebPlayerViewComponent` at all.
|
||||
* `WebPlayerViewComponent` for the web video players, plus the two surfaces
|
||||
* that render `AudioPlayerComponent` for radio and therefore never mount a
|
||||
* `WebPlayerViewComponent` at all — the Stalker live layout, and the unified
|
||||
* live tab behind the Favorites / Recently Viewed collection routes.
|
||||
*
|
||||
* `apply()` extracts the full header set from the resolved playback —
|
||||
* including the portal Cookie/Authorization that auth-gated streams require —
|
||||
|
||||
Reference in new issue
Block a user