Commit Graph
47 Commits
Author SHA1 Message Date
4gray 2455165043 chore(electron): route the worker heap probe 2026-07-27 02:15:58 +02:00
4gray d1e268f1dd chore(electron): add perf-only worker heap probe 2026-07-27 02:15:58 +02:00
4gray df7eee7e52 fix(perf): exclude profiler flush from cancel latency 2026-07-26 23:14:57 +02:00
4gray da3b657277 fix(perf): make worker profiling request scoped 2026-07-26 22:37:52 +02:00
4gray f147d4fe37 perf(m3u): stop cancelled refresh workers (#1268) 2026-07-26 09:25:51 +02:00
4grayandClaude Opus 4.8 0b967d66d4 feat(tmdb): metadata cache panel with a clear button in settings (#1244)
* feat(tmdb): metadata cache panel with a clear button in settings

Adds "Metadata cache — N entries · X MB" with a Clear button to
Settings > Metadata (TMDB), next to the API key it belongs to.

Three things it is good for: dropping stale or wrong metadata so the next
open refetches it, seeing what the cache actually costs on disk, and
reclaiming rows that a lookup-key version bump has orphaned — a bump makes
rows unreachable, not deleted, so nothing else would ever collect them.

Sizing the cache is a full table scan (LENGTH() on TEXT counts characters,
so the SUM casts to BLOB to get bytes), which is why stats load lazily and
only once the TMDB section is the active one rather than on every settings
open. Clearing is always safe: enrichment refetches on demand, so the only
cost is the next few requests.

Works in both environments — the PWA has no bridge, so the service reports
and clears its session-scoped in-memory map instead.

i18n: 4 keys across all 19 locales via the tools/i18n workflow;
placeholder integrity verified. Contract fixtures updated for both the
preload bridge and the DB-worker payload shapes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): make cache clearing durable and stop reporting failures as empty

Four review findings, all real:

- A metadata write already in flight when the user cleared would land
  afterwards and silently restore what they removed. Writes now carry the
  generation they started in; a write that outlives a clear is dropped
  (PWA) or undone (Electron).
- The PWA byte count used String.length, i.e. UTF-16 code units, so
  localized payloads under-reported and disagreed with the SQLite BLOB
  byte count. TextEncoder now measures actual bytes.
- A failed stats read returned a valid zero-entry result, so the panel
  claimed an empty cache and disabled Clear while rows were still there.
  getStats/clear now return null on failure and the panel says so instead
  of inventing state.
- No behavioural coverage existed for either side.

Tests: SQL ops (entry/byte reporting, empty table, missing row, delete
count) and the service (encoded bytes, clear count, and a write racing a
clear).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): make the cache clear precise and version skew visible

Review follow-ups on the cache panel:

- A write that was in flight when the user cleared used to trigger a
  second full-table clear once it landed, which also deleted anything
  written in between. clear() now waits for the writes issued before it
  and lets the single clear take them; later writes survive.
- An Electron shell without the maintenance ops fell through to the
  renderer map, which is always empty there — it reported an empty cache
  and disabled the Clear button while SQLite was full. Both operations
  now report unsupported instead.
- Component coverage for the panel (deferred scan, clear + re-read,
  failed clear, failed read) and Electron-path service coverage.
- The canonical IPC and settings sections of the enrichment doc, plus
  the matching CLAUDE.md lines, now list the maintenance ops.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): drop Promise.allSettled from the cache clear

The web target compiles against lib es2018, so allSettled broke the
Windows frontend build (TS2550). The pending writes swallow their own
errors, so a plain Promise.all over neutralized promises does the job.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): keep a synchronous bridge throw inside the cache write

Moving the write into a tracked promise dropped the try/catch that used
to cover the call itself, so a bridge that threw synchronously would
escape set(). Wrap it in an async IIFE, which turns that back into a
rejection the same handler swallows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): retry the cache size read when the section is reopened

The effect skipped the read once cacheError was set, so one transient
IPC failure left the panel showing "could not read the cache" for the
life of the settings page — and the only enabled control that could
shift it was the destructive Clear button. Gate on the stats signal
alone: reopening the section retries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): queue writes that start while the cache is being cleared

Awaiting the in-flight writes closed one side of the race and left the
other open: a set() that started during that wait dispatched its IPC
immediately, was absent from the snapshot, and could reach SQLite just
before the delete — so a row written after the user clicked Clear was
removed anyway.

