mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
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:
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;
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user