mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(epg): avoid duplicate track keys for archive rows (#1096)
* fix(epg): avoid duplicate track keys for archive rows * refactor(epg): extract epg list helpers
This commit is contained in:
1 parent
148affdfca
commit
a84090026b
4 files changed
+138
-57
No files matched your search
@@ -35,7 +35,7 @@
|
||||
<!-- program list -->
|
||||
<div id="program-list" class="app-scrollbar" #programList>
|
||||
@if (items.length > 0) {
|
||||
@for (program of items; track program.start) {
|
||||
@for (program of items; track trackProgram($index, program)) {
|
||||
@if (!canActivateProgram(program)) {
|
||||
<div
|
||||
class="program-item"
|
||||
|
||||
@@ -266,6 +266,61 @@ describe('EpgListComponent', () => {
|
||||
expect(emitted).toEqual([{ program: programs[0], type: 'timeshift' }]);
|
||||
});
|
||||
|
||||
it('renders duplicate-start programs without duplicate track-key errors and keeps clicks bound to each row', () => {
|
||||
const programs = [
|
||||
buildProgram(
|
||||
'duplicate-a',
|
||||
'Duplicate Start A',
|
||||
'2026-04-05T09:00:00.000Z',
|
||||
'2026-04-05T09:30:00.000Z'
|
||||
),
|
||||
buildProgram(
|
||||
'duplicate-b',
|
||||
'Duplicate Start B',
|
||||
'2026-04-05T09:00:00.000Z',
|
||||
'2026-04-05T10:00:00.000Z'
|
||||
),
|
||||
buildProgram(
|
||||
'current',
|
||||
'Current Show',
|
||||
'2026-04-05T11:30:00.000Z',
|
||||
'2026-04-05T12:30:00.000Z'
|
||||
),
|
||||
];
|
||||
const emitted: unknown[] = [];
|
||||
const consoleErrorSpy = jest
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => undefined);
|
||||
component.programActivated.subscribe((event) => emitted.push(event));
|
||||
fixture.componentRef.setInput('controlledPrograms', programs);
|
||||
fixture.componentRef.setInput('archivePlaybackAvailable', true);
|
||||
|
||||
try {
|
||||
expect(component.trackProgram(0, programs[0])).not.toBe(
|
||||
component.trackProgram(1, programs[1])
|
||||
);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const duplicateTrackKeyError = consoleErrorSpy.mock.calls.some(
|
||||
(args) => args.some((arg) => String(arg).includes('NG0955'))
|
||||
);
|
||||
const rows =
|
||||
fixture.nativeElement.querySelectorAll<HTMLElement>(
|
||||
'.program-item.clickable'
|
||||
);
|
||||
rows[1].click();
|
||||
|
||||
expect(duplicateTrackKeyError).toBe(false);
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(emitted).toEqual([
|
||||
{ program: programs[1], type: 'timeshift' },
|
||||
]);
|
||||
} finally {
|
||||
consoleErrorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('updates the selected-day header when the app language changes', () => {
|
||||
fixture.componentRef.setInput('controlledPrograms', buildPrograms());
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -20,16 +20,22 @@ import { normalizeDateLocale } from '@iptvnator/pipes';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { EpgActions, selectActive } from '@iptvnator/m3u-state';
|
||||
import { format, subDays } from 'date-fns';
|
||||
import { subDays } from 'date-fns';
|
||||
import { startWith } from 'rxjs';
|
||||
import { Channel, EpgChannel, EpgProgram } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
EPG_DATE_KEY_FORMAT,
|
||||
EpgDateNavigationDirection,
|
||||
getTodayEpgDateKey,
|
||||
shiftEpgDateKey,
|
||||
} from '../epg-date';
|
||||
import { EpgListItemComponent } from './epg-list-item/epg-list-item.component';
|
||||
import {
|
||||
areProgramsSame,
|
||||
buildScrollContextKey,
|
||||
getProgramDateKey,
|
||||
getProgramTimeMs,
|
||||
trackProgram,
|
||||
} from './epg-list.utils';
|
||||
|
||||
export interface EpgProgramActivationEvent {
|
||||
program: EpgProgram;
|
||||
@@ -62,6 +68,7 @@ export class EpgListComponent {
|
||||
readonly showDateNavigator = input(true);
|
||||
readonly programActivated = output<EpgProgramActivationEvent>();
|
||||
readonly selectedDateChange = output<string>();
|
||||
readonly trackProgram = trackProgram;
|
||||
|
||||
private readonly store = inject(Store);
|
||||
private readonly epgService = inject(EpgService);
|
||||
@@ -371,57 +378,3 @@ export class EpgListComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildScrollContextKey(
|
||||
channel: Channel | null,
|
||||
programs: EpgProgram[]
|
||||
): string | null {
|
||||
if (!channel && programs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const channelKey =
|
||||
channel?.tvg?.id || channel?.name || channel?.url || 'unknown-channel';
|
||||
const programKey = programs
|
||||
.map(
|
||||
(program) =>
|
||||
`${getProgramTimeMs(program.start, program.startTimestamp)}-${getProgramTimeMs(program.stop, program.stopTimestamp)}`
|
||||
)
|
||||
.join('|');
|
||||
|
||||
return `${channelKey}:${programKey}`;
|
||||
}
|
||||
|
||||
function getProgramTimeMs(
|
||||
isoValue: string,
|
||||
timestampValue?: number | null
|
||||
): number {
|
||||
if (Number.isFinite(timestampValue) && Number(timestampValue) > 0) {
|
||||
return Number(timestampValue) * 1000;
|
||||
}
|
||||
|
||||
return Date.parse(isoValue);
|
||||
}
|
||||
|
||||
function getProgramDateKey(
|
||||
isoValue: string,
|
||||
timestampValue?: number | null
|
||||
): string {
|
||||
const programTimeMs = getProgramTimeMs(isoValue, timestampValue);
|
||||
|
||||
if (!Number.isFinite(programTimeMs)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return format(new Date(programTimeMs), EPG_DATE_KEY_FORMAT);
|
||||
}
|
||||
|
||||
function areProgramsSame(left: EpgProgram, right: EpgProgram): boolean {
|
||||
return (
|
||||
(left.channel ?? '') === (right.channel ?? '') &&
|
||||
getProgramTimeMs(left.start, left.startTimestamp) ===
|
||||
getProgramTimeMs(right.start, right.startTimestamp) &&
|
||||
getProgramTimeMs(left.stop, left.stopTimestamp) ===
|
||||
getProgramTimeMs(right.stop, right.stopTimestamp)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { format } from 'date-fns';
|
||||
import type { Channel, EpgProgram } from '@iptvnator/shared/interfaces';
|
||||
import { EPG_DATE_KEY_FORMAT } from '../epg-date';
|
||||
|
||||
export function trackProgram(index: number, program: EpgProgram): string {
|
||||
const start = getProgramTimeMs(program.start, program.startTimestamp);
|
||||
const stop = getProgramTimeMs(program.stop, program.stopTimestamp);
|
||||
|
||||
return [
|
||||
program.channel ?? '',
|
||||
Number.isFinite(start) ? start : program.start,
|
||||
Number.isFinite(stop) ? stop : program.stop,
|
||||
program.title ?? '',
|
||||
index,
|
||||
].join('|');
|
||||
}
|
||||
|
||||
export function buildScrollContextKey(
|
||||
channel: Channel | null,
|
||||
programs: EpgProgram[]
|
||||
): string | null {
|
||||
if (!channel && programs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const channelKey =
|
||||
channel?.tvg?.id || channel?.name || channel?.url || 'unknown-channel';
|
||||
const programKey = programs
|
||||
.map(
|
||||
(program) =>
|
||||
`${getProgramTimeMs(program.start, program.startTimestamp)}-${getProgramTimeMs(program.stop, program.stopTimestamp)}`
|
||||
)
|
||||
.join('|');
|
||||
|
||||
return `${channelKey}:${programKey}`;
|
||||
}
|
||||
|
||||
export function getProgramTimeMs(
|
||||
isoValue: string,
|
||||
timestampValue?: number | null
|
||||
): number {
|
||||
if (Number.isFinite(timestampValue) && Number(timestampValue) > 0) {
|
||||
return Number(timestampValue) * 1000;
|
||||
}
|
||||
|
||||
return Date.parse(isoValue);
|
||||
}
|
||||
|
||||
export function getProgramDateKey(
|
||||
isoValue: string,
|
||||
timestampValue?: number | null
|
||||
): string {
|
||||
const programTimeMs = getProgramTimeMs(isoValue, timestampValue);
|
||||
|
||||
if (!Number.isFinite(programTimeMs)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return format(new Date(programTimeMs), EPG_DATE_KEY_FORMAT);
|
||||
}
|
||||
|
||||
export function areProgramsSame(
|
||||
left: EpgProgram,
|
||||
right: EpgProgram
|
||||
): boolean {
|
||||
return (
|
||||
(left.channel ?? '') === (right.channel ?? '') &&
|
||||
getProgramTimeMs(left.start, left.startTimestamp) ===
|
||||
getProgramTimeMs(right.start, right.startTimestamp) &&
|
||||
getProgramTimeMs(left.stop, left.stopTimestamp) ===
|
||||
getProgramTimeMs(right.stop, right.stopTimestamp)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user