mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
* feat(settings): add PIN-protected parental lock for categories (#285) Locks are per category (Xtream category ids, Stalker genre ids, M3U group titles) and kept in one renderer lock store persisted to app_state / localStorage; `categories.locked` is the SQLite index re-stamped from it. While the lock is active the DB worker filters every content read, the PWA data source, the Stalker store and the M3U channel list filter in memory, and the enforcement service reloads the stores and steps off withheld selections. Settings → Parental lock sets the PIN (PBKDF2, never in Settings), the relock timeout and Lock now; lock toggles live in the Xtream/M3U management dialogs and a new Stalker lock dialog, all behind the PIN. Backups carry the locks per playlist entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): harden the parental lock after review - The M3U group dialog opens only after the PIN, like the Xtream and Stalker dialogs: it lists locked group names and can rewrite the locks. - Change PIN and Disable always verify the stored hash, even while the session is unlocked, so an app left unlocked cannot lose its lock. - Stalker paging judges progress on the raw portal page: withheld ids the list has not seen count as progress, a page made only of locked rows requests the next one itself, and the VOD total is reduced by withheld ids so the grid stops asking once every visible row is in. - Parental lock contract linked from the agent context map after the guidance reorganization; bridge helpers split out to stay under the file-size cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): guard locked categories on routes, PWA search and paging - Xtream and Stalker `:categoryId` routes carry a parental-lock guard: a locked category reached by URL prompts for the PIN and redirects to the section root on refusal (Electron row ids are mapped to provider ids). - PWA search filters withheld categories like the catalog reads. - Electron warm-cache detection confirms an empty, lock-filtered read with the unfiltered existence check instead of refetching from the provider. - A Stalker lock flip past page 1 drops withheld rows at once and restarts the list from page 1 instead of appending onto stale pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): compile the PIN hashing helper in the Node backend build The web backend compiles the shared interfaces library without DOM typings, so the DOM-only `SubtleCrypto` / `BufferSource` names broke its Docker build. The helper now describes the WebCrypto surface it needs structurally and reaches it through `globalThis`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close the remaining parental-lock gaps from review - Detail routes check the item's own category: a locked movie or series paired with an unlocked category id in the URL is still refused. - `requestUnlock()` awaits the settings load before it can answer "not active", so a slow startup cannot open a management dialog unguarded. - `SETTINGS_UPDATE` only persists the `parentalLockEnabled` mirror and releases the worker on switch-off; it no longer re-locks the worker on every ordinary settings save under a renderer that shows "unlocked". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): cover PWA cold navigation, Stalker search and the PWA lock editor - The Xtream detail guard hydrates the PWA session cache before judging an item on a cold navigation and fails closed when the catalog cannot place the item. - The dedicated Stalker search route filters withheld genres, re-fires on lock changes, judges paging on the raw page and restarts from page 1 on a lock flip. - The Xtream category dialog loads its lock candidates through the capability-selected data source; the PWA source now lists its raw categories with lock flags, so locks can be configured there too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): arm the relock timer on enable and harden Stalker search relock - The idle timer follows the unlocked transition instead of `active`, so the session that just enabled the lock still locks itself later. - Stalker search closes an open detail whose genre became withheld on relock and advances by itself past pages made only of locked rows (only while they add ids the list has not seen). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close lock editors on relock and clear withheld details opened from All The Xtream, Stalker and M3U category editors are gated by the PIN only when they open; an idle relock left them on screen listing locked names with a lock-rewriting Save. Each now closes itself when the session relocks. ParentalLockEnforcementService also judges the selected Xtream/Stalker item by its own category: a detail opened from All, recently added or search has no selected category to vanish with, so it stayed open after a relock. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): fail closed on unreadable settings and finish relock clean-up - Unreadable settings (IndexedDB load failure) left the feature switch at its default and announced "unlocked" to the main process. A stored PIN now stands in for the switch, and without one nothing is announced, so the worker keeps its mirrored locked default. - Lock applies run one at a time and abandon superseded results; the Electron data source keys its in-flight share by lock version so a relock can never reuse an unlock refresh's unfiltered rows. - The stored in-portal Xtream search is re-run on a lock change. - Stalker live/radio selections are judged by tv_genre_id, and both live layouts drop the playback of a channel whose category became withheld. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): fail closed on an unreadable lock store and make lock writes reliable - A lock store that cannot be read is no longer treated as empty: while the lock is active every category is withheld (renderer predicates and set-based filters alike) until the PIN is entered or the store reads again, and writes are refused meanwhile so an empty in-memory store can never wipe the persisted locks. The lock set now lives in its own ParentalLockLockStore service. - The M3U group dialog's lock write is awaited and a failed save is reported in a snackbar instead of being silently dropped. - The Electron categories.locked re-stamp clears and re-locks inside one transaction, so a failed restamp keeps the previous index. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): drop pre-relock Stalker search pages and fail closed on a corrupt lock store - A Stalker search page issued before a relock was filtered with the pre-relock withheld set and could still be applied after it; the staleness check now includes the parental lock version. - A lock store payload that does not parse or is not an object is a failed read (everything withheld until it reads again), no longer an empty store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): close the startup, re-stamp, relock-refresh and switch-persistence gaps - The window before the initial lock store read settles now withholds everything, like an unreadable store: settings can report the feature as on before the locks are known. - The store commits before the SQLite index re-stamp; a failed re-stamp now rolls the store back, a failed rollback re-stamps on the next access, and every launch re-derives the index from the store. - Xtream category/content reloads fail closed: a rejected reload empties the affected lists (content types drop back to idle) instead of keeping rows read under the previous lock state. - Enabling/disabling the feature persists through one guarded path that undoes the in-memory switch and skips the Electron mirror on a failed settings write. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(xtream): move the parental-lock reload specs beside the content spec The content feature spec sits at the 1200-line spec cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): await the startup lock-index reconciliation and withhold genre-less rows when failing closed - The lock store is readable only once the SQLite index has been re-derived from it, and a re-stamp that keeps failing keeps the session fail-closed, so catalog reads can never serve rows stamped unlocked by a stale index. - While everything is withheld, Stalker rows without a genre are withheld as well (the store filter and the renderer predicate). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): await the enablement mirror, restore partial lock stamps and validate nested lock-store entries - The Electron mirror of the feature switch is awaited; a mirror that cannot be written undoes the settings write, so a reload never starts from a mirror that disagrees with the persisted switch. - A failed multi-type re-stamp rolls the store back AND re-stamps every touched type from it, since earlier types may already carry the new locks; a failed rollback keeps the playlist stale (fail-closed). - A persisted lock store whose nested entries are not what writeLocks produces is a failed read, not an empty store. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): withhold the Xtream catalog at relock time, keep exact M3U titles in backups, roll back a failed relock-timeout save - A relock now fails closed immediately: the selected detail is stepped off against the lock store, the catalog lists and stored search results are emptied, and the filtered reloads publish only while the captured lock version is still current. - Backups carry M3U lock titles verbatim (exact dedup), since the locks match group titles exactly. - A relock-timeout write that fails reverts the in-memory value and shows the settings save-failure snackbar. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): clear the lock index before a playlist's last lock leaves the store, retry failed PIN reads, guard backups on the lock store - A write that removes a playlist's last lock clears the SQLite index first and drops the store key afterwards, so an interruption between the two can only leave a state the startup reconcile repairs toward locked. - A PIN hash read failure is distinct from an absent PIN: the session stays locked and every PIN-protected step re-reads it first. - Backup export awaits parental lock initialization and refuses to run while the lock store is not readable, since an absent lock field means "no opinion" on restore. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): withhold Electron Xtream reads while locks are unknown, persist the switch when settings are unreadable, re-stamp after a recovered read - ElectronXtreamDataSource serves no categories, content or search hits while the lock store withholds everything; its SQLite index may still carry a stale stamp. - setupPin decides whether to persist the switch from the settings value before the PIN is stored, since enabled follows hasPin while the switch is unknown. - A lock store recovered by a later read marks its playlists stale so the index is re-derived, a persisted entry must carry all three lists, and a stale Stalker search page is dropped before touching the withheld-id bookkeeping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): keep unlocked category routes reachable and defer a relock reload that overtakes the initial hydration - The Xtream category guard no longer runs the item check on category-only routes (Number(null) is 0), which prompted for the PIN on every unlocked VOD and series category while the lock was active. - A lock change during the initial Xtream hydration withholds the rows the hydration publishes and runs the filtered reload once it has settled, on every path that marks the content initialized. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): resolve hidden live categories before relocking playback and reload categories in the deferred hydration path - The Xtream live layout resolves a playing channel's category through the unfiltered rows when the visible list lacks it (search can play a hidden category's channel); until that lookup lands the category is unknown and a relock stops the channel. - A relock that overtakes the initial hydration now withholds the category publications too and reloads categories with the content. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): step off the M3U channel and Stalker selection before awaiting the Xtream relock reload The Xtream store stays populated after leaving that portal, so its reload runs on every apply; a locked M3U channel no longer keeps playing behind a slow database or provider read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): gate the workspace on parental lock init, edit only a readable lock store, guard the deferred reload, validate backup lock entries - The workspace route resolver awaits ParentalLockService.initialize() next to the settings load, so no route or catalog activates before the PIN and lock store are known. - Every lock write re-reads a failed store before building its edit, so a recovered store is edited rather than overwritten. - The deferred hydration reload runs under the publish guard of the request that deferred it. - Backup import validates every parental lock entry and rejects a damaged list instead of erasing the persisted locks on restore. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): discard stale hidden-category lookups and key withheld Stalker rows by their real identity - A hidden-category lookup that lands after a later playback (same provider id, another playlist) no longer overwrites the newer channel's category; resolutions are generation- and playlist-checked. - Withheld Stalker rows are keyed by id, stream_id, movie_id, series_id or the row's cmd/name, so id-less rows no longer collapse onto one key and stall paging past locked pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): gate the Electron cached category/content reads while locks are unknown The warm-route hydration reads the cache directly; it now returns nothing while the lock store withholds everything, like the live reads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): retire in-flight searches on relock clearing and publish lock revisions after the stamps - clearSearchResults() advances the search request version, so a search issued under the previous lock state cannot republish what a relock just cleared. - A lock write publishes its store revision only once every touched type is stamped, so a reload triggered by it cannot read a later type through its old stamps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): retire a resolving Stalker live playback when the session relocks The embedded player defers selecting the channel until its stream resolves, so the enforcement service's cleared selection could not retire the request; it now carries the lock version it was issued under and is dropped when a relock happened meanwhile. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(settings): one lock entry point per rail plus a right-click Lock/Unlock - Stalker's dedicated lock button becomes the same "Manage categories" (tune) button the Xtream rail has; it opens the lock-only dialog, so every portal type shares one entry point and the rail header keeps three actions. - Right-clicking a category (Xtream, Stalker) or an M3U group offers a single-row Lock / Unlock through the shared CategoryLockMenuComponent, behind the same PIN gate and lock store as the dialog. - The settings hint explains where locks are set; group lock strings added to all locales (ru/de translated). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(settings): serialize lock-store writes and drop a deleted playlist's locks - Lock-store mutations run through one write queue: each rewrites the whole persisted store, so overlapping edits could otherwise snapshot the same store and the later write would drop the earlier edit. - Deleting a playlist removes its locks through the PLAYLIST_DELETE_CLEANUP hook; "Remove all playlists" clears the lock store once the deletion has succeeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): apply single-row lock toggles inside the lock store's write queue The right-click Lock/Unlock (portal categories and M3U groups) built the new list before entering the queue, so two quick toggles shared one snapshot and the second dropped the first. Lock writes now accept an edit of the current list, evaluated inside the queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed settings read before any parental-lock settings write updateSettings writes the whole settings object, which after a failed startup read is the defaults; enabling the lock or changing the relock timeout then replaced the user's persisted preferences. The read is retried first and the write refused while settings stay unreadable. The settings writes move to parental-lock-settings-writer.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): show the locked-groups row on the M3U rail and roll the relock timeout back to the recovered value - The M3U groups rail now renders the same "N locked · Enter PIN to show" row as the portal category rail, so locked groups no longer vanish without an in-context unlock. - A failed relock-timeout write rolls back to the value read after the settings retry, not to the pre-retry default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed clear-all of the lock store and keep restored new playlists free of stale locks A lock-store clear that failed after "Remove all playlists" only logged, so a later restore reusing a playlist id could inherit the deleted playlist's locks. The in-memory store now empties at once and the persisted clear is retried on the next access; a restore that creates a playlist starts it from empty locks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): count radio playback as lock activity and read the lock store before a restore's stale-id check - The idle relock no longer interrupts a playing radio station: playing <audio> counts as activity, like video. - A restore retries a failed lock-store read before checking a reused id for stale locks, and aborts while the store stays unreadable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): drop withheld Stalker search rows at relock time and keep the M3U unlock row when every group is locked - A relock during a page-1 Stalker search now filters the rows already on screen at once, so old unlocked results are not clickable while the replacement page is pending. - When every M3U group is locked the groups rail still renders, with its "N locked · Enter PIN to show" row, instead of the plain empty state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(settings): keep the PIN dialog and the Stalker enforcement step off the initial path Master (#1712) moved the UI component barrel and the Stalker data layer out of main.js and tightened the initial budget to 2 MB. The parental-lock prompt imported the PIN dialog through the ui/components barrel and the enforcement service injected the Stalker store at startup, which pulled both back in (2.55 MB, over budget). The PIN dialog now loads through a local lazy file on the first prompt, and the Stalker step loads only while a Stalker route is open: initial total 1.65 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore the lock index when an emptying write fails and publish rollbacks after re-stamping - Removing a playlist's last lock clears the SQLite index first; if the clear or the store write then fails, the index is re-stamped from the previous locks at once. Title matching and multi-source discovery query the worker directly and trust the index, so the stale flag alone did not protect them. - A rollback publishes its store revision only after every type is re-stamped, so a reload cannot read a later type through the attempted stamps. - docs: restore the index rules the earlier surfaces rewrite dropped from the contract, now in the Lock store lifetime section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the M3U groups view when every group is locked With every group locked the filtered channel list is empty, so the container showed its generic empty state and the groups rail's "N locked · Enter PIN to show" row never appeared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed when the lazy Stalker enforcement step cannot load A rejected chunk (e.g. a stale PWA page after a deployment) escaped applyStalker(), so a locked Stalker selection kept playing after a relock and the Xtream step was skipped. The step now leaves the Stalker route on a load failure, which clears the selection and stops playback, and the Xtream step still runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): defer a stale Xtream hydration as soon as the catalog is withheld On Electron a relock that overtook the initial content hydration waited for the category reload before the content reload set the deferral flag; the older unlocked hydration could publish its streams in that window. withholdCatalog() now sets the flag itself, before anything is awaited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore backup locks after the Xtream merge and snapshot the Stalker lock dialog's categories - A backup restore now writes the parental locks last, so a failed Xtream merge leaves the playlist's previous locks in place instead of the backup's possibly smaller set. - The Stalker lock dialog snapshots the category list before its lazy import and opens only if the route is unchanged, so another portal's categories can never be saved under this playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed on relock ahead of the apply queue and judge Xtream selections by the lock store - On relock the synchronous fail-closed steps (M3U channel, Stalker selection, the locked Xtream detail, catalog lists, stored search) run immediately instead of queueing behind an earlier apply that may still wait on a slow or hung read. - The post-reload Xtream checks decide by the lock store through the unfiltered category rows rather than by absence from the reloaded list, which also omits merely hidden categories; unreadable rows fail closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): require the PIN for lock-bearing backup restores and fail closed during index re-stamps - A backup carrying lock lists replaces the matching playlists' locks, possibly with an emptier set; the import now asks for the PIN (after the file was chosen) and aborts when it is refused. - While a write re-stamps the SQLite index the playlist counts as stale, so a relock inside that window reloads fail-closed instead of through the old stamps. The internal store write now needs only a readable store, so a rollback can still land. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): drop parental-lock Xtream reloads once the playlist is switched A reload issued for playlist A no longer publishes into the shared Xtream store after the user opened playlist B: the store's reloads guard on the playlist they read for, and the enforcement apply retires its search refresh and selection checks on a playlist switch as on a newer lock version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): re-ask the PIN before a relocked backup merge and keep the PIN cooldown across prompts A backup merge now asks for the PIN again right before it replaces a playlist's locks when the app relocked during the import, instead of relying on the answer given at the start. The wrong-PIN count and the 30-second pause move from the dialog into the lock service, so dismissing and reopening the prompt no longer resets them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): refuse lock removals that commit after a relock and keep the index stale until the store write lands Lock edits that take a lock away now commit only while the session is unlocked, checked inside the write queue at commit time, so an editor save still in flight (or queued) when the app relocks cannot remove locks. Adding locks stays allowed. The Xtream category dialog drops its lock draft after a relock, and a backup restore re-asks the PIN only when it would remove a lock. Clearing a playlist's last lock keeps its index stale until the store write has landed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): clear an Xtream detail from a hidden category synchronously on relock The synchronous relock step now clears a selected Xtream item whose category the visible category list cannot place (a manually hidden category opened through search), instead of leaving it usable until the awaited reloads and lookup finish. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed when the Electron bridge lacks the parental lock worker filter A new runtime capability requires the lock-state and index-stamping IPC. When Electron reads Xtream through the SQLite worker without it (a partial or older preload), the locked session withholds every category instead of trusting a worker that never learned the lock state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): re-check lock removals at their durable commit points A lock removal that passed the unlocked check before its write is asked again right after the store write and, for Xtream, after the index stamps. A relock in between writes the previous store back or rolls the stamps back before anything is published. The stale-index bookkeeping, index stamping and store merge move into helpers to keep the lock store within the file size limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retry a failed revert of a refused lock removal and fail closed meanwhile When writing the previous store back after a relock-refused removal fails, the lock store now keeps a pending rewrite, is not readable (the locked session withholds everything) and rewrites the persisted store from memory on the next access, so a restart cannot load the removal. A failed "Remove all playlists" clear shares the same retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): authorize lock removals when issued and close genre-less Stalker details in fail-closed mode A lock removal is now authorized right before its first write is issued; a relock that lands after that is ordered after the write, which completes. This drops the post-write rollback, whose own failure could leave the persisted store diverged from memory across a restart. The Stalker search closes a detail without a genre on relock while every category is withheld. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): restore the previous locks when an Xtream rollback write fails When an Xtream lock edit's re-stamp fails and the rollback store write fails too, memory now goes back to the previous locks and a pending rewrite persists them on the next store access before the index is re-stamped, so the failed edit cannot take effect through that re-stamp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(stalker): cover page-one rows leaving the screen on relock while the reload hangs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): offer the portal unlock row in fail-closed mode When the lock store cannot be read every portal category is withheld but no locked ids are known, so the rail showed no "Enter PIN to show" row. It now shows the row without a count in that state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed for direct worker title lookups and capture the Stalker lock dialog context before the PIN Catalog title matching and multi-source discovery query the SQLite worker directly; they now return nothing while the parental lock withholds everything (unreadable store or a bridge without the worker filter). The Stalker lock dialog captures its playlist, provider and section before the PIN prompt and re-checks them after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): retire multi-source alternatives on a lock change and keep reconcile off in-flight stamps The VOD multi-source host keys its discovery session to the parental lock version: a lock change drops the discovered sources, retires discoveries and switches in flight, and rediscovers through the worker's new lock state. Stale-index entries of a write still stamping are no longer retried by a concurrent reconcile, which could re-stamp from a store the write had not committed yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): fail closed on a rejected worker lock sync, tear down Stalker synchronously and capture the Xtream dialog context before the PIN - A rejected lock-state sync to the SQLite worker makes the locked session withhold everything until a later sync succeeds. - The Stalker enforcement chunk is preloaded when a Stalker route opens; a relock runs it synchronously, or leaves the route at once while it is not loaded, instead of awaiting the chunk. - Xtream "Manage categories" captures playlist, provider and section before the PIN prompt and re-checks them after it and after the dialog import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the relock timeout behind the PIN and bind M3U group lock toggles to their playlist A locked session can no longer change the relock timeout: the Settings selector is disabled until the PIN is entered and the service refuses the change while locked. An M3U right-click lock toggle now captures its playlist before the PIN prompt and is saved only if that playlist is still open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): reset a locked M3U channel however it becomes active The enforcement service now checks the active M3U channel whenever it changes while locked, so numeric zapping, next/previous and remote commands, which select from the full channel list, cannot start a channel of a locked group. Numeric zapping also skips such a channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): bind the M3U group management result to its playlist The groups view captures the playlist before the PIN prompt and drops the management dialog's hidden and locked group lists once another playlist is open, so they cannot be saved under that playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(parental-lock): record the accepted restart case of a failed rollback write Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): build bulk lock drafts only from a readable lock store The Xtream and M3U management dialogs offer lock toggles, and the Stalker lock dialog opens, only once the lock store has been read. A draft built from the empty fail-closed snapshot would otherwise replace the real locks with nothing on Save if storage recovered in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): keep the parental lock switch on the saved state and roll back to the recovered value The Settings switch snaps back to the saved state when clicked and follows it once the PIN action succeeds, so a cancelled or refused PIN no longer leaves it showing the opposite state. A failed switch write is undone to the value read after the settings retry instead of the hard-coded inverse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): match noncanonical PWA Xtream category ids against their locks The PWA data source compared raw provider category ids such as "009" with locks stored as numbers, so such a category stayed visible while locked. Both sides are now compared in canonical numeric form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): close the M3U group editor on any relock The group management dialog lists every group name, locked ones included, even when it opened without lock toggles (unreadable lock store). It now closes on any relock, and the groups view re-checks the lock state before opening it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1409 lines
48 KiB
TypeScript
1409 lines
48 KiB
TypeScript
import type { AppUpdateChannel } from './app-update-channel.util';
|
||
import type { SourceProbeContext, SourceHealthResult } from './source-health';
|
||
import type { XtreamConnectionFailure } from './xtream-connection-test';
|
||
import type { ZoomLevelAction } from './zoom-level.util';
|
||
import type {
|
||
CatchupDownloadMetadata,
|
||
DownloadRecoveryResult,
|
||
DownloadContentType,
|
||
} from './catchup-download.interface';
|
||
import {
|
||
EmbeddedMpvBounds,
|
||
EmbeddedMpvRecordingStartOptions,
|
||
EmbeddedMpvSession,
|
||
EmbeddedMpvSubtitleStyle,
|
||
EmbeddedMpvSupport,
|
||
} from './embedded-mpv-session.interface';
|
||
import { ContentMetadataPatch } from './content-metadata.interface';
|
||
import { DownloadMetadataSnapshot } from './download-metadata.interface';
|
||
import { EpgChannelMetadata } from './epg-channel-metadata.model';
|
||
import { EpgProgram } from './epg-program.model';
|
||
import { ExternalPlayerSession } from './external-player-session.interface';
|
||
import {
|
||
GlobalSearchPaginationOptions,
|
||
GlobalSearchResult,
|
||
GlobalSearchResultSource,
|
||
} from './global-search-result.interface';
|
||
import { M3uFavoriteChannel } from './m3u-favorite-channel.interface';
|
||
import {
|
||
RecordingProgramSnapshot,
|
||
RecordingSourceType,
|
||
RecordingStatus,
|
||
} from './recording-metadata.interface';
|
||
import { PlaybackPositionData } from './playback-position.interface';
|
||
import { AutoUpdatePlaylistsResult } from './playlist-auto-update.interface';
|
||
import {
|
||
XtreamBackupFavoriteItem,
|
||
XtreamBackupHiddenCategory,
|
||
XtreamBackupRecentlyViewedItem,
|
||
} from './playlist-backup.interface';
|
||
import {
|
||
PlaylistRefreshCancelledResult,
|
||
PlaylistRefreshEvent,
|
||
PlaylistRefreshPayload,
|
||
} from './playlist-refresh.interface';
|
||
import { Playlist } from './playlist.interface';
|
||
import {
|
||
PlayerContentInfo,
|
||
ResolvedPortalPlayback,
|
||
} from './portal-playback.interface';
|
||
import { PortalDebugEvent } from './portal-debug.interface';
|
||
import { CatalogTitleMatch } from './catalog-title-match.interface';
|
||
import {
|
||
StreamProbeHeaders,
|
||
VodSourceCandidateRow,
|
||
VodSourcePin,
|
||
} from './vod-source.interface';
|
||
import { Settings } from './settings.interface';
|
||
import {
|
||
TmdbCacheEntry,
|
||
TmdbCacheMediaType,
|
||
TmdbCacheStats,
|
||
} from './tmdb.interface';
|
||
import { XtreamCategory } from './xtream-category.interface';
|
||
|
||
export const ELECTRON_BRIDGE_CONTENT_TYPES = {
|
||
Episode: 'episode',
|
||
Live: 'live',
|
||
Movie: 'movie',
|
||
Series: 'series',
|
||
Vod: 'vod',
|
||
} as const;
|
||
|
||
export type ElectronBridgeContentType =
|
||
(typeof ELECTRON_BRIDGE_CONTENT_TYPES)[keyof typeof ELECTRON_BRIDGE_CONTENT_TYPES];
|
||
|
||
export type ElectronBridgePortalContentType =
|
||
| typeof ELECTRON_BRIDGE_CONTENT_TYPES.Live
|
||
| typeof ELECTRON_BRIDGE_CONTENT_TYPES.Movie
|
||
| typeof ELECTRON_BRIDGE_CONTENT_TYPES.Series;
|
||
|
||
export type ElectronBridgePlaybackContentType =
|
||
| typeof ELECTRON_BRIDGE_CONTENT_TYPES.Vod
|
||
| typeof ELECTRON_BRIDGE_CONTENT_TYPES.Episode;
|
||
|
||
export const ELECTRON_BRIDGE_PLAYLIST_TYPES = {
|
||
M3uFile: 'm3u-file',
|
||
M3uText: 'm3u-text',
|
||
M3uUrl: 'm3u-url',
|
||
Stalker: 'stalker',
|
||
Xtream: 'xtream',
|
||
} as const;
|
||
|
||
export type ElectronBridgePlaylistType =
|
||
(typeof ELECTRON_BRIDGE_PLAYLIST_TYPES)[keyof typeof ELECTRON_BRIDGE_PLAYLIST_TYPES];
|
||
|
||
export const ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES = {
|
||
Cancelled: 'cancelled',
|
||
Complete: 'complete',
|
||
Error: 'error',
|
||
Loading: 'loading',
|
||
Queued: 'queued',
|
||
} as const;
|
||
|
||
export type ElectronBridgeEpgProgressStatus =
|
||
(typeof ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES)[keyof typeof ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES];
|
||
|
||
export const ELECTRON_BRIDGE_SECURITY_ERROR_CODES = {
|
||
EpgPrivateNetworkBlocked: 'epg-private-network-blocked',
|
||
InvalidTlsCertificate: 'invalid-tls-certificate',
|
||
} as const;
|
||
|
||
export type ElectronBridgeSecurityErrorCode =
|
||
(typeof ELECTRON_BRIDGE_SECURITY_ERROR_CODES)[keyof typeof ELECTRON_BRIDGE_SECURITY_ERROR_CODES];
|
||
|
||
export const ELECTRON_BRIDGE_DB_OPERATION_STATUSES = {
|
||
Cancelled: 'cancelled',
|
||
Completed: 'completed',
|
||
Error: 'error',
|
||
Progress: 'progress',
|
||
Started: 'started',
|
||
} as const;
|
||
|
||
export type ElectronBridgeDbOperationStatus =
|
||
(typeof ELECTRON_BRIDGE_DB_OPERATION_STATUSES)[keyof typeof ELECTRON_BRIDGE_DB_OPERATION_STATUSES];
|
||
|
||
export const ELECTRON_BRIDGE_REMOTE_CONTROL_COMMAND_TYPES = {
|
||
ChannelSelectNumber: 'channel-select-number',
|
||
VolumeDown: 'volume-down',
|
||
VolumeToggleMute: 'volume-toggle-mute',
|
||
VolumeUp: 'volume-up',
|
||
} as const;
|
||
|
||
export type ElectronBridgeRemoteControlCommandType =
|
||
(typeof ELECTRON_BRIDGE_REMOTE_CONTROL_COMMAND_TYPES)[keyof typeof ELECTRON_BRIDGE_REMOTE_CONTROL_COMMAND_TYPES];
|
||
|
||
export const ELECTRON_BRIDGE_REMOTE_PORTALS = {
|
||
M3u: 'm3u',
|
||
Stalker: 'stalker',
|
||
Unknown: 'unknown',
|
||
Xtream: 'xtream',
|
||
} as const;
|
||
|
||
export type ElectronBridgeRemotePortal =
|
||
(typeof ELECTRON_BRIDGE_REMOTE_PORTALS)[keyof typeof ELECTRON_BRIDGE_REMOTE_PORTALS];
|
||
|
||
export const ELECTRON_BRIDGE_DOWNLOAD_STATUSES = {
|
||
Canceled: 'canceled',
|
||
Completed: 'completed',
|
||
Downloading: 'downloading',
|
||
Failed: 'failed',
|
||
Paused: 'paused',
|
||
Queued: 'queued',
|
||
} as const;
|
||
|
||
export type ElectronBridgeDownloadStatus =
|
||
(typeof ELECTRON_BRIDGE_DOWNLOAD_STATUSES)[keyof typeof ELECTRON_BRIDGE_DOWNLOAD_STATUSES];
|
||
|
||
export const ELECTRON_BRIDGE_DOWNLOAD_START_REASONS = {
|
||
AlreadyDownloaded: 'already-downloaded',
|
||
AlreadyInProgress: 'already-in-progress',
|
||
} as const;
|
||
|
||
export type ElectronBridgeDownloadStartReason =
|
||
(typeof ELECTRON_BRIDGE_DOWNLOAD_START_REASONS)[keyof typeof ELECTRON_BRIDGE_DOWNLOAD_START_REASONS];
|
||
|
||
export const ELECTRON_BRIDGE_EPISODE_IDENTITY_SCOPES = {
|
||
StalkerEmbeddedVod: 'stalker-embedded-vod',
|
||
StalkerLazyVod: 'stalker-lazy-vod',
|
||
StalkerRegularSeries: 'stalker-regular-series',
|
||
} as const;
|
||
|
||
export type ElectronBridgeEpisodeIdentityScope =
|
||
(typeof ELECTRON_BRIDGE_EPISODE_IDENTITY_SCOPES)[keyof typeof ELECTRON_BRIDGE_EPISODE_IDENTITY_SCOPES];
|
||
|
||
export type ElectronDownloadFileAvailability =
|
||
'available' | 'missing' | 'not-applicable';
|
||
|
||
export const ELECTRON_BRIDGE_APP_UPDATE_STATUSES = {
|
||
Unsupported: 'unsupported',
|
||
Idle: 'idle',
|
||
Checking: 'checking',
|
||
Available: 'available',
|
||
NotAvailable: 'not-available',
|
||
Downloading: 'downloading',
|
||
Downloaded: 'downloaded',
|
||
Error: 'error',
|
||
} as const;
|
||
|
||
export type ElectronBridgeAppUpdateStatusValue =
|
||
(typeof ELECTRON_BRIDGE_APP_UPDATE_STATUSES)[keyof typeof ELECTRON_BRIDGE_APP_UPDATE_STATUSES];
|
||
|
||
export const ELECTRON_BRIDGE_GLOBAL_RECENTLY_ADDED_KINDS = {
|
||
All: 'all',
|
||
Series: 'series',
|
||
Vod: 'vod',
|
||
} as const;
|
||
|
||
export type ElectronBridgeGlobalRecentlyAddedKind =
|
||
(typeof ELECTRON_BRIDGE_GLOBAL_RECENTLY_ADDED_KINDS)[keyof typeof ELECTRON_BRIDGE_GLOBAL_RECENTLY_ADDED_KINDS];
|
||
|
||
export interface ElectronBridgeResult {
|
||
success: boolean;
|
||
}
|
||
|
||
export interface ElectronBridgeErrorResult extends ElectronBridgeResult {
|
||
error?: string;
|
||
}
|
||
|
||
export interface ElectronBridgeCountResult extends ElectronBridgeResult {
|
||
count: number;
|
||
}
|
||
|
||
export interface ElectronBridgeDialogFilter {
|
||
name: string;
|
||
extensions: string[];
|
||
}
|
||
|
||
export interface ElectronBridgeWindowState {
|
||
isMaximized: boolean;
|
||
isFullScreen: boolean;
|
||
}
|
||
|
||
/**
|
||
* Portal credentials for an auth-gated stream, passed alongside the scoped
|
||
* header override. The main process attaches them only to requests going to
|
||
* the exact origin of the override's `scopeUrl`, keeps them in memory only,
|
||
* and drops them when the scoped override is cleared or replaced.
|
||
*/
|
||
export interface ElectronBridgeStreamCredentials {
|
||
authorization?: string | null;
|
||
cookie?: string | null;
|
||
}
|
||
|
||
/**
|
||
* A playlist file the operating system asked the app to open — a command line
|
||
* argument, a file association double-click, or macOS' `open-file` event. The
|
||
* main process resolves the path to an absolute one before handing it over.
|
||
*/
|
||
export interface ElectronBridgePlaylistOpenRequest {
|
||
fileName: string;
|
||
filePath: string;
|
||
/** Pass back to `acknowledgePlaylistOpenRequest` once received. */
|
||
requestId: string;
|
||
}
|
||
|
||
export interface ElectronBridgeAppUpdateRelease {
|
||
version: string;
|
||
releaseDate?: string;
|
||
releaseName?: string | null;
|
||
releaseNotes?: string | null;
|
||
}
|
||
|
||
export interface ElectronBridgeAppUpdateProgress {
|
||
bytesPerSecond?: number;
|
||
percent: number;
|
||
total?: number;
|
||
transferred?: number;
|
||
}
|
||
|
||
export interface ElectronBridgeAppUpdateStatus {
|
||
status: ElectronBridgeAppUpdateStatusValue;
|
||
currentVersion: string;
|
||
latestVersion?: string;
|
||
release?: ElectronBridgeAppUpdateRelease;
|
||
progress?: ElectronBridgeAppUpdateProgress;
|
||
error?: string;
|
||
supportedSelfUpdate: boolean;
|
||
/** Release page for the configured channel; opened for manual installs. */
|
||
manualDownloadUrl: string;
|
||
/** Channel the updater checks (`Settings.updateChannel`, mirrored). */
|
||
channel: AppUpdateChannel;
|
||
/**
|
||
* Channel the current verdict — `latestVersion`, `release`, a download
|
||
* in flight or finished — was produced on. Absent until the first
|
||
* check. Differs from `channel` after a channel change saved while a
|
||
* download was running or done: the download is kept, the new channel
|
||
* has not been checked yet.
|
||
*/
|
||
verdictChannel?: AppUpdateChannel;
|
||
/** Channel the running build came from, read off its version string. */
|
||
installedChannel: AppUpdateChannel;
|
||
}
|
||
|
||
export type ElectronBridgeAppUpdateReleaseNotesDirection = 'previous' | 'next';
|
||
|
||
export interface ElectronBridgeAppUpdateReleaseNotesRequest {
|
||
version?: string;
|
||
direction?: ElectronBridgeAppUpdateReleaseNotesDirection;
|
||
fallbackToLatest?: boolean;
|
||
}
|
||
|
||
export interface ElectronBridgeAppUpdateReleaseNotes {
|
||
version: string;
|
||
tagName: string;
|
||
releaseName?: string | null;
|
||
publishedAt?: string | null;
|
||
bodyMarkdown: string;
|
||
htmlUrl: string;
|
||
hasPrevious: boolean;
|
||
hasNext: boolean;
|
||
}
|
||
|
||
export interface ElectronBridgeAiSettings {
|
||
aiProvider: string;
|
||
aiModelName: string;
|
||
aiApiKey: string;
|
||
}
|
||
|
||
export interface ElectronBridgeStalkerRequestPayload {
|
||
probe?: SourceProbeContext;
|
||
url: string;
|
||
macAddress: string;
|
||
params: Record<string, string>;
|
||
token?: string;
|
||
serialNumber?: string;
|
||
requestId?: string;
|
||
/**
|
||
* Endpoint-discovery probes only: exempt this request from the main
|
||
* process' per-host connectivity guard. Discovery walks several candidate
|
||
* paths on one host and expects most of them to fail, so its failures must
|
||
* neither count towards the guard nor be fast-failed by it.
|
||
*/
|
||
skipConnectionGuard?: boolean;
|
||
}
|
||
|
||
export interface ElectronBridgeXtreamRequestPayload {
|
||
probe?: SourceProbeContext;
|
||
connectionTest?: boolean;
|
||
url: string;
|
||
params: Record<string, string>;
|
||
requestId?: string;
|
||
sessionId?: string;
|
||
suppressErrorLog?: boolean;
|
||
}
|
||
|
||
export interface ElectronBridgeXtreamResponse {
|
||
connectionFailure?: XtreamConnectionFailure;
|
||
payload: unknown;
|
||
action: string;
|
||
}
|
||
|
||
export interface ElectronBridgeXtreamCancelResult extends ElectronBridgeResult {
|
||
cancelled: number;
|
||
}
|
||
|
||
export interface ElectronBridgeXtreamProbeResult {
|
||
/**
|
||
* HTTP status, or 0 when no response was obtained.
|
||
*
|
||
* 0 covers a timeout and a URL rejected by the redirect-safety policy as
|
||
* well as a dead host, so it means "could not check" — never "offline".
|
||
*/
|
||
status: number;
|
||
url: string;
|
||
/** Round-trip time; present whenever the request completed either way. */
|
||
latencyMs?: number;
|
||
error?: string;
|
||
}
|
||
|
||
export interface ElectronBridgeEpgFetchResult extends ElectronBridgeResult {
|
||
message?: string;
|
||
skipped?: string[];
|
||
}
|
||
|
||
export interface ElectronBridgeTrustOptions {
|
||
trustedPrivateNetworkEpgUrls?: string[];
|
||
trustedInsecureTlsHosts?: string[];
|
||
}
|
||
|
||
export interface ElectronBridgePlaylistFetchOptions extends ElectronBridgeTrustOptions {
|
||
userAgent?: string;
|
||
}
|
||
|
||
export interface ElectronBridgeEpgFreshnessResult {
|
||
staleUrls: string[];
|
||
freshUrls: string[];
|
||
}
|
||
|
||
export interface ElectronBridgeEpgMapping {
|
||
id: number;
|
||
channelKey: string;
|
||
epgChannelId: string;
|
||
playlistId: string | null;
|
||
}
|
||
|
||
/** A programme-search hit: the programme plus the XMLTV channel's display name. */
|
||
export interface ElectronBridgeEpgSearchProgram extends EpgProgram {
|
||
channelName: string | null;
|
||
}
|
||
|
||
export interface ElectronBridgeEpgSearchResult {
|
||
id: string;
|
||
displayName: string;
|
||
iconUrl: string | null;
|
||
}
|
||
|
||
export interface ElectronBridgeEpgLookupOptions {
|
||
sourceUrls?: string[];
|
||
}
|
||
|
||
/**
|
||
* Options for the batched "currently airing" lookup. `nowMs` is the instant
|
||
* the lookup is evaluated at: the renderer passes its wall clock shifted into
|
||
* the provider's EPG clock (`epgProviderClockMs`, see
|
||
* `epg-display-offset.util.ts`) so the row the backend picks is the one the
|
||
* renderer will also present as "now". Absent, the main process uses its own
|
||
* clock.
|
||
*/
|
||
export interface ElectronBridgeCurrentProgramsOptions extends ElectronBridgeEpgLookupOptions {
|
||
nowMs?: number;
|
||
}
|
||
|
||
export interface ElectronBridgeEpgGuideWindow extends ElectronBridgeEpgLookupOptions {
|
||
/**
|
||
* Playlist channel lookup keys (tvg-id, else name), ≤100 keys for
|
||
* programmes, ≤2000 for coverage; larger batches are truncated and the
|
||
* cut keys are absent from the answer.
|
||
*/
|
||
channelIds: string[];
|
||
/** Provider-clock window bounds in epoch ms. */
|
||
fromMs: number;
|
||
toMs: number;
|
||
}
|
||
|
||
export interface ElectronBridgeEpgProgressStats {
|
||
totalChannels: number;
|
||
totalPrograms: number;
|
||
}
|
||
|
||
export interface ElectronBridgeEpgProgress {
|
||
url: string;
|
||
generation?: number;
|
||
status: ElectronBridgeEpgProgressStatus;
|
||
stats?: ElectronBridgeEpgProgressStats;
|
||
error?: string;
|
||
errorCode?: ElectronBridgeSecurityErrorCode;
|
||
errorHost?: string;
|
||
queuePosition?: number;
|
||
}
|
||
|
||
export interface ElectronBridgeEpgChannelSummary {
|
||
id: string;
|
||
displayName: string;
|
||
}
|
||
|
||
export interface ElectronBridgeEpgChannelListResult {
|
||
channels: ElectronBridgeEpgChannelSummary[];
|
||
/** Always empty for this endpoint; retained for wire-format compatibility. */
|
||
programs: [];
|
||
}
|
||
|
||
export interface ElectronBridgeDbOperationEvent {
|
||
operationId?: string;
|
||
operation: string;
|
||
playlistId?: string;
|
||
status: ElectronBridgeDbOperationStatus;
|
||
phase?: string;
|
||
current?: number;
|
||
total?: number;
|
||
increment?: number;
|
||
error?: string;
|
||
}
|
||
|
||
export interface ElectronBridgePlaylistInput {
|
||
id?: string;
|
||
_id?: string;
|
||
name?: string;
|
||
title?: string;
|
||
serverUrl?: string;
|
||
username?: string;
|
||
password?: string;
|
||
macAddress?: string;
|
||
url?: string;
|
||
type?: ElectronBridgePlaylistType | string;
|
||
lastUpdated?: string;
|
||
}
|
||
|
||
export interface ElectronBridgePlaylistRow {
|
||
id: string;
|
||
name: string;
|
||
serverUrl: string;
|
||
username: string;
|
||
password: string;
|
||
type: string;
|
||
macAddress?: string;
|
||
url?: string;
|
||
lastUpdated?: string;
|
||
}
|
||
|
||
export type ElectronBridgePlaylistUpsertInput =
|
||
Playlist | ElectronBridgePlaylistInput;
|
||
|
||
export interface ElectronBridgeCategoryRow {
|
||
id: number;
|
||
name: string;
|
||
playlist_id: string;
|
||
type: 'movies' | 'live' | 'series';
|
||
xtream_id: number;
|
||
hidden: boolean;
|
||
/** Parental lock index, stamped from the renderer's lock store. */
|
||
locked: boolean;
|
||
}
|
||
|
||
export interface ElectronBridgeXtreamContent {
|
||
id: number;
|
||
category_id: number;
|
||
title: string;
|
||
rating: string;
|
||
added: string;
|
||
poster_url: string;
|
||
backdrop_url?: string | null;
|
||
epg_channel_id?: string | null;
|
||
tv_archive?: number | null;
|
||
tv_archive_duration?: number | null;
|
||
direct_source?: string | null;
|
||
xtream_id: number;
|
||
type: string;
|
||
added_at?: string;
|
||
viewed_at?: string;
|
||
position?: number | null;
|
||
}
|
||
|
||
export type ElectronBridgeXtreamContentStream =
|
||
| {
|
||
category_id: string | number;
|
||
rating?: string | number;
|
||
rating_imdb?: string;
|
||
last_modified?: string;
|
||
added?: string;
|
||
stream_icon?: string;
|
||
poster?: string;
|
||
cover?: string;
|
||
name?: string;
|
||
title?: string;
|
||
epg_channel_id?: string;
|
||
tv_archive?: string | number;
|
||
tv_archive_duration?: string | number;
|
||
direct_source?: string;
|
||
series_id?: string | number;
|
||
stream_id?: string | number;
|
||
}
|
||
| Record<string, unknown>;
|
||
|
||
export type ElectronBridgeGlobalSearchResult = GlobalSearchResult;
|
||
|
||
export interface ElectronBridgeGlobalRecentItem extends ElectronBridgeXtreamContent {
|
||
playlist_id: string;
|
||
playlist_name: string;
|
||
viewed_at: string;
|
||
}
|
||
|
||
export interface ElectronBridgeGlobalFavoriteItem extends ElectronBridgeXtreamContent {
|
||
playlist_id: string;
|
||
playlist_name: string;
|
||
added_at: string;
|
||
}
|
||
|
||
export interface ElectronBridgeGlobalRecentlyAddedItem extends ElectronBridgeXtreamContent {
|
||
playlist_id: string;
|
||
playlist_name: string;
|
||
added_at: string;
|
||
}
|
||
|
||
export interface ElectronBridgeFavoriteReorderUpdate {
|
||
content_id: number;
|
||
/** Favorites are playlist-scoped — scope the position write per playlist */
|
||
playlist_id: string;
|
||
position: number;
|
||
}
|
||
|
||
export interface ElectronBridgeRecentItemsBatchItem {
|
||
contentId: number;
|
||
playlistId: string;
|
||
}
|
||
|
||
export interface ElectronBridgeRemoteControlCommand {
|
||
type: ElectronBridgeRemoteControlCommandType;
|
||
number?: number;
|
||
}
|
||
|
||
export interface ElectronBridgeRemoteControlStatus {
|
||
portal: ElectronBridgeRemotePortal;
|
||
isLiveView: boolean;
|
||
channelName?: string;
|
||
channelNumber?: number;
|
||
epgTitle?: string;
|
||
epgStart?: string;
|
||
epgEnd?: string;
|
||
supportsVolume?: boolean;
|
||
volume?: number;
|
||
muted?: boolean;
|
||
}
|
||
|
||
export interface ElectronBridgePlayerError {
|
||
player: string;
|
||
error: string;
|
||
originalError: string;
|
||
}
|
||
|
||
export interface ElectronBridgePlaybackPositionInput extends Omit<
|
||
PlaybackPositionData,
|
||
'playlistId' | 'updatedAt'
|
||
> {
|
||
playlistType?: ElectronBridgePlaylistType;
|
||
}
|
||
|
||
export interface ElectronBridgeDownloadHeaders {
|
||
userAgent?: string;
|
||
referer?: string;
|
||
origin?: string;
|
||
}
|
||
|
||
export interface ElectronBridgeDownloadStartPayload {
|
||
playlistId: string;
|
||
xtreamId: number;
|
||
contentType: DownloadContentType;
|
||
catchup?: CatchupDownloadMetadata | null;
|
||
title: string;
|
||
url: string;
|
||
posterUrl?: string;
|
||
metadataSnapshot?: DownloadMetadataSnapshot;
|
||
downloadFolder: string;
|
||
headers?: ElectronBridgeDownloadHeaders;
|
||
seriesXtreamId?: number;
|
||
seasonNumber?: number;
|
||
episodeNumber?: number;
|
||
episodeIdentityScope?: ElectronBridgeEpisodeIdentityScope;
|
||
playlistName?: string;
|
||
playlistType?: ElectronBridgePlaylistType;
|
||
serverUrl?: string;
|
||
portalUrl?: string;
|
||
macAddress?: string;
|
||
}
|
||
|
||
export interface ElectronBridgeDownloadStartResult extends ElectronBridgeErrorResult {
|
||
id?: number;
|
||
reason?: ElectronBridgeDownloadStartReason;
|
||
}
|
||
|
||
export interface ElectronBridgeDownloadRedownloadResult extends ElectronBridgeErrorResult {
|
||
recovered?: boolean;
|
||
}
|
||
|
||
export interface ElectronDownloadItem {
|
||
id: number;
|
||
playlistId: string;
|
||
xtreamId: number;
|
||
contentType: DownloadContentType;
|
||
catchup?: CatchupDownloadMetadata | null;
|
||
seriesXtreamId?: number;
|
||
seasonNumber?: number;
|
||
episodeNumber?: number;
|
||
episodeIdentityScope?: ElectronBridgeEpisodeIdentityScope | null;
|
||
title: string;
|
||
url: string;
|
||
fileName?: string;
|
||
filePath?: string;
|
||
posterUrl?: string;
|
||
metadataSnapshot?: DownloadMetadataSnapshot;
|
||
status: ElectronBridgeDownloadStatus;
|
||
fileAvailability: ElectronDownloadFileAvailability;
|
||
bytesDownloaded?: number;
|
||
totalBytes?: number;
|
||
errorMessage?: string;
|
||
createdAt?: string;
|
||
updatedAt?: string;
|
||
}
|
||
|
||
/**
|
||
* Renderer-facing live-TV recording row. `fileAvailability` is derived at
|
||
* read time (never persisted) and `programs` is the decoded `programs_json`
|
||
* column. Recordings reuse the download availability vocabulary so the
|
||
* manager UI can share its missing-file affordances — extended by
|
||
* `'unknown'` for an inconclusive probe (timeout, permission or I/O error):
|
||
* only proven absence may move a recording to Needs attention or hide its
|
||
* file actions, so consumers gate on `=== 'missing'`, never on
|
||
* `!== 'available'`.
|
||
*/
|
||
export interface ElectronRecordingItem {
|
||
id: number;
|
||
sessionId?: string;
|
||
status: RecordingStatus;
|
||
filePath: string;
|
||
fileSizeBytes?: number;
|
||
channelName: string;
|
||
channelLogoUrl?: string;
|
||
playlistId?: string;
|
||
playlistName?: string;
|
||
sourceType?: RecordingSourceType;
|
||
epgChannelId?: string;
|
||
programTitle?: string;
|
||
programDescription?: string;
|
||
programStart?: string;
|
||
programStop?: string;
|
||
programs?: RecordingProgramSnapshot[];
|
||
errorMessage?: string;
|
||
startedAt: string;
|
||
endedAt?: string;
|
||
createdAt?: string;
|
||
updatedAt?: string;
|
||
fileAvailability: ElectronDownloadFileAvailability | 'unknown';
|
||
}
|
||
|
||
export interface ElectronBridgeApi {
|
||
onPortalDebugEvent?: (
|
||
callback: (data: PortalDebugEvent) => void
|
||
) => () => void;
|
||
onPlaylistRefreshEvent?: (
|
||
callback: (data: PlaylistRefreshEvent) => void
|
||
) => () => void;
|
||
getAppVersion: () => Promise<string>;
|
||
platform: string;
|
||
getAppUpdateStatus: () => Promise<ElectronBridgeAppUpdateStatus>;
|
||
checkForAppUpdate: () => Promise<ElectronBridgeAppUpdateStatus>;
|
||
downloadAppUpdate: () => Promise<ElectronBridgeAppUpdateStatus>;
|
||
installAppUpdate: () => Promise<ElectronBridgeAppUpdateStatus>;
|
||
getAppUpdateReleaseNotes: (
|
||
request?: ElectronBridgeAppUpdateReleaseNotesRequest
|
||
) => Promise<ElectronBridgeAppUpdateReleaseNotes>;
|
||
onAppUpdateStatusChange: (
|
||
callback: (status: ElectronBridgeAppUpdateStatus) => void
|
||
) => () => void;
|
||
minimizeWindow: () => Promise<void>;
|
||
toggleMaximizeWindow: () => Promise<ElectronBridgeWindowState>;
|
||
/**
|
||
* Toggles OS-level window fullscreen (F11). Reports the requested state;
|
||
* the `onWindowStateChange` push stays authoritative once the window
|
||
* manager has acted.
|
||
*/
|
||
toggleFullScreenWindow: () => Promise<ElectronBridgeWindowState>;
|
||
closeWindow: () => Promise<void>;
|
||
/**
|
||
* Zoom shortcuts (Cmd/Ctrl and +/−/0). Synchronous and preload-local:
|
||
* steps the FRAME-BOUND temporary zoom level through `webFrame` (never a
|
||
* main-process `webContents.setZoomLevel`, whose per-URL entry the app's
|
||
* `file://` path routing resets — issue #1109) by `stepZoomLevel`'s
|
||
* rules — one `ZOOM_LEVEL_STEP` inside `ZOOM_LEVEL_MIN..ZOOM_LEVEL_MAX`,
|
||
* a stored level already outside them never moved against the request —
|
||
* and returns the level now applied, which is therefore not itself
|
||
* guaranteed to be within the limits. Persistence needs nothing from the
|
||
* caller: the main process reads the live level back on close, quit and
|
||
* reload.
|
||
*/
|
||
adjustZoomLevel: (action: ZoomLevelAction) => number;
|
||
getWindowState: () => Promise<ElectronBridgeWindowState>;
|
||
onWindowStateChange: (
|
||
callback: (state: ElectronBridgeWindowState) => void
|
||
) => () => void;
|
||
/**
|
||
* While active, the main process intercepts window close / app quit and
|
||
* pushes a close request to the renderer instead of closing. Used by the
|
||
* settings page while its form holds unsaved edits.
|
||
*/
|
||
setWindowCloseGuard: (active: boolean) => Promise<void>;
|
||
/**
|
||
* Completes a close the guard intercepted: the main process re-runs the
|
||
* original intent (window close or app quit) with the guard bypassed.
|
||
*/
|
||
confirmWindowClose: () => Promise<void>;
|
||
/**
|
||
* Abandons a close the guard intercepted (the user stays), so the
|
||
* remembered close-vs-quit intent cannot leak into a later attempt.
|
||
* Carries the id of the request being abandoned: a cancellation that
|
||
* arrives after a NEWER interception must not wipe that newer intent.
|
||
*/
|
||
cancelWindowClose: (requestId?: number) => Promise<void>;
|
||
onWindowCloseRequested: (
|
||
callback: (requestId: number) => void
|
||
) => () => void;
|
||
/**
|
||
* While active, the main process holds a
|
||
* `powerSaveBlocker('prevent-display-sleep')` so the screen stays awake
|
||
* during built-in video playback. The flag is cleared automatically when
|
||
* the renderer navigates, reloads, or dies, so a crashed page can never
|
||
* leave the display pinned awake.
|
||
*/
|
||
setPlaybackKeepAwake: (active: boolean) => Promise<void>;
|
||
/**
|
||
* Parental lock enforcement for the SQLite worker: `true` withholds
|
||
* every row in a locked category from content reads, `false` returns
|
||
* them. Reset to the mirrored `parentalLockEnabled` setting when the
|
||
* renderer reloads or dies, so a crashed page can never leave the
|
||
* library unlocked.
|
||
*/
|
||
setParentalLockState: (active: boolean) => Promise<void>;
|
||
fetchPlaylistByUrl: (
|
||
url: string,
|
||
title?: string,
|
||
options?: ElectronBridgePlaylistFetchOptions
|
||
) => Promise<Playlist>;
|
||
updatePlaylistFromFilePath: (
|
||
filePath: string,
|
||
title: string
|
||
) => Promise<Playlist>;
|
||
openPlaylistFromFile: () => Promise<Playlist | null>;
|
||
onPlaylistOpenRequest: (
|
||
callback: (request: ElectronBridgePlaylistOpenRequest) => void
|
||
) => () => void;
|
||
/**
|
||
* Tells the main process that an `onPlaylistOpenRequest` listener is
|
||
* attached, so it can push the playlist files the OS handed over — both
|
||
* the ones queued before this renderer existed and any that arrive later.
|
||
* Call it *after* subscribing.
|
||
*/
|
||
announcePlaylistOpenListener: () => Promise<void>;
|
||
/**
|
||
* Confirms a pushed request reached this renderer. Until it does, the main
|
||
* process keeps the request and replays it to the next renderer.
|
||
*/
|
||
acknowledgePlaylistOpenRequest: (requestId: string) => Promise<void>;
|
||
getPathForFile: (file: File) => string;
|
||
saveFileDialog: (
|
||
defaultPath: string,
|
||
filters?: ElectronBridgeDialogFilter[]
|
||
) => Promise<string | null>;
|
||
writeFile: (
|
||
filePath: string,
|
||
content: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
setUserAgent: (
|
||
userAgent?: string | null,
|
||
referer?: string | null,
|
||
scopeUrl?: string | null,
|
||
credentials?: ElectronBridgeStreamCredentials | null
|
||
) => Promise<boolean>;
|
||
openInMpv: (
|
||
url: string,
|
||
title: string,
|
||
thumbnail: string,
|
||
userAgent: string | undefined,
|
||
referer?: string,
|
||
origin?: string,
|
||
contentInfo?: PlayerContentInfo,
|
||
startTime?: number,
|
||
headers?: Record<string, string>
|
||
) => Promise<ExternalPlayerSession>;
|
||
openInVlc: (
|
||
url: string,
|
||
title: string,
|
||
thumbnail: string,
|
||
userAgent: string | undefined,
|
||
referer?: string,
|
||
origin?: string,
|
||
contentInfo?: PlayerContentInfo,
|
||
startTime?: number,
|
||
headers?: Record<string, string>
|
||
) => Promise<ExternalPlayerSession>;
|
||
autoUpdatePlaylists: (
|
||
playlists: Playlist[],
|
||
options?: ElectronBridgeTrustOptions
|
||
) => Promise<AutoUpdatePlaylistsResult>;
|
||
fetchEpg: (
|
||
urls: string[],
|
||
options?: ElectronBridgeTrustOptions
|
||
) => Promise<ElectronBridgeEpgFetchResult>;
|
||
getChannelPrograms: (
|
||
channelId: string,
|
||
options?: ElectronBridgeEpgLookupOptions
|
||
) => Promise<EpgProgram[]>;
|
||
getCurrentProgramsBatch: (
|
||
channelIds: string[],
|
||
options?: ElectronBridgeCurrentProgramsOptions
|
||
) => Promise<Record<string, EpgProgram | null>>;
|
||
getEpgChannelMetadata: (
|
||
channelIds: string[],
|
||
options?: ElectronBridgeEpgLookupOptions
|
||
) => Promise<Record<string, EpgChannelMetadata | null>>;
|
||
getEpgChannels: () => Promise<ElectronBridgeEpgChannelListResult>;
|
||
getEpgProgramsForChannels: (
|
||
window: ElectronBridgeEpgGuideWindow
|
||
) => Promise<Record<string, EpgProgram[]>>;
|
||
getEpgProgramCoverage: (
|
||
window: ElectronBridgeEpgGuideWindow
|
||
) => Promise<string[]>;
|
||
forceFetchEpg: (
|
||
url: string,
|
||
options?: ElectronBridgeTrustOptions
|
||
) => Promise<ElectronBridgeEpgFetchResult>;
|
||
/** Native picker for a local XMLTV file; resolves null when cancelled. */
|
||
openEpgFileDialog: () => Promise<string | null>;
|
||
clearEpgData: () => Promise<ElectronBridgeResult>;
|
||
reconcileEpgSources: (urls: string[]) => Promise<ElectronBridgeResult>;
|
||
clearEpgDataForSource: (sourceUrl: string) => Promise<ElectronBridgeResult>;
|
||
checkEpgFreshness: (
|
||
urls: string[],
|
||
maxAgeHours?: number
|
||
) => Promise<ElectronBridgeEpgFreshnessResult>;
|
||
searchEpgPrograms: (
|
||
searchTerm: string,
|
||
limit?: number
|
||
) => Promise<ElectronBridgeEpgSearchProgram[]>;
|
||
|
||
// EPG channel mapping (manual user overrides)
|
||
getEpgMapping: (
|
||
channelKey: string
|
||
) => Promise<ElectronBridgeEpgMapping | null>;
|
||
getEpgMappingsBatch: (
|
||
channelKeys: string[]
|
||
) => Promise<Record<string, string>>;
|
||
setEpgMapping: (
|
||
channelKey: string,
|
||
epgChannelId: string,
|
||
playlistId?: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
deleteEpgMapping: (channelKey: string) => Promise<ElectronBridgeResult>;
|
||
searchEpgChannels: (
|
||
searchTerm: string,
|
||
limit?: number
|
||
) => Promise<ElectronBridgeEpgSearchResult[]>;
|
||
|
||
updateSettings: (settings: Partial<Settings>) => Promise<void>;
|
||
getAiSettings: () => Promise<ElectronBridgeAiSettings>;
|
||
setMpvPlayerPath: (mpvPlayerPath: string) => Promise<void>;
|
||
setVlcPlayerPath: (vlcPlayerPath: string) => Promise<void>;
|
||
stalkerRequest: (
|
||
payload: ElectronBridgeStalkerRequestPayload
|
||
) => Promise<Record<string, unknown>>;
|
||
/**
|
||
* Forgets the connection failures recorded for the host `url` points at, so
|
||
* the next request contacts it for real instead of being fast-failed.
|
||
*/
|
||
resetHostConnectivityGuard: (url: string) => Promise<ElectronBridgeResult>;
|
||
xtreamRequest: (
|
||
payload: ElectronBridgeXtreamRequestPayload
|
||
) => Promise<ElectronBridgeXtreamResponse>;
|
||
xtreamCancelSession: (
|
||
sessionId: string
|
||
) => Promise<ElectronBridgeXtreamCancelResult>;
|
||
xtreamProbeUrl: (
|
||
url: string,
|
||
method?: 'GET' | 'HEAD'
|
||
) => Promise<ElectronBridgeXtreamProbeResult>;
|
||
/** Generic stream reachability probe (VOD multi-source availability) */
|
||
cancelSourceProbe: (id: string) => Promise<void>;
|
||
probeM3uSource: (payload: {
|
||
url: string;
|
||
userAgent?: string;
|
||
trustedInsecureTlsHosts?: string[];
|
||
probe: SourceProbeContext;
|
||
}) => Promise<SourceHealthResult>;
|
||
probeStreamUrl: (
|
||
url: string,
|
||
method?: 'GET' | 'HEAD',
|
||
/** Playback headers the owning playlist requires, when it has any. */
|
||
headers?: StreamProbeHeaders
|
||
) => Promise<ElectronBridgeXtreamProbeResult>;
|
||
refreshPlaylist: (
|
||
payload: PlaylistRefreshPayload
|
||
) => Promise<Playlist | PlaylistRefreshCancelledResult>;
|
||
cancelPlaylistRefresh: (
|
||
operationId: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbCreatePlaylist: (
|
||
playlist: ElectronBridgePlaylistUpsertInput
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbGetPlaylist: (
|
||
playlistId: string
|
||
) => Promise<ElectronBridgePlaylistRow | null>;
|
||
/**
|
||
* Records the panel clock learned from account info as one conditional
|
||
* UPDATE on the row payload; a no-op unless the row still points at the
|
||
* given connection (issue #1562).
|
||
*/
|
||
dbSetPlaylistServerTimezone: (
|
||
playlistId: string,
|
||
connection: { serverUrl: string; username: string; password: string },
|
||
serverTimezone: string
|
||
) => Promise<{ updated: boolean }>;
|
||
dbUpsertAppPlaylist: (
|
||
playlist: Playlist,
|
||
/**
|
||
* Instrumentation-only; stripped before the DB invoke.
|
||
* Never enters the DB worker payload or persisted playlist data.
|
||
*/
|
||
operationId?: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbMigrateAppPlaylists: (
|
||
playlists: Playlist[]
|
||
) => Promise<{ success: boolean; count: number }>;
|
||
dbRecoverLegacyPlaylists: () => Promise<void>;
|
||
dbUpsertAppPlaylists: (
|
||
playlists: Playlist[]
|
||
) => Promise<ElectronBridgeCountResult>;
|
||
dbGetAppPlaylists: () => Promise<Playlist[]>;
|
||
dbGetAppPlaylistMetas: () => Promise<Playlist[]>;
|
||
dbGetAppPlaylist: (
|
||
playlistId: string,
|
||
/**
|
||
* Instrumentation-only; stripped before the DB invoke.
|
||
* Never enters the DB worker payload or persisted playlist data.
|
||
*/
|
||
operationId?: string
|
||
) => Promise<Playlist | null>;
|
||
dbGetAppPlaylistFavoriteChannels: (
|
||
playlistId: string
|
||
) => Promise<M3uFavoriteChannel[]>;
|
||
dbUpdatePlaylist: (
|
||
playlistId: string,
|
||
updates: Partial<Playlist> | ElectronBridgePlaylistInput
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbDeletePlaylist: (
|
||
playlistId: string,
|
||
operationId?: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbDeleteXtreamContent: (
|
||
playlistId: string,
|
||
operationId?: string
|
||
) => Promise<{
|
||
success: boolean;
|
||
favorites: XtreamBackupFavoriteItem[];
|
||
recentlyViewed: XtreamBackupRecentlyViewedItem[];
|
||
hiddenCategories: XtreamBackupHiddenCategory[];
|
||
}>;
|
||
dbRestoreXtreamUserData: (
|
||
playlistId: string,
|
||
favorites: XtreamBackupFavoriteItem[],
|
||
recentlyViewed: XtreamBackupRecentlyViewedItem[],
|
||
operationId?: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbHasCategories: (playlistId: string, type: string) => Promise<boolean>;
|
||
dbGetCategories: (
|
||
playlistId: string,
|
||
type: string
|
||
) => Promise<ElectronBridgeCategoryRow[]>;
|
||
dbSaveCategories: (
|
||
playlistId: string,
|
||
categories: XtreamCategory[],
|
||
type: string,
|
||
hiddenCategoryXtreamIds?: number[],
|
||
lockedCategoryXtreamIds?: number[]
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbGetAllCategories: (
|
||
playlistId: string,
|
||
type: string
|
||
) => Promise<ElectronBridgeCategoryRow[]>;
|
||
dbUpdateCategoryVisibility: (
|
||
categoryIds: number[],
|
||
hidden: boolean
|
||
) => Promise<ElectronBridgeResult>;
|
||
/**
|
||
* Re-stamps `categories.locked` for one playlist and type from the given
|
||
* provider category ids: rows in the list become locked, every other row
|
||
* of that playlist/type unlocked. Idempotent, so the renderer can replay
|
||
* its lock store after a refresh or a backup restore.
|
||
*/
|
||
dbSetCategoryLocks: (
|
||
playlistId: string,
|
||
type: string,
|
||
lockedXtreamIds: number[]
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbHasContent: (playlistId: string, type: string) => Promise<boolean>;
|
||
dbGetContent: (
|
||
playlistId: string,
|
||
type: string
|
||
) => Promise<ElectronBridgeXtreamContent[]>;
|
||
dbSaveContent: (
|
||
playlistId: string,
|
||
streams: ElectronBridgeXtreamContentStream[],
|
||
type: string,
|
||
operationId?: string
|
||
) => Promise<ElectronBridgeCountResult>;
|
||
dbClearXtreamImportCache: (
|
||
playlistId: string,
|
||
type: ElectronBridgePortalContentType
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbSearchContent: (
|
||
playlistId: string,
|
||
searchTerm: string,
|
||
types: string[],
|
||
excludeHidden?: boolean
|
||
) => Promise<ElectronBridgeXtreamContent[]>;
|
||
dbGlobalSearch: (
|
||
searchTerm: string,
|
||
types: string[],
|
||
excludeHidden?: boolean,
|
||
sources?: GlobalSearchResultSource[],
|
||
options?: GlobalSearchPaginationOptions
|
||
) => Promise<ElectronBridgeGlobalSearchResult[]>;
|
||
dbGetGlobalRecentlyAdded: (
|
||
kind: ElectronBridgeGlobalRecentlyAddedKind,
|
||
limit?: number,
|
||
playlistType?: ElectronBridgePlaylistType
|
||
) => Promise<ElectronBridgeGlobalRecentlyAddedItem[]>;
|
||
dbGetRecentlyViewed: () => Promise<ElectronBridgeGlobalRecentItem[]>;
|
||
dbClearRecentlyViewed: () => Promise<ElectronBridgeResult>;
|
||
dbAddFavorite: (
|
||
contentId: number,
|
||
playlistId: string,
|
||
backdropUrl?: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbRemoveFavorite: (
|
||
contentId: number,
|
||
playlistId: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbIsFavorite: (contentId: number, playlistId: string) => Promise<boolean>;
|
||
dbGetFavorites: (
|
||
playlistId: string
|
||
) => Promise<ElectronBridgeXtreamContent[]>;
|
||
dbGetGlobalFavorites: () => Promise<ElectronBridgeGlobalFavoriteItem[]>;
|
||
dbGetAllGlobalFavorites: () => Promise<ElectronBridgeGlobalFavoriteItem[]>;
|
||
dbReorderGlobalFavorites: (
|
||
updates: ElectronBridgeFavoriteReorderUpdate[]
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbGetRecentItems: (
|
||
playlistId: string
|
||
) => Promise<ElectronBridgeXtreamContent[]>;
|
||
dbAddRecentItem: (
|
||
contentId: number,
|
||
playlistId: string,
|
||
backdropUrl?: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbClearPlaylistRecentItems: (
|
||
playlistId: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbRemoveRecentItem: (
|
||
contentId: number,
|
||
playlistId: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbRemoveRecentItemsBatch: (
|
||
items: ElectronBridgeRecentItemsBatchItem[]
|
||
) => Promise<ElectronBridgeCountResult>;
|
||
dbGetContentByXtreamId: (
|
||
xtreamId: number,
|
||
playlistId: string,
|
||
contentType?: ElectronBridgePortalContentType
|
||
) => Promise<ElectronBridgeXtreamContent | null>;
|
||
dbSetContentMetadataIfMissing: (
|
||
contentId: number,
|
||
patch?: ContentMetadataPatch
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbGetAppState: (key: string) => Promise<string | null>;
|
||
dbSetAppState: (
|
||
key: string,
|
||
value: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
// TMDB metadata cache
|
||
dbGetTmdbMetadata: (
|
||
mediaType: TmdbCacheMediaType,
|
||
lookupKey: string,
|
||
language: string
|
||
) => Promise<TmdbCacheEntry | null>;
|
||
dbSetTmdbMetadata: (entry: TmdbCacheEntry) => Promise<ElectronBridgeResult>;
|
||
/** Row count + payload bytes for the settings cache panel */
|
||
dbGetTmdbCacheStats: () => Promise<TmdbCacheStats>;
|
||
dbClearTmdbMetadata: () => Promise<
|
||
ElectronBridgeResult & { deleted: number }
|
||
>;
|
||
/** Cross-playlist title matching (actor page "All portals" scope) */
|
||
dbMatchTitles: (titles: string[]) => Promise<CatalogTitleMatch[]>;
|
||
/** VOD multi-source: the same movie in the user's other Xtream playlists */
|
||
dbFindTitleSources: (request: {
|
||
title: string;
|
||
year?: number | null;
|
||
excludePlaylistId?: string | null;
|
||
/** A stream id inside the excluded playlist to keep anyway (a pin). */
|
||
keepContentId?: number | null;
|
||
}) => Promise<VodSourceCandidateRow[]>;
|
||
/** Per-movie pinned source; keys are passed most-trusted first */
|
||
dbGetVodSourcePin: (matchKeys: string[]) => Promise<VodSourcePin | null>;
|
||
/** Every pin pointing at this playlist — used by playlist backup. */
|
||
dbListVodSourcePins: (playlistId: string) => Promise<VodSourcePin[]>;
|
||
/** Bulk clear: not keyed, so no `MAX_KEYS_PER_LOOKUP` truncation. */
|
||
dbClearVodSourcePinsForPlaylist: (
|
||
playlistId: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
/**
|
||
* `aliasKeys` receive the same pin, and `retireKeys` are removed, all in
|
||
* the SAME transaction as the write. Aliases keep a pin readable under the
|
||
* poorer key forms the movie had before enrichment.
|
||
*/
|
||
dbSetVodSourcePin: (
|
||
pin: VodSourcePin,
|
||
retireKeys?: string[],
|
||
aliasKeys?: string[]
|
||
) => Promise<ElectronBridgeResult>;
|
||
/** Makes the playlist's pins exactly `pins`, in one transaction. */
|
||
dbReplaceVodSourcePins: (
|
||
playlistId: string,
|
||
pins: VodSourcePin[]
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbClearVodSourcePin: (matchKeys: string[]) => Promise<ElectronBridgeResult>;
|
||
onChannelChange?: (
|
||
callback: (data: { direction: 'up' | 'down' }) => void
|
||
) => () => void;
|
||
onRemoteControlCommand?: (
|
||
callback: (data: ElectronBridgeRemoteControlCommand) => void
|
||
) => () => void;
|
||
updateRemoteControlStatus?: (
|
||
status: ElectronBridgeRemoteControlStatus
|
||
) => void;
|
||
onPlayerError?: (
|
||
callback: (data: ElectronBridgePlayerError) => void
|
||
) => void;
|
||
getLocalIpAddresses: () => Promise<string[]>;
|
||
onEpgProgress?: (
|
||
callback: (data: ElectronBridgeEpgProgress) => void
|
||
) => void;
|
||
onDbSaveContentProgress: (callback: (count: number) => void) => void;
|
||
removeDbSaveContentProgress: () => void;
|
||
onDbOperationEvent?: (
|
||
callback: (data: ElectronBridgeDbOperationEvent) => void
|
||
) => () => void;
|
||
dbDeleteAllPlaylists: (
|
||
operationId?: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbCancelOperation: (operationId: string) => Promise<ElectronBridgeResult>;
|
||
dbSavePlaybackPosition: (
|
||
playlistId: string,
|
||
data: ElectronBridgePlaybackPositionInput
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbGetPlaybackPosition: (
|
||
playlistId: string,
|
||
contentXtreamId: number,
|
||
contentType: ElectronBridgePlaybackContentType
|
||
) => Promise<PlaybackPositionData | null>;
|
||
dbGetSeriesPlaybackPositions: (
|
||
playlistId: string,
|
||
seriesXtreamId: number
|
||
) => Promise<PlaybackPositionData[]>;
|
||
dbGetRecentPlaybackPositions: (
|
||
playlistId: string,
|
||
limit?: number
|
||
) => Promise<PlaybackPositionData[]>;
|
||
dbGetAllPlaybackPositions: (
|
||
playlistId: string
|
||
) => Promise<PlaybackPositionData[]>;
|
||
dbClearAllPlaybackPositions: (
|
||
playlistId: string
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbClearPlaybackPosition: (
|
||
playlistId: string,
|
||
contentXtreamId: number,
|
||
contentType: ElectronBridgePlaybackContentType
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbSavePlaybackPositionsBatch: (
|
||
playlistId: string,
|
||
items: ElectronBridgePlaybackPositionInput[]
|
||
) => Promise<ElectronBridgeResult>;
|
||
dbClearPlaybackPositionsBatch: (
|
||
playlistId: string,
|
||
items: {
|
||
contentXtreamId: number;
|
||
contentType: ElectronBridgePlaybackContentType;
|
||
}[]
|
||
) => Promise<ElectronBridgeResult>;
|
||
onPlaybackPositionUpdate: (
|
||
callback: (data: PlaybackPositionData) => void
|
||
) => () => void;
|
||
onExternalPlayerSessionUpdate?: (
|
||
callback: (data: ExternalPlayerSession) => void
|
||
) => () => void;
|
||
onEmbeddedMpvSessionUpdate?: (
|
||
callback: (data: EmbeddedMpvSession) => void
|
||
) => () => void;
|
||
closeExternalPlayerSession: (
|
||
sessionId: string
|
||
) => Promise<ExternalPlayerSession | null>;
|
||
getEmbeddedMpvSupport: () => Promise<EmbeddedMpvSupport>;
|
||
prepareEmbeddedMpv?: () => Promise<EmbeddedMpvSupport>;
|
||
createEmbeddedMpvSession: (
|
||
bounds: EmbeddedMpvBounds,
|
||
title?: string,
|
||
initialVolume?: number
|
||
) => Promise<EmbeddedMpvSession>;
|
||
loadEmbeddedMpvPlayback: (
|
||
sessionId: string,
|
||
playback: ResolvedPortalPlayback
|
||
) => Promise<void>;
|
||
setEmbeddedMpvBounds: (
|
||
sessionId: string,
|
||
bounds: EmbeddedMpvBounds
|
||
) => Promise<void>;
|
||
setEmbeddedMpvPaused: (
|
||
sessionId: string,
|
||
paused: boolean
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
seekEmbeddedMpv: (
|
||
sessionId: string,
|
||
seconds: number
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
/**
|
||
* Relative seek by `deltaSeconds` (negative = backwards), resolved by mpv
|
||
* against its own playback position. Keyboard and button steps must use
|
||
* this instead of `seekEmbeddedMpv(position + delta)`: the renderer's
|
||
* `positionSeconds` is a whole-second snapshot refreshed at most every
|
||
* 500 ms and a seek reply does not carry the new position yet, so rapid
|
||
* presses computed from it collapse onto one target. mpv merges queued
|
||
* relative seeks instead, so presses accumulate.
|
||
*/
|
||
seekEmbeddedMpvBy?: (
|
||
sessionId: string,
|
||
deltaSeconds: number
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
setEmbeddedMpvVolume: (
|
||
sessionId: string,
|
||
volume: number
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
setEmbeddedMpvAudioTrack: (
|
||
sessionId: string,
|
||
trackId: number
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
setEmbeddedMpvSubtitleTrack?: (
|
||
sessionId: string,
|
||
trackId: number
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
/** Loads an external subtitle file (absolute path) via mpv `sub-add`. */
|
||
addEmbeddedMpvSubtitle?: (
|
||
sessionId: string,
|
||
filePath: string
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
setEmbeddedMpvSubtitleDelay?: (
|
||
sessionId: string,
|
||
seconds: number
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
setEmbeddedMpvSubtitleStyle?: (
|
||
sessionId: string,
|
||
style: EmbeddedMpvSubtitleStyle
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
/** Opens the main-process subtitle file dialog; null when cancelled. */
|
||
selectEmbeddedMpvSubtitleFile?: () => Promise<string | null>;
|
||
setEmbeddedMpvSpeed?: (
|
||
sessionId: string,
|
||
speed: number
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
setEmbeddedMpvAspect?: (
|
||
sessionId: string,
|
||
aspect: string
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
startEmbeddedMpvRecording?: (
|
||
sessionId: string,
|
||
options: EmbeddedMpvRecordingStartOptions
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
stopEmbeddedMpvRecording?: (
|
||
sessionId: string
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
getEmbeddedMpvDefaultRecordingFolder?: () => Promise<string>;
|
||
selectEmbeddedMpvRecordingFolder?: () => Promise<string | null>;
|
||
disposeEmbeddedMpvSession: (
|
||
sessionId: string
|
||
) => Promise<EmbeddedMpvSession | null>;
|
||
/**
|
||
* Frame-copy engine only: start/stop the preload frame pump that
|
||
* uploads helper frames onto the renderer's
|
||
* `<canvas data-embedded-mpv-frame>` element. Optional because older
|
||
* preload builds do not ship the pump.
|
||
*/
|
||
attachEmbeddedMpvFrameView?: (sessionId: string) => Promise<boolean>;
|
||
detachEmbeddedMpvFrameView?: () => void;
|
||
downloadsStart: (
|
||
data: ElectronBridgeDownloadStartPayload
|
||
) => Promise<ElectronBridgeDownloadStartResult>;
|
||
downloadsCancel: (downloadId: number) => Promise<ElectronBridgeErrorResult>;
|
||
downloadsPause: (downloadId: number) => Promise<ElectronBridgeErrorResult>;
|
||
downloadsResume: (
|
||
downloadId: number,
|
||
downloadFolder: string
|
||
) => Promise<ElectronBridgeErrorResult>;
|
||
downloadsRetry: (
|
||
downloadId: number,
|
||
downloadFolder: string
|
||
) => Promise<ElectronBridgeErrorResult>;
|
||
downloadsRedownloadMissing: (
|
||
downloadId: number
|
||
) => Promise<ElectronBridgeDownloadRedownloadResult>;
|
||
downloadsRemove: (downloadId: number) => Promise<DownloadRecoveryResult>;
|
||
downloadsGetList: (playlistId?: string) => Promise<ElectronDownloadItem[]>;
|
||
downloadsGet: (downloadId: number) => Promise<ElectronDownloadItem | null>;
|
||
downloadsUpdateMetadata: (
|
||
downloadId: number,
|
||
metadataSnapshot: DownloadMetadataSnapshot
|
||
) => Promise<ElectronBridgeErrorResult>;
|
||
downloadsGetDefaultFolder: () => Promise<string>;
|
||
downloadsSelectFolder: () => Promise<string | null>;
|
||
downloadsRevealFile: (
|
||
filePath: string
|
||
) => Promise<ElectronBridgeErrorResult>;
|
||
downloadsPlayFile: (filePath: string) => Promise<ElectronBridgeErrorResult>;
|
||
downloadsClearCompleted: (
|
||
playlistId?: string
|
||
) => Promise<DownloadRecoveryResult>;
|
||
onDownloadsUpdate: (callback: () => void) => () => void;
|
||
// Live-TV recordings surface. Optional: older Electron builds have no
|
||
// recordings bridge, and `supportsRecordings` (not `supportsDownloads`)
|
||
// gates every consumer.
|
||
recordingsGetList?: (
|
||
playlistId?: string
|
||
) => Promise<ElectronRecordingItem[]>;
|
||
recordingsGet?: (
|
||
recordingId: number
|
||
) => Promise<ElectronRecordingItem | null>;
|
||
recordingsStop?: (
|
||
recordingId: number
|
||
) => Promise<ElectronBridgeErrorResult>;
|
||
recordingsRemove?: (
|
||
recordingId: number
|
||
) => Promise<ElectronBridgeErrorResult>;
|
||
recordingsUpdatePrograms?: (
|
||
targetPath: string,
|
||
programs: RecordingProgramSnapshot[]
|
||
) => Promise<ElectronBridgeErrorResult>;
|
||
recordingsRevealFile?: (
|
||
filePath: string
|
||
) => Promise<ElectronBridgeErrorResult>;
|
||
recordingsPlayFile?: (
|
||
filePath: string
|
||
) => Promise<ElectronBridgeErrorResult>;
|
||
onRecordingsUpdate?: (callback: () => void) => () => void;
|
||
}
|