clear() now holds its own promise for the whole operation and set() waits
on it, which puts such a write on the far side of the delete. Rows are
stamped when they are dispatched rather than when set() was called, since
a write may have waited. Covered by a test that fails without the guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(tmdb): add the release note for the cache panel

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(tmdb): cover the cache panel with an Electron E2E

The panel drives IPC and SQLite, and nothing exercised that path end to
end. The new test seeds a row through the preload bridge — enrichment
itself needs a TMDB key that CI does not have — then opens the section,
asserts the reported size, clears, and reads the database back to confirm
the row is gone rather than merely hidden.

Verified both ways: dropping the DELETE from clearTmdbMetadata fails it.

Settings nav buttons gained a data-test-id so the section can be opened
without matching translated labels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 01:13:02 +02:00
4grayandClaude Opus 5 fc81f4f941 fix(playlist): bound playlist downloads and isolate startup auto-update (#1233)
Playlist downloads in the main process ran without an axios timeout, so a
host that accepted the connection and then went silent kept the request
pending forever. The startup auto-update refreshed playlists sequentially,
so that one unresponsive source also withheld every playlist that did
refresh, and the URL import dialog could spin with no way out.

- Add PLAYLIST_FETCH_TIMEOUT_MS (30s) to the main-process fetch and reuse it
  in the refresh worker instead of its duplicated literal. Redirects are
  followed one hop at a time, so each hop is bounded separately.
- Move auto-update into playlist-auto-update.ts and refresh at most
  AUTO_UPDATE_CONCURRENCY (3) playlists at once, isolating each failure while
  preserving the requested order. An unbounded fan-out would download and
  parse arbitrarily many large M3U files in the main process at once.
- Redact refresh log URLs, which routinely carry Xtream username/password
  query parameters.

The existing auto-update spec handed out fixtures by call order, which no
longer holds once refreshes overlap; it now keys them by source type.

Fixes #931

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 10:37:11 +02:00
4grayandClaude Fable 5 bc6e7018e0 fix(epg): harden three latent edges from the #1165 manual-mapping review (#1214)
* fix(epg): harden three latent edges from the #1165 manual-mapping review

Follow-up to #1165 (manual EPG-to-channel mapping). Three minor but real
issues flagged by the bot reviews on #1173, all in already-merged #1165
code rather than the Stalker delta:

1. EPG program dedup ignored source_url. The unique index and upsert key
   (channel_id, start, title) collapsed programmes imported from different
   XMLTV sources that shared those columns, and the upsert reassigned
   source_url to the last importer — so source-scoped queries could miss a
   programme and source-scoped deletes could drop another source's row.
   The key and index now include source_url (migrated via a _v2 index that
   drops the old source-blind one); the upsert no longer overwrites
   source_url.

2. Xtream getMapping fallback capped candidate streams at an unordered
   first five, so a mapping saved under a later stream sharing the
   provider epg_channel_id was silently ignored. Replaced the two-step
   fetch-then-lookup with a single content⋈categories⋈mappings join that
   finds a mapping under any matching stream, with no arbitrary cap.

3. The Xtream mapping dialog did not refresh after closing, unlike the
   Stalker path, so a remapped visible/selected channel kept its stale
   preview until the 5-minute TTL or a rescroll. Added
   EpgQueueService.invalidate(streamId) and a before/after mapping compare
   in the channel-list dialog flow that invalidates the cache and refetches
   the current viewport when the mapping actually changed.

Tests: source-aware dedup index/upsert assertions; join-based getMapping
resolution regardless of stream position; EpgQueueService.invalidate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): address review feedback on the #1165 follow-up

- portal-channels-list: forward the already-validated playlistId into the
  mapping dialog instead of re-reading currentPlaylist() after the async
  getEpgMapping roundtrip (which could return undefined on navigation)
- EpgQueueService.invalidate(): bump a per-stream invalidation epoch and
  clear inFlight so a request already running when the mapping changes has
  its (pre-change) result discarded via an epoch check in fetchEpg, and the
  immediate re-enqueue can schedule a fresh mapping-aware fetch
- getMapping Xtream fallback: order the join deterministically before
  limit(1) so the resolved mapping is stable; documented that this backend
  layer has no caller playlist context and is a best-effort net behind the
  renderer's playlist-scoped resolution

