mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
80af9257a074d840927c748ebc42680c9edbf20c
635
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
80af9257a0 |
refactor(portals): share external-button and position-writer logic (#1298)
The Xtream and Stalker VOD detail views each carried a private copy of two behaviours: deriving the Play/Stop button state from the active external (MPV/VLC) session, and throttled persistence of the inline player position. A Play button or a resume point that behaves differently per portal is the kind of divergence users notice, so both now read from one implementation. Extracts `createExternalPlaybackButtonState` and `createInlinePlaybackPositionWriter` into portal/shared/util, and lifts the Stalker VOD download errand into its own helper. Behaviour is unchanged; the shared helpers are deliberately identical to the copies they replace. This also brings both hosts back under the 400-line ESLint limit, neither of which was baselined: vod-details.component.ts 389 -> 333 stalker-catalog-detail.component 394 -> 325 vod-details-playback.service.ts 345 -> 275 Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a2fafcfc08 |
test(performance): add end-to-end Xtream benchmark harness (#1300)
* docs(performance): plan Xtream benchmark * feat(xtream-mock-server): add deterministic 100k fixture * style(xtream-mock-server): apply repository formatting * fix(xtream-mock-server): harden performance fixture data * feat(xtream-mock-server): add performance control plane * docs(performance): correct Xtream capture plan * fix(xtream-mock-server): harden performance controls * fix(xtream-mock-server): harden control lifecycle * feat(performance): add Xtream preload markers * feat(performance): trace Xtream main phases * feat(performance): mark Xtream store publications * feat(performance): trace Xtream database phases * feat(performance): trace Xtream delete cancellation * feat(performance): capture Xtream phase attribution * feat(performance): mark Sources Xtream refresh * test(performance): define Xtream benchmark evidence contracts * test(performance): add Xtream benchmark runner * test(performance): surface failure evidence writes * test(performance): align database read clock * test(performance): preserve capture failure contracts |
||
|
|
bfad82c26c |
fix(settings): stop settings silently reverting on restart (#1272)
Settings live in the renderer's IndexedDB, and two failure modes made them look saved while nothing reached disk. A second app instance sharing the same userData directory cannot take the Chromium storage lock, so its renderer reads defaults and every write is dropped. The app now holds a single-instance lock and focuses the running window instead of starting a rival copy. The lock is requested after the userData override so E2E runs with their own data dir keep independent locks, and after Squirrel event handling. IPTVNATOR_ALLOW_MULTIPLE_INSTANCES=1 opts out for local CDP debugging. updateSettings() patches in-memory state before persisting and the submit path had no rejection handler, so a failed write produced an unhandled rejection and no user-visible feedback. SettingsStore now records which half of the round trip failed, and the settings page surfaces it through a dismissible error snackbar; the dialog stays open on failure so the save can be retried. Two follow-ups from review, both wider than the report: - a second launch now re-creates the main window when the lock owner has none left, so closing the last window on macOS no longer leaves a second launch quitting silently with nothing on screen - App.onMainWindowCreated() re-runs window-owned bindings for every rebuilt window, so the downloads broadcaster stops holding a destroyed window. This also fixes the same bug on the pre-existing dock `activate` path. Closes #1156 Closes #102 |
||
|
|
24f0dee6f0 |
test(performance): add formal M3U import benchmark (#1287)
* test(performance): add formal M3U import benchmark * test(performance): harden formal capture validity * test(performance): address benchmark review feedback |
||
|
|
e55d55b47f |
feat(mock-data): add shared screenshot-safe poster catalog (#1271)
Moves the fictional movie catalog into `libs/shared/marketing-fixtures` so the
Xtream and Stalker mocks describe the same titles, and adds 20 rendered posters
plus the shared fixture types behind them.
Supporting changes made while getting it green:
- `shared-marketing-fixtures` is classified Tier B in the coverage policy. Not
Tier A: it is fictional fixture data, so a statement percentage over it means
nothing, and a Tier A entry would pull it into the merged coverage map and the
ratchet. Tier B still runs its spec in CI. `stalker-mock-server` needs no entry
of its own — it is already Tier C and the Tier B/C runner falls back to
`pnpm nx test <project>`, so its new `marketing-poster-url.spec.ts` runs.
- Two release-capture defects the catalog reorder introduced, both fixed in
`tools/release/capture-app-driver.ts`:
- VOD stream ids are `MARKETING_VOD_STREAM_ID_BASE + index` and the generator
now lists the showcase movies first, so 62000-62002 became Black Harbor, The
Paper Astronaut and Summer Static while the dashboard seeding still mapped
those ids to the previous titles' backdrops.
- the raw `tsx` spawn of the Xtream mock lacked `--tsconfig
tsconfig.base.json`, so the mock could not resolve
`@iptvnator/shared/marketing-fixtures` and the capture never started. Both
mock projects' own serve targets already passed the flag.
|
||
|
|
0579d253c4 | fix(electron): fail closed on unknown performance completions | ||
|
|
f9e71a6d8d | fix(electron): isolate refresh performance correlation | ||
|
|
e3ce60a35e | chore(electron): add preload performance markers | ||
|
|
08b868d6c1 |
test(electron): harden runtime boundary coverage (#1267)
Adds contract-focused regression coverage for the Electron HTTP server, remote-control events, settings events, and managed download paths, and makes Tier A coverage fail closed when instrumentation fails or a runtime-owning production file disappears from a project or from the merged Istanbul report. The old `coverage:ci` exited 0 despite a `Failed to collect coverage` diagnostic: libs/m3u-state/src/lib/effects.ts was simply absent from the merged map. All 30 Tier A reports are now required, the merged map covers 710 files, and effects.ts is reported as 0/159 instead of silently disappearing. Also fixes remote static-file path containment for encoded, malformed, NUL, POSIX and Win32-style traversal inputs, with behavior-preserving testability seams. Statements 69.27% -> 69.54%; http-server.ts 0% -> 90.21%, remote-control.events.ts 0% -> 96.55%, settings.events.ts 59.25% -> 96.29%. |
||
|
|
1ab82b04a1 |
refactor(deps): drop uuid for a shared crypto-based id helper (#1266)
Supersedes #1252 and #872. uuid 14 is ESM-only, apps/web/jest.config.ts only kept v9 working by mapping `^uuid$` at a `wrapper.mjs` that v14 no longer ships, and the specifier also has to be synced in libs/shared/m3u-utils/package.json or @nx/dependency-checks fails lint. All four call sites only used `v4()`, so the dependency goes away instead. `createRandomId()` prefers `crypto.randomUUID()` and falls back to building the same v4 shape from `crypto.getRandomValues()` — that fallback is load-bearing, because randomUUID is only exposed in secure contexts and the self-hosted PWA is regularly served over plain http on a LAN address. getRandomValues stays available there, and it is what uuid's own v4 used. `@types/uuid` goes too; it only existed for the untyped v9 package. |
||
|
|
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 |
||
|
|
f147d4fe37 | perf(m3u): stop cancelled refresh workers (#1268) | ||
|
|
3032cfa88d | fix(m3u): avoid persisting hydrated favorites (#1232) | ||
|
|
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>
|
||
|
|
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> |
||
|
|
d76b2d2a57 |
fix(tmdb): stop a broken provider tmdb_id from suppressing enrichment (#1239)
* fix(tmdb): stop a broken provider tmdb_id from suppressing enrichment
Providers ship dead and stale tmdb_id values, and enrich() trusted them
unconditionally:
parseProviderTmdbId(query.tmdbId) ?? await resolveIdBySearch(...)
A garbage-but-integer id short-circuited the title search entirely. The
details fetch then 404'd, the outer catch swallowed it, and the item was
left permanently unenriched — no plot, no cast, no artwork — for a title
the search would have matched. Failed detail fetches cache nothing, so
the wasted request repeated on every re-open. The stale-but-valid case
was worse: it never threw, nothing sanity-checked the resolved title, and
we confidently rendered another film's metadata.
enrich() now treats the provider id as a hint. If it fails to resolve, or
resolves to something whose title matches none of the search variants we
would have queried, the confidence-gated title search gets its turn — and
proven-bad ids are negative-cached (7d, language-independent row) so the
404 is not repeated forever.
Deliberately NOT a hard rejection on title mismatch: TMDB returns titles
in the REQUEST language, so a Russian provider title legitimately fails
the name check against an en-US payload. A mismatch only lets the search
compete; when the search finds nothing confident, the provider payload is
kept. The change can therefore only add enrichment, never remove it.
Extracts the search resolution and the bad-id cache into
TmdbIdResolverService — tmdb-enrichment.service.ts was at 290 lines
against the 300-line target, and the resolver is independently testable.
Tests: new tmdb-enrichment.service.spec.ts covers the happy path issuing
exactly one details call and no search, 404 fallback, stale-id override,
the keep-the-payload safety property, bad-id skip, and the no-match case;
matcher spec covers detailsMatchProviderTitle and the namespaced cache key.
Refs docs/architecture/tmdb-roadmap.md A1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tmdb): only blame a provider id when TMDB confirms it does not exist
Review found the bad-id negative cache too eager in two ways, both of
which could deny enrichment to items whose id was fine.
1. Any failure recorded the verdict. A 401, 429, 5xx or an offline blip
would mark a perfectly valid id as dead for seven days, so after the
service recovered — or the user fixed their API key — titles that the
search cannot resolve confidently stayed unenriched until the marker
expired. TmdbApiService now throws a typed TmdbApiError carrying the
status, and only a confirmed 404 is recorded.
2. Title mismatches were recorded too. That id EXISTS; it is merely wrong
for this item. The row is keyed by id alone and shared across
playlists, so a stale mapping on one item disabled the direct lookup
for every other item that legitimately used the same id. Mismatches
are no longer cached at all — the search verdict is cached anyway, so
the repeat cost is a single details fetch.
Documents the row kind in the cache contract, which listed only two of
the (now six) lookup_key shapes.
Tests: 404 records, 429 does not, network error does not, mismatch does
not.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tmdb): keep provider details when the competing search fails
detailsForProviderId only runs the search to see whether it can beat a
title-mismatched provider payload. A throw from that best-effort search
(offline, rate limit, 5xx) propagated to enrich()'s outer catch and threw
away details we already had — the searched-details fetch right below it
was already tolerant. Fail to the details in hand instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tmdb): decide a suspect provider id on evidence, not on the title
The title check alone was both too weak and too dangerous.
Too weak: normalizeTitle strips trailing years, so "Blade Runner 2049"
carrying the 1982 film's id matched and the wrong film was rendered —
exactly the stale-id case this was meant to catch.
Too dangerous: an ALL-CAPS leading token reads as a language tag, so
"IT - Chapter Two" normalizes to "chapter two". The correct payload
failed the name check, and a year-less search for "chapter two" would
confidently return the 1979 film and overwrite it. Master trusted the
provider id here and got it right.
assessProviderId weighs both signals: title or year agrees means use the
details; both years known and incompatible means the search may take
over; a title-only mismatch is inconclusive and keeps the details. The
search branch now always has a year, so its own gate corroborates
whatever it returns instead of matching on name alone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tmdb): do not search after a transient provider-id failure
enrich() reads a null from detailsForProviderId as "the id is unusable,
try the search". A 401/429/5xx/offline failure gave it that null, so an
outage turned into a second request that would fail too — and if it did
come back, a title match replaced a provider id that was probably fine.
Only a 404 falls through to the search now; everything else rethrows and
leaves the id retryable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(tmdb): add the release note for the provider-id fix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
6bdd6fd8a8 |
fix(playlists): serialize per-playlist collection writes to prevent lost updates (#1255)
* fix(playlists): serialize per-playlist collection writes to prevent lost updates All per-playlist mutations (portal favorites, recently viewed, playlist meta/favorites updates) used an uncoordinated read -> patch -> replace-whole-row pattern, so two overlapping mutations on the same playlist were last-write-wins and silently dropped each other's changes (flagged by Greptile on PR #1253). Chain every read-modify-write through a per-playlist promise queue (Map<playlistId, Promise>) inside defer(), covering both the SQLite upsert and IndexedDB update paths while keeping the Observable-based public API, laziness, and emitted values unchanged. A failed mutation does not wedge the queue, and different playlists are not serialized against each other. Regression coverage: overlapping favorite+recently-viewed adds, two rapid favorite adds, IndexedDB-path overlap (all three fail on the old code), plus queue-continues-after-error and cross-playlist independence guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playlists): close serialization gaps flagged by review bots Greptile P1 (writers bypassing the queue): route updateManyPlaylists and updatePlaylistPositions through the same per-playlist write queue. Auto-refresh batch writes now re-read the stored row inside the queue and preserve user-owned fields (favorites, recently viewed, position) instead of writing a pre-refresh snapshot over them; position updates re-read and patch inside the queue on both storage paths. Codex P1 (callers precompute stale snapshots): add an atomic PlaylistsService.transformPlaylistFavorites(playlistId, transform) that applies the favorites transform to the freshly-read row inside the queue, and convert every read-then-set call site to it: UnifiedFavoritesDataService M3U add/remove/clear/reorder and Stalker reorder/clear, GlobalFavoritesService M3U removal, DashboardDataService M3U removal. Reorders now keep concurrently added favorites (appended after the dragged order) instead of dropping them. New coverage: overlapping favorites transforms, auto-refresh batch write vs queued favorite add, position update vs queued favorite add, and a public addFavorite race through UnifiedFavoritesDataService; existing specs updated to the transform-based contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playlists): share the canonical refresh merge with the auto-refresh batch The batch path previously spread the stale refresh snapshot over the freshly read row and pinned only favorites/recently-viewed/position, so a queued metadata mutation (hiddenGroupTitles, curated EPG sources) finishing before the refresh write could be reverted. Extract updatePlaylist's merge into mergeRefreshedPlaylist() and use it for both the single-playlist update flow and updateManyPlaylists: refresh-owned data (parsed content, count, EPG detection) comes from the payload, user-owned state comes from the current row, and manual/disabled EPG configuration is resolved through resolvePlaylistEpgSourceState instead of being overwritten. Regression test: queued hiddenGroupTitles meta update overlapping an auto-refresh batch write keeps both the metadata change and the refreshed content, including preserved manualEpgUrls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playlists): let the current row decide autoRefresh during batch refresh The batch path force-set autoRefresh: true after the merge, so disabling auto-refresh while a refresh was in flight was reverted by the completing write. Drop the override — mergeRefreshedPlaylist already prefers the current row's autoRefresh over the snapshot — and add a count fallback to the snapshot value for rows without a stored copy. Regression test: disabling auto-refresh concurrently with the batch write keeps autoRefresh false while still applying the refreshed content. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9885178f32 |
fix(stalker): refresh stale embedded-series snapshots from favorites and dashboard (#1253)
Favorites and recently-viewed rows store Stalker items as full JSON snapshots, so a vclub-style embedded series[] episode list froze at the moment the row was written: a series favorited when only episode 1 was out kept showing one episode forever when opened from favorites, recents, Continue Watching, or any dashboard rail. New withStalkerSnapshotRefresh() store feature renders the stored snapshot immediately and re-fetches the item from the portal in the background via a title search (get_ordered_list&type=vod&search=..., matched by id, paginated up to 5 pages, wildcard-category retry), patching fresh episodes and cmd into the active selection. The patch is guarded on both the item id and the active playlist id, since Stalker ids are only unique per portal. Only the in-memory selection is patched — the stored snapshot row is deliberately left alone, because every entry path into the detail view runs this refresh and writing it back would add an uncontrolled background writer to the whole-playlist read-modify-write that every favorite/recent mutation performs. Also fixes the stalker-mock-server embedded-series scenario, which generated series[] as objects the app's vclub adapters filter out instead of the episode-number arrays real portals send. Regular type=series and Ministra is_series items are unaffected; Xtream is unaffected (get_series_info is never cached). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4d63f76407 |
perf(stalker): skip wasted series-seasons request for non-series items (#1241)
`setSelectedItem` mirrored every selection's id into `selectedSerialId`, and `serialSeasonsResource` fires a `get_ordered_list&type=series&movie_id=<id>` portal request on every change of that id. Opening any Stalker detail page — plain VOD, vclub items with embedded `series[]` (whose result `mapRegularSeriesSeasons` discards), Ministra `is_series` items, and ITV channel clicks — therefore issued a pointless request, on every entry path (browse, favorites, recent, dashboard, search). Set `selectedSerialId` only when `selectedContentType === 'series'`, clearing it otherwise. The gate is deliberately on content type alone, not item shape: under the `series` content type `serialSeasonsResource` is the only episode source (the detail templates render `<app-stalker-series-view />` with no `vodWithSeries` input, and `isVodSeries()` requires content type `vod`), so gating on `is_series`/`series[]` would leave a series-section item carrying either field with a silently empty episode list. Adds selection-state and request-level regression coverage, and corrects a stale invariant in the Stalker architecture docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8e1320cb34 |
feat(tmdb): series production-status chip and person death dates (#1240)
Two fields TMDB already sends us and the merge threw away — no new API calls, no cache-key bump, they light up on existing cached payloads. Series detail views (Xtream and Stalker) gain a production-status chip: "Ended" tells you a show is finished before you commit to it, "Returning" that it is not. TMDB returns `status` as an ENGLISH string even under language=ru-RU, so it is normalized to a stable token (normalizeSeriesStatus) and rendered through translated labels (seriesStatusLabelKey). Unknown values are dropped rather than shown, so a status TMDB adds later can never leak raw English into 19 locales. Person pages render `deathday`, which mapPersonProfile has always parsed into ActorProfile and no template ever read. i18n: 7 keys across all 19 locales via the tools/i18n workflow. Tests: status normalization (token mapping, case-insensitivity, the British "cancelled" spelling, unknown/missing dropped). Docs: tmdb-metadata-enrichment.md, CLAUDE.md. Refs docs/architecture/tmdb-roadmap.md C1 and the zero-extra-call tier. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4ca2b6852e |
feat(playback): Up Next episode rail for the inline series player (#1231)
* feat(playback): Up Next episode rail for the inline series player On wide windows the inline series player now docks left and fills the leftover stage column with a Netflix-style "Up Next" rail: the rest of the current season plus next-season spillover, the playing episode highlighted, and watch-progress bars from playback positions. Clicking an episode plays it inline through the host's existing episode flow (Xtream serial-details and Stalker series view). - New app-up-next-rail component + buildUpNextRailItems() util in ui/playback; entries carry the host's raw episode object so selection needs no id lookup. - PortalInlinePlayerComponent measures the theater stage with a ResizeObserver and docks the rail only when the leftover beside the 16:9 player is >= 320px; narrower stages keep the centered theater/ambient behavior from #1223. Movies and live never show the rail. - New playerUpNextRail setting (Settings > Playback, default on, built-in web players only), mirroring playerAmbientMode; enforced at runtime for non-web engines. - i18n: SETTINGS.PLAYER_UP_NEXT_RAIL(+_DESCRIPTION) and PORTALS.UP_NEXT in all 18 locales. - The rail renders as an opaque panel on top of the stage, so the ambient fill stays behind it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): address Greptile review on the Up Next rail - Stage overflow: `.player-shell__viewport` had no border-box sizing (the repo has no global reset), so the docked-rail modifier's 12px padding widened the stage past its container and the right edge was clipped. - Width gate: compute the width the rail actually receives (stage minus the docked layout's padding, the height-driven 16:9 player, and the flex gap) instead of raw stage slack, and observe the stage's border box so the modifier's own padding cannot feed back into the measurement. - Stalker lazy seasons: Ministra VOD-series seasons hold no episodes until opened, so the rail's next-season spillover stopped at the current season. Prefetch the following season while an episode plays inline. Adds regression coverage for the gate boundary, gate stability across the padding toggle, and the lazy-season prefetch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(stalker): stop the rail spillover prefetch from retrying forever A failed or genuinely empty Ministra season resets isLoading while leaving episodes empty, so the prefetch effect re-requested the same season on every emission for as long as inline playback continued. Remember which seasons this view already requested and ask at most once each. Regression test asserts the empty-response case fetches exactly once and does not retrigger on further playback in the same season. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(stalker): let a failed spillover prefetch recover on the next episode The previous guard was permanent, so a transient network or authorization failure disabled the rail's next-season prefetch for the component's lifetime. Distinguish the two outcomes instead: - Answered (even with zero episodes) — a real answer, never asked again. - Failed — the claim is released, but pinned to the episode that triggered it, so the retry waits for the next playback change. Retrying immediately would loop, since the failure itself flips isLoading and re-runs the effect. The claim is taken synchronously; awaiting first let the isLoading flip re-run the effect and fire a duplicate request before the answer arrived. `loadEpisodesForSeason` now reports whether the portal answered; existing callers ignore the result and are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b94f40dc74 |
feat(i18n): add Hungarian translation (hu) (#1236)
Integrate the community-contributed Hungarian translation by Tibor Hermann (@htibcsike) as the 19th locale: - add apps/web/src/assets/i18n/hu.json (1,142 of 1,175 keys translated; 32 keys added after the contribution fall back to English, plus the new LANGUAGES.HUNGARIAN endonym) - register HUNGARIAN = 'hu' in the Language enum, SUPPORTED_LANGS, Angular date locale registration, and the TMDB language map (hu-HU) - add LANGUAGES.HUNGARIAN = "Magyar" to en.json and all other locales via tools/i18n/fill-missing.mjs - update README.md and CLAUDE.md language counts to 19 Closes #1192, refs #1140. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f852bc7459 |
fix(playlists): report failed playlists in the startup auto-refresh toast (#1235)
The startup auto-refresh always opened the `HOME.PLAYLISTS.AUTO_REFRESH_UPDATE_SUCCESS` snackbar, even when the backend had dropped playlists it could not refresh. Isolating per-playlist failures (#1233) means the result set is lossy by design, so an unreachable source that now fails within `PLAYLIST_FETCH_TIMEOUT_MS` produces a false success toast. `autoUpdatePlaylists()` now returns `AutoUpdatePlaylistsResult` — the refreshed playlists plus one outcome per requested playlist (`updated` / `failed` / `skipped`), in request order — on top of the existing bounded-concurrency refresh. The renderer derives the message from those outcomes: - all updated -> `AUTO_REFRESH_UPDATE_SUCCESS` (unchanged) - some failed -> `AUTO_REFRESH_UPDATE_PARTIAL` (error styling, dismissable) - some failed and some skipped -> `AUTO_REFRESH_UPDATE_PARTIAL_WITH_SKIPPED` - none updated -> `AUTO_REFRESH_UPDATE_FAILED` (error styling, dismissable) - only sourceless playlists left over -> `AUTO_REFRESH_UPDATE_SKIPPED` Playlists with neither a URL nor a file path are reported as skipped rather than failed, since there is no source to refresh them from. The mixed failed+skipped message exists because the plain partial text names only updated/total/failed, which would leave the skipped playlists as an unexplained remainder. Titles of unresolved playlists are logged for diagnosability. Tests: five new `electron.service` cases (one per message branch), outcome assertions across the existing `playlist-auto-update` and `playlist.events` specs, and an Electron E2E that restarts the app against a killed playlist server — verified to fail against the old unconditional toast. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b4c0cce741 |
fix(backup): export and restore hidden Xtream categories by real xtream IDs (#1224)
Category rows crossed the DB-worker IPC boundary with Drizzle's camelCase property names while the renderer contracts declare snake_case, so backup export dropped hidden-category IDs and restore degraded to a type-only match that hid every category. Project category ops to the declared wire shape, normalize restore state from untrusted sources (dropping entries without a numeric xtreamId), reject entries with missing user-state collections, and add full export→import round-trip coverage (unit manifest-equality + Electron e2e) plus regression tests. Closes #1017 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bd07e17857 |
feat(m3u): DASH + ClearKey playback via Shaka Player (#1225)
* feat(m3u): extract ClearKey DRM from #KODIPROP playlist lines Adds the typed ChannelDrm model (shared interfaces) and a KODIPROP post-processing step in createPlaylistObject() — the single funnel for all four playlist import paths. Parses inputstream.adaptive.license_type, license_key and drm_legacy; ClearKey keys accepted as kid:key hex pairs, W3C ClearKey license JSON, or a plain kid→key JSON map. Unsupported license types (Widevine/PlayReady/license URLs) are preserved with supported=false so playback can surface a DRM diagnostic instead of failing silently. Also adds isDashStreamUrl/isDashChannel helpers for DASH routing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(playback): add Shaka DASH source engine with ClearKey support Introduces ShakaVideoSession (libs/ui/playback/src/lib/shaka-engine/): a lazily imported shaka-player engine (separate lazy chunk, ~217 KB transfer) owning attach/configure/load with an operation queue and generation guard against channel-switch races. Channel ClearKey config maps to drm.clearKeys; channels with an unsupported license type emit a DrmOrEncryption diagnostic without starting an engine. Shaka errors are classified into the existing playback diagnostics (PlaybackDiagnosticSource.Shaka). Wires the engine into both built-in players like hls.js/mpegts.js: - HTML5: extension === 'mpd' branch in playChannel(); hls/mpegts/native glue extracted to helpers to keep the component within the size budget - ArtPlayer: customType 'mpd' in ArtPlayerSourceSession (+ getDrm seam) - Shared controls: WebVideoControlsSource kind 'shaka' + WebVideoShakaControls using the Shaka 5 text model (selectTextTrack(null) hides subtitles; Player.setTextTrackVisibility no longer exists) Adds a CJS shaka-player jest stub (video.js precedent) for web specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(m3u): route DASH channels to the inline Shaka-capable player DASH (.mpd) channels always play in a built-in web engine (radio precedent): external MPV/VLC cannot receive KODIPROP ClearKey configuration (VLC upstream #29465) and Video.js has no DASH bridge yet. - shouldShowInlinePlayer() bypasses the external-player setting for DASH - new shouldAutoLaunchExternalPlayer() guard consolidates the MPV/VLC auto-launch conditions in the m3u-state effects (incl. catch-up path) - the M3U page overrides the player for DASH channels: ArtPlayer stays ArtPlayer, everything else falls back to the HTML5 player - ChannelDrm is passed through ResolvedPortalPlayback into the synthetic player-view channel Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): add offline DASH ClearKey fixtures and e2e coverage Fixtures (apps/web-e2e/src/fixtures/dash/): ~4s VP9+Opus DASH, clear and CENC-encrypted variants with fixed synthetic ClearKey credentials. Content synthesized by ffmpeg; encryption done by Shaka Packager because ffmpeg's mp4 muxer writes senc-only metadata (Chromium needs saiz/saio) and cannot produce the subsample encryption the VP9 CENC binding requires. Generation script + README document regeneration. web-e2e (Chromium): import an M3U with KODIPROP ClearKey via raw text, verify encrypted and clear DASH actually play (currentTime advances, no diagnostic banner) and that an unsupported license type (Widevine) surfaces the DRM diagnostic. Fixtures are served through Playwright route interception with HTTP Range support; the Angular service worker is blocked since SW-routed requests bypass interception. electron-backend-e2e: the same happy path + negative against a local Range-aware fixture server — the automated proof that ClearKey EME works in the real Electron runtime (file:// secure context). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: document DASH + ClearKey playback architecture Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(pwa): extract KODIPROP DRM on the web-backend /parse import path The web-backend keeps its own playlist builder for the PWA URL-import path, so the shared createPlaylistObject() DRM hook never ran there and encrypted DASH channels imported by URL reached Shaka without keys. Apply extractDrmFromRaw() in that builder too and cover the path with a regression test. Addresses Codex review on PR #1225. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): interrupt stalled Shaka loads and destroy failed engines Two review findings on the ShakaVideoSession lifecycle: - stop()/start() now tear the current player down immediately instead of queueing the destroy behind the in-flight operation. Shaka's destroy() interrupts a pending load() (LOAD_INTERRUPTED), so a stalled manifest fetch can no longer wedge the operation chain and block the next channel start (Codex P1). - A rejected attach()/load() now destroys the failed player after emitting the diagnostic, so a non-functional engine never stays attached to the media element or exposed to the shared-controls bridge (Greptile P1). Regression tests cover both paths. The Shaka fakes are consolidated into a shared jest-free test double that mirrors the destroy-interrupts-load semantic, and the ArtPlayer source-session spec is split (fixtures + DASH cases) to stay within the max-lines lint budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(m3u): unify DASH URL detection with playback extension normalization isDashStreamUrl() used the simpler getStreamExtensionFromUrl(), so URLs the player engines classify as DASH (stream.MPD, ?ext=mpd, ?format=mpd) were not routed to the Shaka-capable inline player and lost their ClearKey metadata with Video.js or external players configured (Codex P2). The normalized getPlaybackMediaExtensionFromUrl() now lives in @iptvnator/shared/m3u-utils (re-exported unchanged from the playback lib) and both routing and engine selection share it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(lint): satisfy CI lint and CodeQL in DASH support files - replace shell-built tar/npm commands with execFileSync arg arrays in the fixture generator (CodeQL: uncontrolled shell command) - give jest stub methods explicit bodies (no-empty-function) - compact the diagnostic label switches in WebPlayerViewComponent to stay under the max-lines budget Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): tear down the Shaka engine on critical error events too A non-recoverable Shaka error emitted after a successful load left the dead engine attached to the media element and exposed to the shared-controls bridge (Greptile P1, round 2). Critical error events now destroy the player right after the diagnostic is emitted, matching the load-failure path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(m3u): honor DASH catch-up URLs and drop unusable DRM fallbacks Two Codex round-2 findings: - The inline-playback DASH gate only examined the channel URL, while the external-player guard checks the resolved catch-up URL — a replay that resolves to an .mpd manifest with MPV/VLC configured ended up with no player at all. The gate now uses the effective playback URL (activePlaybackUrl ?? channel.url). - The unsupported-DRM diagnostic advertised MPV/VLC fallback actions, but external players cannot receive the KODIPROP license config either — the diagnostic no longer recommends them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): suppress unusable external fallback for ClearKey DRM failures Runtime DRM errors on channels that carry KODIPROP ClearKey config (wrong or rotated keys) advertised MPV/VLC fallback actions, but external players never receive the license config — the fallback could only fail differently. DRM-classified diagnostics from such channels no longer recommend external players; clear channels keep the hint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(m3u): symmetric DASH inline gate and lazy DRM for pre-upgrade playlists - The inline DASH gate is now true when either the channel or the resolved catch-up URL is DASH, mirroring the external-player guard — a .mpd channel whose catch-up resolves to .m3u8 no longer ends up with no player at all. - Playlists imported before the DRM feature carry no drm field, but the raw KODIPROP block survived in the stored items; the M3U page now falls back to extractDrmFromRaw(channel.raw) at playback time, so encrypted channels work without a re-import (Channel gains raw?). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: sync the DASH/Shaka contract across agent docs Mirrors the DASH/Shaka source-engine contract into AGENTS.md and adds Shaka to the shared web-video bridge descriptions in CLAUDE.md and the player-controls contract; documents the lazy raw-KODIPROP DRM fallback for pre-upgrade playlists in the M3U architecture doc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): reset the media element for rejected DRM and widen ClearKey fallback suppression - Switching from a playing stream to an unsupported-DRM DASH channel loads no new source, but play() still ran and the un-loaded element could resume the previous stream underneath the diagnostic banner. The HTML5 player now resets the element instead of playing. - Any inline failure on a KODIPROP ClearKey channel (manifest, codec, media, network — not just DRM-category errors) is unsolvable in MPV/VLC, which never receive the license config; the external fallback hint is now suppressed for all diagnostics of such channels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): restore suppressed DASH captions when the preference re-enables The Shaka bridge dropped the auto-selected text track with selectTextTrack(null) when showCaptions was off, but did not remember it — re-enabling the preference mid-session left captions permanently off (HLS/native bridges already restore). The session now remembers the suppressed track id and reselects it via the bridge's caption-state pass; suppression is also skipped when no track is active. Covered by session and new WebVideoShakaControls specs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: retrigger CI GitHub Actions created no check suites for the last three pushes to this branch (third-party apps received the webhooks); an empty commit re-fires the push and pull_request events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(playback): split oversized Shaka session and HTML5 spec files CI lint enforces max-lines 400: extract ShakaTextTrackSuppression and the shaka-error helpers out of ShakaVideoSession, and move the DASH-specific HTML5 player test into its own spec. No behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: allow manual dispatch of the cross-platform E2E workflow GitHub stopped delivering push/pull_request events for this branch; workflow_dispatch provides a manual escape hatch (CI and build-and-make already have one). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6dc8a10d52 |
feat(playback): show media title overlay in fullscreen shared controls (#1228)
* feat(playback): show media title overlay in fullscreen shared controls In fullscreen with the shared player-controls layer, a pointer-transparent overlay at the top of the player now names the content while controls are revealed: one line for movies and live channels, two lines for series (series name + S01E03 label). The overlay follows the bar's auto-hide transition and stays hidden outside fullscreen, where page chrome already names the content. Data flows top-down: the Xtream/Stalker series detail views pass the series name via a new PortalInlinePlayerComponent.seriesTitle input (Xtream episode playback titles carry the episode name, not the series), the inline player builds the two-line form from the episode metadata label, and WebPlayerViewComponent falls back to playback.title for movies/channels while skipping raw stream-URL fallbacks. All four shared-controls hosts (HTML5, Video.js, ArtPlayer, Embedded MPV frame-copy) forward the value. To stay under the max-lines limit, scrub/timeline state is extracted into ControlsTimeline and the playback-diagnostics display helpers into web-player-view-diagnostics.utils.ts, with behavior unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: mirror fullscreen media-title contract into AGENTS.md Addresses Codex review: the Shared Player Controls section in AGENTS.md must stay in sync with the CLAUDE.md description of the new mediaTitle input, fullscreen overlay behavior, and series-title wiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
188f5c4b56 |
feat(downloads): pause and resume support for the download manager (#1147)
Adds a paused state to the Electron download manager with a full partial-file lifecycle: - Pause keeps the .part and byte progress; cancel discards them; every lifecycle stage (queued, active, mid-transfer) is pausable. - Resume continues via HTTP Range with If-Range entity validation (strong ETag / Last-Modified persisted in the new resume_validator column, idempotent migration incl. legacy-table rebuild). Non-206 answers restart from zero over the same .part; the 206 Content-Range offset is verified; responses that end before the advertised size are retained for a Range retry instead of being committed as completed. - Crash recovery converts interrupted transfers to paused, keeps queued-with-partial rows resumable, and commits finalizations that crashed before the DB update. - Destination collisions are non-destructive (retained partials finalize to the next numbered name); locked .part files never lose their DB owner across cancel/remove/restart; resume claims rows atomically and the queue dedupes ids. - Stored request headers are re-filtered through the User-Agent/Origin/Referer allowlist on read, URL-derived extensions are sanitized, resume appends never follow symlinks, and transfer errors are logged by message only. - UI: pause/resume/cancel/retry/remove surface failures in a snackbar; paused items show an active Resume button in VOD/episode detail views; translations for all 18 locales. - Runtime split into download-runtime/transfer/finalize/broadcast modules; +30 unit tests and an Electron E2E covering pause -> retained .part -> Range/If-Range resume -> byte-exact assembly. Co-authored-by: genrichh93-ui <genrichh93@users.noreply.github.com> 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
0273ded8e2 |
fix(tmdb): resolve season number from title markers for per-season series slices (#1229)
* fix(tmdb): resolve season number from title markers for per-season series slices
Providers often slice a show into per-season catalog items ("The
Mandalorian (2 season)", "Пацаны 2 сезон", "The Boys S05") and renumber
the single contained season to 1, so season enrichment fetched the wrong
TMDB season (season 1 metadata for a season 2 item).
- new season-marker.util.ts in shared/interfaces: extractSeasonFromTitle
(word-first, number-first and S-form markers, bracketed or trailing)
and resolveEnrichmentSeasonNumber (title marker wins only for
single-season items whose provider number disagrees)
- wired into Xtream enrichSerialSeasonWithTmdb and the Stalker
series-view season service (cache/overlay still keyed by provider
season key)
- SEASON_SUFFIX_PATTERN now also strips number-first season suffixes
("2 season", "2 сезон", "2-й сезон" incl. NFD-decomposed ordinals) so
such titles match the show at all
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): wait for the season map before TMDB season fetch
The TMDB match can arrive before the async season resource; fetching
then passed seasonCount 0, suppressed the title-marker override and
cached the wrong season forever (fetchSeason is idempotent). The effect
now reads the season map tracked and skips while it is empty —
overlay-driven re-runs are safe because fetchSeason early-returns per
(tmdbId, seasonKey).
Addresses Greptile review on #1229.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): reset season selection on detail-to-detail navigation
The router reuses the series view for detail-to-detail navigation, and a
retained season selection let the NEW item's tmdb_id pair with the
PREVIOUS series' season context in the TMDB fetch effect, poisoning its
idempotent per-season cache before the new season resource loaded. The
selection is now a linkedSignal keyed on the displayed item's identity —
compared inside the computation, since displayItem produces a fresh
object on every recomputation.
Addresses Greptile review round 2 on #1229.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): include the resolved season in the TMDB season cache identity
Per-season slices of one show share (tmdbId, provider season key "1")
but resolve to different TMDB seasons — plain key idempotency served the
first slice's episodes to every later slice. Each cache entry now
records the resolved season it was fetched for: a call resolving a
different season refetches and overwrites (also self-healing a fetch
made with stale navigation context), an in-flight marker dedups
concurrent runs, and a superseded fetch may not store its result.
Failed fetches stay uncached so later triggers retry.
Addresses Codex review (P1) and Greptile review round 3 on #1229.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): drop a mismatched season cache entry before its replacement fetch
If a replacement fetch (same key, different resolved season) failed, the
previous slice's entry stayed visible indefinitely through overlay() and
descriptions(). The mismatched entry is now removed up front, so a
failed replacement falls back to provider data until a retry succeeds.
Addresses Codex review (P2) on #1229.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): title-based season reset identity and o_name marker support
- The season-selection reset identity now combines provider id and title:
distinct items can share or lack provider ids, and an id-only identity
retained the previous item's selection across such navigation
- The season marker is read from whichever title field carries it via
pickSeasonMarkedTitle: providers put the descriptive title in o_name
while name stays generic, and the show-level match already used o_name
Addresses Greptile review round 4 (P1) and Codex review (P2) on #1229.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(stalker): gate TMDB season fetch on resource coherence, not selection resets
Resetting the parent season selection on item identity change (previous
round) silently disabled enrichment after detail-to-detail navigation
between items sharing one season-key set: the season container keeps its
own selection and deduplicates seasonSelected emissions, so the parent
key stayed null forever. The reset is gone; instead the fetch effect
gates on coherence — it waits while the season resource reloads (the
window in which a reused component pairs the new item's tmdb_id with the
previous item's map) and requires the selected key to exist in the map
with episodes. Stale-snapshot fetches remain self-healing through the
resolution-aware cache.
Addresses Codex review (P2) and Greptile review round 5 on #1229.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
5aa44d19d4 |
feat(tmdb): clickable director/creator chips and directing credits on person pages (#1227)
* feat(tmdb): clickable director/creator chips and directing credits on person pages Directors were plain merged text — no photos, no navigation — while the data was already sitting in the cached TMDB payloads (credits.crew and created_by both carry id + profile_path; they just were not typed or parsed). - tmdb-merge: enrichedDirectors (crew, job === 'Director', deduped by person id) and enrichedCreators (created_by) produce the same chip shape as the cast (TmdbEnrichedCastMember) into a new tmdb_directors field on all three merges (Xtream VOD, Xtream series, Stalker); types widened (crew id/profile_path, created_by id/profile_path). - Detail views (shared VodDetailsComponent, Xtream vod/serial routes, Stalker series view) render the Director row as clickable avatar chips when tmdb_directors is present — same markup and openActor handler as the cast strip — falling back to the plain text otherwise. Stalker re-normalization allowlist preserves the new field. - Person pages: mapPersonFilmography now merges combined_credits.crew (jobs Director/Creator) into the filmography — acting wins the per-title dedup, directing-only titles show the job in the character slot. Everything else (library matching, All-portals scope, filters, search fallback, back button) works unchanged because the person page is role-agnostic. Existing caches work as-is: crew/created_by were always part of the stored payloads. Tests: merge spec (director/creator chips + crew-row dedup ×3 merges), person spec (crew credits, Producer excluded, acting-wins dedup), stalker-vod.utils passthrough. Docs updated (CLAUDE.md + architecture). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(tmdb): address director-pages review — split oversized spec, stable track keys, translated crew roles - tmdb-merge.spec.ts grew past the 400-line lint ceiling — the Stalker merge suite moved to tmdb-merge-stalker.spec.ts (fixes the CI Lint job). - All cast/director chip loops now track by TMDB person id with an index fallback ('p<id>' / 'i<index>') instead of member.name — distinct people can share a name and creator payloads carry no dedup (greptile). - Directing-only filmography credits carry the role in a new crewJob field ('Director' | 'Creator') instead of stuffing TMDB's raw English job into character; ActorViewComponent renders it through translated labels (XTREAM.CREW_JOB_DIRECTOR/CREATOR, added to all 18 locales via the i18n patch workflow, matching each locale's existing glossary — pt "Diretor", de "Regisseur") (Codex). Tests: person spec asserts character/crewJob separation; merge suites green after the split (15 + stalker file). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
db70b07093 |
feat(playback): theater stage and opt-in ambient fill for the inline portal player (#1223)
* fix(tmdb): purge obsolete search cache rows * feat(playback): theater stage and opt-in ambient fill for the inline portal player On wide-short windows the VOD/series inline player left a strip of app surface next to the video: with `width: auto`, the viewport's `max-height` transferred through `aspect-ratio` into a max-width (CSS transferred size constraints), re-clamping the stage to 16:9 and leaving the leftover outside it. - Theater stage: give `.player-shell__viewport` a definite `width: 100%` so it always fills the content row; the player renders as the largest 16:9 box that fits the stage height, centered — the leftover is always the stage's black background, never app surface (YouTube-style letterbox). Applies to every inline engine. - Ambient fill: new `playerAmbientMode` setting (default off, Settings > Playback, web players only) renders a blurred, dimmed copy of the poster behind the player, filling the letterbox margins. Enforced at runtime too: Embedded MPV never gets the extra DOM layer. Live channels and non-http(s) poster URLs are excluded. Verified live via CDP at 1720x760 (stage 1362x532, player 946x532 with symmetric 208px margins) and 1280x950 (stage exactly 16:9, no bars). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(i18n): add ambient-mode setting keys to all remaining locales The i18n drift gate requires SETTINGS.PLAYER_AMBIENT_MODE and its description in every locale; the feature commit only covered en and ru. Translated via the i18n-fill workflow (per-locale patch + mechanical merge, glossary-matched against each existing file). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(settings): include playerAmbientMode in expected default settings settings.component.spec asserts the persisted settings object with toEqual; the new default-off field has to be part of the fixture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b3e130aa65 |
feat(stalker): full Live TV channel list for complete search, count badges, and all-channels grid (#1209)
Load the complete ITV channel list once per portal (Ministra get_all_channels with a paged crawl fallback) and use it for: full local search across every channel, per-genre category count badges, an all-channels paginated grid on Live TV entry, and eager bulk EPG so row previews appear without playing a channel. Censored (adult) genres absent from get_all_channels fall back to the legacy paged flow and show no badge. Includes mock-server support, unit + E2E coverage, and architecture docs. |
||
|
|
e53adcbeaf |
fix(catchup): parse negative sub-hour XMLTV offsets correctly (#1216)
* fix(catchup): parse negative sub-hour XMLTV offsets correctly XMLTV timestamps with offsets like "-0030" were treated as UTC because the hour part "-00" numerically evaluates to -0 and Math.sign(-0) dropped the minutes' sign. Derive the sign from the offset string instead, so sub-hour negative offsets shift the catch-up start time correctly. * test(catchup): cover positive sub-hour XMLTV offsets --------- Co-authored-by: 4gray <serega05@gmail.com> |
||
|
|
d308749e2c | fix(stalker): preserve is_series episode metadata (#1218) | ||
|
|
48ff4b9cc5 |
fix(portal): remove redundant catalog type badges (#1217)
Remove redundant LIVE, VOD, and SERIES badges from homogeneous Xtream and Stalker catalog grids while preserving mixed-content badges and type-driven grid behavior. |
||
|
|
bc6e7018e0 |
fix(epg): harden three latent edges from the #1165 manual-mapping review (#1214)
* fix(epg): harden three latent edges from the #1165 manual-mapping review Follow-up to #1165 (manual EPG-to-channel mapping). Three minor but real issues flagged by the bot reviews on #1173, all in already-merged #1165 code rather than the Stalker delta: 1. EPG program dedup ignored source_url. The unique index and upsert key (channel_id, start, title) collapsed programmes imported from different XMLTV sources that shared those columns, and the upsert reassigned source_url to the last importer — so source-scoped queries could miss a programme and source-scoped deletes could drop another source's row. The key and index now include source_url (migrated via a _v2 index that drops the old source-blind one); the upsert no longer overwrites source_url. 2. Xtream getMapping fallback capped candidate streams at an unordered first five, so a mapping saved under a later stream sharing the provider epg_channel_id was silently ignored. Replaced the two-step fetch-then-lookup with a single content⋈categories⋈mappings join that finds a mapping under any matching stream, with no arbitrary cap. 3. The Xtream mapping dialog did not refresh after closing, unlike the Stalker path, so a remapped visible/selected channel kept its stale preview until the 5-minute TTL or a rescroll. Added EpgQueueService.invalidate(streamId) and a before/after mapping compare in the channel-list dialog flow that invalidates the cache and refetches the current viewport when the mapping actually changed. Tests: source-aware dedup index/upsert assertions; join-based getMapping resolution regardless of stream position; EpgQueueService.invalidate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(epg): address review feedback on the #1165 follow-up - portal-channels-list: forward the already-validated playlistId into the mapping dialog instead of re-reading currentPlaylist() after the async getEpgMapping roundtrip (which could return undefined on navigation) - EpgQueueService.invalidate(): bump a per-stream invalidation epoch and clear inFlight so a request already running when the mapping changes has its (pre-change) result discarded via an epoch check in fetchEpg, and the immediate re-enqueue can schedule a fresh mapping-aware fetch - getMapping Xtream fallback: order the join deterministically before limit(1) so the resolved mapping is stable; documented that this backend layer has no caller playlist context and is a best-effort net behind the renderer's playlist-scoped resolution Tests: in-flight staleness discard for invalidate(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(epg): split EpgQueueService invalidation specs under the max-lines limit The added invalidate() tests pushed epg-queue.service.spec.ts to 415 lines, over the 400-line ESLint cap (and the baseline must not grow). Moved them to a focused epg-queue-invalidation.spec.ts; both files are now under the limit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(epg): resolve second-order review findings on the mapping follow-up Two P2 issues Codex raised on the previous fixes: - Unscoped program lookups could return the same programme twice now that the dedup index preserves per-source rows: the M3U timeline calls getChannelPrograms without sourceUrls, so two sources sharing channel/start/title both surfaced. Added toEpgProgams(), which collapses duplicate channel|start|title slots after mapping/validation, applied at every getChannelPrograms return. - EpgQueueService.fetchEpg unconditionally cleared the in-flight marker in its finally, which could drop a marker a re-enqueued request took over after invalidate(). It now only releases the marker when the completing request still owns it (epoch unchanged), preserving per-stream dedup and concurrency accounting. Tests: unscoped duplicate-slot collapse; stale request preserving a fresh in-flight marker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(epg): deduplicate program rows in SQL before applying row limits Codex follow-up: the JS-level slot dedup ran after the SQL LIMIT, so duplicate cross-source rows consumed the cap and truncated real data. Moved the dedup into SQL with GROUP BY, applied before the limits: - selectChannelPrograms / selectLegacyChannelPrograms: GROUP BY (channel_id, start, title) before ORDER BY start LIMIT 500, so the timeline cap counts distinct programmes rather than duplicate rows - selectCurrentProgramsForChannelIds: GROUP BY channel_id before LIMIT channelIds.length, so duplicate cross-source current slots can't starve other channels of their current-programme preview The JS toEpgPrograms() dedup stays as a safety net (e.g. legacy NULL-source rows the unique index treats as distinct). Test query-chain mocks updated for the new groupBy link. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
402382421c |
feat(matching): strip appended language/quality tags in title normalization (#1211)
* feat(matching): strip appended language/quality tags in title normalization
Real-world portal catalogs duplicate one show under dozens of tagged
variants ("|ALB| Fallout", "4K-DE - The Pitt (2025) (US)",
"Breaking Bad-eng", "Fallout_esp", "The Last of Us (2023) AF"). A third
of them normalized to polluted keys, silently skipping TMDB enrichment
and staying invisible to cross-portal title matching.
normalizeTitleKeys() now handles, conservatively:
- wrapped pipe tags: "|ALB| X", "|MULTI| X"
- longer/compound leads: "EXYU| X", "4K-DE - X", "AR-SUBS - X",
"4K-OSN+ - X" (dash/pipe only; colon stays
2-3 chars so "NCIS: LA" is untouched)
- underscore suffixes: "X_eng", "(US)_msub" (single-underscore only,
"The_Last_of_Us" stays intact)
- double-dash suffixes: "X--esp"
- joined dash tags: "X-DE", "X-eng" (vocabulary-gated and
case-uniform only; "Spider-Man", "Kick-It",
"Peut-être" are untouched)
- bare trailing tags: "X (2025) DE", "Breaking Bad ES" (UPPERCASE
vocabulary only, skipped for ALL-CAPS titles;
"Rocky II", "Made in USA", "Making It" are
untouched)
Every leading-tag segment must contain a letter, so numeric titles
("1917 - ...") are never treated as tags. The display-side
stripCountryPrefix() learns the same compound/plus-sign prefixes and the
numeric guard.
buildSearchLookupKey() gets a |v2 suffix so cached negative TMDB match
resolutions keyed on old polluted titles are invalidated.
Measured on 248 real catalog names from four shows (The Pitt, Fallout,
The Last of Us, Breaking Bad): clean matching keys 65% -> 99%, display
strip 91% -> 100%. The corpora are committed as spec fixtures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(matching): use ES2015-safe trailing trim in title normalization
String.prototype.trimEnd is ES2019; the shared-interfaces lib compiles
against an older lib target (TS2550 in typecheck:web). Replace with a
regex-based trimRight helper. Jest uses its own tsconfig, so this only
surfaced in the CI typecheck, not local unit runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(matching): guard tag stripping against real-title false positives
Address code-review findings on the tag-stripping rules:
- underscore suffix is now vocabulary-gated, so "Mr_Robot",
"Cowboy_Bebop", "Mrs_Davis" keep their second word
- leading single-segment tags before a spaced dash stay 2-3 chars
(only hyphen-compounds like "4K-DE" and pipe-tags like "EXYU|" may be
wider), so "DUNE - Part Two" and "ALIEN - Covenant" are left intact
- "IN" is excluded from the weak joined-dash/underscore paths so
"drive-in" and "Plug-in" are not truncated (India still strips via
the strong "IN| " / "IN - " forms)
The display-side stripCountryPrefix() mirrors the narrowed dash rule.
Corpus coverage is unchanged at 99% (245/248); new counter-example
tests lock in the guards.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
aa1941cf2c |
fix(embedded-mpv): stop native-view video jump by moving control menus into the dock (#1207)
* fix(embedded-mpv): stop native-view video jump by moving menus into the dock Opening any control popover in the native-view embedded MPV dock used to shrink the MPV view by a 300 px bottom cutout so the popover DOM stayed clickable, which made mpv re-letterbox the video on every menu open/close. All five menus now render horizontally inside the fixed-height controls strip, so menu state never changes the native view bounds: - volume expands as an inline horizontal slider next to the mute button - audio/subtitle/speed/aspect morph the dock row into a back button, a panel title, and a scrollable chip ribbon (app-embedded-mpv-dock-panel) with wheel-to-horizontal-scroll mapping, edge fades, active-chip reveal/focus, roving arrow-key navigation, ellipsis + tooltips, and RTL-aware scrolling - boundsProvider loses the menus.anyOpen() cutout branch and the MENU_OPEN_BOTTOM_CUTOUT_PX constant is removed; HIDDEN_BOUNDS for modal overlays is unchanged - global arrow shortcuts (seek/volume) are suspended while a chip panel is open so arrows walk the chips; Esc, click-outside, and close-on-select semantics are preserved - new EMBEDDED_MPV.PLAYER.BACK i18n key in all 18 languages Regression coverage: the new dock-panels spec asserts the bounds provider returns full host bounds while every menu is open (fails against the old cutout behavior), plus panel morph/a11y/selection specs and a dedicated dock-panel component spec (keyboard, wheel, tabindex, emits). Frame-copy shared controls (app-player-controls) are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): address native-view dock review feedback Resolves the actionable P2 review comments on the dock rework: - Inline volume no longer clips the dock actions. At sidebar-constrained player widths (~480-660px, viewport wider than the 720px breakpoint) the new in-flow volume slider widened the non-shrinking actions column and, under overflow:hidden, clipped the fullscreen button. Add min-width:0 to .embedded-mpv-player__actions and __volume-group so the inline volume (a scroll container) compresses its own slider instead of pushing neighbors off-edge. Verified in Chromium: fullscreen stays visible down to 480px. - Space now selects a focused chip. onPanelKeydown stops Space/Enter from bubbling to the global shortcut handler (whose Space case preventDefault'd the button's native activation and toggled playback) without calling preventDefault itself, so the menuitemradio chip activates and emits chipSelected. Matches the WAI-ARIA menu activation-key expectation. - Simplify dock-panel opener tracking: always remember the toggled kind so focus restoration is correct if in-panel switching ever becomes reachable (currently unreachable — the toggle buttons are removed from the DOM while a panel is open); restoreOpenerFocus still no-ops unless focus fell to body. Not changed: the "closePanels no-ops when unavailable" comment — verified unreachable (chip selection closes via menus.close() directly, not through closePopovers; isAvailable() is engine-bound and the native dock only renders while it is true, with engine handoff calling menus.closeAll()). Regression test added for Space/Enter chip activation. The volume-overflow fix is CSS layout (no jsdom layout engine) and was validated in a real browser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c707af1334 |
feat(epg): manual EPG mapping for Stalker portals (#1173)
* feat(epg): manual EPG mapping for Stalker portals Extends the manual EPG-to-channel mapping (PR #1165) to Stalker: - shared key helper buildStalkerEpgMappingKey — playlist-scoped stalker:{playlistId}:{channelId} keys, mirroring the Xtream scheme - ITV sidebar: right-click context menu with "Map EPG channel"; after the dialog closes with a change, the bulk EPG cache is rebuilt so the panel and row previews reflect the new mapping immediately - withStalkerEpg().applyMappedItvEpg(): batch-resolves mappings for rendered channels (one getEpgMappingsBatch IPC per new id set) and overlays uploaded-XMLTV programs onto bulkItvEpgByChannel; overrides survive ensureBulkItvEpg reloads - stream-resolver: mapping check in loadStalkerEpgItems (detail) and batched prefetch in loadStalkerEpgBatch (previews); mappingCandidateKeys/prefetchEpgMappings generalized beyond Xtream - global favorites: stalker branch for the Map-EPG menu entry (item id extracted from the stalker::{playlistId}::{id} uid) - docs: manual-mapping section in docs/architecture/stalker-epg.md and a CLAUDE.md EPG bullet covering the whole mapping feature - tests: applyMappedItvEpg suite, stalker preview mapping in the stream-resolver spec, key-builder specs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(epg): harden Stalker mapping edges found in adversarial review - stream-resolver: stalker items resolve only the playlist-scoped mapping key — bare tvgId/name candidates (tvgId mirrors the raw provider id) could only produce false matches against unrelated M3U mappings, and previews would disagree with the detail path - applyMappedItvEpg: staleness guard after every await so an in-flight call cannot write portal A's mapped EPG into portal B's state after a playlist switch (the store is a root singleton) - applyMappedItvEpg: ids are marked checked only after a successful lookup — a transient IPC failure no longer suppresses the mapping for the rest of the session - live layout: post-dialog refresh re-applies overrides for the unfiltered channel list, so an active search cannot drop the playing channel's mapping - global favorites: new epgMappingChanged output emitted when the dialog actually changed a mapping; unified live tab reloads its EPG previews in response Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b1fc23abbb |
fix(playback): route MKV sources as Matroska (#1210)
* docs(playback): design native MKV source routing * fix(playback): route MKV sources as Matroska * chore(playback): address MKV review feedback |
||
|
|
61fb563fbd |
refactor(database): split EPG mapping schema out of oversized schema.ts (#1213)
schema.ts grew past the 400-line lint budget with the manual EPG-to-channel mapping table (#1165), which breaks lint for every PR. Move the mapping table and its types into epg-mapping.schema.ts and re-export them from schema.ts so the schema namespace and all existing imports stay unchanged. |
||
|
|
ec6fc403a3 |
feat: manual EPG-to-channel mapping with mapping fallback (#1165)
* feat(epg): manual EPG-to-channel mapping with mapping fallback in all EPG paths
* fix: epg mapping in live tv list
* fix(epg): harden manual EPG mapping — upgrade safety, perf, playlist-scoped keys
Follow-up fixes on top of the manual EPG-to-channel mapping feature:
- epg-database: dedupe existing epg_programs rows before creating the
unique (channel_id, start, title) index — a plain CREATE UNIQUE INDEX
crashed the EPG worker on upgrade when historical duplicates exist;
replace INSERT OR REPLACE with ON CONFLICT DO UPDATE so the
epg_programs_fts delete trigger is not bypassed (REPLACE skips delete
triggers unless recursive_triggers is on), with a plain-INSERT
fallback when the index cannot be created
- db: add idx_content_epg_channel — the mapping fallback scanned the
whole content table on every single-channel EPG lookup
- keys: scope Xtream mapping keys per playlist via shared
buildXtreamEpgMappingKey (xtream:{playlistId}:{id}) — bare stream ids
collide across portals; the backend fallback now joins categories to
resolve the playlist id
- pwa: hide "Map EPG channel" entries behind the supportsEpgMapping
capability — the menu item was a dead end in the PWA
- parser: parse the XMLTV offset sign from the string — Math.sign(0)
dropped the minutes of ±00:xx offsets
- cleanup: typed window.electron access instead of ad-hoc casts, drop
unused resolveChannelId and dialog data field, shared
EpgMappingDialogComponent.open() for all seven call sites
- dialog UX: minimum-characters search hint, save/remove snackbars,
current mapping shows the EPG channel display name,
takeUntilDestroyed on the search stream
- i18n: fill the new keys in all 17 locales
- tests: cover mapping CRUD/search escaping, the dedup-index guard,
offset parsing and playlist-scoped keys; update stale stream-resolver
specs for the new 50-item limit and 10s timeout
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(epg): escape backslashes in EPG channel search LIKE pattern
CodeQL js/incomplete-sanitization: a lone trailing backslash in the
search term paired with the closing wildcard under the ESCAPE clause
and corrupted the pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(epg): batch mapping lookups in the viewport preview queue
Expose the existing getEpgMappingsBatch operation over a new
EPG_MAPPING_GET_BATCH IPC channel and use it in resolveManualMappings —
the per-entry lookup issued one IPC round-trip per visible channel on
every scroll event.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perf(epg): batch mapping prefetch in the collection preview loader
Resolve all candidate mapping keys for an Xtream preview batch with a
single getEpgMappingsBatch IPC call instead of per-channel lookups.
Also fix a worker early-exit: a channel without tvgId/name returned out
of the shared-iterator loop and silently killed one of the three
concurrent preview workers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
45b6d8a041 |
fix(m3u): parse playlists with URLs longer than 2084 characters (#1204)
* fix(m3u): parse playlists with URLs longer than 2084 characters Pluto TV style playlists (issue #1189) embed a session JWT in every stream URL (~2200 chars). validator.isURL inside iptv-playlist-parser rejected anything over its IE-era 2084-char default, and the parser's stalled item index then collapsed the whole playlist into a single channel. Sync the 4gray/iptv-playlist-parser fork with upstream v0.15.2, which removes URL validation entirely and adds an explicit branch so '#' comments and unknown directives are never treated as URLs. Two fork deltas are preserved on top: the radio attribute (radio player detection) and pipe stripping (item.url is cut at the first '|' while |User-Agent=/|Referer= params still land in item.http). The now-dead validator/is-valid-path dependencies are dropped from the fork. - pin iptv-playlist-parser to the fork commit SHA - add a parser contract spec guarding long URLs, comment handling, radio, pipe stripping, and header EPG attrs - document the parser fork contract in the M3U architecture doc Fixes #1189 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(m3u): bump parser to optimized fork build Pulls the fork's optimized parse() rewrite (2.5-3x faster: 100k channels ~780ms -> ~285ms, 10k ~79ms -> ~25ms) and the README documenting fork deltas. Output is differential-verified byte-identical to the previous build; all parser-contract, unit, and import E2E suites rerun green against the new pin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(m3u): bump parser for input robustness and library hygiene Pulls the fork's real-world input tolerance: UTF-8 BOM, blank lines and whitespace before the header, and case-insensitive #EXTM3U no longer reject the playlist (all VLC-accepted forms); Node Buffers are decoded as UTF-8 and other non-string input throws a clear TypeError. Also brings truthful types (url?: string), fork metadata, an enforced 100% coverage gate, and the fork CHANGELOG. Extends the contract spec with a BOM regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(m3u): pin parser to the tagged fork release v0.15.2-iptvnator.1 Same commit as before (33f5e9c) — the readable tag replaces the raw SHA in package.json while pnpm-lock still records the immutable codeload tarball by commit. Fork release: https://github.com/4gray/iptv-playlist-parser/releases/tag/v0.15.2-iptvnator.1 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(types): make ParsedPlaylistItem.url optional to match runtime The parser fork's d.ts now truthfully declares url?: string (a trailing #EXTINF without a stream URL yields url === undefined at runtime, and always has). The local ParsedPlaylistItem mirrored the old type lie and made the production typecheck reject the parser's Playlist type. createChannel and createPlaylistObject already tolerate the absent url. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5cae310430 |
fix(logging): redact sensitive portal and Electron diagnostics (#1182)
* fix: redact sensitive log data * fix(ci): keep logging preload self-contained * fix(logging): preserve shared diagnostics * fix(logging): close trace redaction gaps * fix(logging): redact Xtream path credentials * fix(logging): close credential redaction gaps * fix(logging): suppress external player arguments * fix(logging): harden URL and date redaction * fix(logging): redact map keys and URL fragments * fix(logging): redact sensitive map values * fix(logging): redact credentials in diagnostic text * fix(logging): close remaining credential leaks |
||
|
|
b719ed23cd |
fix(playback): position embedded MPV native view correctly on scaled displays (#1206)
* fix(playback): position embedded MPV native view correctly on scaled displays Renderer bounds are measured in CSS pixels, but the native-view engines position OS windows: SetWindowPos (win32) and XMoveResizeWindow (linux) expect physical pixels, NSView setFrame (macOS) expects points. The raw values landed the video toward the window's top-left corner at 1/scale of its size on any display scale or page zoom other than 100%, windowed and fullscreen alike. The main process now converts native-view bounds (x page zoom everywhere, x display scale factor on win32/linux) with edge-based rounding; frame-copy bounds stay unscaled because the adapter owns its render scale. The session controller re-syncs bounds when devicePixelRatio changes, covering moves to a display with a different scale that keep the CSS layout identical. Closes #1145 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(playback): keep CSS bounds unrounded until native scaling Review feedback on #1206: measureBounds() rounded the CSS edges in the renderer, before the main-process CSS-to-native conversion, so fractional layout positions could drift by a pixel per scale factor (a 10.49px edge at 200% must land on 21 physical px, not 20). The renderer now sends raw getBoundingClientRect() edges and rounding happens exactly once, after scaling. Also pins process.platform explicitly in the macOS wiring test instead of relying on the suite default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2f8aee72df |
feat(xtream): add catch-up playback to favorites and recent tabs (#1166)
Enables Xtream catch-up/timeshift from the Favorites and Recent surfaces (per-playlist and global), not just Live TV, and adds start-over replay of the currently-airing programme. Carries tv_archive/tv_archive_duration through the favorites and recently-viewed DB projections and maps them onto UnifiedCollectionItem; tv_archive_duration is interpreted as days, matching live-stream-layout.controlledArchiveDays. Closes #1138. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
8fdac824fd |
feat(packaging): ship Linux embedded MPV frame-copy runtime (#1200)
* docs: design Linux frame-copy packaging * docs: plan Linux frame-copy packaging * feat(packaging): define Linux frame-copy profiles * fix(packaging): reject inherited profile names * feat(embedded-mpv): validate staged Linux runtime * fix(embedded-mpv): require Linux source packages * fix(embedded-mpv): harden Linux runtime staging * feat(embedded-mpv): build LGPL Linux runtime * fix(embedded-mpv): pin Linux runtime inputs * feat(embedded-mpv): build relocatable Linux helper * fix(embedded-mpv): require bundled Linux runtime * fix(embedded-mpv): make Linux runtime portable * feat(packaging): ship Linux frame-copy artifacts * fix(embedded-mpv): verify Linux helper linkage * fix(packaging): enforce Linux frame-copy isolation * fix(embedded-mpv): pin Linux display data * docs(embedded-mpv): document Linux frame-copy packaging * feat(embedded-mpv): probe Linux frame-copy runtime * test(embedded-mpv): smoke packaged Linux frame-copy * docs(embedded-mpv): clarify Linux system runtime baseline * fix(embedded-mpv): harden Linux runtime capability gate * ci: verify Linux frame-copy packages * test(embedded-mpv): harden packaged Linux smoke * test(embedded-mpv): preserve packaged GL mode * test(packaging): harden Linux package probes * fix(embedded-mpv): enable private Snap shared memory * fix(embedded-mpv): sanitize Linux helper environment * fix(packaging): enforce private Snap memory semantics * fix(packaging): reject ambiguous Snap memory metadata * fix(embedded-mpv): prioritize trusted Snap GL * fix(packaging): reject advanced Snap YAML semantics * fix(packaging): reject arbitrary Snap YAML aliases * feat(packaging): ship Linux runtime license notices * docs(embedded-mpv): document Linux runtime distribution * fix(packaging): parse Snap trailing comments safely * fix(release): gate Snap publish on public source release * fix(packaging): strip VCS metadata from source bundle * docs(packaging): clarify Linux source release gate * test(embedded-mpv): smoke missing bundled libmpv * style(embedded-mpv): format final validation inputs * fix(e2e): satisfy fixture index signature typing * fix(ci): declare fontconfig gperf generator * fix(embedded-mpv): hash runtime cache identities * fix(packaging): harden Linux frame-copy delivery * fix(packaging): tighten runtime delivery gates * fix(ci): decouple Linux runtime matrix * fix(packaging): harden Linux frame-copy delivery * fix(packaging): validate Linux frame-copy runtimes * fix(packaging): scope Snap Electron library checks * feat(packaging): ship Linux frame-copy runtimes * fix(packaging): improve Linux runtime smoke diagnostics * fix(packaging): expose bounded helper probe details * test(packaging): trace Snap EGL probe failures * fix(packaging): prefer core22 ABI in Snap helper * fix(packaging): bound helper probe capture * fix(packaging): harden Linux frame-copy releases * fix(packaging): canonicalize libplacebo submodule identity * fix(packaging): make source archive inspection portable * fix(packaging): harden Snap release verification |
||
|
|
643dee1be3 |
feat(xtream): resume the latest series episode (#1187)
Dashboard Continue Watching now carries the exact saved season/episode into Xtream series details and starts it at the persisted offset. Successful external MPV/VLC launches persist the launched episode and retarget the series CTA to "Play episode N". Recent-history rows keyed by an episode id resolve their parent series before navigation. Includes maintainer follow-ups: no zero-offset resume on failed position loads, seriesXtreamId-gated resume targets for legacy rows, and patch coverage raised from 76.7% to 93.9%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d61fd5db19 |
feat: add strip country prefix setting (#1162)
* feat: add strip country prefix setting
* feat: scope country-prefix stripping to live content and cover missing surfaces
- narrow the heuristic: pipes always strip, dash/colon separators only
when the prefix is a short uppercase tag ("UK - BBC One" strips,
"Sky - Sports F1" and "Mission: Impossible - Fallout" stay intact)
- fall back to the original name when stripping would leave nothing
- scope stripping to live content only: grid type (live/itv/radio),
playback isLive, external sessions without contentInfo, dashboard
cards with contentType 'live'
- cover previously missed surfaces: M3U player EPG timeline header,
M3U inline player title, radio player, dashboard live rails
- replace hardcoded settings strings with translate keys and add
SETTINGS.STRIP_COUNTRY_PREFIX(_DESCRIPTION) to all 18 locales
- add unit specs for the utility plus regression specs for
channel-list-item and external-playback-dock
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: cover strip-country-prefix call sites for codecov
- dashboard-rail: new spec for cardTitle live/movie/series scoping
- grid-list: strip enabled/disabled, VOD passthrough, 'No name' fallback
- portal-inline-player: live strip vs VOD passthrough
- unified-live-tab: timeline channel name strip + M3U name precedence
- video-player: timeline/radio/inline titles with the setting on and off
- settings-store: default false + persisted true round-trip
- settings-form.utils: new spec for form default and ?? false fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
c6ed504723 |
feat(playback): add shared picture-in-picture controls (#1199)
* docs(playback): design shared web picture-in-picture * docs(playback): keep picture-in-picture exit available * docs(playback): plan shared web picture-in-picture * feat(playback): add picture-in-picture controls contract * feat(playback): add shared web picture-in-picture * feat(playback): expose shared picture-in-picture action * docs(playback): document shared web picture-in-picture * test(playback): cover shared picture-in-picture flow * test(playback): wait for PiP video in Electron E2E * refactor(playback): extract picture-in-picture controller |
||
|
|
beb62db314 |
feat(settings): add shared web player controls toggle (#1198)
* docs(playback): design shared controls setting * docs(playback): plan shared controls setting * feat(settings): persist shared web controls preference * test(settings): harden shared controls normalization coverage * feat(settings): expose shared web controls toggle * fix(settings): label shared controls toggle * feat(playback): resolve shared controls from settings * test(playback): cover shared controls setting * docs(playback): document shared controls preference * fix(playback): await settings before host creation * fix(settings): normalize shared controls updates |