feat(playback): record recently viewed only after the stream plays

A channel, movie or episode used to enter Recently Viewed (and the
dashboard's Continue Watching hero) the moment it was selected or its
link was resolved, so streams that failed straight away cluttered the
history.

Writers now defer the write to a root PlaybackHistoryGate, keyed by the
stream URL and/or the playback session key. The inline players confirm
those keys once the owned engine's position has advanced by two seconds
(seeks, stalls, pauses and a previous stream's progress do not count),
the radio player does the same, and a launched MPV/VLC session confirms
on `opened`/`playing`. M3U with MPV/VLC configured keeps recording on
selection. Covers M3U (live, radio, movie detail), Stalker (live, radio,
VOD, series), Xtream VOD and series, and the global live collection.

The M3U host's embeddedPlayback is now compared by value: the history
write updates the playlist meta mid-playback, and a new but identical
playback object remounted the engine and restarted the stream.

E2E flows that relied on recording-on-click now play local fixtures
(HLS/TS/WebM routed in place of unreachable or public streams) and wait
for confirmed playback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-27 12:23:00 +02:00
1 parent 254d1fd922
commit 4b7a2449a5
37 files changed
+1634 -118

No files matched your search

+13
View File
@@ -189,6 +189,19 @@ test('@web @m3u @dash ClearKey reopens from recent and favorites collections', a
await importDashPlaylist(page);
await page.getByText('1. ClearKey DASH').click();
await expectVideoPlaying(page);
// Recent history records the channel once it has played two seconds.
await expect
.poll(
() =>
page
.locator('app-web-player-view video')
.first()
.evaluate(
(element: HTMLVideoElement) => element.currentTime
),
{ timeout: 20_000 }
)
.toBeGreaterThan(2.5);
const playlistUrl = page.url().replace(/\/all$/, '');
const channel = page.locator('.channel-list-item').filter({
hasText: '1. ClearKey DASH',
+14 -1
View File
@@ -431,7 +431,7 @@ for (const theme of ['light', 'dark']) {
const channels = Array.from(
{ length: 60 },
(_, index) =>
`#EXTINF:-1 group-title="News",Station ${index + 1}\n${FIXTURE_HOST}/live-${index}.m3u8`
`#EXTINF:-1 group-title="News",Station ${index + 1}\n${FIXTURE_HOST}/live-${index}.webm`
);
await importPlaylist(page, ['#EXTM3U', ...channels].join('\n'), 60);
await page.evaluate(
@@ -462,6 +462,19 @@ for (const theme of ['light', 'dark']) {
await expect(viewport.locator('.channel-list-item').nth(1)).toHaveClass(
/active/
);
// Recently viewed lists the station once it has played two seconds.
await expect
.poll(
() =>
page
.locator('app-web-player-view video')
.first()
.evaluate(
(video: HTMLVideoElement) => video.currentTime
),
{ timeout: 20_000 }
)
.toBeGreaterThan(2.5);
await pressTab(page, browserName);
const favorite = viewport.locator('.favorite-button').nth(1);
await expect(favorite).toBeFocused();