Tests: in-flight staleness discard for invalidate().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(epg): split EpgQueueService invalidation specs under the max-lines limit

The added invalidate() tests pushed epg-queue.service.spec.ts to 415 lines,
over the 400-line ESLint cap (and the baseline must not grow). Moved them to
a focused epg-queue-invalidation.spec.ts; both files are now under the limit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): resolve second-order review findings on the mapping follow-up

Two P2 issues Codex raised on the previous fixes:

- Unscoped program lookups could return the same programme twice now that
  the dedup index preserves per-source rows: the M3U timeline calls
  getChannelPrograms without sourceUrls, so two sources sharing
  channel/start/title both surfaced. Added toEpgProgams(), which collapses
  duplicate channel|start|title slots after mapping/validation, applied at
  every getChannelPrograms return.
- EpgQueueService.fetchEpg unconditionally cleared the in-flight marker in
  its finally, which could drop a marker a re-enqueued request took over
  after invalidate(). It now only releases the marker when the completing
  request still owns it (epoch unchanged), preserving per-stream dedup and
  concurrency accounting.

Tests: unscoped duplicate-slot collapse; stale request preserving a fresh
in-flight marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(epg): deduplicate program rows in SQL before applying row limits

Codex follow-up: the JS-level slot dedup ran after the SQL LIMIT, so
duplicate cross-source rows consumed the cap and truncated real data.
Moved the dedup into SQL with GROUP BY, applied before the limits:

- selectChannelPrograms / selectLegacyChannelPrograms: GROUP BY
  (channel_id, start, title) before ORDER BY start LIMIT 500, so the
  timeline cap counts distinct programmes rather than duplicate rows
- selectCurrentProgramsForChannelIds: GROUP BY channel_id before
  LIMIT channelIds.length, so duplicate cross-source current slots can't
  starve other channels of their current-programme preview

