Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export async function getFavorites(db: AppDatabase, playlistId: string) {
poster_url: schema.content.posterUrl,
xtream_id: schema.content.xtreamId,
type: schema.content.type,
tv_archive: schema.content.tvArchive,
tv_archive_duration: schema.content.tvArchiveDuration,
epg_channel_id: schema.content.epgChannelId,
added_at: schema.favorites.addedAt,
position: schema.favorites.position,
})
Expand Down Expand Up @@ -117,6 +120,9 @@ function selectGlobalFavoriteRows(
: {}),
xtream_id: schema.content.xtreamId,
type: schema.content.type,
tv_archive: schema.content.tvArchive,
tv_archive_duration: schema.content.tvArchiveDuration,
epg_channel_id: schema.content.epgChannelId,
playlist_id: schema.playlists.id,
playlist_name: schema.playlists.name,
added_at: schema.favorites.addedAt,
Expand Down
6 changes: 2 additions & 4 deletions apps/electron-backend/src/app/events/epg-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,7 @@ export class EpgQueryService {
sourceUrls
)
)
.orderBy(schema.epgPrograms.start)
.limit(500);
.orderBy(schema.epgPrograms.start);
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

private async selectLegacyChannelPrograms(
Expand All @@ -532,8 +531,7 @@ export class EpgQueryService {
{ legacyOnly: true }
)
)
.orderBy(schema.epgPrograms.start)
.limit(500);
.orderBy(schema.epgPrograms.start);
}

