From 8508010ea54161a24c0138bcac0d73b6d0102d72 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:23:30 +0200 Subject: [PATCH 1/7] fix(playback): resume an episode whose connection drops instead of skipping it (#1902) (#1908) * fix(playback): resume an episode whose connection drops instead of skipping it (#1902) Embedded MPV sessions run with keep-open, so a provider that closes the connection mid-episode (and refuses ffmpeg's own reconnect) reaches the app as eof-reached: status `ended` at the drop position. The reconnect policy counted `ended` as a loss only for live playback, so on VOD it stood for the end of the file and series autoplay started the next episode. - The reconnect policy now treats a VOD `ended` with more than max(10 s, 1 % of the duration) left as a dropped connection and reloads it at the last position. A reload that ends again before getting 5 s past where it resumed proves the file ends there (bitrate-estimated duration), so it is left as the end and autoplay continues after one attempt. - The service passes the snapshot duration to the policy. - The player emits playbackEnded only for an `ended` session without a pending reconnect. - Xtream mock: `streamdrop` scenario whose episodes drop a third of the way into a local 60 s clip and refuse the retry for 4 s; Electron E2E covers the native-view engine (skips without an Embedded MPV runtime). Co-Authored-By: Claude Opus 5.5 * fix(playback): keep the early-EOF retry on a refused reload or after a seek The resume point that tells a reload ending where it resumed from a new drop was recorded before the reload ran, so a reload the addon refused made the original EOF look like the end and the next poll cancelled the retry. Record it only once the reload started, and forget it when the user seeks: an EOF after a seek is judged on its position alone. Co-Authored-By: Claude Opus 5.5 * test(e2e): fail the stream-drop resume check when the reload restarts the episode A reload that started the episode over would also reach the 25 s mark within the timeout, since the mock drops each episode only once. Every position played after the drop must now stay within 3 s of it. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: 4gray Co-authored-by: Claude Opus 5.5 --- .../playback-embedded-mpv-mid-episode-drop.md | 9 + .../embedded-mpv-series-stream-drop.e2e.ts | 200 +++++++++++ ...edded-mpv-native.service.reconnect.spec.ts | 61 +++- .../services/embedded-mpv-native.service.ts | 13 +- .../services/embedded-mpv-reconnect.spec.ts | 322 ++++++++++++++++++ .../app/services/embedded-mpv-reconnect.ts | 108 +++++- .../src/app/dropping-episode.ts | 76 +++++ apps/xtream-mock-server/src/app/scenarios.ts | 17 +- .../xtream-mock-server/src/app/server.spec.ts | 110 +++++- apps/xtream-mock-server/src/app/server.ts | 9 + .../xtream-mock-server/src/fixtures/README.md | 9 +- .../src/fixtures/episode.mkv | Bin 0 -> 205617 bytes docs/architecture/embedded-mpv-native.md | 22 +- docs/architecture/xtream-mock-server.md | 12 + .../embedded-mpv-player.component.spec.ts | 51 +++ .../embedded-mpv-player.component.ts | 5 +- 16 files changed, 1008 insertions(+), 16 deletions(-) create mode 100644 .changes/playback-embedded-mpv-mid-episode-drop.md create mode 100644 apps/electron-backend-e2e/src/embedded-mpv-series-stream-drop.e2e.ts create mode 100644 apps/xtream-mock-server/src/app/dropping-episode.ts create mode 100644 apps/xtream-mock-server/src/fixtures/episode.mkv diff --git a/.changes/playback-embedded-mpv-mid-episode-drop.md b/.changes/playback-embedded-mpv-mid-episode-drop.md new file mode 100644 index 000000000..3175454e5 --- /dev/null +++ b/.changes/playback-embedded-mpv-mid-episode-drop.md @@ -0,0 +1,9 @@ +--- +type: fix +area: playback +issues: [1902] +--- + +Embedded MPV no longer skips to the next episode when the provider drops the +connection in the middle of an episode. It now reconnects and continues the +episode where it stopped; a movie interrupted the same way resumes as well. diff --git a/apps/electron-backend-e2e/src/embedded-mpv-series-stream-drop.e2e.ts b/apps/electron-backend-e2e/src/embedded-mpv-series-stream-drop.e2e.ts new file mode 100644 index 000000000..e04fbdc8a --- /dev/null +++ b/apps/electron-backend-e2e/src/embedded-mpv-series-stream-drop.e2e.ts @@ -0,0 +1,200 @@ +import type { APIRequestContext, Page } from '@playwright/test'; +import { + addXtreamPortal, + clickCategoryByNameExact, + clickGridListCardByTitle, + closeElectronApp, + expect, + goToDashboard, + launchElectronApp, + openSettings, + openSettingsSection, + openWorkspaceSection, + resetMockServers, + saveSettings, + test, + waitForXtreamImportToFinish, + xtreamMockServer, +} from './electron-test-fixtures'; +import { installEmbeddedMpvSessionCapture } from './embedded-mpv-frame-copy-packaged-fixtures'; +import { + fetchXtreamSeriesFixture, + getXtreamTitle, +} from './portal-mock-fixtures'; + +/** + * Issue #1902: an episode whose connection drops reaches mpv as EOF in the + * middle of the episode, and series autoplay used to take that for the end + * and start the next episode. The `streamdrop` mock cuts every episode a + * third of the way into its 60 s clip and refuses it for a few seconds, so + * ffmpeg's own reconnect fails and only the app-level reconnect can resume. + * Needs the native-view Embedded MPV runtime; skips where it is missing. + */ +const credentials = { password: 'streamdrop', username: 'streamdrop' }; +/** Past the ~21 s cut: the resumed episode has played on. */ +const RESUMED_POSITION_SECONDS = 25; +/** Keyframe rounding and the 500 ms poll before the drop. */ +const RESUME_TOLERANCE_SECONDS = 3; + +type SessionSample = { + episodeId: number | null; + positionSeconds: number; + reconnecting: boolean; + status: string; +}; + +async function fetchSeasonOneEpisodeIds( + request: APIRequestContext, + seriesId: number +): Promise { + const params = new URLSearchParams({ + action: 'get_series_info', + series_id: String(seriesId), + ...credentials, + }); + const response = await request.get( + `${xtreamMockServer}/player_api.php?${params}` + ); + expect(response.ok()).toBeTruthy(); + const info = (await response.json()) as { + episodes?: Record; + }; + return [...(info.episodes?.['1'] ?? [])] + .sort((a, b) => a.episode_num - b.episode_num) + .map((episode) => Number(episode.id)); +} + +/** + * Session updates in arrival order, each tagged with the episode its session + * last loaded: the `loading` update carries the requested URL, while later + * snapshots report mpv's `path`, which not every engine fills in reliably. + */ +function readSessions(page: Page): Promise { + return page.evaluate(() => { + const episodeBySession = new Map(); + return (window.__packagedEmbeddedMpvSessions ?? []).map((session) => { + const match = /\/series\/[^/]+\/[^/]+\/(\d+)\.\w+$/.exec( + session.streamUrl + ); + if (match) { + episodeBySession.set(session.id, Number(match[1])); + } + return { + episodeId: episodeBySession.get(session.id) ?? null, + positionSeconds: session.positionSeconds, + reconnecting: !!session.reconnect, + status: session.status, + }; + }); + }); +} + +/** + * What followed the first episode's mid-episode EOF, in update order. Every + * position played after the drop must stay near the drop: a reload that + * restarted the episode would otherwise reach the resumed position as well. + * A zero is the engine's placeholder before the reload's first `time-pos`. + */ +function outcomeAfterDrop( + sessions: SessionSample[], + [first, second]: number[] +): 'pending' | 'resumed' | 'restarted' | 'skipped' { + const dropped = sessions.findIndex( + (session) => session.episodeId === first && session.status === 'ended' + ); + if (dropped < 0) { + return 'pending'; + } + const dropPosition = sessions[dropped].positionSeconds; + for (const session of sessions.slice(dropped)) { + if (session.episodeId === second) { + return 'skipped'; + } + if (session.episodeId !== first || session.status !== 'playing') { + continue; + } + if ( + session.positionSeconds > 0 && + session.positionSeconds < dropPosition - RESUME_TOLERANCE_SECONDS + ) { + return 'restarted'; + } + if (session.positionSeconds >= RESUMED_POSITION_SECONDS) { + return 'resumed'; + } + } + return 'pending'; +} + +test('@playback @xtream @electron @embedded-mpv resumes an episode whose connection drops instead of skipping to the next one', async ({ + dataDir, + request, +}) => { + test.setTimeout(180_000); + await resetMockServers(request, ['xtream']); + const fixture = await fetchXtreamSeriesFixture(request, credentials); + const series = fixture.items[0]; + if (!series) { + throw new Error('The stream-drop scenario has no series.'); + } + const episodeIds = await fetchSeasonOneEpisodeIds( + request, + Number(series.series_id) + ); + expect(episodeIds).toHaveLength(3); + + const app = await launchElectronApp(dataDir, { + env: { + IPTVNATOR_ENABLE_EMBEDDED_MPV_EXPERIMENT: '1', + IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY: '0', + IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW: '1', + }, + }); + try { + const support = await app.mainWindow.evaluate(() => + window.electron.getEmbeddedMpvSupport() + ); + test.skip( + !support.supported || support.engine !== 'native', + `Native-view Embedded MPV unavailable: ${JSON.stringify(support)}` + ); + await openSettings(app.mainWindow); + await openSettingsSection(app.mainWindow, 'playback'); + await app.mainWindow.getByTestId('select-video-player').click(); + await app.mainWindow.getByTestId('embedded-mpv').click(); + await saveSettings(app.mainWindow); + await goToDashboard(app.mainWindow); + + await addXtreamPortal(app.mainWindow, { + ...credentials, + name: 'Stream Drop', + }); + await waitForXtreamImportToFinish(app.mainWindow); + await installEmbeddedMpvSessionCapture(app); + await openWorkspaceSection(app.mainWindow, 'Series'); + await clickCategoryByNameExact(app.mainWindow, fixture.categoryName); + await clickGridListCardByTitle(app.mainWindow, getXtreamTitle(series)); + const episode = app.mainWindow + .locator('.episode-card, .episode-list-item') + .first(); + await expect(episode).toBeVisible({ timeout: 20_000 }); + await episode.click(); + + await expect + .poll( + async () => + outcomeAfterDrop( + await readSessions(app.mainWindow), + episodeIds + ), + { timeout: 90_000 } + ) + .not.toBe('pending'); + const sessions = await readSessions(app.mainWindow); + expect(outcomeAfterDrop(sessions, episodeIds)).toBe('resumed'); + // The drop was shown as a reconnect, not as the episode's end. + expect(sessions.some((session) => session.reconnecting)).toBe(true); + } finally { + await closeElectronApp(app); + } +}); diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.reconnect.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.reconnect.spec.ts index d682ff2ce..e621b58ce 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.reconnect.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.reconnect.spec.ts @@ -142,6 +142,7 @@ describe('EmbeddedMpvNativeService reconnect', () => { }; let status: EmbeddedMpvSessionStatus; let positionSeconds: number; + let durationSeconds: number | null; let recordingActive: boolean; let errorOrigin: 'playback' | 'engine' | undefined; let rejectedOptionKeys: string[] | undefined; @@ -179,6 +180,7 @@ describe('EmbeddedMpvNativeService reconnect', () => { service = new EmbeddedMpvNativeService(); status = 'idle'; positionSeconds = 0; + durationSeconds = null; recordingActive = false; errorOrigin = undefined; rejectedOptionKeys = undefined; @@ -193,7 +195,7 @@ describe('EmbeddedMpvNativeService reconnect', () => { getSessionSnapshot: jest.fn(() => ({ status, positionSeconds, - durationSeconds: null, + durationSeconds, volume: 1, streamUrl: LIVE.streamUrl, recording: { active: recordingActive }, @@ -596,6 +598,63 @@ describe('EmbeddedMpvNativeService reconnect', () => { }); }); + it('treats EOF in the middle of an episode as a drop, not as its end (#1902)', () => { + service.createSession(BOUNDS, 'Title', 0.5, { + extraOptions: [], + autoReconnect: true, + }); + service.loadPlayback('session-1', VOD); + poll('loading'); + positionSeconds = 1_260; + durationSeconds = 2_700; + poll('playing'); + + // mpv drained its cache after the server closed the connection. + poll('ended'); + expect(lastUpdate()?.status).toBe('ended'); + expect(lastUpdate()?.reconnect?.attempt).toBe(1); + + status = 'loading'; + jest.advanceTimersByTime(2_000); + expect(addon.loadPlayback).toHaveBeenLastCalledWith('session-1', { + ...VOD, + startTime: 1_260, + }); + + // The genuine end of the episode stays the end. + positionSeconds = 2_700; + poll('playing'); + poll('ended'); + jest.advanceTimersByTime(60_000); + expect(lastUpdate()?.status).toBe('ended'); + expect(lastUpdate()?.reconnect).toBeUndefined(); + expect(addon.loadPlayback).toHaveBeenCalledTimes(2); + }); + + it('forgets where the reload resumed once the user seeks', () => { + service.createSession(BOUNDS, 'Title', 0.5, { + extraOptions: [], + autoReconnect: true, + }); + service.loadPlayback('session-1', VOD); + poll('loading'); + positionSeconds = 1_260; + durationSeconds = 2_700; + poll('playing'); + poll('ended'); + status = 'loading'; + jest.advanceTimersByTime(2_000); + poll('playing'); + + // A seek away and back: the next early EOF near the old resume + // point is a new drop, not the reload ending where it resumed. + service.seek('session-1', 600); + service.seekBy('session-1', 662); + poll('ended'); + + expect(lastUpdate()?.reconnect?.attempt).toBe(2); + }); + it('treats EOF on a live stream as a drop', () => { startPlayingLive(); diff --git a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts index bd95e81eb..1b539e4a6 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-native.service.ts @@ -741,6 +741,7 @@ export class EmbeddedMpvNativeService { seek(sessionId: string, seconds: number): EmbeddedMpvSession | null { this.assertEmbeddedMpvEnabled(); + this.onUserSeek(sessionId); this.getAddon().seek(sessionId, seconds); return this.refreshSession(sessionId); } @@ -760,6 +761,7 @@ export class EmbeddedMpvNativeService { if (!Number.isFinite(deltaSeconds)) { return this.refreshSession(sessionId); } + this.onUserSeek(sessionId); if (typeof addon.seekBy === 'function') { addon.seekBy(sessionId, deltaSeconds); } else { @@ -770,6 +772,14 @@ export class EmbeddedMpvNativeService { return this.refreshSession(sessionId); } + /** A seek invalidates the reconnect's resume point (embedded-mpv-reconnect.ts). */ + private onUserSeek(sessionId: string): void { + const session = this.sessions.get(sessionId); + if (session) { + this.reconnect.onUserSeek(session.reconnect); + } + } + setVolume(sessionId: string, volume: number): EmbeddedMpvSession | null { this.assertEmbeddedMpvEnabled(); this.getAddon().setVolume(sessionId, volume); @@ -1333,7 +1343,8 @@ export class EmbeddedMpvNativeService { payload.status, previousStatus, payload.positionSeconds, - snapshot.errorOrigin + snapshot.errorOrigin, + payload.durationSeconds ); if (reconnect) { payload.reconnect = reconnect; diff --git a/apps/electron-backend/src/app/services/embedded-mpv-reconnect.spec.ts b/apps/electron-backend/src/app/services/embedded-mpv-reconnect.spec.ts index 249d65a11..dbb5283aa 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-reconnect.spec.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-reconnect.spec.ts @@ -197,6 +197,328 @@ describe('EmbeddedMpvReconnectCoordinator', () => { expect(reload).not.toHaveBeenCalled(); }); + it('treats a VOD EOF well before the end as a drop and resumes there (#1902)', () => { + startPlaying(VOD); + coordinator.observe( + SESSION, + state, + 'playing', + 'playing', + 1_260, + undefined, + 2_700 + ); + + // The server closed the connection 21 min into a 45 min episode. + const info = coordinator.observe( + SESSION, + state, + 'ended', + 'playing', + 1_261, + undefined, + 2_700 + ); + + expect(info?.attempt).toBe(1); + // The poll keeps reading the same EOF until the attempt fires. + expect( + coordinator.observe( + SESSION, + state, + 'ended', + 'ended', + 1_261, + undefined, + 2_700 + ) + ).toBe(info); + jest.advanceTimersByTime(2_000); + expect(reload).toHaveBeenCalledTimes(1); + expect(reload).toHaveBeenLastCalledWith(SESSION, { + ...VOD, + startTime: 1_260, + }); + }); + + it('keeps a VOD EOF at the end, or with no known duration, as the end', () => { + startPlaying(VOD); + coordinator.observe( + SESSION, + state, + 'playing', + 'playing', + 2_670, + undefined, + 2_700 + ); + // 4 s short of a duration rounded down to whole seconds. + expect( + coordinator.observe( + SESSION, + state, + 'ended', + 'playing', + 2_696, + undefined, + 2_700 + ) + ).toBeNull(); + // A long film's closing seconds fall inside the 1 % margin. + state = createEmbeddedMpvReconnectState(true); + startPlaying(VOD); + expect( + coordinator.observe( + SESSION, + state, + 'ended', + 'playing', + 7_150, + undefined, + 7_200 + ) + ).toBeNull(); + // Without a duration nothing says the EOF came early. + state = createEmbeddedMpvReconnectState(true); + startPlaying(VOD); + expect( + coordinator.observe( + SESSION, + state, + 'ended', + 'playing', + 600, + undefined, + null + ) + ).toBeNull(); + jest.advanceTimersByTime(60_000); + expect(reload).not.toHaveBeenCalled(); + }); + + it('accepts the end when the reload cannot get past where it resumed', () => { + startPlaying(VOD); + coordinator.observe( + SESSION, + state, + 'playing', + 'playing', + 2_400, + undefined, + 2_700 + ); + // The duration was estimated from the bitrate; the file ends at 2 401 s. + expect( + coordinator.observe( + SESSION, + state, + 'ended', + 'playing', + 2_401, + undefined, + 2_700 + )?.attempt + ).toBe(1); + jest.advanceTimersByTime(2_000); + expect(reload).toHaveBeenLastCalledWith(SESSION, { + ...VOD, + startTime: 2_400, + }); + + coordinator.observe(SESSION, state, 'loading', 'ended'); + expect( + coordinator.observe( + SESSION, + state, + 'ended', + 'loading', + 2_401, + undefined, + 2_700 + ) + ).toBeNull(); + expect( + coordinator.observe( + SESSION, + state, + 'ended', + 'ended', + 2_401, + undefined, + 2_700 + ) + ).toBeNull(); + jest.advanceTimersByTime(60_000); + expect(reload).toHaveBeenCalledTimes(1); + }); + + it('reconnects again when the resumed episode drops further on', () => { + startPlaying(VOD); + coordinator.observe( + SESSION, + state, + 'playing', + 'playing', + 600, + undefined, + 2_700 + ); + coordinator.observe( + SESSION, + state, + 'ended', + 'playing', + 600, + undefined, + 2_700 + ); + jest.advanceTimersByTime(2_000); + coordinator.observe(SESSION, state, 'loading', 'ended'); + coordinator.observe( + SESSION, + state, + 'playing', + 'loading', + 601, + undefined, + 2_700 + ); + coordinator.observe( + SESSION, + state, + 'playing', + 'playing', + 900, + undefined, + 2_700 + ); + + expect( + coordinator.observe( + SESSION, + state, + 'ended', + 'playing', + 901, + undefined, + 2_700 + )?.attempt + ).toBe(2); + jest.advanceTimersByTime(4_000); + expect(reload).toHaveBeenLastCalledWith(SESSION, { + ...VOD, + startTime: 900, + }); + }); + + it('keeps retrying an early EOF when the addon refuses the reload', () => { + startPlaying(VOD); + coordinator.observe( + SESSION, + state, + 'playing', + 'playing', + 1_200, + undefined, + 2_700 + ); + coordinator.observe( + SESSION, + state, + 'ended', + 'playing', + 1_200, + undefined, + 2_700 + ); + reload.mockImplementationOnce(() => { + throw new Error('addon exploded'); + }); + jest.advanceTimersByTime(2_000); + + // The refused reload left the original EOF in the snapshot: it is + // still the drop, not a reload that ended where it resumed. + expect( + coordinator.observe( + SESSION, + state, + 'ended', + 'ended', + 1_200, + undefined, + 2_700 + )?.attempt + ).toBe(2); + jest.advanceTimersByTime(4_000); + expect(reload).toHaveBeenCalledTimes(2); + }); + + it('judges an EOF after a user seek on its position alone', () => { + startPlaying(VOD); + coordinator.observe( + SESSION, + state, + 'playing', + 'playing', + 1_200, + undefined, + 2_700 + ); + coordinator.observe( + SESSION, + state, + 'ended', + 'playing', + 1_200, + undefined, + 2_700 + ); + jest.advanceTimersByTime(2_000); + coordinator.observe(SESSION, state, 'loading', 'ended'); + coordinator.observe( + SESSION, + state, + 'playing', + 'loading', + 1_201, + undefined, + 2_700 + ); + + // Back to 10 min; the connection drops again a few minutes later. + coordinator.onUserSeek(state); + coordinator.observe( + SESSION, + state, + 'playing', + 'playing', + 600, + undefined, + 2_700 + ); + coordinator.observe( + SESSION, + state, + 'playing', + 'playing', + 1_202, + undefined, + 2_700 + ); + + expect( + coordinator.observe( + SESSION, + state, + 'ended', + 'playing', + 1_203, + undefined, + 2_700 + )?.attempt + ).toBe(2); + }); + it('resumes a VOD reload where it dropped but keeps live at the live edge', () => { startPlaying(VOD); coordinator.observe(SESSION, state, 'playing', 'playing', 95); diff --git a/apps/electron-backend/src/app/services/embedded-mpv-reconnect.ts b/apps/electron-backend/src/app/services/embedded-mpv-reconnect.ts index 2dc888792..d4940d450 100644 --- a/apps/electron-backend/src/app/services/embedded-mpv-reconnect.ts +++ b/apps/electron-backend/src/app/services/embedded-mpv-reconnect.ts @@ -16,8 +16,13 @@ import { isEmbeddedMpvSessionGoneError } from './embedded-mpv-session-errors'; * - Only a load that already reached `playing` is retried. A URL that never * worked (404, refused credentials, unsupported container) keeps the manual * Retry; hammering a panel six times with the same bad request helps nobody. - * - EOF counts as a loss only for live playback — a broadcast never ends on - * its own, a movie does. + * - EOF counts as a loss for live playback — a broadcast never ends on its + * own. A movie or episode does, so on VOD only an EOF well before the + * known duration counts: mpv reports a connection that the server closed + * and ffmpeg could not reopen as EOF, and taking it for the end made + * series autoplay skip the rest of the episode (#1902). A reload that + * ends again where it resumed proves the file ends there (a duration + * estimated from the bitrate can overshoot), and is left as the end. * - The attempt budget belongs to one outage: it resets after * {@link EMBEDDED_MPV_RECONNECT_STABLE_PLAYBACK_MS} of uninterrupted playing, * not on the first `playing`, so a stream that flaps every few seconds runs @@ -48,6 +53,15 @@ export const EMBEDDED_MPV_RECONNECT_MAX_ATTEMPTS = 6; export const EMBEDDED_MPV_RECONNECT_BASE_DELAY_MS = 2_000; export const EMBEDDED_MPV_RECONNECT_MAX_DELAY_MS = 30_000; export const EMBEDDED_MPV_RECONNECT_STABLE_PLAYBACK_MS = 30_000; +/** + * A VOD EOF is the end of the file when less than this much remained + * (seconds, or the ratio of the duration when that is longer): the margin + * absorbs frame and container rounding and duration estimates. + */ +export const EMBEDDED_MPV_EARLY_EOF_MIN_REMAINING_SECONDS = 10; +export const EMBEDDED_MPV_EARLY_EOF_MIN_REMAINING_RATIO = 0.01; +/** Playback a resumed reload must make before another early EOF counts. */ +export const EMBEDDED_MPV_EARLY_EOF_PROGRESS_SECONDS = 5; export interface EmbeddedMpvReconnectState { /** `Settings.embeddedMpvAutoReconnect`, captured at session creation. */ @@ -74,6 +88,12 @@ export interface EmbeddedMpvReconnectState { * is trusted as a real seek-to-start only once that has happened. */ positionReported: boolean; + /** + * Where the last VOD reload resumed, until playback gets past it or the + * user seeks. An early EOF before then ended where the file does, not + * where it dropped. + */ + resumedAtSeconds: number | null; timer: NodeJS.Timeout | null; /** What the renderer is shown while an attempt is scheduled or in flight. */ pending: EmbeddedMpvReconnectInfo | null; @@ -100,6 +120,7 @@ export function createEmbeddedMpvReconnectState( attemptInFlight: false, lastPositionSeconds: null, positionReported: false, + resumedAtSeconds: null, timer: null, pending: null, }; @@ -131,6 +152,25 @@ export function isEmbeddedMpvStreamLoss( return status === 'error' || (status === 'ended' && isLive); } +/** Whether a VOD EOF at `positionSeconds` came well before the end. */ +export function isEmbeddedMpvEarlyEof( + positionSeconds: number, + durationSeconds: number | null | undefined +): boolean { + if ( + typeof durationSeconds !== 'number' || + !Number.isFinite(durationSeconds) || + durationSeconds <= 0 + ) { + return false; + } + const margin = Math.max( + EMBEDDED_MPV_EARLY_EOF_MIN_REMAINING_SECONDS, + durationSeconds * EMBEDDED_MPV_EARLY_EOF_MIN_REMAINING_RATIO + ); + return durationSeconds - positionSeconds > margin; +} + export class EmbeddedMpvReconnectCoordinator { private readonly now: () => number; private readonly log: Pick; @@ -158,9 +198,18 @@ export class EmbeddedMpvReconnectCoordinator { ? playback.startTime : null; state.positionReported = false; + state.resumedAtSeconds = null; state.pending = null; } + /** + * The user moved the playhead: an EOF after that is not the reload + * ending where it resumed, so it is judged on its position alone. + */ + onUserSeek(state: EmbeddedMpvReconnectState): void { + state.resumedAtSeconds = null; + } + cancel(state: EmbeddedMpvReconnectState): void { this.clearTimer(state); state.attemptInFlight = false; @@ -188,7 +237,8 @@ export class EmbeddedMpvReconnectCoordinator { status: EmbeddedMpvSessionStatus, previousStatus: EmbeddedMpvSessionStatus | null, positionSeconds?: number, - errorOrigin?: 'playback' | 'engine' + errorOrigin?: 'playback' | 'engine', + durationSeconds?: number | null ): EmbeddedMpvReconnectInfo | null { const now = this.now(); @@ -211,6 +261,14 @@ export class EmbeddedMpvReconnectCoordinator { // the seeded `startTime` stays the better resume point. state.lastPositionSeconds = 0; } + if ( + state.resumedAtSeconds !== null && + positionSeconds > + state.resumedAtSeconds + + EMBEDDED_MPV_EARLY_EOF_PROGRESS_SECONDS + ) { + state.resumedAtSeconds = null; + } } if (status === 'playing') { @@ -240,9 +298,13 @@ export class EmbeddedMpvReconnectCoordinator { const isLive = state.playback !== null && isLiveEmbeddedMpvPlayback(state.playback); - if (!isEmbeddedMpvStreamLoss(status, isLive)) { + if ( + !isEmbeddedMpvStreamLoss(status, isLive) && + !this.isDroppedVod(state, status, positionSeconds, durationSeconds) + ) { // paused/idle are user actions, closed is teardown, ended on - // VOD is the movie finishing — none of them is an outage. + // VOD near its end is the movie finishing — none of them is an + // outage. if (status === 'paused') { // Disarmed until the stream plays again: reloading a stream // the user paused would resume it without them asking. @@ -299,6 +361,9 @@ export class EmbeddedMpvReconnectCoordinator { state.attemptInFlight = true; try { this.hooks.reload(sessionId, playback); + // Only a reload that started can end where it resumed; a + // refused one leaves the original EOF, still a drop. + state.resumedAtSeconds = playback.startTime ?? null; } catch (error) { state.attemptInFlight = false; if (isEmbeddedMpvSessionGoneError(error)) { @@ -325,6 +390,39 @@ export class EmbeddedMpvReconnectCoordinator { return state.pending; } + /** + * A VOD `ended` well before the duration is a dropped connection, unless + * it is the reload that resumed there ending again: then the file itself + * ends there and the duration was only an estimate. + */ + private isDroppedVod( + state: EmbeddedMpvReconnectState, + status: EmbeddedMpvSessionStatus, + positionSeconds: number | undefined, + durationSeconds: number | null | undefined + ): boolean { + if (status !== 'ended' || state.playback === null) { + return false; + } + const position = + typeof positionSeconds === 'number' && + Number.isFinite(positionSeconds) && + positionSeconds > 0 + ? positionSeconds + : state.lastPositionSeconds; + if ( + position === null || + !isEmbeddedMpvEarlyEof(position, durationSeconds) + ) { + return false; + } + return ( + state.resumedAtSeconds === null || + position > + state.resumedAtSeconds + EMBEDDED_MPV_EARLY_EOF_PROGRESS_SECONDS + ); + } + /** * Live streams reload at the live edge. Anything else resumes where the * connection dropped: the stored playback still carries the offset the diff --git a/apps/xtream-mock-server/src/app/dropping-episode.ts b/apps/xtream-mock-server/src/app/dropping-episode.ts new file mode 100644 index 000000000..7ab708600 --- /dev/null +++ b/apps/xtream-mock-server/src/app/dropping-episode.ts @@ -0,0 +1,76 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import type { Request, Response } from 'express'; + +/** Share of the clip after which the provider loses the first connection. */ +export const DROPPING_EPISODE_CUT_RATIO = 0.35; +/** + * How long the stream is refused after the cut: long enough to defeat + * ffmpeg's own HTTP reconnect, short enough for an app-level retry. + */ +export const DROPPING_EPISODE_REFUSE_MS = 4_000; + +let fixture: Buffer | null = null; +/** Stream id → when its connection was cut. Every episode drops once. */ +const droppedAt = new Map(); + +export function resetDroppingEpisodes(): void { + droppedAt.clear(); +} + +/** + * `dropping-episode` fixture: every episode plays a local 60 s clip with + * byte ranges, but the first response that reaches a third of the file is + * cut there, and the stream answers 403 for a few seconds — a panel still + * holding the dropped connection's slot. mpv reports such a drop as EOF in + * the middle of the episode (issue #1902). + */ +export function streamDroppingEpisode( + request: Request, + response: Response +): void { + const body = (fixture ??= readFileSync( + join(process.cwd(), 'apps/xtream-mock-server/src/fixtures/episode.mkv') + )); + const streamId = String(request.params['streamId']); + const cutAt = droppedAt.get(streamId); + response.set('Cache-Control', 'no-store'); + if ( + cutAt !== undefined && + Date.now() - cutAt < DROPPING_EPISODE_REFUSE_MS + ) { + response.status(403).end(); + return; + } + + const range = /^bytes=(\d+)-(\d*)$/.exec(request.get('range') ?? ''); + const start = range ? Number(range[1]) : 0; + const end = range?.[2] + ? Math.min(Number(range[2]), body.length - 1) + : body.length - 1; + if (start > end) { + response + .status(416) + .set('Content-Range', `bytes */${body.length}`) + .end(); + return; + } + response + .status(range ? 206 : 200) + .type('video/x-matroska') + .set('Accept-Ranges', 'bytes') + .set('Content-Length', String(end - start + 1)); + if (range) { + response.set('Content-Range', `bytes ${start}-${end}/${body.length}`); + } + + const cut = Math.floor(body.length * DROPPING_EPISODE_CUT_RATIO); + if (cutAt === undefined && start < cut && end >= cut) { + response.write(body.subarray(start, cut), () => { + droppedAt.set(streamId, Date.now()); + response.socket?.destroy(); + }); + return; + } + response.end(body.subarray(start, end + 1)); +} diff --git a/apps/xtream-mock-server/src/app/scenarios.ts b/apps/xtream-mock-server/src/app/scenarios.ts index 7878733c0..c9fa0f731 100644 --- a/apps/xtream-mock-server/src/app/scenarios.ts +++ b/apps/xtream-mock-server/src/app/scenarios.ts @@ -43,8 +43,10 @@ export interface ScenarioConfig { * episodes slowly from generated bytes so download screenshots complete * without any request leaving the machine (the release capture's network * gate rejects the public HLS stub every other scenario redirects to). + * `dropping-episode` plays a local clip whose connection is lost + * mid-episode (see dropping-episode.ts). */ - downloadStreamFixture?: 'slow-series' | 'local-media'; + downloadStreamFixture?: 'slow-series' | 'local-media' | 'dropping-episode'; /** * Optional `player_api.php` actions the portal accepts and then never * answers: the connection stays open until the client gives up. Models a @@ -172,6 +174,19 @@ export const SCENARIOS: Record = { expiryDate: '2099-12-31', downloadStreamFixture: 'slow-series', }, + 'streamdrop:streamdrop': { + name: 'stream-drop', + description: + 'One series whose episodes lose their connection mid-playback — embedded MPV reconnect tests', + seed: 8181, + categoryCount: { live: 0, vod: 0, series: 1 }, + itemsPerCategory: 1, + seasonsPerSeries: 1, + episodesPerSeason: 3, + accountStatus: 'Active', + expiryDate: '2099-12-31', + downloadStreamFixture: 'dropping-episode', + }, 'epg:epg': { name: 'epg-fixture', description: diff --git a/apps/xtream-mock-server/src/app/server.spec.ts b/apps/xtream-mock-server/src/app/server.spec.ts index 9ce71d931..bdffbe350 100644 --- a/apps/xtream-mock-server/src/app/server.spec.ts +++ b/apps/xtream-mock-server/src/app/server.spec.ts @@ -1,6 +1,13 @@ +import { statSync } from 'node:fs'; import { connect } from 'node:net'; +import { join } from 'node:path'; import express from 'express'; import { resetAll } from './data-store.js'; +import { + DROPPING_EPISODE_CUT_RATIO, + DROPPING_EPISODE_REFUSE_MS, + resetDroppingEpisodes, +} from './dropping-episode.js'; import { LONG_PROGRAMME_TITLE } from './scenarios.js'; import { createXtreamMockApp, @@ -46,9 +53,37 @@ jest.mock('@faker-js/faker', () => { jest.setTimeout(60_000); +const DROPPING_EPISODE_URL = '/series/streamdrop/streamdrop/80000.mkv'; +const droppingFixtureSize = statSync( + join(process.cwd(), 'apps/xtream-mock-server/src/fixtures/episode.mkv') +).size; +const droppingCut = Math.floor( + droppingFixtureSize * DROPPING_EPISODE_CUT_RATIO +); + +/** Bytes received before the body ended, and whether it ended cleanly. */ +async function readDroppingBody( + response: Response +): Promise<{ bytes: number; complete: boolean }> { + const reader = response.body?.getReader(); + let bytes = 0; + try { + for (;;) { + const chunk = await reader?.read(); + if (!chunk || chunk.done) { + return { bytes, complete: true }; + } + bytes += chunk.value.length; + } + } catch { + return { bytes, complete: false }; + } +} + describe('Xtream mock server factory', () => { afterEach(() => { resetAll(); + resetDroppingEpisodes(); jest.restoreAllMocks(); }); @@ -192,7 +227,9 @@ describe('Xtream mock server factory', () => { expect(xml).toContain('Aurora News'); expect(xml).toContain('channel="aurora-news.fictional"'); - expect(xml).toContain(`${running.origin}/assets/marketing/logo/aurora-news.svg`); + expect(xml).toContain( + `${running.origin}/assets/marketing/logo/aurora-news.svg` + ); expect(xml).not.toMatch(/https?:\/\/(?!127\.0\.0\.1|localhost)/); } finally { await running.close(); @@ -259,9 +296,9 @@ describe('Xtream mock server factory', () => { } // Movies must finish quickly while episodes stay slow enough to // keep a progress row in frame. - expect( - Number(movie.headers.get('content-length')) - ).toBeGreaterThan(Number(episode.headers.get('content-length'))); + expect(Number(movie.headers.get('content-length'))).toBeGreaterThan( + Number(episode.headers.get('content-length')) + ); expect(ordinaryMovie.status).toBe(302); } finally { @@ -301,6 +338,71 @@ describe('Xtream mock server factory', () => { } }); + it('drops stream-drop episodes a third of the way in, refuses the retry, then serves ranges again', async () => { + const running = await startLoopbackServer( + createXtreamMockApp({ host: '127.0.0.1', port: 0 }) + ); + try { + const first = await fetch( + `${running.origin}${DROPPING_EPISODE_URL}`, + { + headers: { range: 'bytes=0-' }, + } + ); + expect(first.status).toBe(206); + expect(Number(first.headers.get('content-length'))).toBe( + droppingFixtureSize + ); + expect(await readDroppingBody(first)).toEqual({ + bytes: droppingCut, + complete: false, + }); + + // ffmpeg's own reconnect right after the drop. + const retry = await fetch( + `${running.origin}${DROPPING_EPISODE_URL}`, + { + headers: { range: `bytes=${droppingCut}-` }, + } + ); + expect(retry.status).toBe(403); + await retry.body?.cancel(); + + const later = Date.now() + DROPPING_EPISODE_REFUSE_MS + 1; + jest.spyOn(Date, 'now').mockReturnValue(later); + const resumed = await fetch( + `${running.origin}${DROPPING_EPISODE_URL}`, + { + headers: { range: `bytes=${droppingCut}-` }, + } + ); + expect(resumed.status).toBe(206); + expect(resumed.headers.get('content-range')).toBe( + `bytes ${droppingCut}-${droppingFixtureSize - 1}/${droppingFixtureSize}` + ); + expect(await readDroppingBody(resumed)).toEqual({ + bytes: droppingFixtureSize - droppingCut, + complete: true, + }); + + // A reset arms the drop again for the next test. + expect( + (await fetch(`${running.origin}/reset`, { method: 'POST' })) + .status + ).toBe(200); + const afterReset = await fetch( + `${running.origin}${DROPPING_EPISODE_URL}` + ); + expect(afterReset.status).toBe(200); + expect(await readDroppingBody(afterReset)).toEqual({ + bytes: droppingCut, + complete: false, + }); + } finally { + await running.close(); + } + }); + it('never answers the silent scenario detail actions while the rest of the portal works', async () => { const running = await startLoopbackServer( createXtreamMockApp({ host: '127.0.0.1', port: 0 }) diff --git a/apps/xtream-mock-server/src/app/server.ts b/apps/xtream-mock-server/src/app/server.ts index 4e24f7341..41f73eb86 100644 --- a/apps/xtream-mock-server/src/app/server.ts +++ b/apps/xtream-mock-server/src/app/server.ts @@ -9,6 +9,10 @@ import express, { import { installLiveFormatFixture } from './live-format-fixture.js'; import { resolveXtreamMockPortString } from './mock-port.js'; import { resetAll } from './data-store.js'; +import { + resetDroppingEpisodes, + streamDroppingEpisode, +} from './dropping-episode.js'; import { renderMarketingAssetSvg } from './generators/marketing.generator.js'; import { installPerformanceControlRoutes } from './performance-control-routes.js'; import { @@ -97,6 +101,7 @@ export function createXtreamMockApp( return; } resetAll(); + resetDroppingEpisodes(); response.json({ status: 'reset' }); }); app.get('/playlist.m3u', (_request, response) => { @@ -316,6 +321,10 @@ function installStreamRoutes( ); return; } + if (fixture === 'dropping-episode') { + streamDroppingEpisode(request, response); + return; + } response.redirect(HLS_STUB); }; app.get('/live/:username/:password/:streamId.m3u8', streamResponse); diff --git a/apps/xtream-mock-server/src/fixtures/README.md b/apps/xtream-mock-server/src/fixtures/README.md index 330cf7f64..bacea0682 100644 --- a/apps/xtream-mock-server/src/fixtures/README.md +++ b/apps/xtream-mock-server/src/fixtures/README.md @@ -10,6 +10,13 @@ Regenerate from the repository root with FFmpeg: ffmpeg -f lavfi -i testsrc2=size=160x90:rate=25 -f lavfi -i sine=frequency=440:sample_rate=48000 -t 6 -c:v libx264 -preset ultrafast -pix_fmt yuv420p -g 25 -c:a aac -b:a 48k -f mpegts apps/xtream-mock-server/src/fixtures/live.mpegts ``` -The mock returns the fixture only for synthetic live-format account media; +`episode.mkv` is sixty seconds of the same pattern, H.264 only, keyframes every +second, played by the `streamdrop` account's episodes: + +```sh +ffmpeg -f lavfi -i testsrc2=size=160x90:rate=10 -t 60 -c:v libx264 -preset veryslow -crf 40 -pix_fmt yuv420p -g 10 -fflags +bitexact -flags:v +bitexact -map_metadata -1 apps/xtream-mock-server/src/fixtures/episode.mkv +``` + +The mock returns `live.mpegts` only for synthetic live-format account media; manifest and denied-segment routes return controlled 200/403 responses. Web and Electron E2E tests check advancing playback, not HTTP status alone. diff --git a/apps/xtream-mock-server/src/fixtures/episode.mkv b/apps/xtream-mock-server/src/fixtures/episode.mkv new file mode 100644 index 0000000000000000000000000000000000000000..ea412ad814d0040f1b134c6336c3d356647cc00f GIT binary patch literal 205617 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 Playback > Extra embedded MPV options` is a free-form textarea, @@ -701,8 +708,9 @@ included. `EmbeddedMpvReconnectCoordinator` (`apps/electron-backend/src/app/services/embedded-mpv-reconnect.ts`) reloads the last user-requested playback when a session reports a stream loss: an -`error` status, or `ended` while the playback is live (`isLive`, else "no -`contentInfo`", the same rule the renderer uses). It is driven by +`error` status, `ended` while the playback is live (`isLive`, else "no +`contentInfo`", the same rule the renderer uses), or `ended` on VOD well +before the known duration. It is driven by `refreshSession()` status transitions, so it covers every engine, and it is captured per session from `Settings.embeddedMpvAutoReconnect` (default on; mirrored as `EMBEDDED_MPV_AUTO_RECONNECT`). The policy is deliberately narrow: @@ -716,6 +724,16 @@ mirrored as `EMBEDDED_MPV_AUTO_RECONNECT`). The policy is deliberately narrow: the IPC socket so a stream that ended reports `ended` rather than the `paused` that keep-open would otherwise look like, and an mpv process that exits abnormally reports `error`. +- A VOD `ended` counts only when more than `max(10 s, 1 % of the duration)` + remained (`isEmbeddedMpvEarlyEof`): the margin absorbs container rounding + and the whole-second snapshot values, and with no known duration an EOF is + always the end. A reload the engine accepted records where it resumed; an + early EOF before playback gets 5 s past that point (and with no user seek + in between) ended where the file does (a duration mpv estimated from the + bitrate can overshoot) and is left as the end, so autoplay still continues + after one attempt instead of a reload loop. A refused reload records + nothing, so the original EOF keeps its backoff. + With the setting off, an early EOF stays a plain `ended`. - Backoff is 2 s, 4 s, 8 s, 16 s, 30 s, 30 s, at most six attempts per outage. The budget resets only after 30 s of uninterrupted `playing`, so a stream that flaps every few seconds runs out of attempts instead of being diff --git a/docs/architecture/xtream-mock-server.md b/docs/architecture/xtream-mock-server.md index 28f9ffbe9..e11a404af 100644 --- a/docs/architecture/xtream-mock-server.md +++ b/docs/architecture/xtream-mock-server.md @@ -405,6 +405,7 @@ sometimes only respond to that misspelled action. | `tzoffset:tzoffset` | 6006 | live:2, vod:1, series:1 | 3 | active | | `emptyvod:emptyvod` | 7007 | 2 each | 5 | active | | `marketing:marketing` | 8020 | live:4, vod:4, series:4 | curated | active | +| `streamdrop:streamdrop` | 8181 | series:1 | 1 | active | | `expired:expired` | 4004 | 4 each | 10 | Expired | | `inactive:inactive` | 5005 | 4 each | 10 | Disabled | | `` | hash | 6 each | 30 | active | @@ -428,6 +429,17 @@ Timeshift URLs of `epg` and `epglong` play the local MPEG-TS clip the catch-up seek bar's label bounds; `epg:epg` keeps its short titles, which other tests assert verbatim. +### `streamdrop:streamdrop` fixture details + +Its three episodes play the local 60 s Matroska clip +(`src/fixtures/episode.mkv`, generation command in the fixtures README) with +byte ranges, but `dropping-episode.ts` cuts the first response that reaches +35 % of the file and answers 403 for the next 4 s, so ffmpeg's own HTTP +reconnect fails and mpv reports EOF mid-episode. `POST /reset` re-arms the +drop. The Electron E2E `embedded-mpv-series-stream-drop.e2e.ts` uses it to +check that Embedded MPV resumes the episode instead of autoplaying the next +one (issue #1902). + ### `marketing:marketing` fixture details This scenario is reserved for release screenshots and marketing materials: diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.spec.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.spec.ts index 6562853e0..17e3bb54a 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.spec.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.spec.ts @@ -166,6 +166,57 @@ describe('EmbeddedMpvPlayerComponent series navigation', () => { expect(player.isPlaying()).toBe(false); }); + it('does not end the episode while the main process reloads a dropped stream', () => { + // A connection lost mid-episode reaches mpv as EOF (#1902); the + // reconnect owns it, and autoplay would skip the rest of the episode. + const reconnect = { + attempt: 1, + maxAttempts: 6, + nextAttemptAt: '2026-06-06T12:00:52Z', + }; + controller.session.update((session) => + session + ? { + ...session, + status: 'ended', + positionSeconds: 50, + reconnect, + updatedAt: '2026-06-06T12:00:50Z', + } + : session + ); + fixture.detectChanges(); + controller.session.update((session) => + session + ? { + ...session, + status: 'loading', + updatedAt: '2026-06-06T12:00:52Z', + } + : session + ); + fixture.detectChanges(); + + expect(fixture.componentInstance.endedCount).toBe(0); + expect(player.statusLabel()).toContain('RECONNECTING'); + + // The reloaded episode later finishes for real. + controller.session.update((session) => + session + ? { + ...session, + status: 'ended', + positionSeconds: 120, + reconnect: undefined, + updatedAt: '2026-06-06T12:02:00Z', + } + : session + ); + fixture.detectChanges(); + + expect(fixture.componentInstance.endedCount).toBe(1); + }); + it('hides episode navigation for live playback', () => { fixture.destroy(); fixture = TestBed.createComponent(EmbeddedMpvPlayerHostComponent); diff --git a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts index d22c51931..175a00bae 100644 --- a/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts +++ b/libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-player.component.ts @@ -578,7 +578,10 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy { effect(() => { const session = this.session(); - if (!session || session.status !== 'ended') { + // An `ended` the main process is reloading is a dropped + // connection, not the end: autoplay would skip the rest of the + // episode (#1902). + if (!session || session.status !== 'ended' || session.reconnect) { return; } if (this.lastEndedSessionId === session.id) { From b546fe01232b16d8d9bf39e508ac11e0205e361a Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:06:39 +0200 Subject: [PATCH 2/7] fix(m3u): keep groups hidden in Manage groups after a restart (#1894) (#1907) * fix(m3u): keep groups hidden in Manage groups after a restart (#1894) Electron fills the playlist store from DB_GET_APP_PLAYLIST_METAS, which reads the metadata columns only. hiddenGroupTitles lives only in the JSON payload, so after a restart the store had none and the groups rail showed every group, although SQLite still held the saved selection; saving the dialog from that state then overwrote it. M3uWorkspaceRouteSession already reads the full playlist to publish its channels and favorites. It now copies hiddenGroupTitles from that read into the store (updatePlaylistMeta, persist: false) before the channels, unless the store value changed while the read was queued behind a save. Co-Authored-By: Claude Opus 5.5 * fix(m3u): wait for the playlist entity before hydrating hidden groups The startup inventory publishes playlists only after settings and EPG reconciliation, while a restored or deep-linked playlist route waits for the metadata read alone. The route session's full read could therefore finish first, and updateOne dropped the hydration for the missing entity for the whole session. The hydration now waits for the entity. Co-Authored-By: Claude Opus 5.5 * test(m3u): cover hidden groups when the Groups route opens before the inventory An Electron E2E reloads the renderer on the Groups route while the startup inventory is held (the EPG reconcile fails once and its retry is parked), and records from document start whether a hidden group is ever rendered visibly. It fails on the previous hydration that did not wait for the playlist entity. A service test pins that getPlaylist reads hidden groups from a meta update that is still being written. Co-Authored-By: Claude Opus 5.5 * test(m3u): type empty hidden-group arrays in the route session spec The CI spec typecheck (apps/web/tsconfig.spec.json) rejected `[]` inferred as never[] inside the PlaylistMeta and Playlist casts with TS2352. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: 4gray Co-authored-by: Claude Opus 5.5 --- .changes/m3u-hidden-groups-restart.md | 8 + .../src/m3u-group-management.e2e.ts | 301 ++++++++++++++++++ docs/architecture/m3u-playlist-module.md | 13 + ...orkspace-route-session.performance.spec.ts | 7 +- ...3u-workspace-route-session.service.spec.ts | 133 +++++++- .../m3u-workspace-route-session.service.ts | 66 +++- .../src/lib/playlists.service.spec.ts | 29 ++ 7 files changed, 551 insertions(+), 6 deletions(-) create mode 100644 .changes/m3u-hidden-groups-restart.md create mode 100644 apps/electron-backend-e2e/src/m3u-group-management.e2e.ts diff --git a/.changes/m3u-hidden-groups-restart.md b/.changes/m3u-hidden-groups-restart.md new file mode 100644 index 000000000..4c08c74fd --- /dev/null +++ b/.changes/m3u-hidden-groups-restart.md @@ -0,0 +1,8 @@ +--- +type: fix +area: m3u +issues: [1894] +--- + +Groups hidden in "Manage groups" stay hidden after the desktop app restarts, +instead of every group of the playlist showing again. diff --git a/apps/electron-backend-e2e/src/m3u-group-management.e2e.ts b/apps/electron-backend-e2e/src/m3u-group-management.e2e.ts new file mode 100644 index 000000000..1dfcf7f82 --- /dev/null +++ b/apps/electron-backend-e2e/src/m3u-group-management.e2e.ts @@ -0,0 +1,301 @@ +import type { Locator, Page } from '@playwright/test'; +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import type { ElectronBridgeApi } from '@iptvnator/shared/interfaces'; +import { + closeElectronApp, + electronMainPath, + expect, + type LaunchedElectronApp, + importM3uPlaylistFromNativeDialog, + launchElectronApp, + openSources, + openWorkspaceSection, + restartElectronApp, + sourceRowByTitle, + test, + waitForM3uCatalog, + writeTemporaryM3uFile, +} from './electron-test-fixtures'; +import { + expectRendererReloadedOnRoute, + reloadFromMainProcess, +} from './renderer-reload.support'; + +const SOURCE_TITLE = 'group-management-source'; +const HIDDEN_GROUPS = ['Movies', 'Sports']; +const VISIBLE_HIDDEN_GROUPS_KEY = '__e2eVisibleHiddenGroups'; +const RECONCILE_CHANNEL = 'EPG_RECONCILE_SOURCES'; + +interface ReconcileScript { + calls: number; + gate: Promise; + release: () => void; +} + +type ReconcileGlobal = typeof globalThis & { + __e2eReconcileScript?: ReconcileScript; +}; + +// Serialized into the test bootstrap; all runtime dependencies are arguments. +function installReconcileScript( + { ipcMain }: Pick, + channel: string +): void { + const register = ipcMain.handle; + const scope = globalThis as ReconcileGlobal; + ipcMain.handle = function (name, listener) { + if (name !== channel) { + return register.call(this, name, listener); + } + return register.call(this, name, async (...args) => { + const script = scope.__e2eReconcileScript; + if (script) { + script.calls += 1; + if (script.calls === 1) return { success: false }; + await script.gate; + } + return listener(...args); + }); + }; +} + +function launchWithReconcileScript( + dataDir: string +): Promise { + const entryPoint = join(dataDir, 'reconcile-script.cjs'); + writeFileSync( + entryPoint, + [ + `(${installReconcileScript.toString()})(require('electron'), ${JSON.stringify(RECONCILE_CHANNEL)});`, + `require(${JSON.stringify(electronMainPath)});`, + ].join('\n') + ); + return launchElectronApp(dataDir, { entryPoint }); +} + +/** + * The next renderer's startup reconciliation fails, and the retry the + * playlist inventory awaits is parked until `release`: the M3U route then + * activates (its settings resolver has finished) while the store still has no + * playlists. + */ +async function delayPlaylistInventory(app: LaunchedElectronApp) { + await app.electronApp.evaluate(() => { + let release!: () => void; + const gate = new Promise((resolve) => { + release = resolve; + }); + (globalThis as ReconcileGlobal).__e2eReconcileScript = { + calls: 0, + gate, + release, + }; + }); + return { + waitUntilParked: () => + expect + .poll(() => + app.electronApp.evaluate( + () => + (globalThis as ReconcileGlobal).__e2eReconcileScript + ?.calls ?? 0 + ) + ) + .toBeGreaterThan(1), + release: () => + app.electronApp.evaluate(() => { + const scope = globalThis as ReconcileGlobal; + const script = scope.__e2eReconcileScript; + delete scope.__e2eReconcileScript; + script?.release(); + }), + }; +} +const CHANNELS = ['News', 'Sports', 'Movies'].flatMap((group) => + [1, 2].map((index) => ({ + groupTitle: group, + name: `${group} Channel ${index}`, + url: `https://streams.example.test/${group.toLowerCase()}-${index}.m3u8`, + })) +); + +function groupNavLabels(page: Page): Locator { + return page.locator('app-groups-view .group-nav-item .nav-item-label'); +} + +async function openGroupManagement(page: Page): Promise { + await page + .getByRole('button', { name: 'Manage groups', exact: true }) + .click(); + const dialog = page.locator('mat-dialog-container').last(); + await expect(dialog.locator('.group-item')).toHaveCount(3); + return dialog; +} + +function groupCheckbox(dialog: Locator, group: string): Locator { + return dialog + .locator('.group-item') + .filter({ hasText: group }) + .getByRole('checkbox'); +} + +async function storedHiddenGroupTitles(page: Page): Promise { + return page.evaluate(async () => { + const playlistId = location.pathname.match( + /\/workspace\/playlists\/([^/]+)/ + )?.[1]; + if (!playlistId) throw new Error('M3U playlist route is missing'); + const api = (window as unknown as { electron: ElectronBridgeApi }) + .electron; + const playlist = (await api.dbGetAppPlaylist(playlistId)) as { + hiddenGroupTitles?: unknown; + } | null; + const hidden = playlist?.hiddenGroupTitles; + return Array.isArray(hidden) ? [...hidden].sort() : hidden; + }); +} + +/** Imports the playlist and hides {@link HIDDEN_GROUPS} in Manage groups. */ +async function importAndHideGroups( + app: LaunchedElectronApp, + dataDir: string +): Promise { + const filePath = writeTemporaryM3uFile( + dataDir, + `${SOURCE_TITLE}.m3u`, + CHANNELS + ); + await importM3uPlaylistFromNativeDialog(app, filePath); + await waitForM3uCatalog(app.mainWindow); + await openWorkspaceSection(app.mainWindow, 'Groups'); + await expect(groupNavLabels(app.mainWindow)).toHaveText([ + 'Movies', + 'News', + 'Sports', + ]); + + const dialog = await openGroupManagement(app.mainWindow); + for (const group of HIDDEN_GROUPS) { + await groupCheckbox(dialog, group).uncheck(); + } + await dialog.getByRole('button', { name: 'Save', exact: true }).click(); + await expect(dialog).toBeHidden(); + await expect(groupNavLabels(app.mainWindow)).toHaveText(['News']); + await expect + .poll(() => storedHiddenGroupTitles(app.mainWindow)) + .toEqual(HIDDEN_GROUPS); +} + +/** + * Records, from the start of every new document, each hidden group the + * groups rail ever renders visibly — including before the startup inventory + * publishes the playlist. + */ +async function recordVisibleHiddenGroups( + app: LaunchedElectronApp +): Promise { + await app.electronApp.context().addInitScript( + ({ hiddenGroups, key }) => { + const seen: string[] = []; + Reflect.set(window, key, seen); + const check = () => { + for (const item of document.querySelectorAll( + 'app-groups-view .group-nav-item' + )) { + const group = item.getAttribute('data-group-key') ?? ''; + if ( + hiddenGroups.includes(group) && + !seen.includes(group) && + item.checkVisibility() + ) { + seen.push(group); + } + } + }; + new MutationObserver(check).observe(document, { + attributes: true, + attributeFilter: ['class', 'style'], + childList: true, + subtree: true, + }); + }, + { hiddenGroups: HIDDEN_GROUPS, key: VISIBLE_HIDDEN_GROUPS_KEY } + ); +} + +test.describe('Electron M3U group management', () => { + test('keeps groups hidden in Manage groups after a restart (#1894)', async ({ + dataDir, + }) => { + const app = await launchElectronApp(dataDir); + + try { + await importAndHideGroups(app, dataDir); + + const restarted = await restartElectronApp(app, dataDir); + app.electronApp = restarted.electronApp; + app.mainWindow = restarted.mainWindow; + + await openSources(app.mainWindow); + await sourceRowByTitle(app.mainWindow, SOURCE_TITLE) + .first() + .click(); + await waitForM3uCatalog(app.mainWindow); + await openWorkspaceSection(app.mainWindow, 'Groups'); + await expect(groupNavLabels(app.mainWindow)).toHaveText(['News']); + + // The dialog opens from the restored state, so saving it again + // cannot silently unhide the groups. + const reopened = await openGroupManagement(app.mainWindow); + await expect(groupCheckbox(reopened, 'News')).toBeChecked(); + await expect(groupCheckbox(reopened, 'Movies')).not.toBeChecked(); + await expect(groupCheckbox(reopened, 'Sports')).not.toBeChecked(); + await reopened + .getByRole('button', { name: 'Close', exact: true }) + .click(); + await expect(reopened).toBeHidden(); + } finally { + await closeElectronApp(app); + } + }); + + test('never shows hidden groups when the Groups route opens before the playlist inventory', async ({ + dataDir, + }) => { + const app = await launchWithReconcileScript(dataDir); + + try { + await importAndHideGroups(app, dataDir); + await recordVisibleHiddenGroups(app); + const inventory = await delayPlaylistInventory(app); + + // A reload boots a fresh store straight on this route. + await reloadFromMainProcess(app); + await inventory.waitUntilParked(); + // The route session has read the playlist and published its + // channels, still behind the startup screen, before the store + // has the playlist the hidden groups belong to. + const rail = app.mainWindow.locator( + 'app-groups-view .group-nav-item' + ); + await expect(rail).toHaveCount(3); + await expect(rail.first()).toBeHidden(); + + await inventory.release(); + await expectRendererReloadedOnRoute( + app.mainWindow, + /\/workspace\/playlists\/[^/]+\/groups$/ + ); + await expect(groupNavLabels(app.mainWindow)).toHaveText(['News']); + expect( + await app.mainWindow.evaluate( + (key) => Reflect.get(window, key), + VISIBLE_HIDDEN_GROUPS_KEY + ) + ).toEqual([]); + } finally { + await closeElectronApp(app); + } + }); +}); diff --git a/docs/architecture/m3u-playlist-module.md b/docs/architecture/m3u-playlist-module.md index dc9991bb4..fa0e4d6de 100644 --- a/docs/architecture/m3u-playlist-module.md +++ b/docs/architecture/m3u-playlist-module.md @@ -633,6 +633,19 @@ channel-list-container/ - `PlaylistsService.updatePlaylistMeta()` persists `hiddenGroupTitles` into the stored playlist payload, and M3U refresh/update flows preserve the existing value when refreshed playlist data omits the field. +- Electron loads the playlist store from `DB_GET_APP_PLAYLIST_METAS`, which + reads columns only, so `hiddenGroupTitles` is absent from the store after a + restart. `M3uWorkspaceRouteSession` copies it from the full playlist read into + the store (`updatePlaylistMeta` with `persist: false`) before publishing the + channels, unless the store value changed while that read was queued (#1894). + A restored or deep-linked route can finish that read before the startup + inventory, which waits for settings and EPG reconciliation, publishes the + playlist; the hydration then waits for the entity, since `updateOne` drops an + update for a missing one. The startup screen keeps the router outlet hidden + until that inventory is published, and the hydration runs in the same store + update, so the rail never renders a hidden group. A save's meta write joins + the per-playlist queue that `getPlaylist()` reads through, so a reopened + route reads the saved value. - The groups route keeps the manage action reachable even when every group is hidden by separating "playlist has no groups" from "no visible/search-matching groups" empty states. diff --git a/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.performance.spec.ts b/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.performance.spec.ts index ae41228fa..3fe831e86 100644 --- a/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.performance.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.performance.spec.ts @@ -1,3 +1,4 @@ +import { signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; @@ -30,7 +31,11 @@ async function flushEffects(): Promise { describe('M3uWorkspaceRouteSession performance phases', () => { const hookSymbol = Symbol.for(RENDERER_PERFORMANCE_PHASE_HOOK_KEY); - const store = { dispatch: jest.fn() }; + const store = { + dispatch: jest.fn(), + select: jest.fn(() => of({})), + selectSignal: jest.fn(() => signal({})), + }; beforeEach(() => { Object.defineProperty(window, 'electron', { diff --git a/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.service.spec.ts b/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.service.spec.ts index 4581781d4..1ae68eff7 100644 --- a/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.service.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.service.spec.ts @@ -1,10 +1,14 @@ import { TestBed } from '@angular/core/testing'; import { NavigationEnd, Router } from '@angular/router'; -import { of, Subject, throwError } from 'rxjs'; -import { ChannelActions, FavoritesActions } from '@iptvnator/m3u-state'; +import { BehaviorSubject, of, Subject, throwError } from 'rxjs'; +import { + ChannelActions, + FavoritesActions, + PlaylistActions, +} from '@iptvnator/m3u-state'; import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util'; import { PlaylistsService } from '@iptvnator/services'; -import { Channel, Playlist } from '@iptvnator/shared/interfaces'; +import { Channel, Playlist, PlaylistMeta } from '@iptvnator/shared/interfaces'; import { Store } from '@ngrx/store'; import { M3uWorkspaceRouteSession } from './m3u-workspace-route-session.service'; @@ -76,8 +80,13 @@ describe('M3uWorkspaceRouteSession', () => { const playlistsService = { getPlaylist: jest.fn(), }; + const playlistEntities = new BehaviorSubject< + Record + >({}); const store = { dispatch: jest.fn(), + select: jest.fn(() => playlistEntities.asObservable()), + selectSignal: jest.fn(() => () => playlistEntities.value), }; const electronApi = { setUserAgent: jest.fn().mockResolvedValue(true), @@ -99,6 +108,9 @@ describe('M3uWorkspaceRouteSession', () => { electronApi.setUserAgent.mockClear(); playlistsService.getPlaylist.mockReset(); store.dispatch.mockClear(); + playlistEntities.next({ + [PLAYLIST_ID]: { _id: PLAYLIST_ID } as PlaylistMeta, + }); await TestBed.configureTestingModule({ providers: [ @@ -205,6 +217,121 @@ describe('M3uWorkspaceRouteSession', () => { ).toBe(false); }); + it('hydrates the hidden groups the startup metadata list leaves out, before the channels (#1894)', async () => { + playlistsService.getPlaylist.mockReturnValue( + of({ + hiddenGroupTitles: ['Sports'], + playlist: { + items: [PRIMARY_CHANNEL], + }, + } as Playlist) + ); + + TestBed.inject(M3uWorkspaceRouteSession); + await flushEffects(); + + const types = store.dispatch.mock.calls.map(([action]) => action.type); + expect(store.dispatch).toHaveBeenCalledWith( + PlaylistActions.updatePlaylistMeta({ + playlist: { + _id: PLAYLIST_ID, + hiddenGroupTitles: ['Sports'], + } as PlaylistMeta, + persist: false, + }) + ); + expect( + types.indexOf(PlaylistActions.updatePlaylistMeta.type) + ).toBeLessThan(types.indexOf(ChannelActions.setChannels.type)); + }); + + it('hydrates no hidden groups for a playlist that never saved any', async () => { + playlistsService.getPlaylist.mockReturnValue( + of({ playlist: { items: [PRIMARY_CHANNEL] } } as Playlist) + ); + + TestBed.inject(M3uWorkspaceRouteSession); + await flushEffects(); + + expect(store.dispatch).toHaveBeenCalledWith( + PlaylistActions.updatePlaylistMeta({ + playlist: { + _id: PLAYLIST_ID, + hiddenGroupTitles: [] as string[], + } as PlaylistMeta, + persist: false, + }) + ); + }); + + it('keeps hidden groups saved while the playlist read was queued', async () => { + const response = new Subject(); + playlistsService.getPlaylist.mockReturnValue(response.asObservable()); + + TestBed.inject(M3uWorkspaceRouteSession); + await flushEffects(); + + // The dialog's save lands in the store before the queued read returns + // the row as it was before that save. + playlistEntities.next({ + [PLAYLIST_ID]: { + _id: PLAYLIST_ID, + hiddenGroupTitles: ['News'], + } as PlaylistMeta, + }); + response.next({ + hiddenGroupTitles: [] as string[], + playlist: { items: [PRIMARY_CHANNEL] }, + } as Playlist); + response.complete(); + await flushEffects(); + + expect(store.dispatch).toHaveBeenCalledWith( + ChannelActions.setChannels({ channels: [PRIMARY_CHANNEL] }) + ); + expect( + store.dispatch.mock.calls.some( + ([action]) => + action.type === PlaylistActions.updatePlaylistMeta.type + ) + ).toBe(false); + }); + + it('waits for the startup inventory when a restored route reads the playlist first', async () => { + playlistEntities.next({}); + playlistsService.getPlaylist.mockReturnValue( + of({ + hiddenGroupTitles: ['Sports'], + playlist: { items: [PRIMARY_CHANNEL] }, + } as Playlist) + ); + const hydration = PlaylistActions.updatePlaylistMeta({ + playlist: { + _id: PLAYLIST_ID, + hiddenGroupTitles: ['Sports'], + } as PlaylistMeta, + persist: false, + }); + + TestBed.inject(M3uWorkspaceRouteSession); + await flushEffects(); + + // An update for an entity that is not in the store yet is dropped. + expect(store.dispatch).not.toHaveBeenCalledWith(hydration); + + playlistEntities.next({ + [PLAYLIST_ID]: { _id: PLAYLIST_ID } as PlaylistMeta, + }); + + expect(store.dispatch).toHaveBeenCalledWith(hydration); + expect( + store.dispatch.mock.calls.filter( + ([action]) => + action.type === PlaylistActions.updatePlaylistMeta.type + ) + ).toHaveLength(1); + }); + it('clears in-memory favorites without dispatching persistence when playlist loading fails', async () => { playlistsService.getPlaylist.mockReturnValue( throwError(() => new Error('Playlist loading failed')) diff --git a/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.service.ts b/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.service.ts index c0bba152b..334050945 100644 --- a/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.service.ts +++ b/libs/playlist/m3u/feature-player/src/lib/m3u-workspace-route-session.service.ts @@ -8,14 +8,20 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { NavigationEnd, Router } from '@angular/router'; import { Store } from '@ngrx/store'; -import { ChannelActions, FavoritesActions } from '@iptvnator/m3u-state'; +import { + ChannelActions, + FavoritesActions, + PlaylistActions, + selectPlaylistEntities, +} from '@iptvnator/m3u-state'; import { measureRendererPerformancePhase, RENDERER_PERFORMANCE_PHASE, } from '@iptvnator/shared/logging'; -import { filter, firstValueFrom } from 'rxjs'; +import { filter, firstValueFrom, map, Subscription, take } from 'rxjs'; import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util'; import { PlaylistsService } from '@iptvnator/services'; +import { PlaylistMeta } from '@iptvnator/shared/interfaces'; type M3uLoadedSection = 'all' | 'groups'; @@ -26,10 +32,14 @@ export class M3uWorkspaceRouteSession { private readonly playlistsService = inject(PlaylistsService); private readonly router = inject(Router); private readonly store = inject(Store); + private readonly playlistEntities = this.store.selectSignal( + selectPlaylistEntities + ); private currentPlaylistId: string | null = null; private currentSection: string | null = null; private loadRequestId = 0; + private pendingHiddenGroupsHydration: Subscription | null = null; constructor() { this.router.events @@ -93,6 +103,9 @@ export class M3uWorkspaceRouteSession { ChannelActions.setChannelsLoading({ loading: true }) ); + const hiddenGroupTitlesBeforeRead = + this.playlistEntities()[playlistId]?.hiddenGroupTitles; + try { const playlist = await firstValueFrom( this.playlistsService.getPlaylist(playlistId) @@ -102,6 +115,12 @@ export class M3uWorkspaceRouteSession { return; } + this.hydrateHiddenGroupTitles( + playlistId, + playlist.hiddenGroupTitles, + hiddenGroupTitlesBeforeRead + ); + void window.electron ?.setUserAgent(playlist.userAgent, playlist.referrer) .catch((error: unknown) => { @@ -143,6 +162,49 @@ export class M3uWorkspaceRouteSession { } } + /** + * Electron's startup playlist list reads the metadata columns only, which + * leave out the hidden groups stored in the payload (#1894). Published + * before the channels so the groups rail never shows them; a save made + * while the read was queued is newer and stays. + * + * A restored or deep-linked route can finish this read before the startup + * inventory publishes the playlist (that waits for settings and EPG + * reconciliation), and `updateOne` drops an update for a missing entity, + * so the hydration waits for it. + */ + private hydrateHiddenGroupTitles( + playlistId: string, + storedHiddenGroupTitles: string[] | undefined, + hiddenGroupTitlesBeforeRead: string[] | undefined + ): void { + this.pendingHiddenGroupsHydration?.unsubscribe(); + this.pendingHiddenGroupsHydration = this.store + .select(selectPlaylistEntities) + .pipe( + map((entities) => entities[playlistId]), + filter((playlist) => playlist !== undefined), + take(1), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe((playlist) => { + if ( + playlist.hiddenGroupTitles !== hiddenGroupTitlesBeforeRead + ) { + return; + } + this.store.dispatch( + PlaylistActions.updatePlaylistMeta({ + playlist: { + _id: playlistId, + hiddenGroupTitles: storedHiddenGroupTitles ?? [], + } as PlaylistMeta, + persist: false, + }) + ); + }); + } + private isLoadedSection( section: string | null ): section is M3uLoadedSection { diff --git a/libs/services/src/lib/playlists.service.spec.ts b/libs/services/src/lib/playlists.service.spec.ts index 7c44c6acf..f59e131a4 100644 --- a/libs/services/src/lib/playlists.service.spec.ts +++ b/libs/services/src/lib/playlists.service.spec.ts @@ -2005,6 +2005,35 @@ describe('PlaylistsService', () => { } ); + it('reads hidden groups saved by a meta update that is still being written', async () => { + const original = createBasePlaylist('catalog-hidden-groups'); + const { store, electron } = createStatefulElectronStore(original); + testWindow.electron = electron; + let release!: () => void; + const gate = new Promise((resolve) => { + release = resolve; + }); + electron.dbUpsertAppPlaylist.mockImplementationOnce( + async (playlist) => { + await gate; + store.current = playlist; + } + ); + const service = createService(); + // The meta effect subscribes while the save is dispatched, so the + // write is queued before the M3U route reopens the playlist. + const write = firstValueFrom( + service.updatePlaylistMeta({ + _id: original._id, + hiddenGroupTitles: ['Sports'], + } as PlaylistMeta) + ); + const read = firstValueFrom(service.getPlaylist(original._id)); + release(); + await write; + expect((await read).hiddenGroupTitles).toEqual(['Sports']); + }); + it('orders a browser catalog read after a queued IndexedDB refresh', async () => { const original = createBasePlaylist('browser-catalog'); const state = { current: original }; From 434cbf3681f460ab60c2cae5b512ba7d705cf32b Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:08:35 +0200 Subject: [PATCH 3/7] fix(playlist): keep cleared optional fields cleared on desktop (#1904) Clearing an optional playlist field (User-Agent, Referrer, Origin, credentials, EPG URL lists, file path) did not persist in Electron: the codec maps an empty value to `undefined`, `ON CONFLICT DO UPDATE` skips undefined keys, and `parseAppPlaylist` prefers the stale column over the updated payload, so the old value came back on the next read. `playlistConflictUpdate` now sets every optional column the document leaves empty to NULL, so both desktop upserts (`DB_UPSERT_APP_PLAYLIST` and `DB_UPSERT_APP_PLAYLISTS`) replace the stored row. The cleared-columns list moves from the web backend's playlist repository into the shared conflict module, and the server now uses the shared version. `date_created`, `name`, `type`, `autoRefresh`, `payload` and `last_usage` are never cleared. Co-Authored-By: Claude Opus 5.5 --- .changes/playlist-clear-optional-fields.md | 6 ++ .../operations/playlist.operations.spec.ts | 87 ++++++++++++++++++- .../app/storage/server-playlist-repository.ts | 50 +---------- docs/architecture/sqlite-db-worker.md | 11 +++ .../src/lib/app-playlist-conflict.spec.ts | 80 +++++++++++++++++ .../database/src/lib/app-playlist-conflict.ts | 70 ++++++++++++--- 6 files changed, 244 insertions(+), 60 deletions(-) create mode 100644 .changes/playlist-clear-optional-fields.md create mode 100644 libs/shared/database/src/lib/app-playlist-conflict.spec.ts diff --git a/.changes/playlist-clear-optional-fields.md b/.changes/playlist-clear-optional-fields.md new file mode 100644 index 000000000..237e86795 --- /dev/null +++ b/.changes/playlist-clear-optional-fields.md @@ -0,0 +1,6 @@ +--- +type: fix +area: playlist +--- + +In the desktop app, emptying an optional playlist field such as User-Agent, Referrer, Origin, the login details, the EPG sources or the file path now stays empty after saving instead of bringing the old value back. diff --git a/apps/electron-backend/src/app/database/operations/playlist.operations.spec.ts b/apps/electron-backend/src/app/database/operations/playlist.operations.spec.ts index 3370d633e..c827d4f8f 100644 --- a/apps/electron-backend/src/app/database/operations/playlist.operations.spec.ts +++ b/apps/electron-backend/src/app/database/operations/playlist.operations.spec.ts @@ -46,7 +46,7 @@ function runPlaylistUpdateScenario(scenario: string): unknown { const { default: Database } = await import('better-sqlite3'); const { drizzle } = await import('drizzle-orm/better-sqlite3'); const schema = await import('@iptvnator/shared/database/schema'); - const { updatePlaylist, upsertAppPlaylist, upsertAppPlaylists } = await import(${JSON.stringify(operationsUrl)}); + const { getAppPlaylist, getAppPlaylistMetas, updatePlaylist, upsertAppPlaylist, upsertAppPlaylists } = await import(${JSON.stringify(operationsUrl)}); const { setPlaylistServerTimezone } = await import(${JSON.stringify(timezoneOperationsUrl)}); const { __databaseConnectionTestHooks } = await import(${JSON.stringify(connectionUrl)}); const sqlite = new Database(':memory:'); @@ -317,6 +317,91 @@ describe('playlist.operations', () => { expect(result['moved'].payload).not.toHaveProperty('serverTimezone'); }); + it('clears the optional columns a full upsert leaves empty, so a cleared field stays cleared', () => { + // The codec maps an empty value to `undefined`, which ON CONFLICT DO + // UPDATE skips; reads prefer the column, so the old value would come + // back. Covers the single and batch writers and both conflict-update + // shapes (a clockless document and one that carries its own clock). + const ids = ['single', 'batch', 'with-clock']; + const result = runPlaylistUpdateScenario(` + const source = (id, extra = {}) => ({ + _id: id, + title: id, + type: 'm3u-url', + url: 'http://example.com/list.m3u', + userAgent: 'Agent/1', + referrer: 'http://ref.example', + origin: 'http://origin.example', + username: 'demo', + password: 'secret', + filePath: '/playlists/list.m3u', + manualEpgUrls: ['http://epg.example/manual.xml'], + favorites: ['1'], + ...(id === 'with-clock' ? { serverTimezone: 'UTC' } : {}), + ...extra, + }); + const cleared = { + userAgent: '', + referrer: ' ', + origin: '', + username: '', + password: '', + filePath: undefined, + manualEpgUrls: undefined, + }; + for (const id of ${JSON.stringify(ids)}) { + await upsertAppPlaylist(db, source(id)); + } + await upsertAppPlaylist(db, source('single', cleared)); + await upsertAppPlaylists(db, [source('batch', cleared)]); + await upsertAppPlaylist(db, source('with-clock', cleared)); + const docs = {}; + for (const id of ${JSON.stringify(ids)}) { + docs[id] = await getAppPlaylist(db, id); + } + const metas = Object.fromEntries( + (await getAppPlaylistMetas(db)).map((meta) => [meta._id, meta]) + ); + const rows = Object.fromEntries( + sqlite.prepare('SELECT id, date_created FROM playlists').all() + .map((row) => [row.id, row.date_created]) + ); + process.stdout.write(JSON.stringify({ docs, metas, rows })); + `) as { + docs: Record>; + metas: Record>; + rows: Record; + }; + + for (const id of ids) { + // Metadata reads see only the columns; the full read merges the + // payload in. Both must lose every cleared field. + for (const playlist of [result.docs[id], result.metas[id]]) { + expect(playlist).toEqual( + expect.objectContaining({ + _id: id, + title: id, + url: 'http://example.com/list.m3u', + favorites: ['1'], + manualEpgUrls: [], + }) + ); + for (const field of [ + 'userAgent', + 'referrer', + 'origin', + 'username', + 'password', + 'filePath', + ]) { + expect(playlist).not.toHaveProperty(field); + } + } + expect(result.rows[id]).toEqual(expect.any(String)); + } + expect(result.docs['with-clock']['serverTimezone']).toBe('UTC'); + }); + it('loads app playlist metadata without selecting the large payload column', async () => { const from = jest.fn().mockResolvedValue([ { diff --git a/apps/web-backend/src/app/storage/server-playlist-repository.ts b/apps/web-backend/src/app/storage/server-playlist-repository.ts index 0f7f18d9d..6d27ba3c8 100644 --- a/apps/web-backend/src/app/storage/server-playlist-repository.ts +++ b/apps/web-backend/src/app/storage/server-playlist-repository.ts @@ -6,10 +6,7 @@ import { playlistConflictUpdate, } from '@iptvnator/shared/database/app-playlist'; import { appState, playlists } from '@iptvnator/shared/database/schema'; -import type { - NewPlaylist, - Playlist as PlaylistRow, -} from '@iptvnator/shared/database/schema'; +import type { Playlist as PlaylistRow } from '@iptvnator/shared/database/schema'; import { SERVER_PAYLOAD_FIELDS } from '@iptvnator/shared/interfaces'; import { metadataDocument, @@ -42,46 +39,6 @@ function metadataColumns() { const metaColumns = metadataColumns(); -/** - * Columns a whole-document upsert must rewrite even when the document no - * longer has a value: the codec maps empty values to `undefined`, which an - * `ON CONFLICT DO UPDATE` skips, so a cleared field would keep its old - * column value and `parseAppPlaylist` would bring it back. - */ -const REPLACEABLE_COLUMNS = [ - 'serverUrl', - 'username', - 'password', - 'lastUpdated', - 'userAgent', - 'origin', - 'referrer', - 'filePath', - 'epgUrls', - 'detectedEpgUrls', - 'manualEpgUrls', - 'disabledEpgUrls', - 'macAddress', - 'url', - 'portalUrl', - 'count', - 'importDate', - 'updateDate', - 'position', - 'favorites', - 'recentlyViewed', -] as const satisfies readonly (keyof NewPlaylist)[]; - -function clearedColumns(row: NewPlaylist): Partial { - const cleared: Partial> = {}; - for (const column of REPLACEABLE_COLUMNS) { - if (row[column] === undefined) { - cleared[column] = null; - } - } - return cleared as Partial; -} - /** Playlist documents in the shared `playlists` table, same rows as desktop. */ export class ServerPlaylistRepository { constructor(private readonly db: DatabaseInstance) {} @@ -146,10 +103,7 @@ export class ServerPlaylistRepository { throw new InvalidPlaylistDocumentError(); } - const update = { - ...playlistConflictUpdate(row, next), - ...clearedColumns(row), - }; + const update = playlistConflictUpdate(row, next); this.db.transaction((tx) => { tx.insert(playlists) .values(row) diff --git a/docs/architecture/sqlite-db-worker.md b/docs/architecture/sqlite-db-worker.md index 582a0b429..e0d213da2 100644 --- a/docs/architecture/sqlite-db-worker.md +++ b/docs/architecture/sqlite-db-worker.md @@ -618,6 +618,17 @@ Xtream-only row shape: 12. `DB_GET_APP_STATE` 13. `DB_SET_APP_STATE` +`DB_UPSERT_APP_PLAYLIST(S)` write whole documents: the row built by the shared +codec (`buildPlaylistRow` in `libs/shared/database`) replaces the stored one. +`playlistConflictUpdate` sets every optional column the document leaves empty +(an empty string or a missing key) to NULL instead of skipping it, because +`parseAppPlaylist` prefers a column over the payload and would otherwise bring +a cleared User-Agent, Referrer, Origin, credential, EPG list or file path back. +Only `date_created` is kept from the first insert. Callers must send the full +document (`PlaylistsService` reads, patches and writes the whole row); a +column-only change goes through `DB_UPDATE_PLAYLIST`. The self-hosted web +backend's playlist repository uses the same conflict update. + `DB_GET_APP_PLAYLIST_METAS` is the preferred path for summary surfaces such as the workspace sidebar and dashboard source rail. It selects playlist metadata columns only and deliberately skips the large `payload` column, which can diff --git a/libs/shared/database/src/lib/app-playlist-conflict.spec.ts b/libs/shared/database/src/lib/app-playlist-conflict.spec.ts new file mode 100644 index 000000000..631e06841 --- /dev/null +++ b/libs/shared/database/src/lib/app-playlist-conflict.spec.ts @@ -0,0 +1,80 @@ +import { SQL } from 'drizzle-orm'; +import { buildPlaylistRow } from './app-playlist-codec'; +import { playlistConflictUpdate } from './app-playlist-conflict'; +import type { NewPlaylist } from './schema'; + +function rowOf(document: Record): NewPlaylist { + const row = buildPlaylistRow(document); + if (!row) { + throw new Error('Expected the document to produce a row'); + } + return row; +} + +/** Every optional column of a document that only has an id, title and URL. */ +const CLEARED_COLUMNS = { + serverUrl: null, + username: null, + password: null, + lastUpdated: null, + userAgent: null, + origin: null, + referrer: null, + filePath: null, + epgUrls: null, + detectedEpgUrls: null, + manualEpgUrls: null, + disabledEpgUrls: null, + macAddress: null, + portalUrl: null, + count: null, + importDate: null, + updateDate: null, + position: null, + favorites: null, + recentlyViewed: null, +}; + +describe('playlistConflictUpdate', () => { + const document = { + _id: 'm3u-1', + title: 'Family', + url: 'https://example.test/list.m3u', + userAgent: '', + referrer: ' ', + }; + + it('sets every optional column the document leaves empty to NULL', () => { + const row = rowOf(document); + + expect(playlistConflictUpdate(row, document)).toMatchObject({ + ...CLEARED_COLUMNS, + url: 'https://example.test/list.m3u', + }); + }); + + it('never clears the creation date or the columns every row carries', () => { + const row = rowOf(document); + const update = playlistConflictUpdate(row, document); + + expect(update).not.toHaveProperty('dateCreated'); + expect(update).toMatchObject({ + id: 'm3u-1', + name: 'Family', + type: 'm3u-url', + autoRefresh: false, + lastUsage: row.lastUsage, + }); + // A clockless document keeps a stored panel clock through SQL. + expect(update.payload).toBeInstanceOf(SQL); + }); + + it('clears the same columns when the document carries its own clock', () => { + const withClock = { ...document, serverTimezone: 'UTC' }; + const row = rowOf(withClock); + const update = playlistConflictUpdate(row, withClock); + + expect(update).toMatchObject(CLEARED_COLUMNS); + expect(update.payload).toBe(row.payload); + }); +}); diff --git a/libs/shared/database/src/lib/app-playlist-conflict.ts b/libs/shared/database/src/lib/app-playlist-conflict.ts index e972cc81a..629b9a249 100644 --- a/libs/shared/database/src/lib/app-playlist-conflict.ts +++ b/libs/shared/database/src/lib/app-playlist-conflict.ts @@ -14,22 +14,70 @@ export type PlaylistConflictUpdate = { const stored = schema.playlists; /** - * The `DO UPDATE` half of an app-playlist upsert. A full upsert is built - * from a snapshot the caller read earlier; when that snapshot carries no - * panel clock while the stored row does — `setPlaylistServerTimezone` - * landed in between — the stored clock is carried over as long as the row - * still points at the same panel, so a favorites, recent-items or metadata - * write cannot hand a clockless payload back to the past. A snapshot that - * carries its own clock, or moves the source, wins as is. Nested CASE, not - * AND: SQLite may reorder AND terms, and the json_* readers raise on a - * malformed payload unless json_valid ran first. + * Columns a whole-document upsert must rewrite even when the document no + * longer has a value: the codec maps empty values to `undefined`, which an + * `ON CONFLICT DO UPDATE` skips, so a cleared field would keep its old + * column value and `parseAppPlaylist` would bring it back. `dateCreated` + * belongs to the first insert, and `name`, `type`, `autoRefresh`, + * `payload` and `lastUsage` always carry a value. + */ +const REPLACEABLE_COLUMNS = [ + 'serverUrl', + 'username', + 'password', + 'lastUpdated', + 'userAgent', + 'origin', + 'referrer', + 'filePath', + 'epgUrls', + 'detectedEpgUrls', + 'manualEpgUrls', + 'disabledEpgUrls', + 'macAddress', + 'url', + 'portalUrl', + 'count', + 'importDate', + 'updateDate', + 'position', + 'favorites', + 'recentlyViewed', +] as const satisfies readonly (keyof schema.NewPlaylist)[]; + +function withClearedColumns(row: schema.NewPlaylist): schema.NewPlaylist { + const cleared: Partial> = + {}; + for (const column of REPLACEABLE_COLUMNS) { + if (row[column] === undefined) { + cleared[column] = null; + } + } + return { ...row, ...cleared }; +} + +/** + * The `DO UPDATE` half of an app-playlist upsert. The document replaces + * the stored one, so every optional column it leaves empty is set to NULL + * rather than skipped (see REPLACEABLE_COLUMNS). + * + * A full upsert is built from a snapshot the caller read earlier; when + * that snapshot carries no panel clock while the stored row does — + * `setPlaylistServerTimezone` landed in between — the stored clock is + * carried over as long as the row still points at the same panel, so a + * favorites, recent-items or metadata write cannot hand a clockless + * payload back to the past. A snapshot that carries its own clock, or + * moves the source, wins as is. Nested CASE, not AND: SQLite may reorder + * AND terms, and the json_* readers raise on a malformed payload unless + * json_valid ran first. */ export function playlistConflictUpdate( row: schema.NewPlaylist, playlist: Record ): PlaylistConflictUpdate { + const replacement = withClearedColumns(row); if (readString(playlist['serverTimezone'])) { - return row; + return replacement; } const excludedPayload = sql.raw(`excluded.${stored.payload.name}`); const payload = sql`CASE @@ -48,7 +96,7 @@ export function playlistConflictUpdate( END ELSE ${excludedPayload} END`; - return { ...row, payload }; + return { ...replacement, payload }; } /** From f60d797011bf0245dd6c6e8bff6827eb8eb7c50b Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:32:50 +0200 Subject: [PATCH 4/7] fix(workspace): don't flash the import overlay on a cached Xtream open (#1906) * fix(workspace): don't flash the import overlay on a cached Xtream open Opening an already-imported Xtream portal reads the saved catalog in tens of milliseconds, but the session made the shell mount the full-screen import overlay right away, so it flashed for about two frames between the skeleton and the loaded page (J2 open-source). A session that has only read the saved catalog (loading-cached, store isImporting still false) now shows the overlay after a 300 ms delay. Remote import phases and refresh preparation still show it immediately, and a slow or stuck cache read still blocks and stays cancellable. Co-Authored-By: Claude Opus 5.5 * fix(workspace): keep the overlay delay while another playlist refreshes Refresh preparation skipped the cached-load delay for any playlist, so opening a cached portal while another playlist prepared a refresh still flashed the overlay. Only preparation of the shown playlist (any on the dashboard) now shows it at once. Also words the J2 layout-shift history from #1895 in the past tense: the cached-load overlay no longer mounts on a J2 click. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: 4gray Co-authored-by: Claude Opus 5.5 --- .../workspace-cached-open-no-overlay-flash.md | 9 ++ docs/architecture/performance-journeys.md | 9 +- docs/architecture/workspace-shell.md | 8 +- .../helpers/workspace-shell-constants.ts | 9 ++ ...kspace-shell-xtream-import.service.spec.ts | 117 ++++++++++++++++++ .../workspace-shell-xtream-import.service.ts | 55 +++++++- .../services/workspace-shell.facade.spec.ts | 34 +++-- 7 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 .changes/workspace-cached-open-no-overlay-flash.md diff --git a/.changes/workspace-cached-open-no-overlay-flash.md b/.changes/workspace-cached-open-no-overlay-flash.md new file mode 100644 index 000000000..9a5b222ea --- /dev/null +++ b/.changes/workspace-cached-open-no-overlay-flash.md @@ -0,0 +1,9 @@ +--- +type: fix +area: workspace +--- + +Opening an already-imported Xtream portal no longer flashes the "Syncing +playlist" overlay for a split second before the catalog appears. The overlay +still shows for real downloads and for saved catalogs that take a while to +load. diff --git a/docs/architecture/performance-journeys.md b/docs/architecture/performance-journeys.md index cfbaecc47..b83dbfc00 100644 --- a/docs/architecture/performance-journeys.md +++ b/docs/architecture/performance-journeys.md @@ -704,6 +704,11 @@ source's default section, which is VOD (`getPlaylistLink` links to and the first page is the "All items" grid. The plan's "live category list" would need a second click and is not measured; the landed section is recorded under `evidence.firstPage.section`. +The seeded portal is already imported, so the click only reads the saved +catalog and finishes well inside the shell's show delay for the import +overlay ([workspace shell](workspace-shell.md)): a J2 click must not mount +`app-workspace-shell-import-overlay`, and before that delay it flashed for +about two frames between the skeleton and the first page. **HTTP requests to the mock.** The J2 profile is seeded with the origin of a loopback proxy in the test process @@ -1317,8 +1322,8 @@ largest distance in it, the content pane's 323 px, which is why Back added buttons appearing when the categories arrive. The skeletons of the category list and the grid are replaced by content without any shift. Nothing was hidden behind the startup overlay: the "Syncing playlist" overlay of the -cached catalog load flashes for about two frames after the shift and covers -no further one. +cached catalog load flashed for about two frames after the shift and covered +no further one. It no longer mounts on a J2 click (see the J2 section). The content frame and the header controls now slide into place instead of jumping ([Layout slides](./workspace-shell.md#layout-slides)): the new layout diff --git a/docs/architecture/workspace-shell.md b/docs/architecture/workspace-shell.md index 243842ad3..58b09ed0c 100644 --- a/docs/architecture/workspace-shell.md +++ b/docs/architecture/workspace-shell.md @@ -195,7 +195,13 @@ services: 5. `WorkspaceShellCommandPaletteService` owns command-palette dialog lifecycle and recent-command recording. 6. `WorkspaceShellXtreamImportService` owns Xtream import/refresh overlay - state and labels. + state and labels. Remote import phases and refresh preparation of the + shown playlist (of any playlist on the dashboard) show the overlay at + once. A session that has only read the saved catalog (`loading-cached`, + `isImporting` still false) shows it only after + `XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS` (300 ms), so a warm open goes from + the skeleton to the page without flashing the overlay, while a slow or + stuck cache read still blocks and stays cancellable. When adding shell behavior, prefer placing it in the service that owns the nearest existing state. Keep `WorkspaceShellFacade` as a stable re-export layer diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/services/helpers/workspace-shell-constants.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/services/helpers/workspace-shell-constants.ts index 7464a2552..c1e38a4c3 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/services/helpers/workspace-shell-constants.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/services/helpers/workspace-shell-constants.ts @@ -2,6 +2,15 @@ import { PortalRailSection } from '@iptvnator/portal/shared/util'; export const SEARCH_INPUT_DEBOUNCE_MS = 350; +/** + * How long a purely local Xtream load (reading the saved catalog from SQLite) + * may run before the full-screen import overlay appears. A warm catalog + * finishes in tens of milliseconds; showing the overlay for that only flashes + * it between the skeleton and the loaded page. Remote imports and refresh + * preparation are not delayed. + */ +export const XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS = 300; + export const SEARCH_PLAYLIST_PLACEHOLDER = 'WORKSPACE.SHELL.SEARCH_PLAYLIST_PLACEHOLDER'; export const SEARCH_SECTION_PLACEHOLDER = diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell-xtream-import.service.spec.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell-xtream-import.service.spec.ts index 3802adb96..31fe6c19f 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell-xtream-import.service.spec.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell-xtream-import.service.spec.ts @@ -4,6 +4,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { PlaylistRefreshActionService } from '@iptvnator/playlist/shared/ui'; import { XtreamStore } from '@iptvnator/portal/xtream/data-access'; import { RuntimeCapabilitiesService } from '@iptvnator/services'; +import { XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS } from './helpers/workspace-shell-constants'; import { WorkspaceShellRouteStateService } from './workspace-shell-route-state.service'; import { WorkspaceShellXtreamImportService } from './workspace-shell-xtream-import.service'; @@ -51,4 +52,120 @@ describe('WorkspaceShellXtreamImportService', () => { expect(service.xtreamImportTitleLabel()).toBe('Импорт'); }); + + describe('import overlay visibility', () => { + const activeImportSessionId = signal(null); + const isImporting = signal(false); + const refreshPreparation = signal<{ playlistId: string } | null>( + null + ); + let service: WorkspaceShellXtreamImportService; + + beforeEach(() => { + jest.useFakeTimers(); + activeImportSessionId.set(null); + isImporting.set(false); + refreshPreparation.set(null); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + providers: [ + WorkspaceShellXtreamImportService, + { + provide: XtreamStore, + useValue: { + activeImportSessionId, + isImporting, + contentInitBlockReason: signal(null), + }, + }, + { + provide: PlaylistRefreshActionService, + useValue: { refreshPreparation }, + }, + { provide: RuntimeCapabilitiesService, useValue: {} }, + { + provide: WorkspaceShellRouteStateService, + useValue: { + currentRoute: signal({ kind: 'xtreams' }), + currentContext: signal({ + provider: 'xtreams', + playlistId: 'portal-1', + }), + currentSection: signal('vod'), + }, + }, + ], + }); + service = TestBed.inject(WorkspaceShellXtreamImportService); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + /** Lets the delay effect react, then advances the fake clock. */ + function advance(ms: number): void { + TestBed.tick(); + jest.advanceTimersByTime(ms); + TestBed.tick(); + } + + it('never shows the overlay for a saved-catalog load that finishes quickly', () => { + // A warm open: the session starts and only reads SQLite + // ('loading-cached' leaves isImporting false). + activeImportSessionId.set('session-1'); + advance(XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS - 1); + expect(service.showXtreamImportOverlay()).toBe(false); + + activeImportSessionId.set(null); + advance(XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS); + expect(service.showXtreamImportOverlay()).toBe(false); + }); + + it('shows the overlay once a saved-catalog load outlasts the delay', () => { + activeImportSessionId.set('session-1'); + advance(XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS); + expect(service.showXtreamImportOverlay()).toBe(true); + + activeImportSessionId.set(null); + advance(0); + expect(service.showXtreamImportOverlay()).toBe(false); + }); + + it('restarts the delay for the next load', () => { + activeImportSessionId.set('session-1'); + advance(XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS); + activeImportSessionId.set(null); + advance(0); + + activeImportSessionId.set('session-2'); + advance(0); + expect(service.showXtreamImportOverlay()).toBe(false); + }); + + it('shows the overlay at once for a remote import', () => { + activeImportSessionId.set('session-1'); + advance(0); + expect(service.showXtreamImportOverlay()).toBe(false); + + // A remote phase marks the session as a real import. + isImporting.set(true); + expect(service.showXtreamImportOverlay()).toBe(true); + }); + + it('keeps the delay while another playlist prepares a refresh', () => { + refreshPreparation.set({ playlistId: 'other-playlist' }); + activeImportSessionId.set('session-1'); + advance(0); + expect(service.showXtreamImportOverlay()).toBe(false); + + advance(XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS); + expect(service.showXtreamImportOverlay()).toBe(true); + }); + + it('shows the overlay at once while a refresh prepares the playlist', () => { + refreshPreparation.set({ playlistId: 'portal-1' }); + expect(service.showXtreamImportOverlay()).toBe(true); + }); + }); }); diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell-xtream-import.service.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell-xtream-import.service.ts index 8ada0d20d..056bb82d4 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell-xtream-import.service.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell-xtream-import.service.ts @@ -1,4 +1,4 @@ -import { computed, inject, Injectable } from '@angular/core'; +import { computed, effect, inject, Injectable, signal } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { PlaylistRefreshActionService } from '@iptvnator/playlist/shared/ui'; import { XtreamStore } from '@iptvnator/portal/xtream/data-access'; @@ -6,7 +6,10 @@ import { RuntimeCapabilitiesService, injectTranslationTick, } from '@iptvnator/services'; -import type { XtreamImportPhaseTone } from './helpers/workspace-shell-constants'; +import { + XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS, + type XtreamImportPhaseTone, +} from './helpers/workspace-shell-constants'; import { buildXtreamImportDetailLabel, buildXtreamImportPhaseLabel, @@ -76,7 +79,8 @@ export class WorkspaceShellXtreamImportService { Boolean(this.xtreamStore.activeImportSessionId()) && !this.xtreamStore.isCancellingImport() ); - readonly showXtreamImportOverlay = computed(() => { + /** Whether the current route is waiting on an import or refresh. */ + private readonly isOverlayWanted = computed(() => { const route = this.routeState.currentRoute(); const context = this.routeState.currentContext(); const section = this.routeState.currentSection(); @@ -103,6 +107,35 @@ export class WorkspaceShellXtreamImportService { ); }); + /** + * Remote imports (the store flags `isImporting` for every phase except + * `loading-cached`) and refresh preparation of the shown playlist (any + * on the dashboard) show the overlay at once. A session that has only + * read the saved catalog so far waits `XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS`, + * so a warm open never flashes it, even while another playlist prepares + * a refresh. + */ + private readonly isOverlayImmediate = computed(() => { + if (this.xtreamStore.isImporting()) { + return true; + } + if (this.routeState.currentRoute().kind === 'dashboard') { + return Boolean(this.refreshPreparation()); + } + const playlistId = this.routeState.currentContext()?.playlistId; + return ( + playlistId !== undefined && + this.isRefreshPreparationRunningForPlaylist(playlistId) + ); + }); + private readonly localLoadDelayElapsed = signal(false); + + readonly showXtreamImportOverlay = computed( + () => + this.isOverlayWanted() && + (this.isOverlayImmediate() || this.localLoadDelayElapsed()) + ); + readonly xtreamImportTitleLabel = computed(() => { this.languageTick(); if (this.activeRefreshPreparation()) { @@ -198,6 +231,22 @@ export class WorkspaceShellXtreamImportService { ); }); + constructor() { + // Restart the delay for every wait: it runs from the moment the + // route starts waiting and resets as soon as the wait ends. + effect((onCleanup) => { + if (!this.isOverlayWanted()) { + this.localLoadDelayElapsed.set(false); + return; + } + const timer = setTimeout( + () => this.localLoadDelayElapsed.set(true), + XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS + ); + onCleanup(() => clearTimeout(timer)); + }); + } + readonly isImportRunning = computed( () => !this.xtreamStore.contentInitBlockReason() && diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts index 7d76bd215..8064d6b96 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.spec.ts @@ -30,6 +30,7 @@ import { } from '@iptvnator/workspace/shell/util'; import { RecentCommandsService } from '../../recent-commands'; import { WorkspacePlayerCommandsContributor } from '../../workspace-player-commands'; +import { XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS } from './helpers/workspace-shell-constants'; import { WorkspaceShellFacade } from './workspace-shell.facade'; import { WorkspaceShellXtreamImportService } from './workspace-shell-xtream-import.service'; import { WorkspaceShellCommandPaletteService } from './workspace-shell-command-palette.service'; @@ -503,16 +504,31 @@ describe('WorkspaceShellFacade', () => { expect(facade.showXtreamImportOverlay()).toBe(true); }); - it('keeps a cache-only Xtream import session blocking and cancellable', () => { - const xtreamStore = TestBed.inject( - XtreamStore - ) as unknown as MockXtreamStore; - const xtreamImport = TestBed.inject(WorkspaceShellXtreamImportService); - xtreamStore.activeImportSessionId.set('xtream-import-session'); + it('keeps a slow cache-only Xtream import session blocking and cancellable', () => { + jest.useFakeTimers(); + try { + const xtreamStore = TestBed.inject( + XtreamStore + ) as unknown as MockXtreamStore; + const xtreamImport = TestBed.inject( + WorkspaceShellXtreamImportService + ); + xtreamStore.activeImportSessionId.set('xtream-import-session'); + TestBed.tick(); - expect(xtreamStore.isImporting()).toBe(false); - expect(facade.showXtreamImportOverlay()).toBe(true); - expect(xtreamImport.canCancelXtreamImport()).toBe(true); + // A saved-catalog read waits out the show delay so a quick one + // never flashes the overlay, but stays cancellable meanwhile. + expect(xtreamStore.isImporting()).toBe(false); + expect(facade.showXtreamImportOverlay()).toBe(false); + expect(xtreamImport.canCancelXtreamImport()).toBe(true); + + jest.advanceTimersByTime(XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS); + + expect(facade.showXtreamImportOverlay()).toBe(true); + expect(xtreamImport.canCancelXtreamImport()).toBe(true); + } finally { + jest.useRealTimers(); + } }); it('shows the Xtream overlay during refresh preparation on the dashboard', () => { From 70e771ff27387de642b0b7086d179c2a3932bc9f Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:04:48 +0200 Subject: [PATCH 5/7] perf(search): one 300 ms debounce between typing and the global search query (#1899) * perf(search): one 300 ms debounce between typing and the global search query Typing in the header search box waited twice before a query ran: 350 ms in the workspace shell (SEARCH_INPUT_DEBOUNCE_MS), then another 300 ms in SearchResultsComponent. On J4 (large catalog, "system") the query and first page take 30-40 ms; about 650 ms of the ~690 ms from the last key to settled results was waiting. The results component now searches as soon as the applied term reaches the store, and the shell debounce is the only one, lowered to 300 ms: above the gap between keys of steady typing, so a word typed in one go still queries once. Superseded queries are dropped by the existing request versions. The component's route sync reads the store term untracked, so a term the shell applied before writing q is not pushed back to the stale q and re-queried. J4 locally (3 runs per build): lastKeystrokeToSettledMs P50 692-695 ms -> 336-337 ms, ipcCallsPerSearch 1 and sqlStatementsPerSearch 2 unchanged. At 400 ms per key both builds run five queries; only typing with 300-350 ms gaps queries per key where it did not before. Co-Authored-By: Claude Opus 5.5 * perf(journeys): enforce J4 ipcCallsPerSearch 1 and sqlStatementsPerSearch 2 Both counters were identical in all 816 J4 iterations of the 136 CI journey jobs since #1816 and in all 18 iterations of three runner runs of this PR (run 38165392046, attempts 1-3). They guard the single debounce: a search that queries per keystroke or twice for one term raises them. Guard-only note, since the wall-clock gain came without moving them. Added to the Performance journeys --only list; the direction check reports 2 added, nothing weakened. Co-Authored-By: Claude Opus 5.5 * fix(search): run a prefilled in-portal search again once the catalog has loaded Review follow-up on #1899 (Greptile P1). Without the results component's 300 ms timer, an in-portal search applied while the portal's catalog is still loading (a reload or deep link with q) runs against the incomplete catalog, and nothing re-ran it afterwards. The old timer only helped when loading finished within 300 ms. The component now runs the current in-portal search once more when nothing of the catalog is loading or importing any more, the readiness the Discover route already uses. Co-Authored-By: Claude Opus 5.5 * fix(search): one search effect for a new term and the loaded catalog Review follow-up on #1899 (Greptile). With separate effects, a term and the end of catalog loading arriving in the same flush each ran executeSearch for the same term: the request version dropped the first answer, but the catalog scan or desktop search still ran twice. One effect now tracks both triggers and searches once. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: 4gray Co-authored-by: Claude Opus 5.5 --- .changes/search-single-debounce.md | 6 + .github/workflows/ci.yml | 2 + .../src/journeys/search-journey-app.ts | 2 +- .../src/performance/search-journey-record.ts | 2 +- docs/architecture/performance-journeys.md | 80 +++++-- .../search-results.component.spec.ts | 196 +++++++++++++++++- .../search-results.component.ts | 55 ++++- .../helpers/workspace-shell-constants.ts | 9 +- tools/performance/journey-baselines.json | 22 ++ 9 files changed, 345 insertions(+), 29 deletions(-) create mode 100644 .changes/search-single-debounce.md diff --git a/.changes/search-single-debounce.md b/.changes/search-single-debounce.md new file mode 100644 index 000000000..1b00e5520 --- /dev/null +++ b/.changes/search-single-debounce.md @@ -0,0 +1,6 @@ +--- +type: perf +area: search +--- + +Search results appear about twice as fast after you stop typing: the search now waits once for typing to pause instead of twice. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5a2e1418..b5a581c80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -353,6 +353,8 @@ jobs: --only open-source/renderer.layoutShiftScore --only playback/renderer.httpRequestsToPlaying --only playback/renderer.layoutShiftScore + --only search/renderer.ipcCallsPerSearch + --only search/renderer.sqlStatementsPerSearch - name: Upload journey summaries if: always() diff --git a/apps/electron-backend-e2e/src/journeys/search-journey-app.ts b/apps/electron-backend-e2e/src/journeys/search-journey-app.ts index 7d76b54fe..b57418091 100644 --- a/apps/electron-backend-e2e/src/journeys/search-journey-app.ts +++ b/apps/electron-backend-e2e/src/journeys/search-journey-app.ts @@ -60,7 +60,7 @@ export const SEARCH_JOURNEY_PORTAL_NAME = 'Journey search portal'; * matches 170 series titles, more than the first page of 100. */ export const SEARCH_JOURNEY_QUERY = 'system'; -/** Well below the shell's 350 ms input debounce, like steady typing. */ +/** Well below the shell's 300 ms input debounce, like steady typing. */ export const SEARCH_JOURNEY_KEY_DELAY_MS = 100; const QUIET_MS = 1_000; const QUIET_POLL_MS = 100; diff --git a/apps/electron-backend-e2e/src/performance/search-journey-record.ts b/apps/electron-backend-e2e/src/performance/search-journey-record.ts index 556f378e8..0e941379c 100644 --- a/apps/electron-backend-e2e/src/performance/search-journey-record.ts +++ b/apps/electron-backend-e2e/src/performance/search-journey-record.ts @@ -34,7 +34,7 @@ export const SEARCH_JOURNEY_UNAVAILABLE_COUNTERS: Readonly< export const SEARCH_JOURNEY_QUERY_METHOD = 'dbGlobalSearch'; /** - * Longest accepted gap between two keydowns. Below the shell's 350 ms input + * Longest accepted gap between two keydowns. Below the shell's 300 ms input * debounce, so a late key on a busy machine cannot let an intermediate term * run a query that steady typing would not. */ diff --git a/docs/architecture/performance-journeys.md b/docs/architecture/performance-journeys.md index b83dbfc00..fac0b08be 100644 --- a/docs/architecture/performance-journeys.md +++ b/docs/architecture/performance-journeys.md @@ -927,11 +927,13 @@ put behind the request ledger: global search reads only the local database. **What typing does.** The header search box (`app-workspace-shell-header .search-field input[type="search"]`) applies -its term after `SEARCH_INPUT_DEBOUNCE_MS` (350 ms, +its term after `SEARCH_INPUT_DEBOUNCE_MS` (300 ms, `WorkspaceShellSearchSyncService`) and writes it to the URL as `q`. On `/workspace/search` (`app.routes.ts`, Electron only) `SearchResultsComponent` -adopts `q` and runs `executeSearch` after its own 300 ms debounce, which -calls the `dbGlobalSearch` bridge method once with a limit of 101. +runs `executeSearch` as soon as the applied term reaches the store, with no +debounce of its own, which calls the `dbGlobalSearch` bridge method once +with a limit of 101. A newer term supersedes a query still in flight: its +result is dropped by request version. **Start.** After J1 has ended, the test clicks the rail's **Global search** link and focuses the header search box (not measured), installs the IPC @@ -941,7 +943,7 @@ been quiet for 1 s: no DOM mutation, no new or pending bridge call, and an unchanged `main.sqlStatements` total (30 s timeout, which fails the iteration). J1's capture is detached. The test then types the query with one `keyboard.type` call per character on a fixed schedule, 100 ms apart from -the first key (well below the 350 ms debounce, as steady typing would be), +the first key (well below the 300 ms debounce, as steady typing would be), and samples the main process just before each key. The probe's capture-phase `keydown` listener on `window` stamps every key in the search box before the app sees it; the first one starts the journey and sends @@ -950,7 +952,7 @@ rejects an iteration whose DOM mutations, bridge calls or SQL statements moved between the quiet snapshot and the first key. **End.** "No DOM mutation for 200 ms after the last keystroke" alone would -end inside the 650 ms of debounce, before any query ran: nothing in the DOM +end inside the 300 ms of debounce, before any query ran: nothing in the DOM changes while the term waits. So after the sixth key the probe waits for the results of the final term to be shown (the path ends with `/workspace/search`, the URL `q` is the query, no `.loading-state` is @@ -975,7 +977,7 @@ window closed until its results replace the old ones. `evidence.finalQuery` keeps its term, result length and duration. The record also rejects an iteration in which two keydowns were more than 250 ms apart (`typing-cadence`; the gaps are `evidence.keyIntervalsMs`). -At that point a late key on a busy machine could let the 350 ms debounce +At that point a late key on a busy machine could let the 300 ms debounce apply an intermediate term, which steady typing does not. A settle that times out fails the iteration with what the probe and the @@ -1017,9 +1019,10 @@ counter: global search does not touch the network. | `firstKeystrokeToFirstResultMs.p50/.p90` | First mutation batch with a visible result card (for any term) minus the first keydown. | | `lastKeystrokeToSettledMs.p50/.p90` | Settled moment (the last mutation batch before the 200 ms quiet window) minus the last keydown. The quiet window itself is not included. | -Both are taken in the renderer. With the current debounces both include -the 650 ms the term waits (350 ms in the shell, then 300 ms in the results -component); the first also includes the 500 ms of typing. +Both are taken in the renderer. Both include the 300 ms the term waits in +the shell; the first also includes the 500 ms of typing. Until the single +debounce below, the wait was 650 ms (350 ms in the shell, then 300 ms in the +results component). ### First measurement @@ -1045,6 +1048,46 @@ debounces (350 ms in the shell, then 300 ms in the results component). The query itself, from the bridge call to the rendered first page, takes the remaining 30-40 ms. +### One debounce + +The results component's own 300 ms timer is gone: the shell debounce is +now the only one, and global search runs as soon as the shell applies the +term. `SEARCH_INPUT_DEBOUNCE_MS` went from 350 to 300 ms. It has to stay +above the gap between keys of steady typing, or a word typed in one go +queries once per key; 300 ms covers an average typist (about 200-250 ms +between keys) with some margin, while the query and first page take only +30-40 ms of the remaining time. The route sync in the component reads the +store term untracked, so a term the shell applied before it writes `q` is +no longer pushed back to the previous `q` and queried again. + +Local, macOS, 2026-10-11, three J4 runs per build (five measured +iterations each), the same machine, master `633fde3e0` against this +change: + +| Measurement | Two debounces | One debounce | +| ------------------------------------ | -------------------- | -------------------- | +| `lastKeystrokeToSettledMs` P50 / P90 | 692-695 / 695-697 ms | 336-337 / 337-339 ms | +| `firstKeystrokeToFirstResultMs` P50 | 1,190 ms | 832-834 ms | +| `renderer.ipcCallsPerSearch` | 1 | 1 | +| `renderer.sqlStatementsPerSearch` | 2 | 2 | +| `renderer.domMutationsToResults` | 402 | 392 | + +Per keystroke (query calls / SQL / DOM mutations / CD ticks) at 100 ms per +key, keys 1-5 are unchanged at `0/0/0/1`; key 6 is `1/2/402/9` before and +`1/2/392/7` after. + +Slower typing was measured with a scratch change to the journey (key +interval and cadence guard raised, quiet window 1 s; not committed). At +400 ms per key both builds run one query per key from the second +character, five in all with 2 statements each: the shell debounce already +expired between keys before this change, and the results timer only +delayed each query. Before, the queries trail the keys by 650 ms (keys +`y s t e m` read `0 1 1 1 2`); after, each follows its own key. At 325 ms +per key the two builds differ: before, one query in 17 of 20 iterations +(two or four when a gap stretched past 350 ms); after, five. Typing with +gaps between 300 and 350 ms is the only range where this change adds +queries; each is a 2-statement query whose stale result is dropped. + About one launch in sixty showed the empty-results view: the single `dbGlobalSearch` call went out with the same arguments (`system`, all three types, hidden categories included, limit 101) and the main process answered @@ -1069,8 +1112,8 @@ persisted status (`xtream-import-status::`) reads `completed` and the type has rows, unless the portal has no categories of that type. Rows are required because a refresh deletes the catalog without resetting the status. `xtream-import-wait.e2e.ts` holds the account check past the overlay -window and fails without it. No J4 baseline exists yet; J4 counters join the -ratchet once three runner runs agree. +window and fails without it. Two J4 counters are enforced since #1899 +(see [Enforced journey counters](#enforced-journey-counters)). ## `renderer.initialBytes` @@ -1337,6 +1380,14 @@ changes) and the wall-clock entries did not move. Under `prefers-reduced-motion` there is no slide, so a user with that preference still sees the jump that the journey no longer measures. +The two J4 entries, `search/renderer.ipcCallsPerSearch` 1 and +`search/renderer.sqlStatementsPerSearch` 2 (#1899, `evidenceRun` +38165392046), were identical in all 816 J4 iterations of the 136 CI journey +jobs since #1816 and in all 18 iterations of three runs of #1899. They guard +the debounce: a search that queried per keystroke, or ran a second query for +the same term, raises them. Like the entries in the table they carry the +guard-only note: #1899 halved the J4 wall clock without moving either. + Being deterministic is not the same as being validated. Principle 3 of the plan promotes a counter to a guardrail once a PR has shown that lowering it lowered the journey's wall-clock. None of these counters has that evidence @@ -1372,7 +1423,12 @@ Not enforced, with the reason: - J3 `renderer.ipcSerialDepthToPlaying` (4 on the runner, 3 on a Mac): added after these runs and measured in one runner run so far; a candidate once `master` runs agree. -- J4: not measured yet. +- J4 `renderer.domMutationsToResults` (402 on `master`, 392 since #1899), + `renderer.ipcSerialDepthToResults` (1) and `renderer.layoutShiftScore` + (0): identical in the three #1899 runs; candidates once `master` runs + agree. +- J4 `renderer.cdTicksToResults` (11 to 13) and `renderer.longTasks`: see + the general reasons above. Runner counters differ from a Mac (the Linux-only `getWindowState` call, for one), so take every journey baseline value from the runner only. diff --git a/libs/portal/xtream/feature/src/lib/search-results/search-results.component.spec.ts b/libs/portal/xtream/feature/src/lib/search-results/search-results.component.spec.ts index 5cd30c85a..6ede59898 100644 --- a/libs/portal/xtream/feature/src/lib/search-results/search-results.component.spec.ts +++ b/libs/portal/xtream/feature/src/lib/search-results/search-results.component.spec.ts @@ -1,7 +1,7 @@ import { signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; -import { of } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; import { DatabaseService } from '@iptvnator/services'; import { SearchResultsComponent } from './search-results.component'; import { @@ -54,6 +54,9 @@ class MockXtreamStore { readonly searchFilters = signal(DEFAULT_SEARCH_FILTERS); readonly searchResults = signal([]); readonly isSearching = signal(false); + readonly isLoadingContent = signal(false); + readonly isLoadingCategories = signal(false); + readonly isImporting = signal(false); setSearchTerm = jest.fn((term: string) => { this.searchTerm.set(term); @@ -377,7 +380,7 @@ describe('SearchResultsComponent global search', () => { it('refuses to append pages while an edited query is still debouncing', async () => { // Regression: the auto-fill can request more results right after the - // reset identity changes, i.e. inside the 300ms search debounce. An + // reset identity changes, before the search effect has run. An // append with the edited term would fetch a page of the NEW query at // the OLD offset and interleave it into the old query's results. const firstPage = Array.from({ length: 101 }, (_, index) => @@ -402,7 +405,7 @@ describe('SearchResultsComponent global search', () => { databaseService.globalSearchContent.mockClear(); // The user edits the query; the fresh offset-zero search has not run - // yet (debounce pending), so appending must be refused entirely. + // yet (effects not flushed), so appending must be refused entirely. store.searchTerm.set('matrix reloaded'); await component.loadMoreGlobalResults(); @@ -506,3 +509,190 @@ describe('SearchResultsComponent in-portal result window', () => { } }); }); + +describe('SearchResultsComponent search trigger', () => { + let queryParams: BehaviorSubject>; + let globalSearchContent: jest.Mock; + + function setUp(initialQuery: string): void { + queryParams = new BehaviorSubject( + convertToParamMap({ q: initialQuery }) + ); + globalSearchContent = jest.fn().mockResolvedValue([]); + TestBed.configureTestingModule({ + providers: [ + { provide: XtreamStore, useClass: MockXtreamStore }, + { provide: Router, useValue: { navigate: jest.fn() } }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: { isGlobalSearch: true }, + queryParamMap: queryParams.value, + }, + queryParamMap: queryParams.asObservable(), + }, + }, + { + provide: DatabaseService, + useValue: { globalSearchContent }, + }, + ], + }); + } + + function searchedTerms(): string[] { + return globalSearchContent.mock.calls.map(([term]) => term as string); + } + + afterEach(() => { + jest.useRealTimers(); + }); + + it('queries an applied term at once, without a second debounce', () => { + // The shell already debounces typing; a second timer here only added + // 300 ms to every search. + jest.useFakeTimers(); + setUp(''); + const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; + TestBed.runInInjectionContext(() => new SearchResultsComponent()); + TestBed.flushEffects(); + + store.setSearchTerm('system'); + TestBed.flushEffects(); + + expect(searchedTerms()).toEqual(['system']); + expect(jest.getTimerCount()).toBe(0); + }); + + it('queries only the route term when the store still holds an older one', () => { + setUp('system'); + const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; + store.searchTerm.set('matrix'); + + TestBed.runInInjectionContext(() => new SearchResultsComponent()); + TestBed.flushEffects(); + + expect(searchedTerms()).toEqual(['system']); + }); + + it('keeps a term applied ahead of the URL instead of re-querying the stale q', () => { + // The shell sets the store term first and writes `q` afterwards; the + // route sync must not snap the store back to the previous `q`. + setUp('syst'); + const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; + TestBed.runInInjectionContext(() => new SearchResultsComponent()); + TestBed.flushEffects(); + globalSearchContent.mockClear(); + + store.setSearchTerm('system'); + TestBed.flushEffects(); + queryParams.next(convertToParamMap({ q: 'system' })); + TestBed.flushEffects(); + + expect(searchedTerms()).toEqual(['system']); + expect(store.searchTerm()).toBe('system'); + }); + + it('ignores an older query that resolves after a newer term', async () => { + setUp(''); + const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; + const older = createDeferred(); + const newer = createDeferred(); + globalSearchContent + .mockReturnValueOnce(older.promise) + .mockReturnValueOnce(newer.promise); + TestBed.runInInjectionContext(() => new SearchResultsComponent()); + TestBed.flushEffects(); + + store.setSearchTerm('syst'); + TestBed.flushEffects(); + store.setSearchTerm('system'); + TestBed.flushEffects(); + + newer.resolve([ + createSearchItem({ title: 'System' }) as GlobalSearchResult, + ]); + await newer.promise; + older.resolve([ + createSearchItem({ title: 'Syst' }) as GlobalSearchResult, + ]); + await older.promise; + await Promise.resolve(); + + expect(searchedTerms()).toEqual(['syst', 'system']); + expect(store.searchResults().map((item) => item.title)).toEqual([ + 'System', + ]); + }); +}); + +describe('SearchResultsComponent in-portal search and catalog loading', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: XtreamStore, useClass: MockXtreamStore }, + { provide: Router, useValue: { navigate: jest.fn() } }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: {}, + queryParamMap: convertToParamMap({ q: 'matrix' }), + }, + queryParamMap: of(convertToParamMap({ q: 'matrix' })), + }, + }, + { + provide: DatabaseService, + useValue: { + globalSearchContent: jest.fn().mockResolvedValue([]), + }, + }, + ], + }); + }); + + function searchedTerms(store: MockXtreamStore): string[] { + return store.searchContent.mock.calls.map( + ([params]) => (params as { term: string }).term + ); + } + + it('runs a prefilled search again once the catalog has loaded', () => { + // The in-portal search reads the loaded catalog, so a term applied + // while it loads would otherwise stay without results. + const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; + store.isLoadingContent.set(true); + TestBed.runInInjectionContext(() => new SearchResultsComponent()); + TestBed.flushEffects(); + expect(searchedTerms(store)).toEqual(['matrix']); + + store.isLoadingContent.set(false); + TestBed.flushEffects(); + + expect(searchedTerms(store)).toEqual(['matrix', 'matrix']); + }); + + it('searches once when a new term and the loaded catalog arrive together', () => { + const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; + store.isLoadingContent.set(true); + TestBed.runInInjectionContext(() => new SearchResultsComponent()); + TestBed.flushEffects(); + store.searchContent.mockClear(); + + store.setSearchTerm('matrix reloaded'); + store.isLoadingContent.set(false); + TestBed.flushEffects(); + + expect(searchedTerms(store)).toEqual(['matrix reloaded']); + }); + + it('searches once when the catalog is already loaded', () => { + const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; + TestBed.runInInjectionContext(() => new SearchResultsComponent()); + TestBed.flushEffects(); + + expect(searchedTerms(store)).toEqual(['matrix']); + }); +}); diff --git a/libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts b/libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts index 23299dfe5..a2c6b0e5f 100644 --- a/libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts +++ b/libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts @@ -7,6 +7,7 @@ import { inject, linkedSignal, signal, + untracked, } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -121,6 +122,14 @@ export class SearchResultsComponent implements AfterViewInit { readonly isGlobalSearch = this.activatedRoute.snapshot.data?.['isGlobalSearch'] === true; + /** Nothing of the portal's catalog is loading or importing any more. */ + private readonly isCatalogReady = computed( + () => + !this.xtreamStore.isLoadingContent() && + !this.xtreamStore.isLoadingCategories() && + !this.xtreamStore.isImporting() + ); + /** Whether to group global search results by playlist */ readonly groupByPlaylist = signal( localStorage.getItem(SearchResultsComponent.GROUP_BY_STORAGE_KEY) !== @@ -249,24 +258,48 @@ export class SearchResultsComponent implements AfterViewInit { } } - effect((onCleanup) => { - const term = this.searchTerm(); - if (term.length >= this.minSearchLength) { - const timeout = setTimeout(() => this.executeSearch(), 300); - onCleanup(() => clearTimeout(timeout)); - } else if (term.length < this.minSearchLength) { - this.clearResultsOnly(); - } - }); - + // Created first so a route `q` reaches the store before the search + // effect's first run: a term left in the store by an earlier visit + // must not be queried on the way to the route's term. effect(() => { const queryTerm = this.routeSearchTerm(); - if (queryTerm === this.searchTerm()) { + if (queryTerm === untracked(this.searchTerm)) { return; } this.xtreamStore.setSearchTerm(queryTerm); }); + + // One effect for both triggers, so a term and the end of catalog + // loading arriving in the same flush run one search, not two. + // - A new term runs at once: typed terms arrive already debounced by + // the workspace shell (SEARCH_INPUT_DEBOUNCE_MS), and a term from + // the URL or a filter change is a single deliberate change. A newer + // term supersedes an older query still in flight (request versions). + // - A prefilled in-portal search (a reload or deep link with `q`) can + // run while the portal's catalog is still loading, and the in-portal + // search reads that catalog: it runs again once loading has finished. + let previousTerm: string | undefined; + let wasCatalogReady: boolean | undefined; + effect(() => { + const term = this.searchTerm(); + const ready = this.isCatalogReady(); + const termChanged = term !== previousTerm; + const becameReady = + wasCatalogReady === false && ready && !this.isGlobalSearch; + previousTerm = term; + wasCatalogReady = ready; + if (!termChanged && !becameReady) { + return; + } + untracked(() => { + if (term.length >= this.minSearchLength) { + this.executeSearch(); + } else if (termChanged) { + this.clearResultsOnly(); + } + }); + }); } ngAfterViewInit() { diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/services/helpers/workspace-shell-constants.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/services/helpers/workspace-shell-constants.ts index c1e38a4c3..30923b92a 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-shell/services/helpers/workspace-shell-constants.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-shell/services/helpers/workspace-shell-constants.ts @@ -1,6 +1,13 @@ import { PortalRailSection } from '@iptvnator/portal/shared/util'; -export const SEARCH_INPUT_DEBOUNCE_MS = 350; +/** + * The one debounce between a keystroke in the header search box and the + * applied term; pages that search on the applied term (global search + * included) run at once. 300 ms sits above the inter-key gap of steady + * typing, so a word typed in one go is applied once. + * See docs/architecture/performance-journeys.md (J4). + */ +export const SEARCH_INPUT_DEBOUNCE_MS = 300; /** * How long a purely local Xtream load (reading the saved catalog from SQLite) diff --git a/tools/performance/journey-baselines.json b/tools/performance/journey-baselines.json index 382d39762..ee8841485 100644 --- a/tools/performance/journey-baselines.json +++ b/tools/performance/journey-baselines.json @@ -112,6 +112,28 @@ "evidenceRun": "https://github.com/4gray/iptvnator/actions/runs/37184230956", "measuredWith": "pnpm run perf:journeys (ubuntu-latest, xvfb), identical in all 55 measured iterations of 11 master runs, 2026-10-03 to 2026-10-04" } + }, + "search": { + "renderer.ipcCallsPerSearch": { + "value": 1, + "unit": "calls", + "slack": 0, + "note": "guard only, not validated: no PR has yet shown that lowering this counter lowers the journey wall-clock (Principle 3)", + "updatedAt": "2026-10-12", + "evidencePr": 1899, + "evidenceRun": "https://github.com/4gray/iptvnator/actions/runs/38165392046", + "measuredWith": "pnpm run perf:journeys (ubuntu-latest, xvfb), identical in all 18 measured iterations of 3 runs of #1899 (attempts 1-3), and in all 816 J4 iterations of the 136 CI journey jobs since #1816" + }, + "renderer.sqlStatementsPerSearch": { + "value": 2, + "unit": "statements", + "slack": 0, + "note": "guard only, not validated: no PR has yet shown that lowering this counter lowers the journey wall-clock (Principle 3)", + "updatedAt": "2026-10-12", + "evidencePr": 1899, + "evidenceRun": "https://github.com/4gray/iptvnator/actions/runs/38165392046", + "measuredWith": "pnpm run perf:journeys (ubuntu-latest, xvfb), identical in all 18 measured iterations of 3 runs of #1899 (attempts 1-3), and in all 816 J4 iterations of the 136 CI journey jobs since #1816" + } } } } From ebb1ab34672b741880c71f2e5ff595b8b745c932 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:06:03 +0200 Subject: [PATCH 6/7] fix(portals): hide provider placeholder metadata (N/A, -, 0) in details and cards (#1923) * fix(portals): drop provider placeholder metadata at the mapping layer Stalker and Xtream portals send "N/A", "-" or an unrated "0" score for fields they have nothing for, and the detail templates rendered them as data. Normalize them where provider rows are mapped: createStalkerInfo, the shared VOD view-model adapters and the Xtream detail selection. Co-Authored-By: Claude Opus 5.5 * fix(portals): hide placeholder ratings on catalog cards formatGridRating passed a provider "N/A" through as the badge text and rendered an unrated zero as "0.0". Read the value through the shared readProviderRating so both hide the badge and resolveGridRating reaches the generic rating behind a placeholder rating_imdb. Co-Authored-By: Claude Opus 5.5 * fix(xtream): keep placeholder ratings off the sparse VOD fallback page A get_vod_info made only of placeholders is now stripped to sparse and routed to the fallback page, which read the top-level catalog ratings raw: "N/A" survived and a zero became "0.0". Read every candidate through readProviderRating so the next usable rating wins. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .changes/portals-hide-na-metadata.md | 6 + docs/architecture/embedded-inline-playback.md | 8 ++ docs/architecture/stalker-portal.md | 11 ++ .../xtream-portal-compatibility.md | 6 + .../grid-list/grid-list.component.spec.ts | 18 +++ .../grid-list/grid-list.component.ts | 22 ++-- .../src/lib/stalker-vod.utils.spec.ts | 76 ++++++++++++ .../data-access/src/lib/stalker-vod.utils.ts | 62 ++++++---- .../lib/stores/xtream-detail-metadata.spec.ts | 99 ++++++++++++++++ .../src/lib/stores/xtream-detail-metadata.ts | 98 ++++++++++++++++ .../lib/stores/xtream-vod-selection.spec.ts | 22 ++++ .../src/lib/stores/xtream-vod-selection.ts | 4 + .../src/lib/stores/xtream.store.spec.ts | 33 ++++++ .../src/lib/stores/xtream.store.ts | 4 + .../vod-details-fallback.util.spec.ts | 33 ++++++ .../vod-details/vod-details-fallback.util.ts | 21 ++-- libs/shared/interfaces/src/index.ts | 1 + .../src/lib/provider-metadata.util.spec.ts | 49 ++++++++ .../src/lib/provider-metadata.util.ts | 59 ++++++++++ .../src/lib/vod-details-adapters.spec.ts | 110 ++++++++++++++++++ .../src/lib/vod-details-adapters.ts | 53 +++++---- .../vod-details/vod-details.component.spec.ts | 44 ++++++- 22 files changed, 770 insertions(+), 69 deletions(-) create mode 100644 .changes/portals-hide-na-metadata.md create mode 100644 libs/portal/xtream/data-access/src/lib/stores/xtream-detail-metadata.spec.ts create mode 100644 libs/portal/xtream/data-access/src/lib/stores/xtream-detail-metadata.ts create mode 100644 libs/shared/interfaces/src/lib/provider-metadata.util.spec.ts create mode 100644 libs/shared/interfaces/src/lib/provider-metadata.util.ts diff --git a/.changes/portals-hide-na-metadata.md b/.changes/portals-hide-na-metadata.md new file mode 100644 index 000000000..baace44be --- /dev/null +++ b/.changes/portals-hide-na-metadata.md @@ -0,0 +1,6 @@ +--- +type: fix +area: portals +--- + +Movie and series details and catalog cards from Stalker and Xtream portals no longer show "N/A", "-" or a zero rating in place of missing information. Fields the provider left empty are simply hidden, and TMDB can now fill in the rating and release year for them. diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index 628a4ad89..64cbe46f9 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -157,6 +157,14 @@ Contracts: - Hosts provide hero chips/meta/actions as `*appDetailTags` / `*appDetailMeta` / `*appDetailActions` templates; the shell stamps them into the hero and again into the About block. Degradation stays "missing → not rendered". +- Provider stand-ins count as missing: `N/A` (any case), a lone `-`, and a + `0` IMDb/Kinopoisk score are dropped where provider rows are mapped, by + `readProviderText` / `readProviderRating` + (`libs/shared/interfaces/src/lib/provider-metadata.util.ts`) — in + `createStalkerInfo()`, the `vod-details-adapters.ts` normalizers and the + Xtream store's detail selection. Templates keep plain `@if` guards; do not + teach them the placeholder list. Widen the list only with evidence of a + provider sending the new stand-in. - The shell never wraps the `[detail-player]` slot in a conditional; the host's `@if (inlinePlayback())` is the only creator/destroyer of the player subtree, so shell state changes cannot recreate the `