The JS toEpgPrograms() dedup stays as a safety net (e.g. legacy NULL-source
rows the unique index treats as distinct). Test query-chain mocks updated
for the new groupBy link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:21:40 +02:00
ec6fc403a3 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>
2026-07-19 09:00:14 +02:00
StefanandClaude 2f8aee72df feat(xtream): add catch-up playback to favorites and recent tabs (#1166)
Enables Xtream catch-up/timeshift from the Favorites and Recent surfaces (per-playlist and global), not just Live TV, and adds start-over replay of the currently-airing programme. Carries tv_archive/tv_archive_duration through the favorites and recently-viewed DB projections and maps them onto UnifiedCollectionItem; tv_archive_duration is interpreted as days, matching live-stream-layout.controlledArchiveDays.

Closes #1138.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-19 06:38:19 +02:00
4grayandClaude Fable 5 54265755ee fix(favorites): persist custom drag-and-drop order for Xtream favorites (#1143)
* fix(favorites): persist custom drag-and-drop order for Xtream favorites

Prepared-statement writes dispatched via drizzle's `.execute()` on the
better-sqlite3 driver return a promise and defer the write to a microtask.
Inside a synchronous `db.transaction(() => ...)` callback (which cannot
await), the transaction commits before that promise settles, so the write
is a silent no-op — no error, no rows changed.

This bit `reorderGlobalFavorites`: the custom favorites order never
persisted for the per-playlist ("This playlist") Xtream scope, which relies
solely on the `favorites.position` column. The global ("All playlists")
scope masked the bug because it also persists an order to the `appState`
`global-favorites-channel-order-v1` key and re-applies it on read.
`removeRecentItemsBatch` had the same latent bug — batch "clear recent
items" silently did nothing.

Switch both writers to synchronous `.run()`. Add regression coverage that
asserts `.run()` (not `.execute()`) is used and would fail on the old
behavior, and document the gotcha in the DB worker architecture doc.

Verified over CDP against a live Electron instance: reorder writes
positions 0..N, and the order survives navigation and a full reload.

Fixes #1137

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(favorites): scope reorder position writes by playlist

The global favorites reorder wrote the new position filtering only by
content_id, so two Xtream playlists holding a favorite with the same
content_id would clobber each other's persisted order (greptile P1).

Thread playlist_id through the whole reorder path — the renderer builder
(UnifiedCollectionItem already carries playlistId), the IPC contract
(ElectronBridgeFavoriteReorderUpdate + inline payload types), the worker
op — and scope the prepared UPDATE by (contentId, playlistId), matching
the favorites composite unique index.

Tests: favorites.operations.spec asserts the playlistId placeholder and
per-row playlistId payload; preload contract fixture updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(favorites): include playlist_id in workspace global favorites reorder payload

The workspace global-favorites reorder path still sent updates with only
content_id and position. Since the backend UPDATE is now scoped by
(contentId, playlistId), that payload binds an undefined playlist id and
matches no rows — the DB write silently no-ops (flagged by Greptile P1).

Also scope the prepared-statement example in the sqlite-db-worker gotcha
doc by (contentId, playlistId) so it no longer documents the
cross-playlist rewrite this PR fixes (flagged by Codex P3).

Regression spec asserts the reorder payload carries playlist_id per item
(fails on the old payload shape) and that the appState uid order is
still persisted for non-Xtream items.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 13:24:58 +02:00
4grayandClaude Fable 5 dc05a2566e feat(tmdb): opt-in TMDB metadata enrichment for Xtream and Stalker portals (#1123)
* feat(tmdb): opt-in TMDB metadata enrichment for Xtream and Stalker portals

Adds an opt-in TMDB integration (Settings > Metadata) that enriches
detail views with a field-level merge — the provider stays authoritative
for stream data, TMDB fills editorial fields when the match is confident.

Enrichment:
- Movie/series details: plot, cast (avatar chips), director, genres,
  rating, poster/backdrop, official YouTube trailers
- Confidence-gated matching: provider tmdb_id trusted; otherwise
  normalized-title search with year gate (±1; series accept earlier
  premieres), season-suffix stripping, Cyrillic search-language override,
  and language-prefix fallback variants
- Lazy season/episode enrichment: real episode names, overviews, stills
- "Similar" rail (Xtream): TMDB recommendations matched to the catalog
- Actor pages per portal with full filmography, availability filter and
  an Electron-only "All portals" scope backed by a batched DB_MATCH_TITLES
  worker op over the trigram FTS index

Infrastructure:
- SQLite cache table tmdb_metadata (details, search verdicts, seasons,
  persons; per-language, TTL-guarded), in-memory fallback for the PWA
- Settings: enable toggle, own-API-key override with a live "check key"
  button; TMDB attribution in Settings and About
- Embedded key stays an empty placeholder; CI injects TMDB_API_KEY via
  tools/tmdb/inject-tmdb-key.mjs when the secret is configured
- normalizeTitle shared between renderer and DB worker
- CSP: allow YouTube embeds (frame-src was 'none'; trailers never worked)

Fixes and refactors along the way:
- fix(stalker): Advanced Search sent bare get_ordered_list requests and
  skipped the auth handshake when isFullStalkerPortal was missing on the
  active-playlist meta — full portals answered "Authorization failed."
  and search looked empty; now mirrors the catalog request shape and
  routes through makeAuthenticatedRequest with URL-based detection
- fix(stalker): TMDB fields survive info re-normalization; detail views
  prefer the store copy patched by async enrichment over stale snapshots
- refactor(xtream): split oversized vod/serial detail components into
  component-scoped playback services; detail routes re-initialize on
  route param changes (router reuses them for detail-to-detail nav)
- i18n: all new keys translated across the 18 locales

Docs: docs/architecture/tmdb-metadata-enrichment.md + CLAUDE.md updates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tmdb): provide route params observable to inline collection details, linearize regexes

The global-collection inline detail host builds a fake ActivatedRoute for
VodDetailsRouteComponent/SerialDetailsComponent with only snapshot.params.
Since the detail components now read route.params via toSignal() (detail->
detail re-init), the missing observable crashed component construction and
the content hero never rendered — broke dashboard-activation, favorites and
recent Electron E2E on all platforms. Provide the params observable
alongside the snapshot and assert it in the component spec.

Also resolves both CodeQL js/polynomial-redos alerts: bracket-stripping in
normalizeTitle now excludes opening delimiters inside the classes, and
youtubeEmbedUrl extracts watch?v= ids with a linear two-pass match instead
of "watch\?.*v=". Combining-diacritics range rewritten as explicit \u
escapes (greptile note).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tmdb): surface TMDB-only VOD score in the rating badge, drop youtube.com from CSP

Review follow-ups on PR #1123: the Xtream VOD detail badge renders
rating_imdb, but the merge wrote the TMDB score only into `rating`, so a
TMDB-only score was never displayed (Codex P2) — fill rating_imdb when the
provider left it empty, mirroring the Stalker merge. All trailer iframes
are normalized to youtube-nocookie.com, so the extra youtube.com frame-src
allowance was dead surface (greptile) — removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tmdb): resolve confirmed review findings — matching correctness, race guards, cache schema

