Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 19 additions & 1 deletion apps/electron-backend/src/app/events/epg.events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,25 @@ export default class EpgEvents {
const options =
typeof args === 'string' ? undefined : args.options;
epgWorkerService.deleteFetchedUrl(url);
return await this.handleFetchEpg([url], options);
epgWorkerService.sendProgressToRenderer(
url,
'queued',
undefined,
undefined,
1
);
try {
await this.fetchEpgFromUrl(url, options);
return { success: true };
} catch (error) {
return {
success: false,
message:
error instanceof Error
? error.message
: String(error),
};
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
);

Expand Down
38 changes: 31 additions & 7 deletions apps/electron-backend/src/app/workers/epg-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class EpgDatabase {
private readonly insertProgramStmt: BetterSqlite3.Statement;
private readonly deleteProgramsForSourceStmt: BetterSqlite3.Statement;
private readonly deleteOrphanChannelsForSourceStmt: BetterSqlite3.Statement;
private readonly deleteTodayAndFutureStmt: BetterSqlite3.Statement;

constructor(Database: typeof BetterSqlite3) {
this.db = new Database(getIptvnatorDatabasePath());
Expand All @@ -30,8 +31,24 @@ export class EpgDatabase {
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
`);

// Delete duplicate (channel_id, start, title) rows before creating
// the unique index — multi-source setups may already have dupes from
// overlapping feeds, and CREATE UNIQUE INDEX would throw on them.
this.db.prepare(`
DELETE FROM epg_programs
WHERE rowid NOT IN (
SELECT MIN(rowid) FROM epg_programs
GROUP BY channel_id, start, title
)
`).run();

this.db.prepare(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_epg_programs_dedup
ON epg_programs(channel_id, start, title)
`).run();
Comment thread
Alanon202 marked this conversation as resolved.

this.insertProgramStmt = this.db.prepare(`
INSERT INTO epg_programs (channel_id, start, stop, title, description, category, icon_url, rating, episode_num, source_url)
INSERT OR REPLACE INTO epg_programs (channel_id, start, stop, title, description, category, icon_url, rating, episode_num, source_url)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);

Expand All @@ -48,21 +65,28 @@ export class EpgDatabase {
WHERE epg_programs.channel_id = epg_channels.id
)
`);

this.deleteTodayAndFutureStmt = this.db.prepare(`
DELETE FROM epg_programs
WHERE source_url = ?
AND (start >= date('now') OR start < date('now', '-7 days'))
`);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

/**
* Insert a batch of channels. When `clearFirst` is true, the existing rows
* for `sourceUrl` are deleted inside the same transaction as the insert so
* old data is preserved if the fetch/parse never produces any channels.
* Insert a batch of channels. When `clearTodayAndFuture` is true, the
* selective delete (today+future and older-than-7-days) runs inside the
* same transaction so a parse failure after the delete atomically rolls
* back both — no gap left in the schedule.
*/
insertChannels(
channels: ParsedChannel[],
sourceUrl: string,
clearFirst = false
clearTodayAndFuture = false
): void {
const insertMany = this.db.transaction((channels: ParsedChannel[]) => {
if (clearFirst) {
this.deleteProgramsForSourceStmt.run(sourceUrl);
if (clearTodayAndFuture) {
this.deleteTodayAndFutureStmt.run(sourceUrl);
this.deleteOrphanChannelsForSourceStmt.run(sourceUrl);
this.knownChannelIds.clear();
}
Expand Down
17 changes: 7 additions & 10 deletions apps/electron-backend/src/app/workers/epg-parser.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,7 @@ async function fetchAndParseEpgStreaming(

// Create database connection in worker
const epgDb = new EpgDatabase(Database);

// Old rows for this source are retained until the first successful insert
// batch arrives — see `hasClearedSource` below. That way, a fetch or parse
// that yields zero channels leaves the existing data intact instead of
// wiping it and leaving the URL permanently stale.
let hasClearedSource = false;
let needsClear = true;

try {
// EPG URLs can originate from an untrusted M3U `url-tvg` attribute.
Expand Down Expand Up @@ -168,10 +163,12 @@ async function fetchAndParseEpgStreaming(

const parser = new StreamingEpgParser(
(channels) => {
// Clear old rows in the same transaction as the first insert
// so we never end up with zero rows on a failed/empty parse.
epgDb.insertChannels(channels, url, !hasClearedSource);
hasClearedSource = true;
// On the first channel batch, delete programmes starting today
// or later inside the same transaction so a partial parse
// never leaves a gap in the schedule.
const clearToday = needsClear;
needsClear = false;
epgDb.insertChannels(channels, url, clearToday);
},
(programs) => {
// Insert programs directly into database
Expand Down