private async selectCurrentProgramsForChannelIds(
Expand Down
40 changes: 16 additions & 24 deletions apps/electron-backend/src/app/events/epg.events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,32 +366,15 @@ describe('EpgEvents', () => {

it('falls back to case-insensitive channel id lookup for EPG programs', async () => {
const select = jest.fn();
const programLimitExact = jest.fn().mockResolvedValue([]);
const channelLimit = jest
.fn()
.mockResolvedValue([{ id: 'BBC.ONE.UK', displayName: 'BBC One' }]);
const programLimitResolved = jest.fn().mockResolvedValue([
{
id: 1,
channelId: 'BBC.ONE.UK',
start: '2026-04-14T10:00:00Z',
stop: '2026-04-14T11:00:00Z',
title: 'News',
description: null,
category: null,
iconUrl: null,
rating: null,
episodeNum: null,
},
]);

const from = jest
.fn()
.mockReturnValueOnce({
where: jest.fn().mockReturnValue({
orderBy: jest.fn().mockReturnValue({
limit: programLimitExact,
}),
orderBy: jest.fn().mockResolvedValue([] as any[]),
}),
})
.mockReturnValueOnce({
Expand All @@ -401,9 +384,20 @@ describe('EpgEvents', () => {
})
.mockReturnValueOnce({
where: jest.fn().mockReturnValue({
orderBy: jest.fn().mockReturnValue({
limit: programLimitResolved,
}),
orderBy: jest.fn().mockResolvedValue([
{
id: 1,
channelId: 'BBC.ONE.UK',
start: '2026-04-14T10:00:00Z',
stop: '2026-04-14T11:00:00Z',
title: 'News',
description: null,
category: null,
iconUrl: null,
rating: null,
episodeNum: null,
},
]),
}),
});

Expand Down Expand Up @@ -466,13 +460,11 @@ describe('EpgEvents', () => {
const from = jest.fn();
const where = jest.fn();
const orderBy = jest.fn();
const limit = jest.fn();

select.mockImplementation(() => ({ from }));
from.mockReturnValue({ where });
where.mockReturnValue({ orderBy });
orderBy.mockReturnValue({ limit });
limit.mockResolvedValue([
orderBy.mockResolvedValue([
{
id: 1,
channelId: 'id2e2cd03c90ad',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe('StreamResolverService', () => {
getPlaylistById: jest.fn(),
};
xtreamApi = {
getFullEpg: jest.fn(),
getShortEpg: jest.fn(),
};
xtreamUrl = {
Expand Down Expand Up @@ -228,6 +229,7 @@ describe('StreamResolverService', () => {
xtreamUrl.constructLiveUrl.mockReturnValue(
'https://xtream.example.com/live/1'
);
xtreamApi.getFullEpg.mockResolvedValue([]);
xtreamApi.getShortEpg.mockResolvedValue([
{
id: '1',
Expand Down Expand Up @@ -267,7 +269,7 @@ describe('StreamResolverService', () => {
password: 'pass',
},
1,
10,
50,
{
suppressErrorLog: true,
}
Expand Down Expand Up @@ -317,6 +319,7 @@ describe('StreamResolverService', () => {
password: 'pass',
} satisfies Partial<Playlist>)
);
xtreamApi.getFullEpg.mockResolvedValue([]);
xtreamApi.getShortEpg.mockResolvedValue([]);

const items = [
Expand Down Expand Up @@ -361,6 +364,7 @@ describe('StreamResolverService', () => {
xtreamUrl.constructLiveUrl.mockReturnValue(
'https://xtream.example.com/live/1'
);
xtreamApi.getFullEpg.mockRejectedValue(new Error('EPG failed'));
xtreamApi.getShortEpg.mockRejectedValue(new Error('EPG failed'));

const item = {
Expand Down Expand Up @@ -431,7 +435,7 @@ describe('StreamResolverService', () => {
xtreamId: 1,
} satisfies UnifiedCollectionItem);

await jest.advanceTimersByTimeAsync(3000);
await jest.advanceTimersByTimeAsync(10000);

await expect(detailPromise).resolves.toMatchObject({
epgMode: 'portal',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class StreamResolverService {
private readonly epgBridge = inject(EpgRuntimeBridgeService);
private readonly stalkerSession = inject(StalkerSessionService);
private readonly m3uEpgTimeoutMs = 3000;
private readonly portalEpgTimeoutMs = 3000;
private readonly portalEpgTimeoutMs = 10000;
private readonly xtreamEpgCache = new Map<string, XtreamEpgCacheEntry>();
private readonly xtreamEpgFailureTimestamps = new Map<string, number>();
private readonly xtreamEpgCacheTtlMs = 60 * 1000;
Expand Down Expand Up @@ -410,11 +410,42 @@ export class StreamResolverService {
return [];
}

// Check uploaded XMLTV EPG first using the provider's EPG channel
// identifier (epg_channel_id from the content table), not the
// xtream_id. This respects the user's preferUploadedEpgOverXtream
// setting the same way the main live-view loadEpg() does.
const epgKey = item.epgChannelId?.trim();
if (this.supportsProgramLookup && epgKey) {
const uploaded = await this.epgBridge
.getChannelPrograms(epgKey)
.catch(() => null);
if (uploaded && uploaded.length > 0) {
return this.mapProgramsToEpgItems(uploaded);
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

// Try the full EPG endpoint first (same as the main live-view
// loadEpg() in with-epg.feature.ts) — many providers only
// support get_simple_data_table, not get_short_epg.
try {
const fullEpg = await this.xtreamApi.getFullEpg(
creds,
item.xtreamId,
{ suppressErrorLog: true }
);
if (fullEpg.length > 0) {
return fullEpg;
}
} catch {
// getFullEpg failed — continue to short-EPG fallback below.
}

// Fall back to the short-EPG endpoint with a generous limit.
return await this.fetchXtreamEpgItems(
item.playlistId,
creds,
item.xtreamId,
10
50
);
} catch {
return [];
Expand Down Expand Up @@ -567,19 +598,40 @@ export class StreamResolverService {
}

try {
const items = await this.fetchXtreamEpgItems(
playlistId,
creds,
channel.xtreamId,
2
);
const nowSeconds = Math.floor(now / 1000);
const currentItem =
items.find(
(item) =>
Number(item.start_timestamp) <= nowSeconds &&
nowSeconds < Number(item.stop_timestamp)
) ?? null;
let currentItem: EpgItem | null = null;

// Check uploaded XMLTV EPG first.
const epgChannelKey = channel.epgChannelId?.trim();
if (this.supportsProgramLookup && epgChannelKey) {
const uploaded = await this.epgBridge
.getChannelPrograms(epgChannelKey)
.catch(() => null);
if (uploaded && uploaded.length > 0) {
const items = this.mapProgramsToEpgItems(uploaded);
currentItem =
items.find(
(item) =>
Number(item.start_timestamp) <= nowSeconds &&
nowSeconds < Number(item.stop_timestamp)
) ?? null;
}
}

if (!currentItem) {
const items = await this.fetchXtreamEpgItems(
playlistId,
creds,
channel.xtreamId,
2
);
currentItem =
items.find(
(item) =>
Number(item.start_timestamp) <= nowSeconds &&
nowSeconds < Number(item.stop_timestamp)
) ?? null;
}
const epgKey =
channel.tvgId?.trim() || channel.name?.trim();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,9 @@ export class UnifiedFavoritesDataService {
categoryId: row.category_id,
tvgId: ct === 'live' ? String(row.xtream_id) : undefined,
rating: row.rating ?? undefined,
tvArchive: row.tv_archive ?? null,
tvArchiveDuration: row.tv_archive_duration ?? null,
epgChannelId: row.epg_channel_id ?? null,
addedAt:
normalizeStalkerDate(row.added_at) || new Date(0).toISOString(),
position: row.position ?? 0,
Expand All @@ -635,6 +638,9 @@ export class UnifiedFavoritesDataService {
categoryId: item.category_id,
tvgId: ct === 'live' ? String(item.xtream_id) : undefined,
rating: item.rating ?? undefined,
tvArchive: item.tv_archive ?? null,
tvArchiveDuration: item.tv_archive_duration ?? null,
epgChannelId: item.epg_channel_id ?? null,
addedAt:
normalizeStalkerDate(item.added_at ?? item.added) ||
new Date(0).toISOString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
<app-live-epg-panel
[collapsed]="isLiveEpgPanelCollapsed()"
[summary]="liveEpgPanelSummary()"
[showDateNavigator]="isM3uSelection()"
[showDateNavigator]="true"
[selectedDate]="selectedLiveEpgDate()"
(collapsedChange)="
onLiveEpgPanelCollapsedChange($event)
Expand All @@ -102,8 +102,21 @@
"
/>
} @else {
<app-epg-view
[epgItems]="currentPortalEpgItems()"
<app-epg-list
[controlledPrograms]="
currentPortalEpgPrograms()
"
[controlledArchiveDays]="
currentPortalArchiveDays()
"
[selectedDate]="selectedLiveEpgDate()"
[showDateNavigator]="false"
(selectedDateChange)="
onLiveEpgSelectedDateChange($event)
"
(programActivated)="
onProgramActivated($event)
"
/>
}
</div>
Expand All @@ -117,10 +130,21 @@
[archivePlaybackAvailable]="
currentM3uArchivePlaybackAvailable()
"
(programActivated)="
onProgramActivated($event)
"
/>
} @else {
<app-epg-view
[epgItems]="currentPortalEpgItems()"
<app-epg-list
[controlledPrograms]="
currentPortalEpgPrograms()
"
[controlledArchiveDays]="
currentPortalArchiveDays()
"
(programActivated)="
onProgramActivated($event)
"
/>
}
</div>
Expand Down
Loading