fix(m3u): forward playlist-level custom headers to external players (#1397)

* fix(m3u): forward playlist-level custom headers to external players

The custom User-Agent/Referer/Origin stored on an M3U playlist only
reached the built-in web players (via the Electron webRequest override).
MPV/VLC and the embedded MPV player make their own HTTP requests and
received only the per-channel #EXTVLCOPT values, so a playlist-wide
custom User-Agent was silently dropped for UA-locked providers (#1221).

External launch payloads now resolve each header independently: the
channel-level #EXTVLCOPT value wins, the playlist-level value is the
fallback, blank values count as absent — matching the semantics the
unified favorites/recent stream resolver already had. Covers the
auto-launch and catch-up effects in m3u-state, the manual MPV/VLC
fallback and the embedded MPV payload in VideoPlayerComponent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011id2tdJtkJYRYX8dwYYKwL

* fix(playback): send Origin as a real VLC header and cover header IPC in E2E

Review follow-ups: VLC only used the Origin value as an :http-referrer
fallback while MPV already sent it via --http-header-fields; both VLC
paths (fresh spawn and RC enqueue) now emit the same
buildHttpHeaderFields list, so a real `Origin: ...` header reaches the
provider, deduplicated against an explicit headers-map Origin. The
legacy origin-as-Referer fallback stays.

The dash-clearkey Electron E2E now asserts the new IPC contract (blank
channel-level headers arrive as undefined, not empty strings) and gains
a scenario that sets a playlist-level User-Agent through the source
editor and verifies the captured MPV fallback launch carries it across
the renderer/main IPC boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011id2tdJtkJYRYX8dwYYKwL

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 authored and GitHub committed 2026-08-09 23:34:03 +02:00
1 parent e5bb127ede
commit 9aeb83e515
10 files changed
+336 -67

No files matched your search

@@ -13,7 +13,12 @@ import {
launchElectronApp,
LaunchedElectronApp,
openAddPlaylistDialog,
openSourceEditor,
openSources,
saveSourceDialog,
sourceRowByTitle,
test,
updateSourceDialog,
waitForM3uCatalog,
workspaceRoot,
} from './electron-test-fixtures';
@@ -479,14 +484,17 @@ test('@electron @dash ClearKey DASH filters DRM fallback and reports external la
/web-player-diagnostic__player-card--primary/
);
// Blank channel-level #EXTVLCOPT values resolve to `undefined` (not
// empty strings) since the playlist-level header fallback landed —
// absent means absent on the IPC boundary.
const expectedLaunches = [
{
args: [
`${fixtureServer.origin}/unsupported.mkv`,
'Unsupported MKV',
'',
'',
'',
undefined,
undefined,
undefined,
undefined,
undefined,
@@ -628,6 +636,61 @@ test('@electron @dash ClearKey DASH filters DRM fallback and reports external la
).toBeVisible();
await dock.getByRole('button', { name: 'Dismiss' }).click();
await expect(dock).toBeHidden();
// Playlist-level custom headers must cross the IPC boundary when the
// channel itself carries no #EXTVLCOPT values (#1221): set a
// User-Agent on the source, relaunch the MPV fallback, and expect the
// captured launch to carry it.
await openSources(app.mainWindow);
const sourceDialog = await openSourceEditor(
app.mainWindow,
'Imported as text'
);
await updateSourceDialog(sourceDialog, {
userAgent: 'Playlist Agent E2E/1.0',
});
await saveSourceDialog(app.mainWindow, sourceDialog);
await sourceRowByTitle(app.mainWindow, 'Imported as text')
.first()
.click();
await waitForM3uCatalog(app.mainWindow);
await channelItemByTitle(app.mainWindow, 'Unsupported MKV')
.first()
.click();
await expect(banner).toContainText(
/container is likely unsupported by the browser player/i,
{ timeout: 15_000 }
);
await expect(mpvFallback).toBeVisible();
await mpvFallback.click();
const expectedPlaylistHeaderLaunch = {
args: [
`${fixtureServer.origin}/unsupported.mkv`,
'Unsupported MKV',
'',
'Playlist Agent E2E/1.0',
undefined,
undefined,
undefined,
undefined,
undefined,
],
player: 'mpv',
} satisfies CapturedExternalPlayerLaunch;
await expect
.poll(() => getPlaybackRecommendationCapture(app), {
timeout: 10_000,
})
.toEqual({
closed: ['e2e-recommended-mpv-1'],
completed: 3,
launches: [
...expectedBothLaunches,
expectedPlaylistHeaderLaunch,
],
released: true,
});
} finally {
await releasePlaybackRecommendationCapture(app).catch(() => undefined);
await closeElectronApp(app);