refactor(details): give the detail-page files real max-lines headroom (#1813)

* refactor(xtream): split the series details page into focused services

serial-details.component.ts sat at the 400-line max-lines limit and its
playback service and spec were close behind. Move cohesive concerns out
without changing behavior:

- route params, the provider-only flag and the (re)load of the addressed
  series go to SerialDetailsRouteService; the component still registers
  the effect, so effect order is unchanged
- season descriptions, posters and the TMDB season enrichment go to
  SerialDetailsSeasonsService
- actor, Similar and Discover navigation go to injectXtreamDetailNavigation,
  shared with the movie page
- the watched toggles and the episode playback payload leave
  SerialDetailsPlaybackService for SerialDetailsWatchToggles and
  buildSerialEpisodePlayback
- the spec's TestBed moves to a harness and the watched-toggle cases to
  serial-details.season-watch.spec.ts

Counted lines: component 400 -> 318, playback service 388 -> 342,
component spec 1196 -> 674.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(xtream): move VOD route selection and position state out of the page

vod-details-route.component.ts grew from 618 to 741 counted lines and
vod-details-playback.service.ts reached 395. Extract without changing
behavior:

- the route-derived read model (selected movie, category, catalog item,
  fallback view, multi-source identity, session and content keys) goes to
  VodDetailsSelectionService; the component keeps the same member names
- trailer state and the Similar-rail click move into the hero presenter,
  the cancel-download prompt and the progress-ring geometry into the
  downloads service, navigation into injectXtreamDetailNavigation
- stored positions (last seen, route row, guarded load) become
  VodDetailsPositionState and the external-launch bookkeeping becomes
  VodExternalLaunchClaim
- drop the unused MatTooltip import (NG8113) and eight unused imports

Counted lines: route component 741 -> 492, playback service 395 -> 344.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(stalker): extract series-view positions and watched toggles

stalker-series-view.component.ts grew from 1601 to 1813 counted lines and
its spec reached 1198 of 1200. Move code out verbatim:

- saved positions, their reconcile and the persist/clear writes go to the
  component-provided StalkerSeriesPositionsService; the ordering of reads
  and writes goes to StalkerSeriesPositionQueue
- episode, season and series watched toggles go to
  StalkerSeriesWatchToggleService, the batch core to
  runStalkerWatchToggleBatch
- the lazy VOD season loads go to StalkerVodSeasonEpisodeLoader

Every effect stays registered in the component constructor in its original
order; template bindings and public member names are unchanged.

The spec setup moves to a shared harness and the spillover-prefetch and
TMDB season cases to their own specs; the 184 cases of the directory are
unchanged.

Counted lines: component 1813 -> 1136, component spec 1198 -> 670.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(stalker): extract catalog-detail position and search paging

- stalker-catalog-detail.component.ts (397): the stored VOD position and
  its runtime updates become StalkerCatalogVodPosition, the Play/Resume
  start becomes startStalkerCatalogVodPlayback
- stalker-search.component.ts (776, baselined): the paging resource, the
  accumulated results and the parental-lock bookkeeping become
  StalkerSearchPagingController, with pure helpers in
  stalker-search-results.util.ts. The spec reaches the moved members
  through component.paging; its cases and assertions are unchanged.

Counted lines: catalog detail 397 -> 317, search 776 -> 482.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(ui): give season-container and vod-details real line headroom

- season-container.component.ts (396): season auto-selection and the
  seasonSelected emission move to createSeasonAutoSelectState, called at
  the same place in the constructor so effect order is unchanged; the
  episode subline becomes buildEpisodeSubline
- vod-details.component.ts (393): the cross-portal Similar loader, the
  provider-only download state and the actor/Similar route helpers move to
  sibling modules

Inputs, outputs, selectors and public members are unchanged.

Counted lines: season container 396 -> 341, vod details 393 -> 337.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(web-e2e): move Stalker portal helpers into fixtures

stalker.e2e.ts was at 1196 of 1200 counted lines. Move the mock endpoints,
scenario MACs and page helpers to stalker-portal.fixture.ts and the
embedded-series steps three tests repeated to
stalker-embedded-series.fixture.ts. Every test stays in stalker.e2e.ts in
the same order, with the same serial mode and OWNED_MACS reset.

Counted lines: 1196 -> 981.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: follow the detail-page extractions

Name the Stalker watched-toggle and position services that now own the
batch and reconcile code, point AUTH_REJECTED_MAC and the scenario MACs at
stalker-portal.fixture.ts, and drop two stale statements about components
sitting at the max-lines cap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(xtream): note why the seasons service is injected last

Its TMDB enrichment effect keeps the position it had as the first effect
of the component constructor only while it is created after the other
services' effects.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-04 16:52:53 +02:00
1 parent 7a629f5fe5
commit 2dfdd65f53
51 files changed
+5326 -3850

No files matched your search

@@ -383,15 +383,17 @@ meanwhile. The host reports busy-state back through the
A series-level counterpart lives in a `⋮` menu at the end of the same
header row (`SeasonWatchPresenter` in `libs/ui/components` owns the state
math for both scopes; the container component sits at the max-lines cap).
math for both scopes, which keeps the container component under the
max-lines cap).
`buildSeriesWatchToggleRequest` flattens every LOADED season with the same
mark/unmark semantics, and the direction is always the one the label
advertised (`markWatched: !seriesFullyWatched()`), never re-inferred from
data at persist time. Hosts route the request through the same machinery
as the season toggle — Xtream via the scope-parameterized
`SerialDetailsSeasonWatchService.handle(..., scope)`, Stalker via the
extracted `runWatchToggleBatch` core — sharing the busy flag, the
ownership guards, and the catalog-badge refresh. Stalker lazy-VOD is the
`SerialDetailsSeasonWatchService.handle(..., scope)`, Stalker via
`StalkerSeriesWatchToggleService` and its `runStalkerWatchToggleBatch`
core — sharing the busy flag, the ownership guards, and the catalog-badge
refresh. Stalker lazy-VOD is the
special case: unopened seasons have empty episode lists, so the container
reports them through the `hasUnloadedSeasons` input (blocks the
"fully watched" verdict and switches the label to its countless variant),
@@ -412,7 +414,8 @@ series-toggle hydration join one in-flight request instead of
duplicating it (a second request's failure could abort a toggle whose
original request succeeded).
The host synchronously re-runs the position reconcile
(`applyReconciledSeriesPositions` — the effect-fed maps only update on
(`StalkerSeriesPositionsService.applyReconciledSeriesPositions` — the
effect-fed maps only update on
the next change-detection tick, and enqueuing against stale maps would
miss the hydrated episodes' legacy rows), rebuilds the request from the
now-complete seasons keeping the captured direction, and reports an
+2 -1
View File
@@ -408,7 +408,8 @@ file running in parallel workers, so isolation is per-MAC rather than global:
else is talking to the server — a spec that used it would wipe a sibling
spec's session mid-test.
- `apps/web-e2e/src/stalker.e2e.ts` declares its shared scenario MACs in
`OWNED_MACS` and clears them in one batched request. The sibling specs that
`OWNED_MACS` (the constants live in `stalker-portal.fixture.ts`) and clears
them in one batched request. The sibling specs that
reach this server (`self-hosted.e2e.ts`, the `sources-pwa` helpers) own a
disjoint `00:1A:79:5F:*` range, so neither file can clear the other's state.
- Within each browser project, tests deliberately share content-scenario MACs
+2 -1
View File
@@ -484,7 +484,8 @@ the format at all, so a large share of working installations use a non-Infomir
MAC. Refusing one would stop those users adding or editing a portal that works
for them. The mock encodes the same split (`enforceMacFormat` is set only on
the strict endpoint; `/portal.php` ignores it), and `AUTH_REJECTED_MAC` in
`stalker.e2e.ts` depends on it — a non-Infomir MAC that must reach the strict
`stalker-portal.fixture.ts` (used by `stalker.e2e.ts`) depends on it — a
non-Infomir MAC that must reach the strict
endpoint and be refused _there_, not in the form.
In the edit dialog **both** passes — blur and submit — normalize only a MAC the
+1 -1
View File
@@ -257,7 +257,7 @@ Zero i18n keys. Zero UI change.
- populated in all three of `mergeVodInfoWithTmdb` (`:168`), `mergeSerieInfoWithTmdb` (`:215`), `mergeStalkerInfoWithTmdb` (`:257`)
- through `NormalizedVodMeta` (`libs/shared/interfaces/src/lib/vod-details-item.interface.ts`) + **both** normalizers in `vod-details-adapters.ts` — the single convergence point where Xtream and Stalker meet
**Component.** New standalone `app-tmdb-extras-shelf` in `libs/ui/shared-portals`. It must **not** go inline: `libs/ui/playback/src/lib/vod-details/vod-details.component.ts` is **388 lines and is NOT in `tools/eslint/max-lines-baseline.mjs`** — roughly 12 lines of headroom against the hard 400 lint cap.
**Component.** New standalone `app-tmdb-extras-shelf` in `libs/ui/shared-portals`. It must **not** go inline: `libs/ui/playback/src/lib/vod-details/vod-details.component.ts` is **NOT in `tools/eslint/max-lines-baseline.mjs`** and stays under the hard 400 lint cap only because its state lives in sibling helpers (about 340 counted lines today).
**Render** into the `detail-extras` projection slot at **four** sites (`vod-details.component.html:225`, `serial-details.component.html:201`, `vod-details-route.component.html:247`, `stalker-series-view.component.html:202`) — note Xtream `serial-details` has no trailer block today, so it either gains one or the shelf lands inconsistently. Reuse the existing nocookie iframe + `| safe` pipe so the Electron Referer shim keeps working; clicking swaps the embed `src` rather than opening a new player. Cards use `https://img.youtube.com/vi/{key}/hqdefault.jpg` (CSP verified: `img-src` covers it, `frame-src https://www.youtube-nocookie.com` covers the embed). `@if (extras().length > 1)` … `@else` the existing single-trailer markup **verbatim**.