Commit Graph
471 Commits
Author SHA1 Message Date
4gray aead89e3e6 fix(xtream): keep category management available
Fixes #913
2026-05-09 18:34:57 +02:00
4gray 134b5fb779 Merge pull request #911 from 4gray/codex/stalker-radio
feat(stalker): add radio support
2026-05-09 15:50:10 +02:00
4gray 9261794b4a Merge pull request #902 from weeco/feat/xtream-xmltv-epg-fallback
feat(xtream): fall back to uploaded XMLTV when provider has no EPG
2026-05-09 13:06:51 +02:00
4gray ab905af285 feat(stalker): add radio support
Entire-Checkpoint: f957cd9849e0
2026-05-09 01:28:39 +02:00
4gray 3bafeb8377 Merge remote-tracking branch 'origin/master' into search_all_fix
# Conflicts:
#	libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.html
2026-05-08 22:01:01 +02:00
4gray 6efa8b9072 fix(xtream): preserve live search playback sidebar
Entire-Checkpoint: f957cd9849e0
2026-05-08 21:05:10 +02:00
4gray 268d4b9114 cover external player double-click workflow 2026-05-08 20:01:19 +02:00
4gray 5dbf258a2b refine double-click setting visibility 2026-05-08 19:51:06 +02:00
4gray e350a2ea96 fix double-click external playback handling 2026-05-08 10:59:52 +02:00
Tedd Johnson 626948f2f5 Add double-click stream open setting 2026-05-07 20:03:33 -07:00
weeco e751e82c07 feat(xtream): fall back to uploaded XMLTV when provider has no EPG
Live TV channels in Xtream playlists go blank whenever the provider's
get_short_epg returns nothing, even when the user has working XMLTV
URLs in Settings. The XMLTV pipeline already populates epg_programs
but only the M3U module was reading it.

Wire those uploads up as a fallback for Xtream too: when the provider
returns nothing for a channel with an epg_channel_id, look it up in
the local table. A settings toggle flips the priority for users whose
curated XMLTV is better than the provider's auto guide.

EpgQueueService.enqueue() became async to batch the XMLTV lookup once
per viewport change. To keep the queue consistent under fast scroll,
all shared-state mutations now happen behind a generation counter so
only the latest call commits. Per-method bridge gating in the new
XtreamXmltvFallbackService keeps each path working when the preload
exposes only one of the two endpoints.
2026-05-07 08:52:59 -07:00
4grayandClaude Opus 4.7 f9843fcb0c docs(blog): add release notes for IPTVnator v0.21
Covers the embedded MPV player on macOS, toggleable Live TV sidebars,
command palette upgrades, and module fixes. Credits the Telegram tip
about using IINA as the external player and links the related GitHub
issues.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:39:23 +02:00
4grayandClaude Opus 4.7 9424dedafc fix(embedded-mpv): stop crashing on IPC for not-yet-created sessions
The renderer was reading session.id and forwarding it to the addon, but
during the loading window that id is the placeholder
"embedded-mpv-starting" set by createLoadingSession. If the user adjusted
volume, seeked, or toggled audio/subtitle/speed/aspect before the addon's
createSession returned the real id, that placeholder id reached the
addon — and the addon's getSessionOrThrow threw a raw std::runtime_error
which libc++abi terminated the process on.

Two fixes, defense in depth:

1. Renderer (session controller): use the canonical sessionId() signal,
   which is null until the addon hands back a real id, as the gate for all
   IPC calls. Wrap every IPC call in a guardIpc helper that swallows
   addon-side throws so a torn-down session or race won't surface as an
   uncaught promise rejection.

2. Native (embedded_mpv.mm): change getSessionOrThrow to take a
   Napi::Env and throw Napi::Error::New(env, ...) instead of
   std::runtime_error. node-addon-api converts Napi::Error to a JS
   exception cleanly; the previous std::runtime_error escaped the C++
   frame and aborted the process when the addon was built without
   NAPI_CPP_EXCEPTIONS translation. Refactor splits findSession (returns
   nullptr) from getSessionOrThrow (env-aware) so call paths that just
   probe a session's existence don't pay the throw cost.

