mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
Merge branch 'master' of https://github.com/4gray/iptvnator into claude/server-storage-pwa
# Conflicts: # docs/architecture/pwa-self-hosted.md
This commit is contained in:
commit
de87c1e4d3
78 files changed
+4367
-276
No files matched your search
@@ -157,6 +157,14 @@ Contracts:
|
||||
- Hosts provide hero chips/meta/actions as `*appDetailTags` / `*appDetailMeta` /
|
||||
`*appDetailActions` templates; the shell stamps them into the hero and again
|
||||
into the About block. Degradation stays "missing → not rendered".
|
||||
- Provider stand-ins count as missing: `N/A` (any case), a lone `-`, and a
|
||||
`0` IMDb/Kinopoisk score are dropped where provider rows are mapped, by
|
||||
`readProviderText` / `readProviderRating`
|
||||
(`libs/shared/interfaces/src/lib/provider-metadata.util.ts`) — in
|
||||
`createStalkerInfo()`, the `vod-details-adapters.ts` normalizers and the
|
||||
Xtream store's detail selection. Templates keep plain `@if` guards; do not
|
||||
teach them the placeholder list. Widen the list only with evidence of a
|
||||
provider sending the new stand-in.
|
||||
- The shell never wraps the `[detail-player]` slot in a conditional; the
|
||||
host's `@if (inlinePlayback())` is the only creator/destroyer of the
|
||||
player subtree, so shell state changes cannot recreate the `<video>`.
|
||||
|
||||
@@ -636,6 +636,13 @@ button disabled state at current-season boundaries.
|
||||
|
||||
Autoplay is enabled by default for series playback in embedded MPV. On `ended`, Xtream and Stalker series detail views start the next episode only when the current episode has a next item in the same season. Playback stops on the last episode of the current season. Previous always switches to the previous episode in the current season; it does not implement a restart-threshold behavior.
|
||||
|
||||
`ended` alone does not prove the episode is over: mpv reports a connection the
|
||||
server closed (and ffmpeg could not reopen) as EOF too, once its cache runs
|
||||
dry. `EmbeddedMpvPlayerComponent` therefore emits `playbackEnded` only for an
|
||||
`ended` session that carries no `reconnect`; while the main process reloads a
|
||||
VOD that ended well before its duration (see Network Auto-Reconnect), the
|
||||
episode continues instead of autoplay skipping its rest (issue #1902).
|
||||
|
||||
## Session Options (Extra libmpv Options)
|
||||
|
||||
`Settings > Playback > Extra embedded MPV options` is a free-form textarea,
|
||||
@@ -701,8 +708,9 @@ included.
|
||||
`EmbeddedMpvReconnectCoordinator`
|
||||
(`apps/electron-backend/src/app/services/embedded-mpv-reconnect.ts`) reloads
|
||||
the last user-requested playback when a session reports a stream loss: an
|
||||
`error` status, or `ended` while the playback is live (`isLive`, else "no
|
||||
`contentInfo`", the same rule the renderer uses). It is driven by
|
||||
`error` status, `ended` while the playback is live (`isLive`, else "no
|
||||
`contentInfo`", the same rule the renderer uses), or `ended` on VOD well
|
||||
before the known duration. It is driven by
|
||||
`refreshSession()` status transitions, so it covers every engine, and it is
|
||||
captured per session from `Settings.embeddedMpvAutoReconnect` (default on;
|
||||
mirrored as `EMBEDDED_MPV_AUTO_RECONNECT`). The policy is deliberately narrow:
|
||||
@@ -716,6 +724,16 @@ mirrored as `EMBEDDED_MPV_AUTO_RECONNECT`). The policy is deliberately narrow:
|
||||
the IPC socket so a stream that ended reports `ended` rather than the
|
||||
`paused` that keep-open would otherwise look like, and an mpv process that
|
||||
exits abnormally reports `error`.
|
||||
- A VOD `ended` counts only when more than `max(10 s, 1 % of the duration)`
|
||||
remained (`isEmbeddedMpvEarlyEof`): the margin absorbs container rounding
|
||||
and the whole-second snapshot values, and with no known duration an EOF is
|
||||
always the end. A reload the engine accepted records where it resumed; an
|
||||
early EOF before playback gets 5 s past that point (and with no user seek
|
||||
in between) ended where the file does (a duration mpv estimated from the
|
||||
bitrate can overshoot) and is left as the end, so autoplay still continues
|
||||
after one attempt instead of a reload loop. A refused reload records
|
||||
nothing, so the original EOF keeps its backoff.
|
||||
With the setting off, an early EOF stays a plain `ended`.
|
||||
- Backoff is 2 s, 4 s, 8 s, 16 s, 30 s, 30 s, at most six attempts per
|
||||
outage. The budget resets only after 30 s of uninterrupted `playing`, so a
|
||||
stream that flaps every few seconds runs out of attempts instead of being
|
||||
|
||||
@@ -633,6 +633,19 @@ channel-list-container/
|
||||
- `PlaylistsService.updatePlaylistMeta()` persists `hiddenGroupTitles` into the
|
||||
stored playlist payload, and M3U refresh/update flows preserve the existing
|
||||
value when refreshed playlist data omits the field.
|
||||
- Electron loads the playlist store from `DB_GET_APP_PLAYLIST_METAS`, which
|
||||
reads columns only, so `hiddenGroupTitles` is absent from the store after a
|
||||
restart. `M3uWorkspaceRouteSession` copies it from the full playlist read into
|
||||
the store (`updatePlaylistMeta` with `persist: false`) before publishing the
|
||||
channels, unless the store value changed while that read was queued (#1894).
|
||||
A restored or deep-linked route can finish that read before the startup
|
||||
inventory, which waits for settings and EPG reconciliation, publishes the
|
||||
playlist; the hydration then waits for the entity, since `updateOne` drops an
|
||||
update for a missing one. The startup screen keeps the router outlet hidden
|
||||
until that inventory is published, and the hydration runs in the same store
|
||||
update, so the rail never renders a hidden group. A save's meta write joins
|
||||
the per-playlist queue that `getPlaylist()` reads through, so a reopened
|
||||
route reads the saved value.
|
||||
- The groups route keeps the manage action reachable even when every group is
|
||||
hidden by separating "playlist has no groups" from "no visible/search-matching
|
||||
groups" empty states.
|
||||
|
||||
@@ -704,6 +704,11 @@ source's default section, which is VOD (`getPlaylistLink` links to
|
||||
and the first page is the "All items" grid. The plan's "live category list"
|
||||
would need a second click and is not measured; the landed section is
|
||||
recorded under `evidence.firstPage.section`.
|
||||
The seeded portal is already imported, so the click only reads the saved
|
||||
catalog and finishes well inside the shell's show delay for the import
|
||||
overlay ([workspace shell](workspace-shell.md)): a J2 click must not mount
|
||||
`app-workspace-shell-import-overlay`, and before that delay it flashed for
|
||||
about two frames between the skeleton and the first page.
|
||||
|
||||
**HTTP requests to the mock.** The J2 profile is seeded with the origin of a
|
||||
loopback proxy in the test process
|
||||
@@ -922,11 +927,13 @@ put behind the request ledger: global search reads only the local database.
|
||||
|
||||
**What typing does.** The header search box
|
||||
(`app-workspace-shell-header .search-field input[type="search"]`) applies
|
||||
its term after `SEARCH_INPUT_DEBOUNCE_MS` (350 ms,
|
||||
its term after `SEARCH_INPUT_DEBOUNCE_MS` (300 ms,
|
||||
`WorkspaceShellSearchSyncService`) and writes it to the URL as `q`. On
|
||||
`/workspace/search` (`app.routes.ts`, Electron only) `SearchResultsComponent`
|
||||
adopts `q` and runs `executeSearch` after its own 300 ms debounce, which
|
||||
calls the `dbGlobalSearch` bridge method once with a limit of 101.
|
||||
runs `executeSearch` as soon as the applied term reaches the store, with no
|
||||
debounce of its own, which calls the `dbGlobalSearch` bridge method once
|
||||
with a limit of 101. A newer term supersedes a query still in flight: its
|
||||
result is dropped by request version.
|
||||
|
||||
**Start.** After J1 has ended, the test clicks the rail's **Global search**
|
||||
link and focuses the header search box (not measured), installs the IPC
|
||||
@@ -936,7 +943,7 @@ been quiet for 1 s: no DOM mutation, no new or pending bridge call, and an
|
||||
unchanged `main.sqlStatements` total (30 s timeout, which fails the
|
||||
iteration). J1's capture is detached. The test then types the query with one
|
||||
`keyboard.type` call per character on a fixed schedule, 100 ms apart from
|
||||
the first key (well below the 350 ms debounce, as steady typing would be),
|
||||
the first key (well below the 300 ms debounce, as steady typing would be),
|
||||
and samples the main process just before each key. The probe's
|
||||
capture-phase `keydown` listener on `window` stamps every key in the search
|
||||
box before the app sees it; the first one starts the journey and sends
|
||||
@@ -945,7 +952,7 @@ rejects an iteration whose DOM mutations, bridge calls or SQL statements
|
||||
moved between the quiet snapshot and the first key.
|
||||
|
||||
**End.** "No DOM mutation for 200 ms after the last keystroke" alone would
|
||||
end inside the 650 ms of debounce, before any query ran: nothing in the DOM
|
||||
end inside the 300 ms of debounce, before any query ran: nothing in the DOM
|
||||
changes while the term waits. So after the sixth key the probe waits for
|
||||
the results of the final term to be shown (the path ends with
|
||||
`/workspace/search`, the URL `q` is the query, no `.loading-state` is
|
||||
@@ -970,7 +977,7 @@ window closed until its results replace the old ones.
|
||||
`evidence.finalQuery` keeps its term, result length and duration. The
|
||||
record also rejects an iteration in which two keydowns were more than
|
||||
250 ms apart (`typing-cadence`; the gaps are `evidence.keyIntervalsMs`).
|
||||
At that point a late key on a busy machine could let the 350 ms debounce
|
||||
At that point a late key on a busy machine could let the 300 ms debounce
|
||||
apply an intermediate term, which steady typing does not.
|
||||
|
||||
A settle that times out fails the iteration with what the probe and the
|
||||
@@ -1012,9 +1019,10 @@ counter: global search does not touch the network.
|
||||
| `firstKeystrokeToFirstResultMs.p50/.p90` | First mutation batch with a visible result card (for any term) minus the first keydown. |
|
||||
| `lastKeystrokeToSettledMs.p50/.p90` | Settled moment (the last mutation batch before the 200 ms quiet window) minus the last keydown. The quiet window itself is not included. |
|
||||
|
||||
Both are taken in the renderer. With the current debounces both include
|
||||
the 650 ms the term waits (350 ms in the shell, then 300 ms in the results
|
||||
component); the first also includes the 500 ms of typing.
|
||||
Both are taken in the renderer. Both include the 300 ms the term waits in
|
||||
the shell; the first also includes the 500 ms of typing. Until the single
|
||||
debounce below, the wait was 650 ms (350 ms in the shell, then 300 ms in the
|
||||
results component).
|
||||
|
||||
### First measurement
|
||||
|
||||
@@ -1040,6 +1048,46 @@ debounces (350 ms in the shell, then 300 ms in the results component). The
|
||||
query itself, from the bridge call to the rendered first page, takes the
|
||||
remaining 30-40 ms.
|
||||
|
||||
### One debounce
|
||||
|
||||
The results component's own 300 ms timer is gone: the shell debounce is
|
||||
now the only one, and global search runs as soon as the shell applies the
|
||||
term. `SEARCH_INPUT_DEBOUNCE_MS` went from 350 to 300 ms. It has to stay
|
||||
above the gap between keys of steady typing, or a word typed in one go
|
||||
queries once per key; 300 ms covers an average typist (about 200-250 ms
|
||||
between keys) with some margin, while the query and first page take only
|
||||
30-40 ms of the remaining time. The route sync in the component reads the
|
||||
store term untracked, so a term the shell applied before it writes `q` is
|
||||
no longer pushed back to the previous `q` and queried again.
|
||||
|
||||
Local, macOS, 2026-10-11, three J4 runs per build (five measured
|
||||
iterations each), the same machine, master `633fde3e0` against this
|
||||
change:
|
||||
|
||||
| Measurement | Two debounces | One debounce |
|
||||
| ------------------------------------ | -------------------- | -------------------- |
|
||||
| `lastKeystrokeToSettledMs` P50 / P90 | 692-695 / 695-697 ms | 336-337 / 337-339 ms |
|
||||
| `firstKeystrokeToFirstResultMs` P50 | 1,190 ms | 832-834 ms |
|
||||
| `renderer.ipcCallsPerSearch` | 1 | 1 |
|
||||
| `renderer.sqlStatementsPerSearch` | 2 | 2 |
|
||||
| `renderer.domMutationsToResults` | 402 | 392 |
|
||||
|
||||
Per keystroke (query calls / SQL / DOM mutations / CD ticks) at 100 ms per
|
||||
key, keys 1-5 are unchanged at `0/0/0/1`; key 6 is `1/2/402/9` before and
|
||||
`1/2/392/7` after.
|
||||
|
||||
Slower typing was measured with a scratch change to the journey (key
|
||||
interval and cadence guard raised, quiet window 1 s; not committed). At
|
||||
400 ms per key both builds run one query per key from the second
|
||||
character, five in all with 2 statements each: the shell debounce already
|
||||
expired between keys before this change, and the results timer only
|
||||
delayed each query. Before, the queries trail the keys by 650 ms (keys
|
||||
`y s t e m` read `0 1 1 1 2`); after, each follows its own key. At 325 ms
|
||||
per key the two builds differ: before, one query in 17 of 20 iterations
|
||||
(two or four when a gap stretched past 350 ms); after, five. Typing with
|
||||
gaps between 300 and 350 ms is the only range where this change adds
|
||||
queries; each is a 2-statement query whose stale result is dropped.
|
||||
|
||||
About one launch in sixty showed the empty-results view: the single
|
||||
`dbGlobalSearch` call went out with the same arguments (`system`, all three
|
||||
types, hidden categories included, limit 101) and the main process answered
|
||||
@@ -1064,8 +1112,8 @@ persisted status (`xtream-import-status:<id>:<type>`) reads `completed` and
|
||||
the type has rows, unless the portal has no categories of that type. Rows
|
||||
are required because a refresh deletes the catalog without resetting the
|
||||
status. `xtream-import-wait.e2e.ts` holds the account check past the overlay
|
||||
window and fails without it. No J4 baseline exists yet; J4 counters join the
|
||||
ratchet once three runner runs agree.
|
||||
window and fails without it. Two J4 counters are enforced since #1899
|
||||
(see [Enforced journey counters](#enforced-journey-counters)).
|
||||
|
||||
## `renderer.initialBytes`
|
||||
|
||||
@@ -1317,8 +1365,8 @@ largest distance in it, the content pane's 323 px, which is why Back added
|
||||
buttons appearing when the categories arrive. The skeletons of the category
|
||||
list and the grid are replaced by content without any shift. Nothing was
|
||||
hidden behind the startup overlay: the "Syncing playlist" overlay of the
|
||||
cached catalog load flashes for about two frames after the shift and covers
|
||||
no further one.
|
||||
cached catalog load flashed for about two frames after the shift and covered
|
||||
no further one. It no longer mounts on a J2 click (see the J2 section).
|
||||
|
||||
The content frame and the header controls now slide into place instead of
|
||||
jumping ([Layout slides](./workspace-shell.md#layout-slides)): the new layout
|
||||
@@ -1332,6 +1380,14 @@ changes) and the wall-clock entries did not move. Under
|
||||
`prefers-reduced-motion` there is no slide, so a user with that preference
|
||||
still sees the jump that the journey no longer measures.
|
||||
|
||||
The two J4 entries, `search/renderer.ipcCallsPerSearch` 1 and
|
||||
`search/renderer.sqlStatementsPerSearch` 2 (#1899, `evidenceRun`
|
||||
38165392046), were identical in all 816 J4 iterations of the 136 CI journey
|
||||
jobs since #1816 and in all 18 iterations of three runs of #1899. They guard
|
||||
the debounce: a search that queried per keystroke, or ran a second query for
|
||||
the same term, raises them. Like the entries in the table they carry the
|
||||
guard-only note: #1899 halved the J4 wall clock without moving either.
|
||||
|
||||
Being deterministic is not the same as being validated. Principle 3 of the
|
||||
plan promotes a counter to a guardrail once a PR has shown that lowering it
|
||||
lowered the journey's wall-clock. None of these counters has that evidence
|
||||
@@ -1367,7 +1423,12 @@ Not enforced, with the reason:
|
||||
- J3 `renderer.ipcSerialDepthToPlaying` (4 on the runner, 3 on a Mac):
|
||||
added after these runs and measured in one runner run so far; a
|
||||
candidate once `master` runs agree.
|
||||
- J4: not measured yet.
|
||||
- J4 `renderer.domMutationsToResults` (402 on `master`, 392 since #1899),
|
||||
`renderer.ipcSerialDepthToResults` (1) and `renderer.layoutShiftScore`
|
||||
(0): identical in the three #1899 runs; candidates once `master` runs
|
||||
agree.
|
||||
- J4 `renderer.cdTicksToResults` (11 to 13) and `renderer.longTasks`: see
|
||||
the general reasons above.
|
||||
|
||||
Runner counters differ from a Mac (the Linux-only `getWindowState` call, for
|
||||
one), so take every journey baseline value from the runner only.
|
||||
|
||||
@@ -12,6 +12,8 @@ settings screen.
|
||||
- Manifest types: `libs/shared/interfaces/src/lib/playlist-backup.interface.ts`
|
||||
- Xtream pending restore storage:
|
||||
`libs/services/src/lib/xtream-pending-restore.service.ts`
|
||||
- Browser (PWA) Xtream user state:
|
||||
`libs/services/src/lib/pwa-xtream-backup-state.service.ts`
|
||||
|
||||
## Manifest Contract
|
||||
|
||||
@@ -61,9 +63,12 @@ Xtream backups export only connection metadata plus portable user state.
|
||||
- User state:
|
||||
- hidden categories by `{ categoryType, xtreamId }`
|
||||
- favorites by `{ contentType, xtreamId, addedAt?, position? }`
|
||||
- recently viewed by `{ contentType, xtreamId, viewedAt }`
|
||||
- recently viewed by `{ contentType, xtreamId, viewedAt, backdropUrl? }`
|
||||
(`backdropUrl` is written and read by the browser build only)
|
||||
- playback positions as `PlaybackPositionData[]`
|
||||
- optional VOD source pins by `{ matchKey, contentId, updatedAt? }`
|
||||
- optional title snapshots `collectionItems` (browser build only; see
|
||||
[Browser runtime](#browser-runtime))
|
||||
|
||||
An imported Xtream archive can wait for its catalog to download. Export reads
|
||||
that pending snapshot before consulting the catalog: its hidden categories,
|
||||
@@ -225,6 +230,47 @@ categories/favorites/history can
|
||||
remain after a later failure, but the snapshot remains available to retry the
|
||||
complete restore. It is consumed only after every restore step succeeds.
|
||||
|
||||
### Browser Runtime
|
||||
|
||||
Whenever `XTREAM_DATA_SOURCE` resolves to `PwaXtreamDataSource` — the
|
||||
browser, or a desktop bridge without the Xtream SQLite API
|
||||
(`RuntimeCapabilitiesService.supportsXtreamSqliteDataSource`) — Xtream
|
||||
favorites, recently viewed items, playback positions and title snapshots live
|
||||
in the `USER_DATA_STORAGE` maps owned by `PwaXtreamDataSource`
|
||||
(`XTREAM_USER_DATA_KEYS`; see
|
||||
[PWA portal user data](./pwa-self-hosted.md#pwa-portal-user-data)).
|
||||
`PwaXtreamBackupStateService` reads and writes them through that token, so the
|
||||
self-hosted server storage is covered too.
|
||||
|
||||
- Export converts each playlist's entries to the archive shape. Typed keys
|
||||
(`movie:42`) carry their identity; a legacy bare id resolves only through
|
||||
the snapshot stored under it, and is left out when none identifies it.
|
||||
Recently viewed keeps the newest view per title, with the artwork saved
|
||||
for that view as `backdropUrl`. `collectionItems` carries
|
||||
the snapshots of exported favorites and history, and is omitted when there
|
||||
are none. Hidden categories stay `[]` and `sourcePins` stays absent: the
|
||||
browser tracks neither.
|
||||
- An unreadable map (storage error, malformed JSON, a playlist entry that is
|
||||
not a list) fails the export instead of being written as an empty list.
|
||||
- A pending restore snapshot still wins over the stored maps, as on desktop.
|
||||
- Import parks the restore state like the desktop does, then applies it at
|
||||
once through `XtreamPendingRestoreService.applyAndConsume`: the browser has
|
||||
no catalog cache to wait for. Favorites, history and positions of the
|
||||
imported playlist id are replaced (positions get that id); other playlists'
|
||||
entries are untouched. `collectionItems` merge into the playlist's
|
||||
snapshots, keyed by typed identity; an archive without them leaves the
|
||||
snapshots alone, malformed items are skipped, a damaged cache is replaced,
|
||||
and a quota error on this cache does not fail the restore.
|
||||
- The lists are written before the optional title snapshots, so the cache
|
||||
never takes storage space they need (as for additions; see the
|
||||
[snapshot-storage contract](./xtream-portal-compatibility.md)). A failed
|
||||
write fails the entry and keeps the snapshot parked, so the Xtream store
|
||||
replays the complete state when the playlist next opens. That replay
|
||||
carries no title snapshots, so they are still merged after a failed list
|
||||
write, unless storage is full and they would take room the replay needs.
|
||||
|
||||
Older builds ignore `collectionItems`; the manifest version stays `1`.
|
||||
|
||||
## Current UX
|
||||
|
||||
The settings page now exports/imports “playlist backups” instead of the old
|
||||
|
||||
@@ -437,7 +437,11 @@ and sidecar state in localStorage:
|
||||
`PwaXtreamDataSource` and `PwaCollectionStorage` reach these keys through the
|
||||
`USER_DATA_STORAGE` token from `@iptvnator/services` (default:
|
||||
`localStorage`), never through `localStorage` directly, so server storage
|
||||
mode can substitute `ServerUserDataStorage`.
|
||||
mode can substitute `ServerUserDataStorage`. The key names are shared as
|
||||
`XTREAM_USER_DATA_KEYS` with the playlist backup, which exports and restores
|
||||
the favorites, recent items, positions and snapshots through the same token
|
||||
(`PwaXtreamBackupStateService`; see
|
||||
[backup/restore](./playlist-backup-restore.md#browser-runtime)).
|
||||
|
||||
Entries should include a content snapshot when the item is added. Global
|
||||
collection routes and the dashboard can then restore titles, posters, content
|
||||
|
||||
@@ -618,6 +618,17 @@ Xtream-only row shape:
|
||||
12. `DB_GET_APP_STATE`
|
||||
13. `DB_SET_APP_STATE`
|
||||
|
||||
`DB_UPSERT_APP_PLAYLIST(S)` write whole documents: the row built by the shared
|
||||
codec (`buildPlaylistRow` in `libs/shared/database`) replaces the stored one.
|
||||
`playlistConflictUpdate` sets every optional column the document leaves empty
|
||||
(an empty string or a missing key) to NULL instead of skipping it, because
|
||||
`parseAppPlaylist` prefers a column over the payload and would otherwise bring
|
||||
a cleared User-Agent, Referrer, Origin, credential, EPG list or file path back.
|
||||
Only `date_created` is kept from the first insert. Callers must send the full
|
||||
document (`PlaylistsService` reads, patches and writes the whole row); a
|
||||
column-only change goes through `DB_UPDATE_PLAYLIST`. The self-hosted web
|
||||
backend's playlist repository uses the same conflict update.
|
||||
|
||||
`DB_GET_APP_PLAYLIST_METAS` is the preferred path for summary surfaces such as
|
||||
the workspace sidebar and dashboard source rail. It selects playlist metadata
|
||||
columns only and deliberately skips the large `payload` column, which can
|
||||
|
||||
@@ -422,6 +422,17 @@ Failure-handling rule:
|
||||
not `undefined` collections or renderer exceptions. The workspace Stalker
|
||||
context panel and live layout rely on this guarantee.
|
||||
|
||||
Detail metadata rule:
|
||||
|
||||
- `createStalkerInfo()` (`stalker-vod.utils.ts`) is the mapping both detail
|
||||
templates read: the series view binds `info` directly and the shared VOD
|
||||
view reads it through `normalizeStalkerVod()`. Each field takes the first
|
||||
usable candidate, and portal stand-ins (`N/A`, `-`, an unrated `0` score)
|
||||
become `''` there — see the placeholder rule in
|
||||
[Embedded Inline Playback](./embedded-inline-playback.md). It runs before
|
||||
TMDB enrichment, which keeps a non-empty provider rating and release date,
|
||||
so a surviving stand-in would also block the TMDB value.
|
||||
|
||||
## Stalker Identity Policy
|
||||
|
||||
Full Stalker/Ministra portal authentication defaults to MAC-only identity. The
|
||||
|
||||
@@ -200,7 +200,13 @@ services:
|
||||
5. `WorkspaceShellCommandPaletteService` owns command-palette dialog lifecycle
|
||||
and recent-command recording.
|
||||
6. `WorkspaceShellXtreamImportService` owns Xtream import/refresh overlay
|
||||
state and labels.
|
||||
state and labels. Remote import phases and refresh preparation of the
|
||||
shown playlist (of any playlist on the dashboard) show the overlay at
|
||||
once. A session that has only read the saved catalog (`loading-cached`,
|
||||
`isImporting` still false) shows it only after
|
||||
`XTREAM_LOCAL_LOAD_OVERLAY_DELAY_MS` (300 ms), so a warm open goes from
|
||||
the skeleton to the page without flashing the overlay, while a slow or
|
||||
stuck cache read still blocks and stays cancellable.
|
||||
|
||||
When adding shell behavior, prefer placing it in the service that owns the
|
||||
nearest existing state. Keep `WorkspaceShellFacade` as a stable re-export layer
|
||||
|
||||
@@ -405,6 +405,7 @@ sometimes only respond to that misspelled action.
|
||||
| `tzoffset:tzoffset` | 6006 | live:2, vod:1, series:1 | 3 | active |
|
||||
| `emptyvod:emptyvod` | 7007 | 2 each | 5 | active |
|
||||
| `marketing:marketing` | 8020 | live:4, vod:4, series:4 | curated | active |
|
||||
| `streamdrop:streamdrop` | 8181 | series:1 | 1 | active |
|
||||
| `expired:expired` | 4004 | 4 each | 10 | Expired |
|
||||
| `inactive:inactive` | 5005 | 4 each | 10 | Disabled |
|
||||
| `<any other>` | hash | 6 each | 30 | active |
|
||||
@@ -428,6 +429,17 @@ Timeshift URLs of `epg` and `epglong` play the local MPEG-TS clip
|
||||
the catch-up seek bar's label bounds; `epg:epg` keeps its short titles,
|
||||
which other tests assert verbatim.
|
||||
|
||||
### `streamdrop:streamdrop` fixture details
|
||||
|
||||
Its three episodes play the local 60 s Matroska clip
|
||||
(`src/fixtures/episode.mkv`, generation command in the fixtures README) with
|
||||
byte ranges, but `dropping-episode.ts` cuts the first response that reaches
|
||||
35 % of the file and answers 403 for the next 4 s, so ffmpeg's own HTTP
|
||||
reconnect fails and mpv reports EOF mid-episode. `POST /reset` re-arms the
|
||||
drop. The Electron E2E `embedded-mpv-series-stream-drop.e2e.ts` uses it to
|
||||
check that Embedded MPV resumes the episode instead of autoplaying the next
|
||||
one (issue #1902).
|
||||
|
||||
### `marketing:marketing` fixture details
|
||||
|
||||
This scenario is reserved for release screenshots and marketing materials:
|
||||
|
||||
@@ -344,6 +344,12 @@ resolves. An unresolved source exposes no actions. Playback and download
|
||||
descriptors also fall back to the catalog title and poster after `info` and
|
||||
`movie_data`.
|
||||
|
||||
Provider stand-ins in `get_vod_info` / `get_series_info` (`N/A`, `-`, an
|
||||
unrated `0` score) are emptied before the response becomes the store
|
||||
selection (`stores/xtream-detail-metadata.ts`), since both detail routes bind
|
||||
`info` fields directly. A `get_vod_info` response made only of stand-ins
|
||||
therefore counts as sparse and gets the fallback page above.
|
||||
|
||||
## Catch-Up Playback URLs
|
||||
|
||||
Xtream-compatible portals differ on archive playback URL shape. IPTVnator
|
||||
|
||||
Reference in new issue
Block a user