mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
feat: manual EPG-to-channel mapping with mapping fallback (#1165)
* feat(epg): manual EPG-to-channel mapping with mapping fallback in all EPG paths
* fix: epg mapping in live tv list
* fix(epg): harden manual EPG mapping — upgrade safety, perf, playlist-scoped keys
Follow-up fixes on top of the manual EPG-to-channel mapping feature:
- epg-database: dedupe existing epg_programs rows before creating the
unique (channel_id, start, title) index — a plain CREATE UNIQUE INDEX
crashed the EPG worker on upgrade when historical duplicates exist;
replace INSERT OR REPLACE with ON CONFLICT DO UPDATE so the
epg_programs_fts delete trigger is not bypassed (REPLACE skips delete
triggers unless recursive_triggers is on), with a plain-INSERT
fallback when the index cannot be created
- db: add idx_content_epg_channel — the mapping fallback scanned the
whole content table on every single-channel EPG lookup
- keys: scope Xtream mapping keys per playlist via shared
buildXtreamEpgMappingKey (xtream:{playlistId}:{id}) — bare stream ids
collide across portals; the backend fallback now joins categories to
resolve the playlist id
- pwa: hide "Map EPG channel" entries behind the supportsEpgMapping
capability — the menu item was a dead end in the PWA
- parser: parse the XMLTV offset sign from the string — Math.sign(0)
dropped the minutes of ±00:xx offsets
- cleanup: typed window.electron access instead of ad-hoc casts, drop
unused resolveChannelId and dialog data field, shared
EpgMappingDialogComponent.open() for all seven call sites
- dialog UX: minimum-characters search hint, save/remove snackbars,
current mapping shows the EPG channel display name,
takeUntilDestroyed on the search stream
- i18n: fill the new keys in all 17 locales
- tests: cover mapping CRUD/search escaping, the dedup-index guard,
offset parsing and playlist-scoped keys; update stale stream-resolver
specs for the new 50-item limit and 10s timeout
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(epg): escape backslashes in EPG channel search LIKE pattern
CodeQL js/incomplete-sanitization: a lone trailing backslash in the
search term paired with the closing wildcard under the ESCAPE clause
and corrupted the pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(epg): batch mapping lookups in the viewport preview queue
Expose the existing getEpgMappingsBatch operation over a new
EPG_MAPPING_GET_BATCH IPC channel and use it in resolveManualMappings —
the per-entry lookup issued one IPC round-trip per visible channel on
every scroll event.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(epg): batch mapping prefetch in the collection preview loader
Resolve all candidate mapping keys for an Xtream preview batch with a
single getEpgMappingsBatch IPC call instead of per-channel lookups.
Also fix a worker early-exit: a channel without tvgId/name returned out
of the shared-iterator loop and silently killed one of the three
concurrent preview workers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
60 files changed
+2343
-91
No files matched your search
@@ -169,6 +169,7 @@ const CREATE_TABLE_STATEMENTS = [
|
||||
`CREATE INDEX IF NOT EXISTS idx_categories_playlist ON categories(playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_title ON content(title)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_xtream ON content(xtream_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_epg_channel ON content(epg_channel_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_type_added ON content(type, added)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type)`,
|
||||
// Partial covering index for visible categories — supports the dashboard's
|
||||
@@ -261,6 +262,15 @@ const CREATE_TABLE_STATEMENTS = [
|
||||
INSERT INTO epg_programs_fts(rowid, title, description, category)
|
||||
VALUES (new.id, new.title, new.description, new.category);
|
||||
END`,
|
||||
// EPG channel mappings (manual user overrides)
|
||||
`CREATE TABLE IF NOT EXISTS epg_channel_mappings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
channel_key TEXT NOT NULL UNIQUE,
|
||||
epg_channel_id TEXT NOT NULL,
|
||||
playlist_id TEXT,
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_epg_channel_mappings_playlist ON epg_channel_mappings(playlist_id)`,
|
||||
// Playback Positions table
|
||||
`CREATE TABLE IF NOT EXISTS playback_positions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
@@ -110,6 +110,9 @@ export const content = sqliteTable(
|
||||
categoryIdx: index('idx_content_category').on(table.categoryId),
|
||||
titleIdx: index('idx_content_title').on(table.title),
|
||||
xtreamIdx: index('idx_content_xtream').on(table.xtreamId),
|
||||
epgChannelIdx: index('idx_content_epg_channel').on(
|
||||
table.epgChannelId
|
||||
),
|
||||
categoryTypeXtreamUnique: uniqueIndex(
|
||||
'content_category_type_xtream_unique'
|
||||
).on(table.categoryId, table.type, table.xtreamId),
|
||||
@@ -347,6 +350,26 @@ export const downloads = sqliteTable(
|
||||
})
|
||||
);
|
||||
|
||||
// EPG channel mappings (manual user overrides)
|
||||
export const epgChannelMappings = sqliteTable(
|
||||
'epg_channel_mappings',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
channelKey: text('channel_key').notNull().unique(),
|
||||
epgChannelId: text('epg_channel_id').notNull(),
|
||||
playlistId: text('playlist_id'),
|
||||
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
},
|
||||
(table) => ({
|
||||
playlistIdx: index('idx_epg_channel_mappings_playlist').on(
|
||||
table.playlistId
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
export type EpgChannelMapping = typeof epgChannelMappings.$inferSelect;
|
||||
export type NewEpgChannelMapping = typeof epgChannelMappings.$inferInsert;
|
||||
|
||||
export type Download = typeof downloads.$inferSelect;
|
||||
export type NewDownload = typeof downloads.$inferInsert;
|
||||
|
||||
|
||||
Reference in new issue
Block a user