The native fix needs an addon rebuild to take effect; the renderer fix
prevents the crash trigger immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 10:29:23 +02:00
4grayandClaude Opus 4.7 8e3f559033 feat(embedded-mpv): subtitles, playback speed, aspect override, retry on error
Native addon (apps/electron-backend/native/src/embedded_mpv.mm)
- Extend SessionSnapshot with subtitleTracks, selectedSubtitleTrackId,
  playbackSpeed, aspectOverride.
- Refactor track parsing into a shared updateTracksFromNode helper that
  filters by mpv "type" so audio and subtitle tracks share the code path.
- Observe sid, speed, video-aspect-override; clear sub state on
  MPV_EVENT_START_FILE.
- Export setSubtitleTrack (handles trackId === -1 as "no" to disable),
  setSpeed (clamped to 0.25–4.0), setAspect (passthrough string for
  video-aspect-override).
- Snapshot output now includes the new fields.

Service (embedded-mpv-native.service.ts) + IPC + preload
- Mirror methods on EmbeddedMpvNativeService with capability detection: each
  method throws a descriptive error if the loaded addon doesn't expose the
  underlying native function (i.e. user is running an older build).
- New IPC channels EMBEDDED_MPV_SET_SUBTITLE_TRACK, _SET_SPEED, _SET_ASPECT
  registered in events file and exposed via preload.
- Extend EmbeddedMpvSupport with a capabilities probe so the renderer can
  hide controls for features the current addon build doesn't ship.

Renderer (embedded-mpv-player.component.{ts,html})
- Three new popovers anchored above their buttons (subtitle / speed /
  aspect), gated by capabilities() and (for subtitles) by track count.
- Subtitle popover includes an Off entry; speed/aspect use fixed presets.
- Error state now surfaces the same overlay as the stalled state, with a
  Retry button that bumps the existing retryNonce signal — covers #8 from
  the audit.
- All session-payload defaults (loading stub, error stub, refresh fallback,
  dispose payload, native createSession default) updated for the new
  required fields.

NOTE: Existing addon binaries do not expose the new methods. Until the
addon is rebuilt (pnpm run serve:backend:embedded-mpv or the release
build script), capabilities will report subtitles/playbackSpeed/
aspectOverride as false and the new buttons will simply not appear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 09:53:32 +02:00
4grayandClaude Opus 4.7 e192cba776 feat(player): add VLC reuse-instance setting (#893)
VLC was unconditionally spawned per click — VLC's own single-instance
preference fails because the per-launch RC args defeat its D-Bus
forwarder. Mirror the existing MPV reuse pattern so users can opt in to
driving one tracked VLC via its RC interface (clear + add) instead of
opening a new window every stream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-05 00:55:04 +02:00
4gray 6cbf2a1cfb feat(m3u): add context menus to recent channels
Entire-Checkpoint: f957cd9849e0
2026-05-05 00:27:15 +02:00
4grayandClaude Opus 4.7 a74b8f1b81 fix: prevent macOS sleep during embedded MPV playback
The embedded MPV player renders via libmpv into a custom Cocoa view, which
bypasses mpv's built-in screensaver inhibition. Hold an Electron
powerSaveBlocker (prevent-display-sleep) while any session is playing and
release it on pause, dispose, or shutdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-04 23:47:33 +02:00
4gray 4a21579aff fix: clear external player paths on save
Entire-Checkpoint: f957cd9849e0
2026-05-04 23:26:56 +02:00
4gray 2efdfea0c1 fix: preserve file paths for local playlist refresh
Refs https://github.com/4gray/iptvnator/issues/891

Entire-Checkpoint: f957cd9849e0
2026-05-04 10:06:39 +02:00
4gray 568c03fe0e fix: reset catalog pagination on search
Entire-Checkpoint: f957cd9849e0
2026-05-04 09:12:41 +02:00
4gray 1a09e696b4 test: add refresh overlay for Xtream source menu and improve visibility checks
Entire-Checkpoint: f957cd9849e0
2026-05-04 00:52:04 +02:00
4gray d0f590d8fe refactor: improve Xtream refresh functionality with localization support
- Introduced new localization keys for Xtream refresh actions in multiple languages (ar, ary, by, de, el, en, es, fr, it, ja, ko, nl, pl, pt, ru, tr, zh, zhtw).
- Implemented refresh preparation state management in PlaylistRefreshActionService.
- Enhanced WorkspaceShellXtreamImportService to handle refresh preparation states and display appropriate labels.
- Updated tests to cover new refresh preparation scenarios and ensure correct overlay display during refresh operations.

Entire-Checkpoint: f957cd9849e0
2026-05-04 00:51:29 +02:00
4grayandClaude Opus 4.7 bbbe7b4036 perf(renderer): inline splash + sync language hint + drop legacy polyfill
Three first-paint fixes for cold app start.

#1 — Inline splash in index.html
Cold start used to show a blank Material-grey background until the
~1.5MB preloaded chunk + 300KB styles.css downloaded and Angular
bootstrapped. On slow disks/networks that's 1-3+ s of "is the app
frozen?" before any pixel of UI appears.

Add a self-contained splash (inline CSS, no extra HTTP, no assets) —
"IPTVnator" wordmark + a CSS spinner — that paints immediately from
the parsed HTML. Removed in main.ts after bootstrapApplication()
resolves, gated on requestAnimationFrame so the swap happens after
AppComponent's first paint (no flash of empty background between
splash removal and the real UI).

