chore(epg): remove dead EPG_DB_* IPC handlers

Only EPG_DB_SEARCH_PROGRAMS is reachable from the renderer (via
window.electron.searchEpgPrograms); the other nine handlers were never
exposed in the preload bridge. EPG persistence runs through the EPG
worker (epg-database.ts), clearing through EPG_CLEAR_ALL/EPG_CLEAR_SOURCE
in epg.events.ts, and programme lookups through epg-query.service.ts.

Removing them also drops the last lexical start/stop string comparisons
that ignored XMLTV timezone offsets (the live path was already fixed via
datetime() normalization in #1102, see #1108).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-04 16:36:22 +02:00
1 parent 342175fe76
commit 2c4bc649ea
2 files changed
+47 -349

No files matched your search

@@ -10,6 +10,17 @@ jest.mock('../../database/connection', () => ({
getDatabase: (...args: unknown[]) => getDatabase(...args),
}));
function getRegisteredChannels(): string[] {
const { ipcMain } = jest.requireMock('electron') as {
ipcMain: { handle: jest.Mock };
};
return (
ipcMain.handle.mock.calls as Array<
[string, (...args: unknown[]) => unknown]
>
).map(([registeredChannel]) => registeredChannel);
}
function getIpcMainHandler(channel: string): (...args: unknown[]) => unknown {
const { ipcMain } = jest.requireMock('electron') as {
ipcMain: { handle: jest.Mock };
@@ -29,7 +40,6 @@ function getIpcMainHandler(channel: string): (...args: unknown[]) => unknown {
}
describe('epg-db.events', () => {
let consoleLogSpy: jest.SpyInstance;
let consoleErrorSpy: jest.SpyInstance;
beforeEach(async () => {
@@ -39,45 +49,47 @@ describe('epg-db.events', () => {
ipcMain: { handle: jest.Mock };
};
ipcMain.handle.mockClear();
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
await import('./epg-db.events');
});
afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
it('clears a source in a single database transaction', async () => {
const runOrder: string[] = [];
const txDelete = jest
.fn()
.mockReturnValueOnce({
where: jest.fn(() => ({
run: jest.fn(() => runOrder.push('programs')),
})),
})
.mockReturnValueOnce({
where: jest.fn(() => ({
run: jest.fn(() => runOrder.push('channels')),
})),
});
const transaction = jest.fn((callback: (tx: unknown) => void) =>
callback({ delete: txDelete })
);
getDatabase.mockResolvedValue({ transaction });
it('registers only the programme-search channel', () => {
// Every other EPG persistence/lookup path lives in the EPG worker and
// epg-query.service.ts; the removed EPG_DB_* handlers must not return.
expect(getRegisteredChannels()).toEqual(['EPG_DB_SEARCH_PROGRAMS']);
});
it('returns an empty result for a blank search term without querying', async () => {
const all = jest.fn();
getDatabase.mockResolvedValue({ all });
await expect(
getIpcMainHandler('EPG_DB_CLEAR_SOURCE')(
{},
'https://playlist.example.com/guide.xml'
)
).resolves.toEqual({ success: true });
getIpcMainHandler('EPG_DB_SEARCH_PROGRAMS')({}, ' ')
).resolves.toEqual([]);
expect(transaction).toHaveBeenCalledTimes(1);
expect(txDelete).toHaveBeenCalledTimes(2);
expect(runOrder).toEqual(['programs', 'channels']);
expect(all).not.toHaveBeenCalled();
});
it('searches programmes with a LIKE pattern built from the trimmed term', async () => {
const rows = [{ title: 'News', channel_name: 'NHK' }];
const all = jest.fn().mockResolvedValue(rows);
getDatabase.mockResolvedValue({ all });
await expect(
getIpcMainHandler('EPG_DB_SEARCH_PROGRAMS')({}, ' news ', 25)
).resolves.toEqual(rows);
expect(all).toHaveBeenCalledTimes(1);
const query = all.mock.calls[0][0] as {
queryChunks?: unknown[];
};
const boundParams = JSON.stringify(query);
expect(boundParams).toContain('%news%');
expect(boundParams).toContain('25');
});
});
@@ -1,199 +1,18 @@
/**
* EPG Database IPC event handlers
* Operations for storing and querying EPG data in SQLite with FTS5 support
*
* Only the full-text programme search is exposed to the renderer
* (`window.electron.searchEpgPrograms`). All other EPG persistence and
* lookups run through the EPG worker (`workers/epg-database.ts`) and
* `epg-query.service.ts` handlers registered in `epg.events.ts`.
*/
import { and, eq, gte, lte, sql } from 'drizzle-orm';
import { sql } from 'drizzle-orm';
import { ipcMain } from 'electron';
import { getDatabase } from '../../database/connection';
import * as schema from '../../database/schema';
const loggerLabel = '[EPG DB]';
/**
* Save EPG channels in bulk (upsert)
*/
ipcMain.handle(
'EPG_DB_SAVE_CHANNELS',
async (
_event,
sourceUrl: string,
channels: Array<{
id: string;
displayName: string;
iconUrl?: string;
url?: string;
}>
) => {
try {
const db = await getDatabase();
// Delete existing channels from this source first
await db
.delete(schema.epgChannels)
.where(eq(schema.epgChannels.sourceUrl, sourceUrl));
if (channels.length === 0) return { success: true, count: 0 };
// Insert in chunks
const CHUNK_SIZE = 500;
let totalInserted = 0;
for (let i = 0; i < channels.length; i += CHUNK_SIZE) {
const chunk = channels.slice(i, i + CHUNK_SIZE);
const values = chunk.map((ch) => ({
id: ch.id,
displayName: ch.displayName,
iconUrl: ch.iconUrl || null,
url: ch.url || null,
sourceUrl,
}));
await db.insert(schema.epgChannels).values(values);
totalInserted += chunk.length;
}
console.log(
loggerLabel,
`Saved ${totalInserted} channels from ${sourceUrl}`
);
return { success: true, count: totalInserted };
} catch (error) {
console.error(loggerLabel, 'Error saving EPG channels:', error);
throw error;
}
}
);
/**
* Save EPG programs in bulk
*/
ipcMain.handle(
'EPG_DB_SAVE_PROGRAMS',
async (
_event,
programs: Array<{
channelId: string;
start: string;
stop: string;
title: string;
description?: string;
category?: string;
iconUrl?: string;
rating?: string;
episodeNum?: string;
}>
) => {
try {
const db = await getDatabase();
if (programs.length === 0) return { success: true, count: 0 };
// Insert in chunks for better performance
const CHUNK_SIZE = 500;
let totalInserted = 0;
for (let i = 0; i < programs.length; i += CHUNK_SIZE) {
const chunk = programs.slice(i, i + CHUNK_SIZE);
const values = chunk.map((prog) => ({
channelId: prog.channelId,
start: prog.start,
stop: prog.stop,
title: prog.title,
description: prog.description || null,
category: prog.category || null,
iconUrl: prog.iconUrl || null,
rating: prog.rating || null,
episodeNum: prog.episodeNum || null,
}));
await db.insert(schema.epgPrograms).values(values);
totalInserted += chunk.length;
}
console.log(loggerLabel, `Saved ${totalInserted} programs`);
return { success: true, count: totalInserted };
} catch (error) {
console.error(loggerLabel, 'Error saving EPG programs:', error);
throw error;
}
}
);
/**
* Get programs for a specific channel
*/
ipcMain.handle(
'EPG_DB_GET_CHANNEL_PROGRAMS',
async (_event, channelId: string, fromTime?: string, toTime?: string) => {
try {
const db = await getDatabase();
let query;
// Apply time filters if provided
if (fromTime && toTime) {
query = db
.select()
.from(schema.epgPrograms)
.where(
and(
eq(schema.epgPrograms.channelId, channelId),
gte(schema.epgPrograms.stop, fromTime),
lte(schema.epgPrograms.start, toTime)
)
);
} else {
// Return all programs for this channel
query = db
.select()
.from(schema.epgPrograms)
.where(eq(schema.epgPrograms.channelId, channelId));
}
const results = await query.orderBy(schema.epgPrograms.start);
return results;
} catch (error) {
console.error(
loggerLabel,
'Error getting channel programs:',
error
);
throw error;
}
}
);
/**
* Get current program for a channel (what's on now)
*/
ipcMain.handle(
'EPG_DB_GET_CURRENT_PROGRAM',
async (_event, channelId: string) => {
try {
const db = await getDatabase();
const now = new Date().toISOString();
const result = await db
.select()
.from(schema.epgPrograms)
.where(
and(
eq(schema.epgPrograms.channelId, channelId),
lte(schema.epgPrograms.start, now),
gte(schema.epgPrograms.stop, now)
)
)
.limit(1);
return result[0] || null;
} catch (error) {
console.error(loggerLabel, 'Error getting current program:', error);
throw error;
}
}
);
/**
* Full-text search EPG programs using FTS5 with LIKE fallback
* Handles Cyrillic and other Unicode text properly
@@ -238,136 +57,3 @@ ipcMain.handle(
}
}
);
/**
* Get all EPG channels
*/
ipcMain.handle('EPG_DB_GET_CHANNELS', async () => {
try {
const db = await getDatabase();
const results = await db
.select()
.from(schema.epgChannels)
.orderBy(schema.epgChannels.displayName);
return results;
} catch (error) {
console.error(loggerLabel, 'Error getting EPG channels:', error);
throw error;
}
});
/**
* Get channel by ID or display name
*/
ipcMain.handle(
'EPG_DB_GET_CHANNEL',
async (_event, channelIdOrName: string) => {
try {
const db = await getDatabase();
// Try exact ID match first
let result = await db
.select()
.from(schema.epgChannels)
.where(eq(schema.epgChannels.id, channelIdOrName))
.limit(1);
if (result.length > 0) return result[0];
// Try display name match (case-insensitive)
result = await db
.select()
.from(schema.epgChannels)
.where(
sql`LOWER(${schema.epgChannels.displayName}) = LOWER(${channelIdOrName})`
)
.limit(1);
return result[0] || null;
} catch (error) {
console.error(loggerLabel, 'Error getting EPG channel:', error);
throw error;
}
}
);
/**
* Clear all EPG data for a specific source URL
*/
ipcMain.handle('EPG_DB_CLEAR_SOURCE', async (_event, sourceUrl: string) => {
try {
const db = await getDatabase();
await db.transaction((tx) => {
tx.delete(schema.epgPrograms)
.where(eq(schema.epgPrograms.sourceUrl, sourceUrl))
.run();
tx.delete(schema.epgChannels)
.where(sql`
${schema.epgChannels.sourceUrl} = ${sourceUrl}
AND NOT EXISTS (
SELECT 1
FROM ${schema.epgPrograms}
WHERE ${schema.epgPrograms.channelId} = ${schema.epgChannels.id}
)
`)
.run();
});
console.log(loggerLabel, `Cleared EPG data for source: ${sourceUrl}`);
return { success: true };
} catch (error) {
console.error(loggerLabel, 'Error clearing EPG source:', error);
throw error;
}
});
/**
* Clear all EPG data
*/
ipcMain.handle('EPG_DB_CLEAR_ALL', async () => {
try {
const db = await getDatabase();
await db.delete(schema.epgPrograms);
await db.delete(schema.epgChannels);
console.log(loggerLabel, 'Cleared all EPG data');
return { success: true };
} catch (error) {
console.error(loggerLabel, 'Error clearing all EPG data:', error);
throw error;
}
});
/**
* Get EPG statistics
*/
ipcMain.handle('EPG_DB_GET_STATS', async () => {
try {
const db = await getDatabase();
const channelCount = await db
.select({ count: sql<number>`count(*)` })
.from(schema.epgChannels);
const programCount = await db
.select({ count: sql<number>`count(*)` })
.from(schema.epgPrograms);
const now = new Date().toISOString();
const futureProgramCount = await db
.select({ count: sql<number>`count(*)` })
.from(schema.epgPrograms)
.where(gte(schema.epgPrograms.stop, now));
return {
channels: channelCount[0].count,
programs: programCount[0].count,
futurePrograms: futureProgramCount[0].count,
};
} catch (error) {
console.error(loggerLabel, 'Error getting EPG stats:', error);
throw error;
}
});