mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(epg): clear guide search on scope change, match the active duplicate by url, keep failed coverage unknown
Search hits carry scope-local row ids, so a scope change drops them. Two playlist entries can share an id but not a stream, so the active row is matched by id + url before falling back to the id. A failed coverage query now rejects instead of answering an empty set, which the guide already treats as "coverage unknown" (every row stays visible). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
2ab44ebb19
commit
378e57add7
7 files changed
+66
-2
No files matched your search
@@ -443,6 +443,20 @@ describe('EpgGuideQueryService', () => {
|
||||
expect(getChannelMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rethrows a coverage failure after logging it', async () => {
|
||||
getChannelMetadata.mockResolvedValue({
|
||||
a: { id: 'a', displayName: 'A', iconUrl: null },
|
||||
});
|
||||
getDatabase.mockRejectedValue(new Error('locked'));
|
||||
await expect(
|
||||
service.getProgramCoverage({
|
||||
channelIds: ['a'],
|
||||
fromMs: FROM,
|
||||
toMs: TO,
|
||||
})
|
||||
).rejects.toThrow('locked');
|
||||
});
|
||||
|
||||
it('resolves keys through channel metadata and maps rows back onto every requested key', async () => {
|
||||
getChannelMetadata.mockResolvedValue({
|
||||
'ZDF HD': { id: 'zdf.de', displayName: 'ZDF HD', iconUrl: null },
|
||||
|
||||
@@ -207,7 +207,10 @@ export class EpgGuideQueryService {
|
||||
'Error loading guide coverage:',
|
||||
error
|
||||
);
|
||||
return [];
|
||||
// Unlike programmes, coverage must not fail soft: an empty answer
|
||||
// reads as "no channel has EPG" and the "Only with EPG" toggle
|
||||
// would hide every row. A rejection keeps coverage unknown.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -305,6 +305,7 @@ export default class EpgEvents {
|
||||
return answer;
|
||||
}
|
||||
|
||||
/** A thrown error rejects the renderer's `invoke`, which is the intended fail-open path for coverage. */
|
||||
private static async handleGetGuideCoverage(
|
||||
args: ElectronBridgeEpgGuideWindow
|
||||
): Promise<string[]> {
|
||||
|
||||
+27
@@ -164,6 +164,33 @@ describe('M3uEpgGuideSourceService', () => {
|
||||
expect((await service.loadCoverage(window)).size).toBe(0);
|
||||
});
|
||||
|
||||
it('marks the duplicate whose stream url matches the playing channel', () => {
|
||||
const first = makeChannel('dup');
|
||||
const second = {
|
||||
...makeChannel('dup'),
|
||||
url: 'https://example.com/second.m3u8',
|
||||
};
|
||||
channels.set([first, second]);
|
||||
activeChannel.set(second);
|
||||
expect(service.activeChannelId()).toBe('1:dup');
|
||||
activeChannel.set({
|
||||
...makeChannel('dup'),
|
||||
url: 'https://example.com/unknown.m3u8',
|
||||
});
|
||||
expect(service.activeChannelId()).toBe('0:dup');
|
||||
});
|
||||
|
||||
it('propagates a coverage failure so the guide keeps coverage unknown', async () => {
|
||||
getProgramCoverage.mockRejectedValue(new Error('bridge down'));
|
||||
await expect(
|
||||
service.loadCoverage({
|
||||
channels: service.channels(),
|
||||
fromMs: 1,
|
||||
toMs: 2,
|
||||
})
|
||||
).rejects.toThrow('bridge down');
|
||||
});
|
||||
|
||||
it('mirrors the active channel and dispatches playback on activate', () => {
|
||||
expect(service.activeChannelId()).toBe('0:a');
|
||||
service.activate('1:b');
|
||||
|
||||
@@ -166,8 +166,15 @@ export class M3uEpgGuideSourceService implements EpgGuideSource {
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
// Entries can share an id but not a stream: prefer the row whose
|
||||
// url matches too, so selecting the second copy shows it active.
|
||||
const rows = this.scopedRows();
|
||||
const exact = rows.find(
|
||||
(row) =>
|
||||
row.channel.id === active.id && row.channel.url === active.url
|
||||
);
|
||||
return (
|
||||
this.scopedRows().find((row) => row.channel.id === active.id)
|
||||
(exact ?? rows.find((row) => row.channel.id === active.id))
|
||||
?.rowId ?? null
|
||||
);
|
||||
});
|
||||
|
||||
@@ -217,6 +217,16 @@ describe('EpgGuideComponent', () => {
|
||||
expect(activate).toHaveBeenCalledWith('c');
|
||||
});
|
||||
|
||||
it('drops the programme search when the scope changes', async () => {
|
||||
await settle(fixture);
|
||||
component.onSearchQueryChange('news');
|
||||
expect(component.searchQuery()).toBe('news');
|
||||
component.setScope('all');
|
||||
expect(component.searchQuery()).toBe('');
|
||||
expect(component.searchResults()).toEqual([]);
|
||||
expect(setScope).toHaveBeenCalledWith('all');
|
||||
});
|
||||
|
||||
it('closes on Escape and steps days with PageUp/PageDown', async () => {
|
||||
await settle(fixture);
|
||||
const close = jest.fn();
|
||||
|
||||
@@ -295,6 +295,8 @@ export class EpgGuideComponent implements OnDestroy {
|
||||
setScope(scopeId: string): void {
|
||||
this.source.setScope(scopeId);
|
||||
this.focus.set(null);
|
||||
// Search hits carry scope-local row ids, so none may outlive the scope.
|
||||
this.search.setQuery('');
|
||||
}
|
||||
|
||||
setOnlyWithEpg(value: boolean): void {
|
||||
|
||||
Reference in new issue
Block a user