Respects prefers-reduced-motion (spinner stops, opacity dims).

#3 — Drop legacy `global = window` polyfill
The inline <script> in <head> ("if (global === undefined) var global =
window") was a workaround for an older library expecting Node's
`global`. Verified nothing in the current bundle reads window.global
(the only matches are TypeScript `declare global { ... }` blocks and
unrelated route paths). Removes one render-blocking inline <script>
parse from the critical path.

#5 — Synchronous language hint
Settings live in IndexedDB (via @ngx-pwa/local-storage), which the
TranslateModule cannot read synchronously at bootstrap. So the first
render historically used English regardless of the user's preference,
then re-rendered every TranslatePipe in the tree once initSettings()
finished — visible flash for non-English users.

Mirror the active language to localStorage in initSettings() once the
IDB-backed settings resolve. Add getInitialLanguage() in app.config.ts
that reads that key synchronously before bootstrap and passes it to
TranslateModule.forRoot({ defaultLanguage }). First-ever boot still
falls back to English (no hint yet); every subsequent boot renders in
the saved language from the very first frame.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-03 00:00:06 +02:00
4gray 15b6ba985a feat(live-tv): collapsible sidebar in live tv views
Entire-Checkpoint: f957cd9849e0
2026-05-02 23:53:05 +02:00
4grayandClaude Opus 4.7 177148ad5d perf(electron): defer fixPath() off the synchronous startup path
fixPath() was called as the very first statement at module load
(top-level, before app.setName, before app.whenReady), which on
macOS/Linux spawns an interactive login shell — bash/zsh -ilc 'env' —
and waits SYNCHRONOUSLY for it to print the environment back. With
oh-my-zsh / heavy .bashrc setups this is routinely 50-300ms blocking
the Electron main process before window creation can even begin.

The only purpose of fixPath in this app is to populate process.env.PATH
so that subsequently-spawned external player binaries (MPV/VLC) can be
resolved by bare name. That's a user-action path (clicking play with
external player configured), not a startup-critical one. Two callers
exist (player.events.ts) and both fall back to bare 'mpv' / 'vlc' only
after checking well-known absolute paths.

Move the call into a setImmediate scheduled at the END of
bootstrapAppEvents — after DB init, IPC handler registration, and
window load. The user-visible startup sequence no longer carries the
shell-spawn cost. By the time anyone could plausibly click an external
player, PATH is already hydrated.

Idempotent + Windows-gated (fix-path is a no-op on Windows anyway).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
2026-05-02 19:06:28 +02:00
4gray a3275fa215 chore: add missing translations strings in all i18n files
Entire-Checkpoint: f957cd9849e0
2026-05-02 17:16:50 +02:00
4grayandClaude Opus 4.7 d67150d38f perf(dashboard): per-rail skeletons + drop CAST that blocked content index
Two independent fixes that compound: the dashboard now renders progressively
as each rail's data resolves, and the slowest of those rails (Xtream
recently-added) no longer scans the entire content table.