Fixes the confirmed findings from the PR #1123 code review:

- Stalker search: setSelectedContentType now runs BEFORE setSelectedItem,
  so the TMDB enrichment gate in the selection hook no longer sees the
  content type of the previously open tab (wrong/no enrichment after
  ITV -> search -> movie).
- Title normalization is now two-tier (normalizeTitleKeys): the exact
  normalized form keeps a trailing year, the base form strips it and
  remembers the tag. Year stripping is anchored to the end of the title
  ("2001: A Space Odyssey" keeps its year) and language-prefix stripping
  is UPPERCASE-only ("It: Chapter Two" is no longer amputated).
- All catalog matching (similar rail, actor pages, DB worker
  DB_MATCH_TITLES) compares exact forms first and only accepts
  year-stripped matches when the stripped tag is year-compatible (+-1)
  with the TMDB year — "Blade Runner" (1982) can no longer claim a
  catalog "Blade Runner 2049". CatalogTitleMatch carries the stripped
  trailingYear so the renderer can apply the guard to worker matches.
- mergedBackdrops tolerates a plain-string backdrop_path; enrichment
  merge+patch blocks are wrapped in try/catch so a malformed provider
  payload can no longer become an unhandled rejection.
- loadGlobalMatches (both actor routes) guards against actor->actor
  navigation races — a slow match for the previous person no longer
  overwrites the current one's results.
- tmdb_metadata media_type CHECK widened to ('movie','tv','person') and
  person rows now use the honest 'person' type (TmdbCacheMediaType).
  Pre-release dev DBs with the narrow CHECK are rebuilt in place — the
  table is a pure cache, so the migration is a self-healing
  drop-and-recreate keyed off sqlite_master.

Docs updated (tmdb-metadata-enrichment.md, CLAUDE.md). New regression
coverage: title-normalization.util.spec.ts, two-tier cases in
tmdb-similar.util.spec.ts and title-match.operations.spec.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 17:07:46 +02:00
4gray e801028005 feat(epg): support playlist-scoped sources
Add playlist-scoped EPG source support for M3U playlists.
2026-06-21 22:53:07 +02:00
4gray 36a7ce9f3d fix(epg): decode compressed XMLTV responses
Decode HTTP Content-Encoding for EPG XMLTV streams before SAX parsing and harden gzip payload handling for mislabelled providers.

