mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 06:06:18 -08:00
fix(ui-epg): hide duplicate program time slots
This commit is contained in:
1 parent
931a3243c8
commit
cc85d84a0e
3 files changed
+125
-12
No files matched your search
@@ -321,6 +321,62 @@ describe('EpgListComponent', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('shows one row per duplicated time slot and keeps the richer program entry', () => {
|
||||
const sparseProgram = buildProgram(
|
||||
'duplicate-a',
|
||||
'World Cup Breakfast',
|
||||
'2026-04-05T09:00:00.000Z',
|
||||
'2026-04-05T10:00:00.000Z',
|
||||
{
|
||||
channel: 'channel-101',
|
||||
desc: null,
|
||||
}
|
||||
);
|
||||
const richProgram = buildProgram(
|
||||
'duplicate-b',
|
||||
'World Cup Breakfast',
|
||||
'2026-04-05T09:00:00.000Z',
|
||||
'2026-04-05T10:00:00.000Z',
|
||||
{
|
||||
channel: 'channel-101',
|
||||
desc: 'A preview of the day ahead.',
|
||||
category: 'Sports',
|
||||
}
|
||||
);
|
||||
const nextProgram = buildProgram(
|
||||
'next',
|
||||
'World Cup Matchday',
|
||||
'2026-04-05T10:00:00.000Z',
|
||||
'2026-04-05T11:00:00.000Z',
|
||||
{
|
||||
channel: 'channel-101',
|
||||
}
|
||||
);
|
||||
const emitted: unknown[] = [];
|
||||
component.programActivated.subscribe((event) => emitted.push(event));
|
||||
fixture.componentRef.setInput('controlledPrograms', [
|
||||
sparseProgram,
|
||||
richProgram,
|
||||
nextProgram,
|
||||
]);
|
||||
fixture.componentRef.setInput('archivePlaybackAvailable', true);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const rows =
|
||||
fixture.nativeElement.querySelectorAll<HTMLElement>(
|
||||
'.program-item'
|
||||
);
|
||||
rows[0].click();
|
||||
|
||||
expect(component.filteredItems()).toEqual([
|
||||
richProgram,
|
||||
nextProgram,
|
||||
]);
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(emitted).toEqual([{ program: richProgram, type: 'timeshift' }]);
|
||||
});
|
||||
|
||||
it('updates the selected-day header when the app language changes', () => {
|
||||
fixture.componentRef.setInput('controlledPrograms', buildPrograms());
|
||||
fixture.detectChanges();
|
||||
@@ -389,7 +445,8 @@ function buildProgram(
|
||||
channelSuffix: string,
|
||||
title: string,
|
||||
start: string,
|
||||
stop: string
|
||||
stop: string,
|
||||
overrides: Partial<EpgProgram> = {}
|
||||
): EpgProgram {
|
||||
return {
|
||||
start,
|
||||
@@ -400,6 +457,7 @@ function buildProgram(
|
||||
category: null,
|
||||
startTimestamp: Math.floor(Date.parse(start) / 1000),
|
||||
stopTimestamp: Math.floor(Date.parse(stop) / 1000),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { EpgListItemComponent } from './epg-list-item/epg-list-item.component';
|
||||
import {
|
||||
areProgramsSame,
|
||||
buildScrollContextKey,
|
||||
deduplicateProgramsByTimeSlot,
|
||||
getProgramDateKey,
|
||||
getProgramTimeMs,
|
||||
trackProgram,
|
||||
@@ -144,17 +145,19 @@ export class EpgListComponent {
|
||||
() => this.archivePlaybackAvailable() ?? this.archiveDays() > 0
|
||||
);
|
||||
readonly filteredItems = computed(() =>
|
||||
[...this.items()]
|
||||
.filter(
|
||||
(item) =>
|
||||
getProgramDateKey(item.start, item.startTimestamp) ===
|
||||
this.selectedDateKey()
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
getProgramTimeMs(left.start, left.startTimestamp) -
|
||||
getProgramTimeMs(right.start, right.startTimestamp)
|
||||
)
|
||||
deduplicateProgramsByTimeSlot(
|
||||
[...this.items()]
|
||||
.filter(
|
||||
(item) =>
|
||||
getProgramDateKey(item.start, item.startTimestamp) ===
|
||||
this.selectedDateKey()
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
getProgramTimeMs(left.start, left.startTimestamp) -
|
||||
getProgramTimeMs(right.start, right.startTimestamp)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
private scrollScheduled = false;
|
||||
|
||||
@@ -15,6 +15,26 @@ export function trackProgram(index: number, program: EpgProgram): string {
|
||||
].join('|');
|
||||
}
|
||||
|
||||
export function deduplicateProgramsByTimeSlot(
|
||||
programs: EpgProgram[]
|
||||
): EpgProgram[] {
|
||||
const programsByTimeSlot = new Map<string, EpgProgram>();
|
||||
|
||||
for (const program of programs) {
|
||||
const timeSlotKey = buildProgramTimeSlotKey(program);
|
||||
const existingProgram = programsByTimeSlot.get(timeSlotKey);
|
||||
|
||||
programsByTimeSlot.set(
|
||||
timeSlotKey,
|
||||
existingProgram
|
||||
? selectMoreInformativeProgram(existingProgram, program)
|
||||
: program
|
||||
);
|
||||
}
|
||||
|
||||
return Array.from(programsByTimeSlot.values());
|
||||
}
|
||||
|
||||
export function buildScrollContextKey(
|
||||
channel: Channel | null,
|
||||
programs: EpgProgram[]
|
||||
@@ -71,3 +91,35 @@ export function areProgramsSame(
|
||||
getProgramTimeMs(right.stop, right.stopTimestamp)
|
||||
);
|
||||
}
|
||||
|
||||
function buildProgramTimeSlotKey(program: EpgProgram): string {
|
||||
return [
|
||||
getProgramTimeMs(program.start, program.startTimestamp),
|
||||
getProgramTimeMs(program.stop, program.stopTimestamp),
|
||||
].join('|');
|
||||
}
|
||||
|
||||
function selectMoreInformativeProgram(
|
||||
existingProgram: EpgProgram,
|
||||
candidateProgram: EpgProgram
|
||||
): EpgProgram {
|
||||
return getProgramMetadataScore(candidateProgram) >
|
||||
getProgramMetadataScore(existingProgram)
|
||||
? candidateProgram
|
||||
: existingProgram;
|
||||
}
|
||||
|
||||
function getProgramMetadataScore(program: EpgProgram): number {
|
||||
return (
|
||||
getTextScore(program.desc) * 8 +
|
||||
getTextScore(program.category) * 4 +
|
||||
getTextScore(program.iconUrl) * 2 +
|
||||
getTextScore(program.rating) * 2 +
|
||||
getTextScore(program.episodeNum) * 2 +
|
||||
getTextScore(program.title)
|
||||
);
|
||||
}
|
||||
|
||||
function getTextScore(value: string | null | undefined): number {
|
||||
return value?.trim() ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user