Per-rail skeletons (template restructure):
The old @if (!ready()) gate hid the whole dashboard until ALL FOUR loading
flags resolved (playlistsLoaded, globalRecentLoaded, globalFavoritesLoaded,
xtreamRecentlyAddedLoaded). The slowest one pinned the entire skeleton up
for the full tail latency — visibly seconds of "loading" even when 3 of 4
rails could have rendered immediately.

Replace with per-rail conditionals:
- Hero: renders the moment globalRecentItems[0] is available; skeleton
  shows only while data.globalRecentLoading() is true and no item exists.
- Each rail: shows real content if its cards are non-empty, its own
  skeleton if its dedicated loading flag is true, nothing otherwise.
- The Xtream recently-added rail's skeleton is gated on having Xtream
  playlists at all, so M3U-only users never see a skeleton for it.

The loading signals were already exposed on DashboardDataService
(globalRecentLoading, globalFavoritesLoading, xtreamRecentlyAddedLoading)
but went unused because of the monolithic gate. Same skeleton markup is
reused per rail; no styling changes.

Drop CAST(added AS INTEGER) in getGlobalRecentlyAdded:
The query ordered by sql<number>\`CAST(content.added AS INTEGER)\`. SQLite
cannot use an index on a column wrapped in a function, so the existing
idx_content_type_added index was bypassed and the planner did a full table
scan + sort on content (10k–100k+ rows for a typical Xtream catalog) on
every dashboard load.

Sort by schema.content.added directly. Xtream stores Unix-epoch timestamps
as 10-digit numeric strings (anything since 2001-09-09), so lexicographic
and numeric sort are equivalent. The (type, added) index now drives the
ORDER BY too — full table scan becomes an index range scan + LIMIT 20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 1e3392724bc8
2026-05-02 09:09:10 +02:00
4gray ec2f6a702a chore: resolve master conflicts for embedded mpv branch
Entire-Checkpoint: 5b514fe72836
2026-05-02 02:19:54 +02:00
4grayandClaude Opus 4.7 66cd3d5c4f fix(database): wrap placeholder in sql template for prepared favorite update
Drizzle's .set() expects a column-typed value or an SQL fragment, not a
bare Placeholder. The earlier prepared-statement refactor (313230ab)
passed sql.placeholder('position') directly, which compiled clean under
ts-jest's isolated-modules mode but failed the full type check during
nx build:

  TS2322: Type 'Placeholder<"position", any>' is not assignable to
          type 'number | SQL<unknown> | SQLiteColumn<...>'

Wrap the placeholder in sql<number>\`...\` so it resolves to SQL<number>
at compile time. Behavior at runtime is identical — the placeholder is
still bound at execute() time per chunk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 746d41da84ff
2026-05-02 01:00:33 +02:00
4grayandClaude Opus 4.7 2b3c65932d fix(data-service): track and actually remove window message listeners
Both ElectronService.listenOn() and PwaService.listenOn() called
window.addEventListener('message', callback) but neither could remove
the listener afterwards:

- ElectronService.removeAllListeners() called a placeholder
  getListenerForCommand() that returned a fresh () => undefined function
  on every invocation — never matching the registered listener (with a
  comment confessing as much).
- PwaService.removeAllListeners() was a literal `// not implemented`
  no-op.

The result was a latent memory leak: every listenOn() call accumulated
a global window listener that nothing could remove. Calling listenOn()
twice for the same command also stacked duplicate listeners.

Track callbacks in a messageListeners Map keyed by command name. On
listenOn(), drop any existing listener for that command before adding
the new one. On removeAllListeners(type), remove either the named
listener or all of them (when type === 'all').

The fix lands proactively: grep finds no current callers in the
renderer, so today's leak is theoretical. But the API is exposed via
the abstract DataService and the placeholder comment explicitly invited
the bug. Closing the footgun is cheaper than discovering it later.

