From 069100865244b3cdb3dd634b38bf086b7d7aad97 Mon Sep 17 00:00:00 2001 From: Alanon202 Date: Fri, 10 Jul 2026 15:48:58 +0200 Subject: [PATCH] feat: add archive EPG feature --- .../src/app/events/epg.events.ts | 20 +++++++++- .../src/app/workers/epg-database.ts | 38 +++++++++++++++---- .../src/app/workers/epg-parser.worker.ts | 17 ++++----- 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/apps/electron-backend/src/app/events/epg.events.ts b/apps/electron-backend/src/app/events/epg.events.ts index 7a371ca72..39da77d3b 100644 --- a/apps/electron-backend/src/app/events/epg.events.ts +++ b/apps/electron-backend/src/app/events/epg.events.ts @@ -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), + }; + } } ); diff --git a/apps/electron-backend/src/app/workers/epg-database.ts b/apps/electron-backend/src/app/workers/epg-database.ts index 6123619c0..72f5c06bc 100644 --- a/apps/electron-backend/src/app/workers/epg-database.ts +++ b/apps/electron-backend/src/app/workers/epg-database.ts @@ -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()); @@ -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(); + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); @@ -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')) + `); } /** - * 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(); } diff --git a/apps/electron-backend/src/app/workers/epg-parser.worker.ts b/apps/electron-backend/src/app/workers/epg-parser.worker.ts index 3ac510546..f1d3ea9a3 100644 --- a/apps/electron-backend/src/app/workers/epg-parser.worker.ts +++ b/apps/electron-backend/src/app/workers/epg-parser.worker.ts @@ -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. @@ -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