Validation:
- pnpm nx test electron-backend --testFile=epg-stream-decoder.spec.ts
- pnpm nx test electron-backend --testFile=epg-response-utils.spec.ts
- pnpm nx test electron-backend
- pnpm nx lint electron-backend
- pnpm nx run electron-backend:build-worker
2026-06-21 09:34:37 +02:00
4gray 3d4550f6c5 Merge remote-tracking branch 'origin/master' into agent/dashboard-rail-performance
# Conflicts:
#	apps/electron-backend/src/app/database/operations/content.operations.spec.ts
#	libs/shared/interfaces/src/lib/electron-api.interface.ts
2026-06-20 23:43:04 +02:00
4gray 89916af9d8 feat(search): add workspace global search with M3U support
Moves global search into a routed workspace view, adds M3U live/radio results, lazy pagination, and DB-backed matching improvements.
2026-06-20 23:05:48 +02:00
4gray b6c5367d63 fix(dashboard): speed up startup rails 2026-06-20 21:46:42 +02:00
4grayand4gray e8aa7c3a34 [codex] Add scoped EPG security trust controls (#1054)
* Add scoped EPG security trust controls

* fix: address scoped trust review feedback

---------

Co-authored-by: 4gray <fourgray@proton.me>
2026-06-12 20:46:24 +02:00
4grayand4gray 9043116ebd [codex] fix Electron hardening follow-ups (#1053)
* fix: clean up Electron hardening follow-ups

* fix: remove duplicate Xtream probe comment

---------

Co-authored-by: 4gray <fourgray@proton.me>
2026-06-12 20:46:01 +02:00
Salem 2c032cd3c8 fix(security): complete Electron hardening and review follow-ups
* fix(security): harden Electron IPC against MITM, SSRF, path and injection risks

S1 TLS: validate certs by default on playlist/EPG fetches (opt-out via IPTVNATOR_ALLOW_INSECURE_TLS); new util/secure-https.ts.
S2: write-file IPC restricted to save-dialog-authorized paths.
S3: XTREAM_PROBE_URL guarded by assertRemoteUrlAllowed + maxRedirects:0; new events/url-safety.ts (+19 tests).
S4: EPG titles rendered via interpolation, not [innerHTML].
S5: downloads reveal/play limited to recorded download paths.
S6: Stalker cmd encoded (slash-preserving) to block query injection.
EPG-worker and Stalker fetches reject file://-style/credentialed URLs; LAN/self-hosted targets remain allowed.

* perf(player): lazy-load web video players via @defer

Wrap Video.js/HTML5/ArtPlayer in @defer (on immediate) so video.js, hls.js,
artplayer and mpegts.js split into a deferred chunk loaded on first playback
instead of eagerly on the player route. Embedded MPV (native) stays eager.
Spec uses DeferBlockBehavior.Playthrough.

* fix(player): remove leaked HTML video listeners on destroy

volumechange used a mismatched removeEventListener reference, while
loadedmetadata and timeupdate were never removed at all. Bind all three to
stable handler fields used for both add and remove, and add a teardown
regression test asserting each listener is detached on destroy.

* refactor(dashboard): extract pure navigation helpers from DashboardDataService

Move the 8 stateless link/navigation-state/type-kind helpers into a new
dashboard-navigation.util.ts so the routing logic is independently testable and
the 1260-line god-service shrinks. DashboardDataService keeps the public methods
as thin delegators (facade) so the public API and the single consumer
(workspace-dashboard-rails) are unchanged. First slice of the DashboardDataService
decomposition; verified by the existing service spec (33/33) and the app typecheck.

* fix(review): address PR feedback (IPv6 link-local, write-path cap, @defer placeholder)

- url-safety: broaden IPv6 link-local detection to the full fe80::/10 range
  (fe80:: through febf::), not just the fe80:: prefix (+ regression tests).
- playlist.events: cap authorizedWritePaths (evict oldest past 32) so a save
  dialog opened without a following write cannot accumulate entries until restart.
- web-player-view: add a @placeholder to each @defer (on immediate) player block
  to avoid the one-frame blank/layout-shift before the chunk resolves.

* fix(security): close Electron network and download gaps

* test(downloads): cover cancellation and restart cleanup

* fix(downloads): address Greptile review gaps

* test(security): reproduce remaining Greptile findings

* fix(security): close remaining Greptile findings

* test(downloads): reproduce early database queue stall

* fix(downloads): release queue after setup failures

* test(downloads): reproduce completion queue stall

* fix(downloads): release queue after completion failures
2026-06-12 15:24:29 +02:00
4gray d24c77a143 chore(nx): enforce scoped workspace boundaries (#942) 2026-05-15 09:59:06 +02:00
4grayandClaude Opus 4.7 39ddaa2f9c perf(recents): batch Xtream remove-recent-item into a single IPC call
Clearing all recents of a given content type fired N concurrent IPC calls,
each opening its own implicit transaction in the recently_viewed table.
For users with hundreds of recently-watched VOD/series rows this added
real overhead even though the UI updates optimistically.

Add a new DB_REMOVE_RECENT_ITEMS_BATCH path end-to-end:
- removeRecentItemsBatch() Drizzle op: one transaction, one prepared
  statement reused per row
- Wire through worker → IPC handler → preload → window.electron typings
  → DatabaseService
- UnifiedRecentDataService.removeRecentItemsBatch() groups items by
  source. Xtream items go through the new batch IPC. M3U/Stalker items
  still go per-playlist because they update a JSON column on the
  playlist row, not the recently_viewed table — but they now run in
  parallel with the Xtream batch via a single Promise.all.
- Single call site updated: unified-collection-page "Clear all of type"
  confirmation handler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4bfb62f988b7
2026-05-01 23:00:32 +02:00
4grayandClaude Opus 4.7 eedfc498e7 perf(database): run PRAGMA optimize before closing connections
SQLite's recommended approach for keeping query plans current: cheap when
nothing needs analyzing, runs incremental ANALYZE on tables/indexes that
have grown significantly since the last run. Wrapped in try/catch since
optimize is advisory and must never block connection close.

Applied to both the main connection and the EPG worker connection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3d6901072049
2026-05-01 21:09:24 +02:00
4grayandClaude Opus 4.7 e33cb70dec perf(database): add SQLite performance pragmas (synchronous, cache, mmap)
Pair the existing journal_mode=WAL with the companion pragmas it needs to
actually pay off, and reduce read latency on hot query paths.

- synchronous=NORMAL: ~2-10x faster writes when paired with WAL. Safe — only
  risks losing the last committed txn on power loss; DB stays consistent.
- cache_size=-64000: 64MB page cache (default is 2MB).
- temp_store=MEMORY: keep sort/group temp tables in RAM.
- mmap_size=268435456: 256MB memory-mapped I/O for reads.

Applied to both connection sites: the main read-write/read-only connection
in libs/shared/database and the EPG worker connection in electron-backend.
The per-connection pragmas (cache_size, temp_store, mmap_size) apply to the
read-only agent-backend connection too.

Inspired by matracey/iptvnator@4ad8f88; ported manually since the file has
diverged significantly and the worker connection didn't exist in that fork.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3d6901072049
2026-05-01 21:02:52 +02:00
4gray 92cbd44e89 feat: implement content backdrop management and related database operations 2026-04-22 22:22:55 +02:00
4gray 1c1a53743a feat(backend): export playlists functionality
Entire-Checkpoint: c6e522b4276c
2026-04-21 08:52:39 +02:00
4gray 7b46509d21 feat: implement EPG source status component and enhance EPG data management in settings
Entire-Checkpoint: c6e522b4276c
2026-04-20 01:09:33 +02:00
4gray ff605fc024 fix(xtream): enhance content retrieval by adding content type filtering for Xtream ID lookups #756
Entire-Checkpoint: c6e522b4276c
2026-04-19 22:33:49 +02:00
4gray 82c3004b3b fix(electron-backend): avoid double-gunzipping EPG responses
Stop treating content-encoding as a manual gunzip signal, because fetch already returns decoded bodies for transfer-compressed XMLTV responses.

Keep redirect and payload-based gzip detection in place, and add regression coverage for redirected .gz feeds, gzip MIME types, content-disposition filenames, and plain content-encoding responses.

Entire-Checkpoint: c6e522b4276c
2026-04-16 22:43:17 +02:00
Eliran Sapir 5e6f58a758 Fix dropped EPG programs for late channel batches 2026-04-15 19:49:51 -04:00
Eliran Sapir e2b313084c Fix redirected gzipped EPG feeds 2026-04-14 23:04:26 -04:00
4gray 04f9686483 feat: implement detailed tracing for database operations, window events, and renderer API interactions 2026-04-05 23:26:09 +02:00
4gray 1d0b4ca1fe feat: implement category ID normalization and enhance category operations tests 2026-04-04 21:16:32 +02:00
4gray 4e0afe8b20 feat: implement playlist refresh functionality and cache management 2026-04-04 13:39:45 +02:00
4gray 688d9ec123 fix(worker): implement worker runtime path resolution and native module loading 2026-03-28 21:42:19 +01:00
4gray 456173b772 feat(database): implement DatabaseWorkerClient and associated worker functionality
- Added DatabaseWorkerClient to manage database operations in a worker thread.
- Created database-worker-client.ts to handle requests and responses between the main process and the worker.
- Introduced database-worker.types.ts for type definitions related to worker messages and operations.
- Implemented database.worker.ts to execute various database operations, including CRUD operations for playlists, content, and favorites.
- Added database.worker-connection.ts to manage database connections and ensure proper handling of SQLite instances.
- Created tests for DatabaseWorkerClient to ensure correct request handling and error management.
- Updated xtream.events.ts to allow suppression of error logging for specific requests.
- Enhanced EPG parser worker to include a busy timeout for SQLite operations.
- Integrated shutdown logic for the DatabaseWorkerClient in the main application lifecycle.
2026-03-28 15:40:06 +01:00
4gray e579c21486 Merge pull request #803 from GiviMAD/fix_epg_ignored_programs
fix: epg ignored programs
2026-03-17 23:30:42 +01:00
4gray 81c2aab829 test(e2e): add end-to-end tests for playlist import, providers, and settings persistence
- Implemented tests for importing M3U playlists via native dialog.
- Added smoke tests for loading Xtream and Stalker content through IPC.
- Created tests to verify persistence of remote control settings across app restarts.
- Updated smoke tests to check for main window properties and workspace content rendering.

refactor(electron-backend): enhance store service and database path handling

- Modified store service to use a dynamic configuration directory based on environment.
- Refactored database connection to utilize new path utility functions for better path management.

fix(e2e): improve test selectors and structure for better reliability

- Updated test selectors in web-e2e tests to use role-based queries for better accessibility.
- Refactored settings tests to reduce redundancy and improve clarity.

chore(e2e): update TypeScript configuration for better inclusion of test files

- Adjusted tsconfig.json to ensure proper inclusion of e2e test files.

Entire-Checkpoint: bb9a8e2e351e
2026-03-17 23:30:16 +01:00
Miguel Álvarez Díez 39d89c54f1 apply pr review 2026-03-03 23:03:41 +01:00
Miguel Álvarez Díez 75e5866a55 fix: epg ignored programs 2026-03-03 22:56:07 +01:00
4gray c3deb56bff refactor: remove expired program cleanup functionality and adjust EPG program queries 2026-02-01 22:54:00 +01:00
4gray e53555d324 fix(epg): robustly load better-sqlite3 from workerData, resourcesPath, or fallback 2026-01-13 23:31:27 +01:00
4gray b57f4a640a fix(electron): pass nativeModulesPath to workers and load native modules in worker to support packaged app 2026-01-13 22:55:32 +01:00
4gray 895a269200 feat(db): add EPG schema and IPC handlers for EPG storage
Add tables, indexes, FTS5 virtual table, and triggers to support EPG
(electronic program guide) data in the shared SQLite connection. The
changes introduce epg_channels and epg_programs tables, multiple
indexes for efficient lookups (by source, name, channel, start, and
time range), and an epg_programs_fts virtual table with triggers to
keep the full-text index synchronized with epg_programs.

Register EPG database event handlers in the Electron backend and
add an IPC handler for bulk-saving channels. The handler deletes
existing channels for a source and inserts new channels in chunks to
avoid large single inserts, returning the inserted count.

These changes enable storing, querying, and full-text searching EPG
data and prepare backend IPC endpoints for importing program guide
data.
2026-01-02 17:22:09 +01:00
4gray fe8e907e9f refactor(epg): implement streaming SAX-based EPG parser and batching
Replace full-file parsing with a streaming SAX parser to avoid loading
entire XMLTV payloads into memory. Add support for gzipped feeds by
using streaming gunzip (createGunzip) and piping into a SaxesParser,
allowing incremental processing of channels and programmes.

Introduce:
- StreamingEpgParser class that incrementally builds channel and program
  objects and emits batches.
- parseXmltvDate helper to normalize XMLTV datetimes to ISO strings.
- batching constants and response types for channel/program batches and
  progress reporting.
- use of Readable/Saxes for streaming parsing and reduced memory use.

This change improves performance and memory usage for large EPG files,
enables progress reporting and batch emission to the main thread, and
prepares for handling very large or gzipped EPG sources.
2025-12-26 01:32:51 +01:00
4gray a948ce0265 feat(electron): bundle worker assets and resolve worker path
Add webpack config and include it in the electron backend build so 
worker scripts are correctly bundled for packaged apps. Move worker 
files to be packaged as extraResources instead of asar unpack patterns 
so the packaged app can load worker JS from Resources/dist/... at 
runtime. Fix worker path resolution in epg.events: compute resourcesPath
from app.getAppPath() and point to dist/apps/electron-backend/workers 
when app.isPackaged. Remove verbose app path logging and update comments
to reflect the actual packaged layout.
Why: ensure webworker entry points are included in the renderer/backend
bundle and accessible when the Electron app is packaged, preventing
worker loading failures in production builds.
2025-11-24 20:46:15 +01:00
4gray 8e5a643fac feat(epg): worker-based EPG handlers and fetch logic 2025-10-23 21:53:51 +02:00