Inspired by matracey/iptvnator@df7e1dc — extends the fix to PwaService,
which had the same bug in even more obvious form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3f453cc0085a
2026-05-02 00:29:49 +02:00
4grayandClaude Opus 4.7 d1af5f0510 perf(epg): batch current-programs lookup into a single IPC + SQL query
Channel-list rendering called EpgService.getCurrentProgramsForChannels(),
which forkJoined N getChannelPrograms() Observables — each firing its own
IPC round-trip and its own SQL query. For a 500-channel visible window on
first scroll, that was 500 IPC calls and 500 SELECTs hammering the EPG
table.

Add GET_CURRENT_PROGRAMS_BATCH IPC handler that takes the channel-id
array and runs a single SELECT with WHERE channel_id IN (...) AND
start <= now AND stop >= now. The renderer-side cache and TTL behavior
are preserved; only the network of IPC calls collapses to one. A
fallback path keeps the old per-channel behavior if the preload lacks
the new endpoint.

Per-channel display-name fallback (NOCASE id, then NOCASE display name)
is preserved from handleGetChannelPrograms so behavior matches the
existing single-channel handler.

Inspired by matracey/iptvnator@d25a7e8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6719280e397b
2026-05-01 23:45:10 +02:00
4grayandClaude Opus 4.7 94a0756424 fix(ui): unify the three side-by-side empty states in m3u groups view
Three placeholders were rendering at once when a search filter cleared
both the groups rail and the channel list, and they each used a
different visual treatment:

1. Groups rail (.groups-nav-empty) was bare 0.82rem muted text with no
   icon and no hint, and routed through CHANNELS.NO_SEARCH_RESULTS
   ("No channels found") which is wrong copy for the *groups* column.
2. Channel list (.groups-content-empty-state) was the canonical L4
   icon + title + hint pattern (correct).
3. Player area (<app-portal-empty-state>) was a 72px icon at 0.2
   opacity, which read as ghosted on dark theme.

Bring the groups rail into the L4 family scaled down for the narrow
column: 28px search_off / folder_off icon at 0.5 opacity, 0.85rem
title, 0.78rem hint at 0.65 opacity, centered. Add a CHANNELS.NO_GROUPS_FOUND
i18n key so the copy reflects the column ("No groups found").

