Commit Graph
2623 Commits
Author SHA1 Message Date
4gray 7ee6e97ceb fix(stalker): preserve VOD series season shape 2026-07-27 11:04:21 +02:00
4gray 9a69a6d46a feat(stalker): add challenge-driven import flow 2026-07-27 11:01:15 +02:00
4gray 474f4a86cf refactor(stalker): route renderer requests through session leases 2026-07-27 10:58:14 +02:00
4gray 91761f5abe feat(stalker): secure native playback contexts 2026-07-27 10:56:33 +02:00
4gray 57e108b8cb feat(stalker): reflect persisted connections in state 2026-07-27 10:50:01 +02:00
4gray cede2c6bb3 fix(stalker): preserve catalog pagination metadata 2026-07-27 10:48:16 +02:00
4gray 00c85beb70 fix(logging): redact Stalker session references 2026-07-27 10:45:26 +02:00
4gray b76e65c2bf feat(stalker): expose typed session IPC 2026-07-27 10:41:32 +02:00
4gray 854e922c1e feat(stalker): persist verified connections atomically 2026-07-27 10:40:50 +02:00
4gray 580a056035 feat(stalker): bind secret playback contexts in main 2026-07-27 10:37:27 +02:00
4gray d5acb329e0 feat(stalker): map typed portal operations in main 2026-07-27 10:32:14 +02:00
4gray df92bae87e feat(stalker): add safe HAR draft converter 2026-07-27 10:28:47 +02:00
4gray 5b22bee0f4 feat(stalker): add session watchdog scheduler 2026-07-27 10:27:16 +02:00
4gray 97c50071ac feat(stalker): install discovered principals atomically 2026-07-27 10:23:44 +02:00
4gray 79729c4dfc test(stalker): add authentication replay corpus 2026-07-27 10:22:58 +02:00
4gray ffefe48992 feat(stalker): authenticate resolved portal sessions 2026-07-27 10:21:58 +02:00
4gray c12ad52019 feat(stalker): resolve portal endpoints safely 2026-07-27 10:17:21 +02:00
4gray 6726863081 feat(settings): secure playlist backup credentials 2026-07-27 10:15:27 +02:00
4gray 04e9027840 feat(backup): restore redacted portals safely 2026-07-27 10:12:22 +02:00
4gray 581ad6fe8b feat(backup): exclude portal secrets by default 2026-07-27 10:03:40 +02:00
4gray c4974e9b93 feat(stalker): add cookie-aware portal HTTP session 2026-07-27 10:01:55 +02:00
4gray 2646c0c492 feat(stalker): map playlists to session descriptors 2026-07-27 09:56:06 +02:00
4gray a2fd389e74 test(stalker): add fail-closed fixture validation 2026-07-27 09:55:53 +02:00
4gray 375169a69b fix(stalker): harden replay network boundaries 2026-07-27 09:54:30 +02:00
4gray f3acc2ce39 feat(stalker): serialize base identity mutations 2026-07-27 09:52:24 +02:00
4gray 2a88060fae feat(stalker): add one-time challenge registry 2026-07-27 09:49:41 +02:00
4gray 13f05d0a7e feat(stalker): validate portal transport inputs 2026-07-27 09:48:19 +02:00
4gray d0cf96caf8 feat(stalker): add isolated RFC cookie jar 2026-07-27 09:47:32 +02:00
4gray 9166d0f12c feat(stalker): add validated redirect session hooks 2026-07-27 09:35:43 +02:00
4gray 8009233220 refactor(stalker): extract replay fixture schema 2026-07-27 09:28:01 +02:00
4gray af0f210ea5 test(stalker): add replay listeners and control plane 2026-07-27 09:05:40 +02:00
4gray e43d94ded4 fix(stalker): use canonical serial number key 2026-07-27 09:00:56 +02:00
4gray 51d879a734 test(stalker): add replay scenario core 2026-07-27 08:30:33 +02:00
4gray 847d98f035 fix(stalker): prioritize response status semantics 2026-07-27 03:56:27 +02:00
4gray 604d2aca3e fix(stalker): reject ambiguous response failures 2026-07-27 03:52:20 +02:00
4gray 92238ecbbb fix(stalker): tighten session protocol boundaries 2026-07-27 03:37:42 +02:00
4gray f8a544d287 fix(stalker): correlate session operation contracts 2026-07-27 03:16:16 +02:00
4gray 29fa4d0f87 feat(stalker): add pure session protocol contracts 2026-07-27 03:01:52 +02:00
4gray f928e06afb docs(stalker): plan session compatibility implementation 2026-07-27 02:42:04 +02:00
4gray 3502ebe0f9 docs(stalker): design session compatibility foundation 2026-07-27 02:28:19 +02:00
4gray 9ae53e4515 fix(playback): make the "Show subtitles" setting reach the web players (#1269)
The persisted subtitle preference only ever had an owner behind the default-off
shared web-controls flag, so with the shipping controls it did nothing: Video.js
never read it, ArtPlayer declared the input but never used it, and the HTML5
player only ran a one-shot pass after play() resolved — before hls.js had added
its text tracks. No portal host bound the input at all, so it never reached
Xtream or Stalker pages either.

Extract the source-local track controllers into the adapter-free
WebVideoSourceTracks and have WebVideoSourceControlsBridge wrap it, so both
controls modes apply the preference through the same code. The preference-off
players bind it directly (VjsLegacyTracks for Video.js), and
WebPlayerViewComponent reads the preference from SettingsStore instead of an
input so every host inherits it.

The preference means different things depending on who renders the caption UI:
shared controls stay authoritative for the session, while vendor chrome is
source-default — the preference seeds each new source and is released once the
media reports playing, so the engine own caption menu keeps working. Mode
selection is an optional playbackStarted probe passed to the HLS, native and
Shaka helpers; in that mode the HLS helper deselects the track rather than
hiding it, since subtitleDisplay would silently override the vendor menu.

Closes #1155
2026-07-26 09:26:40 +02:00
4gray f147d4fe37 perf(m3u): stop cancelled refresh workers (#1268) 2026-07-26 09:25:51 +02:00
dependabot[bot] 7e8c2ccce1 chore(deps): bump codecov/codecov-action from 6 to 7 (#1246) 2026-07-26 02:22:05 +02:00
dependabot[bot] b05841f121 chore(deps): bump actions/upload-pages-artifact from 3 to 5 (#1248) 2026-07-26 02:21:56 +02:00
4grayandClaude Opus 5 e91a7cde7a fix(deps): patch transitive runtime CVEs via pnpm overrides (#1258)
Closes 13 runtime-scope Dependabot advisories that Dependabot cannot fix itself:
every vulnerable package here is transitive, so the bot has no lever until each
parent publishes a release widening its own pin.

Overrides added (pinned-source form, matching existing convention):

- @xmldom/xmldom 0.8.11 -> 0.8.13  (5 high) via video.js -> mpd-parser
- fast-uri       3.1.0  -> 3.1.4   (4 high) via electron-conf -> ajv
- js-yaml        4.1.1  -> 4.3.0   (2)      via electron-updater
- form-data      4.0.5  -> 4.0.6   (1 high) via axios
- ajv            8.17.1 -> 8.18.0  (1)      via electron-conf

Every target stays inside its parent's declared semver range. For xmldom,
fast-uri and js-yaml the newest published version is outside that range
(0.9.x / 4.x / 5.x), so "latest" would have broken them; the new doc
records that constraint.

Deliberately excluded: axios and uuid are direct deps already covered by open
Dependabot PRs (#1251, #1252). undici is labelled runtime scope but every path
to it is build tooling (electron -> @electron/get, @angular/build,
@module-federation/dts-plugin) and it is not in the packaged app.

Reachability: xmldom arrives via video.js -> VHS -> mpd-parser, but the app
routes every .mpd to Shaka, which uses its own DASH parser, so that one is
defence in depth. The genuinely reachable one is js-yaml, which
electron-updater uses to parse latest.yml from releases.

Adds docs/architecture/dependency-security-overrides.md and a .changes note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 02:21:11 +02:00
4gray 3032cfa88d fix(m3u): avoid persisting hydrated favorites (#1232) 2026-07-26 02:20:32 +02:00
4grayandClaude Opus 4.8 a5bb8dc25c fix(tmdb): series cast was the latest season only, not the show (#1242)
* fix(tmdb): series cast was the latest season only, not the show

TMDB documents a TV id's `credits` as the credits of the LATEST SEASON.
We requested exactly that and rendered it as "the cast", so every
long-running show lost every regular who had left: The Boys showed
whoever appears in the newest season, not the ensemble.

The TV details request now also appends `aggregate_credits`, which spans
the whole run — but per TMDB omits the newest season, so neither payload
alone is the cast. `unifiedTvCast` unions them: whole-run billing order
first, then people who appear only in the newest season, deduplicated by
person id. Characters come from the aggregate `roles[]` shape.

Deliberately NO cache-key bump. Rows cached before this simply lack
`aggregate_credits` and keep the previous behaviour until they expire,
which avoids invalidating every user's details cache twice — the roadmap
schedules one consolidated bump once the remaining append_to_response
additions (images, certifications, alternative_titles) land together.

Movies are untouched: /movie/{id} has no aggregate_credits and its
`credits` is already the full cast.

Tests: departed regulars retained, newest-season arrivals appended after
show billing order, characters read from roles[], no duplicates across
the two payloads, graceful fallback for pre-aggregate cache rows.

Refs docs/architecture/tmdb-roadmap.md A2.

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

* fix(tmdb): reserve cast slots so newest-season arrivals survive the cap

The union was appended aggregate-first and then truncated to ten, so on
exactly the shows it was built for — long-running ones, where the
whole-run cast alone exceeds the limit — every newest-season arrival was
sliced back off. The original fixture had two aggregate members and
could not catch it.

unifiedTvCast now holds back up to three slots for the top-billed
arrivals instead of appending them where the cap discards them, and
gives the slots back when nobody is new.

Tests: a 12-member aggregate plus two arrivals keeps both arrivals and
top billing; an aggregate with no arrivals still gets all ten slots.

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

* test(tmdb): split the series-cast suite out of the merge spec

The merge conflict resolution put both new describes back into
tmdb-merge.spec.ts, pushing it to 499 lines — past the 400-line
max-lines cap. The aggregate-credits suite moves to its own file.

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

* fix(tmdb): stop the cast union from shrinking, and bound what it caches

Three follow-ups from a review pass over the aggregate-credits union:

- The reserved arrival slots were subtracted from the aggregate even when
  the aggregate was shorter than the cap, so a show with four regulars and
  five newcomers returned seven names instead of nine. The reservation is
  a floor for arrivals now, not a quota.
- An aggregate member's character came from the first role with any text,
  so a one-episode cameo could outrank the part the actor is known for.
  Pick the role with the most episodes.
- aggregate_credits carries a show's whole-run cast AND crew, and details
  payloads are cached verbatim — orders of magnitude of JSON for a list
  the merge truncates to ten people. Cache the billing-order prefix and
  drop the crew nothing reads.

Extracting the people-related helpers into tmdb-credits.ts keeps
tmdb-merge.ts under the line cap.

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

* fix(tmdb): keep the aggregate ids the arrival check depends on

Trimming the cached cast to its top 40 broke the property it was supposed
to preserve: `known` is built from the aggregate ids, so a returning actor
billed below the cut read as a new arrival on the cached path and took a
reserved slot. The same show then showed a different top ten on its second
open than on its first.

Keep the whole cast, and cut the two things nothing reads instead: the
aggregate crew, and every `roles[]` entry except the one the merge picks
(most episodes). A merge over the trimmed payload now provably returns
what a merge over the full one does — covered by a test that runs both.

Also points CLAUDE.md and the doc's module table at tmdb-credits.ts, where
the credit helpers now live.

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

* docs(tmdb): add the release note for the series-cast fix

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

* fix(tmdb): let the cache trim reuse the merge's own role choice

The trim picked the role with the most episodes; the merge picks the
NAMED role with the most episodes. TMDB uses unnamed roles for uncredited
appearances, so a member whose blank role outranked their real one lost
their character on every render after the first.

Both now call pickAggregateRole, which is the point — two copies of the
same choice are what let them drift.

Also adds a test pinning the property the earlier truncation defect broke:
the displayed cast is the cap or everyone available, whichever is smaller.
Which people make the cut at the cap is the reservation's job and is
deliberate; the count is not negotiable.

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

* docs(tmdb): state the aggregate-credits contract as TMDB actually words it

TMDB describes the endpoint in one sentence that contradicts itself: "it
does not return the newest season. Instead, it is a view of all the entire
cast & crew for all episodes belonging to a TV show." The doc and the code
comment asserted the first half as settled fact.

The union never depended on that reading — arrivals are a set difference,
so under "whole run" they are simply empty — but the comment implied an
assumption the code does not make. Say what TMDB says, note the ambiguity,
and note why either reading is safe.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 01:55:44 +02:00
4gray c0e065da17 fix(window-controls): derive window-state pushes from events so controls reappear after fullscreen (#1178)
The custom minimize/maximize/close controls stayed hidden forever after
leaving HTML-element (video player) fullscreen on Windows: window state was
polled at event time, and isFullScreen() can still report the pre-transition
value while 'leave-full-screen' fires, leaving a stale push with no later
event to correct it. The same polling on the companion flag cleared
isMaximized during fullscreen transitions and stuck the maximize/restore
glyph on the wrong icon.

attachWindowStateEvents now seeds the state once at window creation and each
event patches only the flag it names, sending a copy per push. The
enter/leave-html-full-screen variants are wired too.

Regression coverage: app-window-state.spec.ts (9 cases, 6 of which fail
against the old implementation) and an Electron E2E case that toggles HTML
element fullscreen and asserts the controls come back.
2026-07-26 01:49:08 +02:00
4grayandClaude Opus 5 6c946978b4 chore(release): drop the superseded v0.20 screenshot script (#1262)
#1261 replaced this one-off capture with a manifest-driven script, so the
v0.20 version is dead weight: hard-coded slugs, paths and output directory,
none of the fail-closed guards, and two `RegExp`-from-string constructions of
the kind CodeQL flags (one of which it flagged on the replacement before that
was rewritten to use predicates).

Removing it also drops its `tools/eslint/max-lines-baseline.mjs` entry, so the
baseline no longer carries a file that does not exist.

Not a pure dead-code deletion, and worth stating: two capabilities go with it,
neither reachable from the new pipeline — `createDesignedCopy` (title/kicker
overlays on captured frames) and `createHeroImage` (a 1600x900 canvas collage
built from three screenshots). The v0.20 assets they produced are already
committed under apps/website/public/blog/v0-20/, so nothing published breaks;
a future release wanting the same collage needs it ported deliberately rather
than resurrected here.

Docs: docs/architecture/xtream-mock-server.md now points at
capture-release-screenshots.ts and notes its mock-identity check.

Verified: no references to the removed file remain anywhere in the repo, and
`pnpm run lint` passes for all 42 projects with the shortened baseline.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 01:25:56 +02:00
4grayandClaude Opus 4.8 0b967d66d4 feat(tmdb): metadata cache panel with a clear button in settings (#1244)
* feat(tmdb): metadata cache panel with a clear button in settings

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

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

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

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

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

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

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

Four review findings, all real:

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

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

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

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

Review follow-ups on the cache panel:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 01:13:02 +02:00