mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
Merge remote-tracking branch 'origin/master' into fix/workspace-route-swap
# Conflicts: # apps/web/src/app/app.config.ts
This commit is contained in:
commit
3a68793df2
78 files changed
+3051
-218
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>`.
|
||||
|
||||
@@ -1033,6 +1033,13 @@ per-theme tokens in `apps/web/src/_settings-theme.scss`.
|
||||
|
||||
## Forms
|
||||
|
||||
Every `<mat-form-field>` is outlined with a dynamic subscript. A component
|
||||
that renders one adds `providers: [provideFormFieldDefaults()]`
|
||||
(`@iptvnator/ui/components/form-field-defaults`); the root injector does not
|
||||
provide the defaults, because the token import there would load Material's
|
||||
form field and `@angular/forms` on startup. `pnpm run
|
||||
ui:form-field-defaults:validate` (in CI) reports a missing provider.
|
||||
|
||||
The add-source forms (M3U URL, Xtream, Stalker) and the edit dialog share one
|
||||
vocabulary, so a field reads the same wherever it appears:
|
||||
|
||||
|
||||
@@ -927,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
|
||||
@@ -941,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
|
||||
@@ -950,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
|
||||
@@ -975,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
|
||||
@@ -1017,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
|
||||
|
||||
@@ -1045,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
|
||||
@@ -1069,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`
|
||||
|
||||
@@ -1337,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
|
||||
@@ -1372,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
|
||||
|
||||
@@ -272,7 +272,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 a deployment can
|
||||
substitute a server-synced storage.
|
||||
substitute a server-synced storage. 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
|
||||
|
||||
@@ -422,6 +422,23 @@ 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.
|
||||
- It also runs AFTER enrichment: the series view re-normalizes the store item
|
||||
on every render, so it carries every `tmdb_*` field over. The key list
|
||||
(`TMDB_ENRICHMENT_KEYS`) is typed as a `Record` over `StalkerVodInfo`'s
|
||||
`tmdb_*` keys, so a new enrichment field fails to compile until it is listed
|
||||
there instead of silently vanishing from the view (as the genre and country
|
||||
Discover chips once did).
|
||||
|
||||
## Stalker Identity Policy
|
||||
|
||||
Full Stalker/Ministra portal authentication defaults to MAC-only identity. The
|
||||
|
||||
@@ -832,8 +832,10 @@ Contracts worth keeping:
|
||||
credits and no recommendations carries none of them and reads as
|
||||
un-enriched. Since the column is never overwritten, getting this wrong is
|
||||
unfixable after the fact — a real provider date arriving later cannot
|
||||
correct it. The same marker is what `trustedReleaseYear` (the
|
||||
recommendations exclusion index) must consult if it ever reads this column.
|
||||
correct it. Stalker entries never reach this column, but the same marker
|
||||
rides in their stored `info`, and `trustedReleaseYear` (the recommendations
|
||||
exclusion index) reads it first: a date TMDB supplied does not gate the
|
||||
exact tier. `createStalkerInfo()` keeps the marker through re-normalization.
|
||||
- **The id is stored unvetted.** Every consumer reaches TMDB through
|
||||
`TmdbEnrichmentService`, whose `detailsForProviderId` runs
|
||||
`assessProviderId` and lets the title search take over when the years
|
||||
|
||||
@@ -918,6 +918,9 @@ Toolchain notes for the Electron 43 baseline:
|
||||
Those dependencies require standalone PRs so their dedicated package,
|
||||
worker, playback, and diagnostic-contract validation cannot be hidden by an
|
||||
unrelated grouped update.
|
||||
Electron majors are ignored: Electron 44 ships no `linux-armv7l` binary,
|
||||
and the release still packages armv7l AppImage, DEB and Snap builds. Remove
|
||||
the ignore together with those targets when 32-bit ARM support is dropped.
|
||||
4. Electron 42 and newer download their development binary on the first
|
||||
Electron command instead of during package `postinstall`, so `electron`
|
||||
must not remain in pnpm's `onlyBuiltDependencies` allowlist. The first local
|
||||
|
||||
@@ -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