Bump portal-empty-state's icon opacity from 0.2 -> 0.35 and title
opacity from 0.5 -> 0.6 so the player-area placeholder feels present
rather than abandoned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6719280e397b
2026-05-01 23:43:36 +02:00
4grayandClaude Opus 4.7 d1e4c0d306 fix(portal): show search-aware empty state when category search yields no results
When a search filter inside a category produces zero results, the grid
@empty fallback used to render the EMPTY_CATEGORY view ("No content in
this category" with the empty-tv illustration). That message was
misleading — the category itself wasn't empty, only the search filter
excluded everything.

grid-list now takes a searchTerm input and branches the @empty fallback:
when a search is active it renders the NO_SEARCH_RESULTS view with the
parameterised "No results found for {term}" title, otherwise it falls
back to EMPTY_CATEGORY as before. category-content-view derives the
searchTerm signal from the ?q= query param and passes it through.

Also: bump the NO_SEARCH_RESULTS icon from a 64px mat-icon to a clamp()
sized icon (120-180px) at 0.5 opacity so it visually balances with the
SVG illustrations used by the other viewTypes, swap the icon to
search_off (struck-through magnifier) so it reads as "no results"
rather than "search now", and fix the EN i18n typo
("change you search request" -> "change your search request").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 2e1a36e2f0f6
2026-05-01 23:17:22 +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 21fe51e73b fix(ui): redesign EPG no-data empty state to match app pattern
Replace the dashed-bordered "Ooops" placeholder in the EPG side panel
(rendered when the selected date has no programmes) with the icon +
title + hint pattern already used by groups view, season container,
and portal empty states.

The dashed border was a visual outlier — every other empty state in
the codebase is borderless, and a dashed stroke conventionally signals
a drop-zone or "add new" affordance, neither of which applies to a
passive informational state. The new layout uses a 48px event_busy
icon at 0.5 opacity, a 1.05rem title, and a 0.85rem hint at 0.65
opacity, centered in the panel via flex.

Soften the EN microcopy ("Ooops, EPG is not available for the selected
date" -> "No program guide", which also fixes the "Ooops" typo) and
add a NO_GUIDE_HINT key with localized strings across all 18 locales,
nudging the user toward the date arrows or playlist refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 4bfb62f988b7
2026-05-01 22:55:17 +02:00
4gray cc239f3b3f chore(config): conditionally enable store devtools based on production mode
Entire-Checkpoint: 4bfb62f988b7
2026-05-01 22:54:21 +02:00
4grayandClaude Opus 4.7 313230ab16 perf(database): hoist prepared statement out of favorites reorder loop
The inner loop in reorderGlobalFavorites() rebuilt the same
update().set().where() AST per row — up to thousands of times for large
favorite lists. Hoist a prepared statement using sql.placeholder() so
Drizzle generates the SQL string once and SQLite caches the parsed plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3d6901072049
2026-05-01 21:15:03 +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 69d106bdb3 perf(database): wrap chunked bulk writes in transactions
Each chunk in a bulk insert/update/delete loop was running as its own
implicit transaction, triggering one WAL commit (and one fsync, even with
synchronous=NORMAL) per chunk-internal statement. Wrap each chunk in a
single Drizzle transaction so the chunk commits as a unit.

Per-chunk transactions (not whole-loop) preserves:
- Cancellation between chunks via checkpointOperation()
- Async progress reporting via reportOperationProgress()
- Bounded write-lock duration (no minutes-long single transaction)

Sites updated:
- content.operations.ts: Xtream content bulk insert + clearXtreamImportCache deletes
- playlist.operations.ts: upsertAppPlaylists loop + cascade delete chunks
- favorites.operations.ts: reorderGlobalFavorites nested update loop
- xtream.operations.ts: cascade content/category deletes + favorites/recently-viewed restore

Highest impact: Xtream content imports (10k-100k+ rows) and M3U playlist
upserts. EPG worker already used transactions correctly — unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3d6901072049
2026-05-01 21:07:47 +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 1af287205f perf: optimize playlist favorite lookups
Use Map and Set lookups for M3U favorite aggregation, dashboard global favorite mapping, and channel-list favorite/group resolution.

Remove the duplicate NgxIndexedDB provider registration and add regression coverage for duplicate URL and earliest-match behavior.

Entire-Checkpoint: c2cea9c530e6
2026-05-01 20:44:30 +02:00
4gray 6ef9e8c4b0 fix(web): lazy load route feature boundaries
Entire-Checkpoint: c2cea9c530e6
2026-05-01 20:38:14 +02:00
4gray c51d8161ea refactor(web): split settings component into sections
Entire-Checkpoint: c2cea9c530e6
2026-05-01 20:16:32 +02:00
4grayandClaude Opus 4.7 2f47ed16cb feat(command-palette): add recently used section and switch-player commands
Adds a "Recently used" section at the top of the command palette (capped at 5,
persisted via StorageMap) and five "Switch player to ..." commands for one-step
player switching from anywhere. MPV/VLC are gated to Electron; the active player
entry is shown disabled. Switching applies to the next playback session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c2cea9c530e6
2026-04-30 23:54:02 +02:00
4gray 290843ef3f fix: restore recent live favorite toggles
Entire-Checkpoint: c2cea9c530e6
2026-04-30 21:35:39 +02:00
4gray 29ed6bd12b test(settings): add default cover size to settings spec
Entire-Checkpoint: c2cea9c530e6
2026-04-30 20:57:00 +02:00
4gray b5a85fdf5f style: add shared count-badge style and update component usage
Entire-Checkpoint: c2cea9c530e6
2026-04-30 20:32:55 +02:00
4gray e9a36b55ff test(e2e): update player close button references and enhance visibility checks
Entire-Checkpoint: c2cea9c530e6
2026-04-30 19:49:18 +02:00
4gray 4b2076fa53 feat(settings): add cover size settings and implement responsive cover sizing
Entire-Checkpoint: c2cea9c530e6
2026-04-30 18:56:06 +02:00
4gray a6e886116c style(dialog): increase dialog width from 300px to 400px for better visibility
Entire-Checkpoint: c2cea9c530e6
2026-04-30 13:19:38 +02:00