fix(stalker): only mint a temporary link when the row asks for one

`create_link` ran on every Stalker playback. The reference client — the
portal's own `player.js`, mirrored by Kodi's pvr.stalker — mints a link
only when the catalog row sets `use_http_tmp_link` or `use_load_balancing`;
otherwise it plays the static `cmd` that `get_all_channels` /
`get_ordered_list` already returned. Neither flag was read anywhere in the
codebase, so every channel paid a round trip and gained a failure point the
reference client does not have.

One helper now owns the decision (`resolveStalkerStaticPlaybackUrl`), used
by `fetchStalkerPlaybackLink()` for ITV/VOD/radio, by the download path,
and by `StreamResolverService` for Favorites/Recently Viewed. Its guards
are deliberately wider than the flags alone and can only route a row back
onto the `create_link` path: no row to read flags from, a relative or
query-only command (the VOD `has_files` rewrite), a non-HTTP scheme, or a
loopback host. An episode always mints, since `series` selects it
server-side. Radio joins the same decision, so a station the portal proxies
now gets its link instead of playing a URL the portal never meant to serve.

Temporary links live ~5 s, so the audit that came with this: favorites and
recently-viewed persist the `cmd`, playback positions store ids, and the
main-process context map stores headers keyed by origin+path — none replay
a resolved URL. Downloads are the documented exception, and honouring the
flags shrinks even that, since an unflagged movie now yields a permanent
URL that survives retry.

`forced_storage` and `play_token` stay unwired, with the reasoning recorded
in the docs rather than left ambiguous.

The mock's ITV/radio rows now carry both flags, and the new
`static-channel-cmd` scenario (MAC 00:1A:79:00:00:0A) serves unflagged rows
with a playable command so the e2e can assert that NO `create_link` request
reaches the portal — verified to fail when the change is reverted, with a
companion test proving the recorder sees a link when one is due.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5 committed 2026-08-03 08:51:31 +02:00
1 parent d3cc18dc72
commit 2adf687b9b
24 files changed
+1139 -141

No files matched your search

+64
View File
@@ -65,6 +65,13 @@ const EMBEDDED_SERIES_MAC = '00:1A:79:00:00:05';
/** Legacy pagination MAC — portal without get_all_channels support */
const LEGACY_PAGINATION_MAC = '00:1A:79:00:00:06';
/**
* Static-cmd MAC — ITV rows carrying a directly playable `cmd` with
* `use_http_tmp_link` and `use_load_balancing` both `'0'`, i.e. a portal that
* expects no `create_link` call at all.
*/
const STATIC_CMD_MAC = '00:1A:79:00:00:0A';
/**
* Dedicated MACs for the full-portal authentication tests. Mock state is keyed
* by MAC, so keeping these distinct from the content scenarios above means an
@@ -120,6 +127,7 @@ const OWNED_MACS = [
MINIMAL_MAC,
EMBEDDED_SERIES_MAC,
LEGACY_PAGINATION_MAC,
STATIC_CMD_MAC,
AUTH_FLOW_MAC,
AUTH_REAUTH_MAC,
AUTH_REJECTED_MAC,
@@ -681,6 +689,62 @@ test('@stalker create_link returns a playable stream URL', async ({
expect(streamUrl).toMatch(/\.m3u8$/);
});
/**
* Play the first channel of the first ITV category and report every
* `create_link` request the page made while doing so.
*/
async function playFirstItvChannel(page: Page): Promise<string[]> {
const createLinkRequests: string[] = [];
page.on('request', (request) => {
if (request.url().includes('action=create_link')) {
createLinkRequests.push(request.url());
}
});
await page.getByRole('link', { name: /live|itv/i }).click();
await page.waitForURL(/stalker.*itv/);
const categories = page.locator('.category-item');
await expect(categories.nth(1)).toBeVisible({ timeout: 10_000 });
await categories.nth(1).click();
const channels = page.locator('[data-test-id="channel-item"]');
await expect(channels.first()).toBeVisible({ timeout: 20_000 });
await channels.first().click();
await expect(channels.first()).toHaveClass(/active/, { timeout: 20_000 });
await expect(page.locator('app-web-player-view')).toBeVisible({
timeout: 20_000,
});
return createLinkRequests;
}
test('@stalker ITV plays an unflagged channel without minting a link', async ({
page,
}) => {
// The reference client only calls create_link when the row sets
// use_http_tmp_link or use_load_balancing; this portal sets neither, so
// the static cmd must reach the player untouched. The companion test
// below proves the recorder does see a create_link when one is due.
await addStalkerPortal(page, {
name: 'Static Cmd Portal',
mac: STATIC_CMD_MAC,
});
expect(await playFirstItvChannel(page)).toEqual([]);
});
test('@stalker ITV mints a link for a channel that asks for one', async ({
page,
}) => {
await addStalkerPortal(page, { name: 'Tmp Link Portal' });
const createLinkRequests = await playFirstItvChannel(page);
expect(createLinkRequests.length).toBeGreaterThan(0);
expect(createLinkRequests[0]).toContain('type=itv');
});
test('@stalker mock server returns radio categories and stations', async ({
request,
}) => {