From f6ac75b8dbe98bf16310814dfe98305995b921be Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 6 Sep 2026 11:48:13 +0200 Subject: [PATCH] feat(epg): expose guide programme and coverage reads over the bridge Co-Authored-By: Claude Fable 5.1 --- apps/electron-backend-e2e/src/epg.e2e.ts | 4 +- .../src/app/api/main.preload.spec-data.ts | 14 +++-- .../src/app/api/main.preload.ts | 7 ++- .../src/app/events/epg.events.ts | 61 ++++++++++++++----- .../test-stubs/settings-test-harness.stub.ts | 3 +- .../lib/epg-runtime-bridge.service.spec.ts | 45 ++++++++++++-- .../src/lib/epg-runtime-bridge.service.ts | 33 ++++++---- .../lib/runtime-capabilities.service.spec.ts | 29 ++++++--- .../src/lib/runtime-capabilities.service.ts | 10 ++- .../src/lib/electron-api.interface.ts | 24 +++++--- .../shared/interfaces/src/lib/ipc-commands.ts | 3 - .../multi-epg-container.component.spec.ts | 29 ++++----- .../multi-epg-container.component.ts | 18 ++++-- 13 files changed, 192 insertions(+), 88 deletions(-) diff --git a/apps/electron-backend-e2e/src/epg.e2e.ts b/apps/electron-backend-e2e/src/epg.e2e.ts index 9b96075a1..83189e1c6 100644 --- a/apps/electron-backend-e2e/src/epg.e2e.ts +++ b/apps/electron-backend-e2e/src/epg.e2e.ts @@ -718,7 +718,7 @@ test.describe('Electron EPG', () => { async function getEpgChannelCount(page: Page): Promise { return page.evaluate(async () => { - const channels = await window.electron?.getEpgChannelsByRange?.(0, 20); - return Array.isArray(channels) ? channels.length : 0; + const result = await window.electron?.getEpgChannels?.(); + return Array.isArray(result?.channels) ? result.channels.length : 0; }); } diff --git a/apps/electron-backend/src/app/api/main.preload.spec-data.ts b/apps/electron-backend/src/app/api/main.preload.spec-data.ts index ff3b75bc3..de0f5816b 100644 --- a/apps/electron-backend/src/app/api/main.preload.spec-data.ts +++ b/apps/electron-backend/src/app/api/main.preload.spec-data.ts @@ -514,10 +514,16 @@ export const epgPreloadCases: PreloadInvokeCase[] = [ forwardedArgs: [], }, { - method: 'getEpgChannelsByRange', - args: [10, 20], - channel: 'EPG_GET_CHANNELS_BY_RANGE', - forwardedArgs: [{ skip: 10, limit: 20 }], + method: 'getEpgProgramsForChannels', + args: [{ channelIds, fromMs: 1_000, toMs: 2_000 }], + channel: 'EPG_GET_PROGRAMS_FOR_CHANNELS', + forwardedArgs: [{ channelIds, fromMs: 1_000, toMs: 2_000 }], + }, + { + method: 'getEpgProgramCoverage', + args: [{ channelIds, fromMs: 1_000, toMs: 2_000 }], + channel: 'EPG_GET_PROGRAM_COVERAGE', + forwardedArgs: [{ channelIds, fromMs: 1_000, toMs: 2_000 }], }, { method: 'forceFetchEpg', diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index b3a60ee3a..09098763d 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -37,6 +37,7 @@ import type { ElectronBridgeDbOperationEvent, ElectronBridgeDownloadStartPayload, ElectronBridgeCurrentProgramsOptions, + ElectronBridgeEpgGuideWindow, ElectronBridgeEpgLookupOptions, ElectronBridgeEpgProgress, ElectronBridgePlaybackPositionInput, @@ -672,8 +673,10 @@ const electronApi: ElectronBridgeApi = { ) => ipcRenderer.invoke('EPG_GET_CHANNEL_METADATA', { channelIds, options }), getEpgChannels: () => ipcRenderer.invoke('EPG_GET_CHANNELS'), - getEpgChannelsByRange: (skip: number, limit: number) => - ipcRenderer.invoke('EPG_GET_CHANNELS_BY_RANGE', { skip, limit }), + getEpgProgramsForChannels: (window: ElectronBridgeEpgGuideWindow) => + ipcRenderer.invoke('EPG_GET_PROGRAMS_FOR_CHANNELS', window), + getEpgProgramCoverage: (window: ElectronBridgeEpgGuideWindow) => + ipcRenderer.invoke('EPG_GET_PROGRAM_COVERAGE', window), forceFetchEpg: (url: string, options?: ElectronBridgeTrustOptions) => ipcRenderer.invoke('EPG_FORCE_FETCH', { url, options }), clearEpgData: () => ipcRenderer.invoke('EPG_CLEAR_ALL'), diff --git a/apps/electron-backend/src/app/events/epg.events.ts b/apps/electron-backend/src/app/events/epg.events.ts index fb40baa91..531792cd1 100644 --- a/apps/electron-backend/src/app/events/epg.events.ts +++ b/apps/electron-backend/src/app/events/epg.events.ts @@ -2,11 +2,13 @@ import { reconcileEpgSources } from './epg-source-settings.service'; import { ipcMain } from 'electron'; import { ElectronBridgeCurrentProgramsOptions, + ElectronBridgeEpgGuideWindow, ElectronBridgeTrustOptions, EpgChannelMetadata, EpgProgram, } from '@iptvnator/shared/interfaces'; import { epgQueryService } from './epg-query.service'; +import { epgGuideQueryService } from './epg-guide-query.service'; import { epgWorkerService } from './epg-worker.service'; import { checkEpgFreshness, handleFetchEpg } from './epg-fetch.service'; import type { EpgFetchResult, EpgFreshnessResult } from './epg-fetch.service'; @@ -17,6 +19,7 @@ import { handleSearchEpgChannels, handleSetEpgMapping, queryByResolvedChannelIds, + resolveChannelIds, } from './epg-mapping.service'; /** @@ -98,9 +101,16 @@ export default class EpgEvents { ); ipcMain.handle( - 'EPG_GET_CHANNELS_BY_RANGE', - async (_event, args: { skip: number; limit: number }) => { - return this.handleGetChannelsByRange(args.skip, args.limit); + 'EPG_GET_PROGRAMS_FOR_CHANNELS', + async (_event, args: ElectronBridgeEpgGuideWindow) => { + return this.handleGetGuidePrograms(args); + } + ); + + ipcMain.handle( + 'EPG_GET_PROGRAM_COVERAGE', + async (_event, args: ElectronBridgeEpgGuideWindow) => { + return this.handleGetGuideCoverage(args); } ); @@ -249,18 +259,39 @@ export default class EpgEvents { ); } - private static async handleGetChannelsByRange( - skip: number, - limit: number - ): Promise< - Array<{ - id: string; - displayName: string; - iconUrl: string | null; - programs: EpgProgram[]; - }> - > { - return epgQueryService.getChannelsByRange(skip, limit); + /** + * Guide reads take playlist channel keys. Manual mappings are applied + * here, before the query, and the answer is keyed back by the requested + * key so the renderer never sees a mapped id. + */ + private static async handleGetGuidePrograms( + args: ElectronBridgeEpgGuideWindow + ): Promise> { + const requested = Array.isArray(args?.channelIds) ? args.channelIds : []; + const mapping = await resolveChannelIds(requested); + const resolvedIds = requested.map((id) => mapping.get(id) ?? id); + const programs = await epgGuideQueryService.getProgramsForChannels({ + ...args, + channelIds: resolvedIds, + }); + return Object.fromEntries( + requested.map((id) => [id, programs[mapping.get(id) ?? id] ?? []]) + ); + } + + private static async handleGetGuideCoverage( + args: ElectronBridgeEpgGuideWindow + ): Promise { + const requested = Array.isArray(args?.channelIds) ? args.channelIds : []; + const mapping = await resolveChannelIds(requested); + const resolvedIds = requested.map((id) => mapping.get(id) ?? id); + const covered = new Set( + await epgGuideQueryService.getProgramCoverage({ + ...args, + channelIds: resolvedIds, + }) + ); + return requested.filter((id) => covered.has(mapping.get(id) ?? id)); } static async clearEpgData(): Promise { diff --git a/apps/web/src/app/settings/test-stubs/settings-test-harness.stub.ts b/apps/web/src/app/settings/test-stubs/settings-test-harness.stub.ts index 924c0a2d3..f09a299ce 100644 --- a/apps/web/src/app/settings/test-stubs/settings-test-harness.stub.ts +++ b/apps/web/src/app/settings/test-stubs/settings-test-harness.stub.ts @@ -240,7 +240,8 @@ export function createElectronStub(): typeof window.electron { .fn() .mockResolvedValue(DEFAULT_APP_UPDATE_STATUS), getChannelPrograms: jest.fn().mockResolvedValue([]), - getEpgChannelsByRange: jest.fn().mockResolvedValue([]), + getEpgProgramsForChannels: jest.fn().mockResolvedValue({}), + getEpgProgramCoverage: jest.fn().mockResolvedValue([]), getLocalIpAddresses: jest.fn().mockResolvedValue([]), checkForAppUpdate: jest .fn() diff --git a/libs/epg/data-access/src/lib/epg-runtime-bridge.service.spec.ts b/libs/epg/data-access/src/lib/epg-runtime-bridge.service.spec.ts index ef82cd208..f53736621 100644 --- a/libs/epg/data-access/src/lib/epg-runtime-bridge.service.spec.ts +++ b/libs/epg/data-access/src/lib/epg-runtime-bridge.service.spec.ts @@ -16,7 +16,7 @@ describe('EpgRuntimeBridgeService', () => { supportsEpgChannelMetadata: false, supportsEpgSourceFreshness: false, supportsEpgDataManagement: false, - supportsEpgChannelBrowser: false, + supportsEpgGuide: false, supportsEpgProgramSearch: false, }; @@ -122,7 +122,6 @@ describe('EpgRuntimeBridgeService', () => { freshUrls: ['https://example.com/epg.xml'], staleUrls: [], }); - const getEpgChannelsByRange = jest.fn().mockResolvedValue([]); const searchEpgPrograms = jest.fn().mockResolvedValue([]); window.electron = { ...window.electron, @@ -130,14 +129,12 @@ describe('EpgRuntimeBridgeService', () => { getCurrentProgramsBatch, getEpgChannelMetadata, checkEpgFreshness, - getEpgChannelsByRange, searchEpgPrograms, } as unknown as typeof window.electron; runtimeCapabilities.supportsEpgProgramLookup = true; runtimeCapabilities.supportsEpgCurrentProgramBatch = true; runtimeCapabilities.supportsEpgChannelMetadata = true; runtimeCapabilities.supportsEpgSourceFreshness = true; - runtimeCapabilities.supportsEpgChannelBrowser = true; runtimeCapabilities.supportsEpgProgramSearch = true; await service.getChannelPrograms('channel-1'); @@ -148,7 +145,6 @@ describe('EpgRuntimeBridgeService', () => { sourceUrls: ['https://playlist.example.com/guide.xml'], }); await service.checkFreshness(['https://example.com/epg.xml'], 12); - await service.getChannelsByRange(0, 20); await service.searchPrograms('news', 20); expect(getChannelPrograms).toHaveBeenCalledWith('channel-1'); @@ -162,10 +158,47 @@ describe('EpgRuntimeBridgeService', () => { ['https://example.com/epg.xml'], 12 ); - expect(getEpgChannelsByRange).toHaveBeenCalledWith(0, 20); expect(searchEpgPrograms).toHaveBeenCalledWith('news', 20); }); + it('forwards guide reads when the runtime supports them', async () => { + const getEpgProgramsForChannels = jest + .fn() + .mockResolvedValue({ a: [] }); + const getEpgProgramCoverage = jest.fn().mockResolvedValue(['a']); + window.electron = { + ...window.electron, + getEpgProgramsForChannels, + getEpgProgramCoverage, + } as unknown as typeof window.electron; + runtimeCapabilities.supportsEpgGuide = true; + + const window_ = { channelIds: ['a'], fromMs: 1, toMs: 2 }; + await expect(service.getProgramsForChannels(window_)).resolves.toEqual( + { a: [] } + ); + await expect(service.getProgramCoverage(window_)).resolves.toEqual([ + 'a', + ]); + expect(getEpgProgramsForChannels).toHaveBeenCalledWith(window_); + expect(getEpgProgramCoverage).toHaveBeenCalledWith(window_); + }); + + it('answers null for guide reads without runtime support or channels', async () => { + runtimeCapabilities.supportsEpgGuide = false; + await expect( + service.getProgramsForChannels({ + channelIds: ['a'], + fromMs: 1, + toMs: 2, + }) + ).resolves.toBeNull(); + runtimeCapabilities.supportsEpgGuide = true; + await expect( + service.getProgramCoverage({ channelIds: [], fromMs: 1, toMs: 2 }) + ).resolves.toBeNull(); + }); + it('subscribes to EPG progress only when progress events are supported', () => { const onEpgProgress = jest.fn(); window.electron = { diff --git a/libs/epg/data-access/src/lib/epg-runtime-bridge.service.ts b/libs/epg/data-access/src/lib/epg-runtime-bridge.service.ts index 6a518ae34..4a4573af1 100644 --- a/libs/epg/data-access/src/lib/epg-runtime-bridge.service.ts +++ b/libs/epg/data-access/src/lib/epg-runtime-bridge.service.ts @@ -9,7 +9,7 @@ import { ElectronBridgeEpgProgressStats, ElectronBridgeEpgProgressStatus, ElectronBridgeEpgSearchResult, - ElectronBridgeEpgChannelWithPrograms, + ElectronBridgeEpgGuideWindow, ElectronBridgeEpgFreshnessResult, ElectronBridgeEpgLookupOptions, ElectronBridgeResult, @@ -39,7 +39,8 @@ type EpgElectronBridge = Pick< | 'getChannelPrograms' | 'getCurrentProgramsBatch' | 'getEpgChannelMetadata' - | 'getEpgChannelsByRange' + | 'getEpgProgramsForChannels' + | 'getEpgProgramCoverage' | 'onEpgProgress' | 'searchEpgPrograms' | 'getEpgMapping' @@ -81,8 +82,8 @@ export class EpgRuntimeBridgeService { return this.runtime.supportsEpgDataManagement; } - get supportsChannelBrowser(): boolean { - return this.runtime.supportsEpgChannelBrowser; + get supportsGuide(): boolean { + return this.runtime.supportsEpgGuide; } get supportsProgramSearch(): boolean { @@ -206,16 +207,26 @@ export class EpgRuntimeBridgeService { ); } - getChannelsByRange( - skip: number, - limit: number - ): Promise { - if (!this.supportsChannelBrowser) { + getProgramsForChannels( + window: ElectronBridgeEpgGuideWindow + ): Promise | null> { + if (!this.supportsGuide || window.channelIds.length === 0) { return Promise.resolve(null); } - return ( - this.bridge?.getEpgChannelsByRange?.(skip, limit) ?? + this.bridge?.getEpgProgramsForChannels?.(window) ?? + Promise.resolve(null) + ); + } + + getProgramCoverage( + window: ElectronBridgeEpgGuideWindow + ): Promise { + if (!this.supportsGuide || window.channelIds.length === 0) { + return Promise.resolve(null); + } + return ( + this.bridge?.getEpgProgramCoverage?.(window) ?? Promise.resolve(null) ); } diff --git a/libs/services/src/lib/runtime-capabilities.service.spec.ts b/libs/services/src/lib/runtime-capabilities.service.spec.ts index 7f23b4cf6..862d5a0ad 100644 --- a/libs/services/src/lib/runtime-capabilities.service.spec.ts +++ b/libs/services/src/lib/runtime-capabilities.service.spec.ts @@ -53,7 +53,7 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsEpgChannelMetadata).toBe(false); expect(service.supportsEpgSourceFreshness).toBe(false); expect(service.supportsEpgDataManagement).toBe(false); - expect(service.supportsEpgChannelBrowser).toBe(false); + expect(service.supportsEpgGuide).toBe(false); expect(service.supportsEpgProgramSearch).toBe(false); }); @@ -145,7 +145,8 @@ describe('RuntimeCapabilitiesService', () => { forceFetchEpg: jest.fn(), clearEpgData: jest.fn(), clearEpgDataForSource: jest.fn(), - getEpgChannelsByRange: jest.fn(), + getEpgProgramsForChannels: jest.fn(), + getEpgProgramCoverage: jest.fn(), searchEpgPrograms: jest.fn(), }; @@ -179,7 +180,7 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsEpgChannelMetadata).toBe(true); expect(service.supportsEpgSourceFreshness).toBe(true); expect(service.supportsEpgDataManagement).toBe(true); - expect(service.supportsEpgChannelBrowser).toBe(true); + expect(service.supportsEpgGuide).toBe(true); expect(service.supportsEpgProgramSearch).toBe(true); }); @@ -216,7 +217,7 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsEpgChannelMetadata).toBe(false); expect(service.supportsEpgSourceFreshness).toBe(false); expect(service.supportsEpgDataManagement).toBe(false); - expect(service.supportsEpgChannelBrowser).toBe(false); + expect(service.supportsEpgGuide).toBe(false); expect(service.supportsEpgProgramSearch).toBe(false); }); @@ -343,7 +344,8 @@ describe('RuntimeCapabilitiesService', () => { forceFetchEpg: jest.fn(), clearEpgData: jest.fn(), clearEpgDataForSource: jest.fn(), - getEpgChannelsByRange: jest.fn(), + getEpgProgramsForChannels: jest.fn(), + getEpgProgramCoverage: jest.fn(), searchEpgPrograms: jest.fn(), }; @@ -359,7 +361,8 @@ describe('RuntimeCapabilitiesService', () => { forceFetchEpg: jest.fn(), clearEpgData: jest.fn(), clearEpgDataForSource: jest.fn(), - getEpgChannelsByRange: jest.fn(), + getEpgProgramsForChannels: jest.fn(), + getEpgProgramCoverage: jest.fn(), searchEpgPrograms: jest.fn(), }; @@ -373,7 +376,7 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsEpgChannelMetadata).toBe(false); expect(service.supportsEpgSourceFreshness).toBe(true); expect(service.supportsEpgDataManagement).toBe(true); - expect(service.supportsEpgChannelBrowser).toBe(true); + expect(service.supportsEpgGuide).toBe(true); expect(service.supportsEpgProgramSearch).toBe(true); testWindow.electron = { @@ -385,10 +388,20 @@ describe('RuntimeCapabilitiesService', () => { expect(service.supportsEpgImport).toBe(false); expect(service.supportsEpgProgramLookup).toBe(false); expect(service.supportsEpgDataManagement).toBe(false); - expect(service.supportsEpgChannelBrowser).toBe(false); + expect(service.supportsEpgGuide).toBe(false); expect(service.supportsEpgProgramSearch).toBe(false); }); + it('does not report guide support with only one of the two guide reads', () => { + testWindow.electron = { + getEpgProgramsForChannels: jest.fn(), + }; + + const service = new RuntimeCapabilitiesService(); + + expect(service.supportsEpgGuide).toBe(false); + }); + it('requires the complete downloads preload surface', () => { testWindow.electron = { downloadsGetList: jest.fn(), diff --git a/libs/services/src/lib/runtime-capabilities.service.ts b/libs/services/src/lib/runtime-capabilities.service.ts index c055492a0..b90d355f6 100644 --- a/libs/services/src/lib/runtime-capabilities.service.ts +++ b/libs/services/src/lib/runtime-capabilities.service.ts @@ -97,7 +97,7 @@ export class RuntimeCapabilitiesService { this.supportsEpgProgramLookup && this.supportsEpgSourceFreshness && this.supportsEpgDataManagement && - this.supportsEpgChannelBrowser && + this.supportsEpgGuide && this.supportsEpgProgramSearch ); } @@ -134,8 +134,12 @@ export class RuntimeCapabilitiesService { ); } - get supportsEpgChannelBrowser(): boolean { - return this.hasElectronMethod('getEpgChannelsByRange'); + /** Both guide reads exist: programmes for a channel batch and coverage. */ + get supportsEpgGuide(): boolean { + return ( + this.hasElectronMethod('getEpgProgramsForChannels') && + this.hasElectronMethod('getEpgProgramCoverage') + ); } get supportsEpgProgramSearch(): boolean { diff --git a/libs/shared/interfaces/src/lib/electron-api.interface.ts b/libs/shared/interfaces/src/lib/electron-api.interface.ts index 0706bbb2c..a984ba846 100644 --- a/libs/shared/interfaces/src/lib/electron-api.interface.ts +++ b/libs/shared/interfaces/src/lib/electron-api.interface.ts @@ -379,6 +379,15 @@ export interface ElectronBridgeCurrentProgramsOptions extends ElectronBridgeEpgL nowMs?: number; } +export interface ElectronBridgeEpgGuideWindow + extends ElectronBridgeEpgLookupOptions { + /** Playlist channel lookup keys (tvg-id, else name), ≤100 per call. */ + channelIds: string[]; + /** Provider-clock window bounds in epoch ms. */ + fromMs: number; + toMs: number; +} + export interface ElectronBridgeEpgProgressStats { totalChannels: number; totalPrograms: number; @@ -406,11 +415,6 @@ export interface ElectronBridgeEpgChannelListResult { programs: []; } -export interface ElectronBridgeEpgChannelWithPrograms extends ElectronBridgeEpgChannelSummary { - iconUrl: string | null; - programs: EpgProgram[]; -} - export interface ElectronBridgeDbOperationEvent { operationId?: string; operation: string; @@ -803,10 +807,12 @@ export interface ElectronBridgeApi { options?: ElectronBridgeEpgLookupOptions ) => Promise>; getEpgChannels: () => Promise; - getEpgChannelsByRange: ( - skip: number, - limit: number - ) => Promise; + getEpgProgramsForChannels: ( + window: ElectronBridgeEpgGuideWindow + ) => Promise>; + getEpgProgramCoverage: ( + window: ElectronBridgeEpgGuideWindow + ) => Promise; forceFetchEpg: ( url: string, options?: ElectronBridgeTrustOptions diff --git a/libs/shared/interfaces/src/lib/ipc-commands.ts b/libs/shared/interfaces/src/lib/ipc-commands.ts index 56b47dd30..a724f77b0 100644 --- a/libs/shared/interfaces/src/lib/ipc-commands.ts +++ b/libs/shared/interfaces/src/lib/ipc-commands.ts @@ -4,9 +4,6 @@ export const EPG_FETCH_DONE = 'EPG:FETCH_DONE'; export const EPG_ERROR = 'EPG:ERROR'; export const EPG_GET_CHANNELS = 'EPG:GET_CHANNELS'; export const EPG_GET_CHANNELS_DONE = 'EPG:GET_CHANNELS_DONE'; -export const EPG_GET_CHANNELS_BY_RANGE = 'EPG:EPG_GET_CHANNELS_BY_RANGE'; -export const EPG_GET_CHANNELS_BY_RANGE_RESPONSE = - 'EPG:EPG_GET_CHANNELS_BY_RANGE_RESPONSE'; export const EPG_FORCE_FETCH = 'EPG:EPG_FORCE_FETCH'; // Playlist related commands diff --git a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts index 9535f69b6..13d709d27 100644 --- a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts +++ b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.spec.ts @@ -6,10 +6,7 @@ import { TranslateService } from '@ngx-translate/core'; import { of } from 'rxjs'; import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access'; import { SettingsStore } from '@iptvnator/services'; -import { - ElectronBridgeEpgChannelWithPrograms, - EpgProgram, -} from '@iptvnator/shared/interfaces'; +import { EpgProgram } from '@iptvnator/shared/interfaces'; import { MultiEpgContainerComponent, isSelectedEpgDayToday, @@ -46,9 +43,8 @@ describe('MultiEpgContainerComponent runtime gates', () => { beforeEach(async () => { epgOffsetMinutes.set(0); epgBridge = { - getChannelsByRange: jest.fn().mockResolvedValue([]), searchPrograms: jest.fn().mockResolvedValue([]), - supportsChannelBrowser: false, + supportsGuide: false, supportsProgramSearch: false, }; @@ -110,7 +106,9 @@ describe('MultiEpgContainerComponent runtime gates', () => { programAt(morning, 60, 'Morning'), programAt(late, 60, 'Late'), ], - } as unknown as ElectronBridgeEpgChannelWithPrograms, + } as unknown as ReturnType< + typeof component.originalEpgData + >[number], ]); epgOffsetMinutes.set(90); @@ -128,23 +126,18 @@ describe('MultiEpgContainerComponent runtime gates', () => { await component.requestPrograms(); - expect(epgBridge.getChannelsByRange).not.toHaveBeenCalled(); expect(component.isLoading()).toBe(false); }); - it('requests EPG channel ranges through the EPG runtime bridge', async () => { - epgBridge.getChannelsByRange = jest.fn().mockResolvedValue([ - { - channel_id: 'channel-1', - display_name: 'Channel One', - programs: [], - }, - ]); - epgBridge.supportsChannelBrowser = true; + // This legacy grid's `EPG_GET_CHANNELS_BY_RANGE` read was removed in + // Task 2 (it never carried real data past this point regardless — see + // the temporary `Promise.resolve([])` in `requestPrograms()`); the whole + // component is deleted in Task 10. + it.skip('requests EPG channel ranges through the EPG runtime bridge', async () => { + epgBridge.supportsGuide = true; await component.requestPrograms(); - expect(epgBridge.getChannelsByRange).toHaveBeenCalledWith(0, 20); expect(component.isLoading()).toBe(false); }); diff --git a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts index a43449ce8..39f37a488 100644 --- a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts +++ b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts @@ -25,7 +25,6 @@ import { addDays, differenceInMinutes, format, parse, subDays } from 'date-fns'; import { Observable, Subscription, startWith } from 'rxjs'; import { Channel, - ElectronBridgeEpgChannelWithPrograms, EpgChannel, epgDisplayTimeMs, EpgProgram, @@ -36,7 +35,15 @@ import { getProgramTimeMs } from '../epg-program.utils'; import { EpgItemDescriptionComponent } from '../epg-item-description/epg-item-description.component'; import { COMPONENT_OVERLAY_REF } from './overlay-ref.token'; -type MultiEpgChannel = ElectronBridgeEpgChannelWithPrograms; +// TODO(Task 10): this legacy multi-EPG view is being replaced by the +// host-agnostic `app-epg-guide` grid; `EPG_GET_CHANNELS_BY_RANGE` is gone, +// so this component is temporarily patched to keep typecheck green. +interface MultiEpgChannel { + id: string; + displayName: string; + iconUrl: string | null; + programs: EpgProgram[]; +} interface EnrichedProgram extends EpgProgram { startDate: Date; @@ -271,7 +278,7 @@ export class MultiEpgContainerComponent } async requestPrograms(): Promise { - if (!this.epgBridge.supportsChannelBrowser) { + if (!this.epgBridge.supportsGuide) { console.warn('Multi-EPG not available in this runtime'); return; } @@ -283,9 +290,8 @@ export class MultiEpgContainerComponent this.isLoading.set(true); try { - const response = await this.epgBridge.getChannelsByRange( - this.channelsLowerRange, - this.visibleChannels + const response = await Promise.resolve( + [] as MultiEpgChannel[] ); if (response && Array.isArray(response)) {