feat(downloads): queue season episode downloads (#1357)

* docs(downloads): specify season queueing

* docs(downloads): plan season queue implementation

* feat(downloads): define episode queue identity

* fix(downloads): align episode identity contract

* feat(downloads): coordinate season queue submissions

* fix(downloads): keep queue coordination provider neutral

* fix(downloads): reconcile legacy episode identities

* fix(downloads): fail closed on invalid stored coordinates

* refactor(downloads): adapt Xtream episode requests

* fix(downloads): use canonical Stalker episode ids

* test(downloads): cover Stalker adapter reactivity

* feat(downloads): add selected season queue action

* refactor(downloads): extract season download presenter

* feat(downloads): localize season queue feedback

* test(downloads): cover series batch queue flow

* test(downloads): harden series queue fixtures

* docs(downloads): describe season queueing

* docs(downloads): clarify season queue IPC contract

* fix(downloads): isolate season header build warnings

* fix(downloads): label season view toggles

* fix(downloads): preserve Xtream episode headers

* fix(downloads): fail closed on stale episode state

* fix(downloads): align renderer queue safeguards

* fix(downloads): block ambiguous episode actions

* fix(downloads): accept nullable legacy coordinates

* fix(downloads): preserve scoped episode ownership

* fix(downloads): probe restored files asynchronously

* fix(downloads): bound restored file probes

* fix(downloads): release timed out file probes

* fix(downloads): bound file probe callers

* fix(downloads): refresh stable season skips

* fix(downloads): fail closed before provider prep

* fix(downloads): preserve retained partial ownership

* fix(downloads): reconcile partial cleanup completion

* fix(downloads): await authoritative list refresh

* fix(downloads): coalesce list refreshes

* fix(downloads): preserve specials season identity

* fix(stalker): preserve specials season mapping

* fix(downloads): distinguish missing Xtream seasons
This commit is contained in:
4gray authored and GitHub committed 2026-08-03 08:53:44 +02:00
1 parent d3cc18dc72
commit 96facd6f49
79 files changed
+8352 -1255

No files matched your search

+75 -5
View File
@@ -54,11 +54,81 @@ variants, contextual buttons, and theme-aware styling.
`DownloadsService.downloads` is the renderer's authoritative **global** list.
`loadDownloads()` therefore always invokes the Electron list IPC without its
legacy optional playlist scope. Route scope, category, and search must never
replace or narrow that signal. Overlapping loads are request-ordered so a
late response cannot replace a newer snapshot. Before each fresh download or
resume the service asks the main process for an authorized folder and calls
the corresponding IPC command. The `onDownloadsUpdate` broadcast triggers a
new global load.
replace or narrow that signal. Loads are serialized: at most one list IPC is
active, and callers arriving during it coalesce behind one trailing refresh.
Each response therefore commits in request order. A caller assigned to the
trailing refresh resolves after that refresh settles even when later
broadcasts queue the following refresh, preventing both stale continuation
and starvation during frequent progress updates.
`hasLoadedDownloads` records that the latest attempt completed, including an
error, while
`hasAuthoritativeDownloadList` becomes true after a successful request,
remains true while a later refresh is in flight, and clears only if the
latest request fails. Series download actions require both loaded and
authoritative state so a failed refresh cannot restart rows missing from a
stale or empty renderer snapshot. Before each fresh download or resume the
service asks the main process for an authorized folder and calls the
corresponding IPC command. The `onDownloadsUpdate` broadcast triggers a new
global load.
- **Series season queueing**
`SeasonDownloadCoordinator` owns synchronous, per-identity pending
reservations and submits an individual episode or selected-season snapshot
through `DownloadsService.startDownload()`. Season batches are sequential
and best-effort: one candidate failing does not stop later candidates. After
added or stable duplicate submissions, one authoritative list refresh closes
the pending-to-queued/downloaded handoff, and the coordinator returns
`added`, `skipped`, and `failed` counts. Xtream and Stalker adapters own
provider URL, request header, and metadata preparation; the coordinator owns
only provider-neutral orchestration. When a reserved candidate matches a
completed-missing row, the coordinator performs one authoritative preflight
refresh before any provider preparation. If another list request is active,
the preflight joins the single trailing refresh; later download-update
broadcasts cannot delay that assigned refresh. Restored files therefore
become stable skips without requiring a Stalker URL/network request. Both
providers use normalized `episode.id` as the canonical
episode `xtreamId`; Stalker `originalCmd` and `originalId` participate only
in URL resolution. Provider adapters preserve numeric season zero, including
a fallback season key of `"0"`, so Specials keep distinct `S00` coordinates.
The exact `(playlistId, contentType, xtreamId)` identity is authoritative.
Complete `(playlistId, seriesXtreamId, seasonNumber, episodeNumber)`
coordinates are a legacy episode-compatibility fallback. Stalker also stores
an `episode_identity_scope` for regular `/series`, embedded VOD `series[]`,
and lazy Ministra VOD `is_series` origins. A known different scope is a
different episode owner; an older coordinate row without a provable scope
fails closed instead of being migrated across modes. Exact canonical legacy
rows remain authoritative. Other ambiguous or conflicting matches resolve to
the same explicit renderer conflict state rather than masquerading as a
missing row, so both the episode action and season count fail closed.
SQLite `null` and optional `undefined` coordinates both mean that a canonical
legacy row is incomplete, matching the backend resolver. Renderer-pending,
queued, downloading, and paused episodes are skipped, as are completed rows
whose file is available or whose availability is still unknown. Failed,
canceled, completed-missing, and unambiguous row-less episodes are eligible;
a completed-missing row is restarted as a fresh download. Before resetting
such a completed row, `DOWNLOADS_START` asynchronously rechecks its retained
path in the main process. A restored file returns stable
`reason: 'already-downloaded'` without mutation; active matches return
`reason: 'already-in-progress'`. The recheck has a one-second caller deadline
that starts before shared-slot acquisition; timeout or probe failure leaves
the row untouched and returns a failed submission, allowing the sequential
season loop to continue. Completed-file list callers have the same deadline
and report a timeout as missing for that snapshot. The underlying filesystem
operation remains coalesced and charged against the four-probe cap until it
actually settles, so later callers get independent bounded waits without
duplicating stalled native work. Only `ENOENT` and `ENOTDIR` are authoritative
absence; permission, I/O, and other probe errors remain unknown, so
`DOWNLOADS_START` leaves the completed row and file path untouched. Before a
completed-missing, failed, or canceled row clears its path, the same start IPC
asynchronously removes any retained `.part`. Cleanup coalesces same-path work
and allows at most four underlying unlinks. A one-second admission deadline
rejects queued work before it can mutate the filesystem; once an unlink
starts, the request awaits its authoritative result so no late side effect can
race a retry. Permission and I/O failures keep the row's ownership intact,
while `ENOENT` and `ENOTDIR` safely proceed. The coordinator counts both stable
duplicate reasons as skipped. There is no batch IPC,
parallel transfer, or queue reordering: destination authorization, persisted
header handling, and the backend's one-active-transfer FIFO semantics remain
unchanged.
- **Pure manager model**
(`download-manager.viewmodel.ts` and `download-library.viewmodel.ts`)
derives the current route scope, search/category filtering, queue partitions,
+6
View File
@@ -538,6 +538,12 @@ records. Both accept the same closed set: boolean `true`, numeric `1`, or string
Series inline playback behavior is shared across all three modes:
- Episode downloads preserve those three origins separately through the
persisted `episode_identity_scope`. Coordinate compatibility may reuse only
a row with the same proven scope. A pre-scope coordinate row is ambiguous and
blocks the action rather than binding regular, embedded, or lazy VOD content
to another mode; an exact canonical episode id still wins.
- `StalkerSeriesViewComponent` maps every mode into `mappedSeasons()` and derives the currently playing episode from `inlinePlayback.contentInfo.contentXtreamId`.
- The inline player header shows the current episode metadata below the title, for example `S01E03 - Episode title`.
- Embedded players receive previous/next episode state for the current season only.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,502 @@
# Season Download Queue Design
**Date:** 2026-08-01
**Status:** Approved
**Scope:** Xtream and Stalker series details in the Electron application
## Summary
IPTVnator will let a user enqueue several episode downloads one after another
and enqueue every eligible episode in the currently selected season. The
existing Electron transfer model remains unchanged: one active transfer and a
FIFO queue behind it.
Download availability becomes episode-specific. A pending, queued,
downloading, paused, or locally available completed episode cannot be enqueued
again, while unrelated episodes remain actionable. Failed, canceled, and
completed-but-missing episodes are eligible for a fresh download. Season batch
processing is best-effort and reports how many episodes were added, skipped,
and failed.
The shared season UI owns presentation and interaction state. A
provider-neutral coordinator owns pending identities, eligibility, sequential
submission, and result aggregation. Xtream and Stalker adapters remain
responsible for producing provider-specific download requests. No batch IPC,
transfer concurrency, or database schema change is introduced.
## Context and Confirmed Findings
- `download-runtime.ts` already processes one active transfer at a time and
appends other tasks to an in-memory FIFO queue.
- The downloads table already has a unique index on
`(xtream_id, playlist_id, content_type)`, and the runtime also rejects a
duplicate managed row id.
- `DownloadsService.downloads` is the renderer's authoritative global list and
is refreshed through the existing download update broadcast.
- `SeasonContainerComponent` currently owns the episode download controls. It
builds Xtream requests itself but emits an event for Stalker, which leaves
the two providers with different orchestration paths.
- Stalker adapters already create a stable per-episode tracking id in
`episode.id`. The current download path instead derives `xtreamId` from
`originalId` or `originalCmd`; those playback fields can be shared by
multiple episodes. That collision makes one managed download appear to own
several episode buttons.
- The backend already allows `failed`, `canceled`, and `completed` rows to be
restarted through `DOWNLOADS_START`. A completed row is eligible in this
feature only when its derived `fileAvailability` is exactly `missing`.
The work starts from `760099358` (`feat(downloads): redesign download manager
(#1313)`) on a dedicated worktree and branch.
## Goals
1. Keep download blocking and pending state scoped to one episode identity.
2. Allow several individual episode Download actions to be accepted in quick
succession.
3. Add `Download season (N)` for the currently selected, loaded season.
4. Make season enqueue idempotent across local pending state, authoritative
renderer state, backend duplicate checks, and legacy Stalker identities.
5. Continue after per-episode errors and report `added`, `skipped`, and
`failed` counts truthfully.
6. Preserve the existing FIFO transfer queue, destination authorization,
partial-file, retry, and resume semantics.
7. Give Xtream and Stalker the same shared orchestration contract while
keeping provider request construction in their respective feature areas.
## Non-goals
- Parallel transfers or configurable transfer concurrency.
- Full-series download across every season.
- Selecting or enqueueing several seasons in one action.
- Queue reordering or priority controls.
- Live recording.
- Bulk pause, resume, cancel, retry, or removal.
- A visual redesign of series details, episode cards, or the download manager.
- A new batch IPC endpoint.
## Architecture and Ownership
### Shared season presentation
`SeasonContainerComponent` remains the reusable UI owner for season selection,
episode rows/cards, and download controls. It will no longer construct an
Xtream URL or branch on Stalker markers. Instead it receives a typed
provider adapter through a signal input.
The component is responsible for:
- rendering `Download season (N)` in the existing section header;
- projecting coordinator state into individual episode buttons;
- snapshotting the selected season's visible episode order on batch click;
- invoking the coordinator for one episode or a season snapshot;
- showing a localized aggregate snackbar after batch completion.
The provider adapter contract returns a candidate for an episode. A candidate
contains a canonical episode identity and an async request factory. The
factory may resolve immediately for Xtream or perform Stalker link resolution
before returning `DownloadStartInput`.
Conceptually:
```ts
interface EpisodeDownloadIdentity {
playlistId: string;
xtreamId: number;
contentType: 'episode';
seriesXtreamId: number;
seasonNumber: number;
episodeNumber: number;
}
interface EpisodeDownloadCandidate {
identity: EpisodeDownloadIdentity;
prepare: () => Promise<DownloadStartInput>;
}
interface SeasonEpisodeDownloadAdapter {
createCandidate(
episode: XtreamSerieEpisode,
fallbackSeasonKey: string | undefined
): EpisodeDownloadCandidate | null;
}
```
The exact exported names may follow existing repository naming conventions,
but the boundary and responsibilities are fixed by this design.
### Provider-neutral orchestration
A coordinator in `libs/portal/shared/data-access` injects the existing
`DownloadsService`. This location owns provider-neutral stateful orchestration
under the repository's Nx placement rules.
The coordinator owns:
- a signal-backed set of pending episode identity keys;
- pure eligibility evaluation against the global download list;
- synchronous reservation of every eligible batch identity before the first
asynchronous request begins;
- sequential invocation of each candidate's request factory and
`DownloadsService.startDownload()`;
- best-effort continuation and aggregate result counts;
- an authoritative list refresh before successful pending state is released.
Pure identity matching and eligibility helpers belong in
`libs/portal/shared/util`. Reusable visual work stays in
`libs/ui/components`. No new project or boundary exception is required.
### Provider adapters
Xtream request construction moves out of the shared season component and next
to `SerialDetailsComponent`. It continues to use the loaded playlist
credentials, series metadata, episode extension, and metadata snapshot.
Stalker request construction moves out of the component method into a focused
adapter next to `StalkerSeriesViewComponent`. It continues to use
`fetchLinkToPlay`, portal headers, the current playlist, and the existing
metadata snapshot builder.
Both adapters use the already-normalized `episode.id` as the canonical
`xtreamId`. Stalker `originalId` and `originalCmd` remain inputs to provider
link resolution only and never determine download ownership.
### Electron backend compatibility guard
The backend keeps the current `DOWNLOADS_START` handler and transfer runtime.
The existing-row lookup gains a compatibility fallback for episode requests
that contain safe integer series, season, and episode coordinates:
1. Look up the canonical `(playlistId, xtreamId, contentType)` identity.
2. Look up an episode row with the same
`(playlistId, contentType, seriesXtreamId, seasonNumber, episodeNumber)`.
3. Use the one row found, or the same row when both lookups agree.
4. If the lookups resolve to different rows, or a canonical row carries
conflicting complete episode coordinates, fail closed as an identity
conflict without mutating either row.
5. Apply the existing status rules to the matched row.
6. When an eligible coordinate-matched legacy row is restarted, update its
`xtreamId` to the canonical value as part of the existing queued reset.
The fallback does not run for VOD rows or coordinate-incomplete legacy rows.
It does not merge or delete ambiguous data. A uniqueness conflict fails closed
without enqueueing a second runtime task.
The existing start result gains an optional machine-readable
`already-in-progress` reason for a queued, downloading, or paused match. The
human-readable error remains for compatibility. The coordinator uses the
reason rather than comparing localized or free-form error strings, so a stale
renderer snapshot is counted as `skipped`. This extends the current result
contract without adding an IPC method.
This is a duplicate-ownership compatibility fix, not a schema migration. The
unique index, download destination authorization, partial ownership, and
single-transfer queue remain unchanged.
## Eligibility and Identity Rules
An episode identity is matched against a download row by canonical identity,
then by the legacy episode coordinates described above. Matching includes the
playlist, so identical provider ids in different sources never share state.
Eligibility is evaluated as follows:
| State | Season batch | Individual episode action |
| ------------------------- | ------------ | ------------------------------- |
| Local pending | Skip | Disabled |
| `queued` | Skip | Disabled |
| `downloading` | Skip | Disabled |
| `paused` | Skip | Show existing Resume action |
| `completed` + `available` | Skip | Show existing Play local action |
| `completed` + `missing` | Eligible | Show Download again action |
| `failed` | Eligible | Show Download action |
| `canceled` | Eligible | Show Download action |
| No row | Eligible | Show Download action |
For safety, a completed row whose file availability is not yet known is
treated as blocked, not as missing. Season batch is disabled until the first
authoritative download-list load has completed. Later background refreshes do
not globally disable unrelated episode controls.
If the provider adapter cannot produce a valid candidate for an otherwise
visible episode, the episode is not included in the eligible count and is
counted as `skipped` when a batch with other eligible episodes runs. An
operation that creates a candidate but fails during provider preparation is
counted as `failed`.
## UX Specification
### Season action
The visible text button `Download season (N)` appears in the season section
header immediately before the existing Grid/List toggle. It is present only
when all of the following are true:
- the runtime supports downloads;
- download presentation is enabled for the current detail view;
- a provider adapter is available.
It is absent in the Web application and provider-only details. It is disabled
when the selected season is loading, empty, has zero eligible episodes, the
initial download list is not ready, or a season batch is already running in
that component.
`N` is the current eligible count for the selected season. It excludes every
item that would be skipped at click time. During execution the button shows a
spinner and `Adding to queue…`. The clicked season key and episode array are
snapshotted, so changing tabs does not alter the in-flight batch.
Only one season batch is initiated by a season component at a time. Individual
actions for unrelated episodes remain available and may be accepted while a
batch prepares Stalker links. The Electron backend's actual receive order
remains the FIFO order; no renderer-side transfer scheduler is added.
### Per-episode controls
The coordinator reserves an episode identity synchronously before awaiting
provider or IPC work. The corresponding Download button immediately becomes a
non-actionable pending/downloading presentation. Every unrelated episode keeps
its own state and remains clickable.
The same coordinator path handles single-item and season actions, preventing
different pending or error behavior between them. A failed operation releases
only that episode's pending key. A successful operation keeps its key pending
until the global list has been refreshed, avoiding a re-enabled gap between
the IPC acknowledgement and the authoritative queued row.
An individual success relies on the visible queued/downloading state and does
not add a redundant snackbar. An individual provider or start failure shows a
generic localized download-failed snackbar; raw backend or provider text is
not displayed.
Existing Resume and Play local actions remain unchanged. The grid and list
views expose equivalent states and actions.
### Result feedback
After every season batch, one Material snackbar announces:
- `Added N · Skipped M` when `failed` is zero;
- `Added N · Skipped M · Failed K` when at least one candidate fails.
The three counts partition every episode in the snapshotted selected season:
- `added`: `DOWNLOADS_START` accepted the item;
- `skipped`: the item had no valid provider candidate, was already blocked
locally or authoritatively, or the backend returned the stable
`already-in-progress` reason;
- `failed`: provider preparation, URL resolution, authorization, or IPC failed.
Processing never stops at the first failure. No raw URL, credential, backend
message, or exception text is rendered in the snackbar. Technical diagnostics
use the existing redacted logger.
## Data Flow
### Individual episode
1. The season component asks the adapter for a candidate.
2. The coordinator evaluates current eligibility and reserves its identity.
3. The adapter prepares the provider-specific request.
4. `DownloadsService.startDownload()` obtains the current authorized folder
and invokes the existing bridge method.
5. The main process validates the folder and remote URL, resolves canonical or
legacy ownership, persists `queued`, and calls the existing
`enqueueDownload()`.
6. The renderer refreshes the global list and hands UI ownership from pending
state to the authoritative row.
### Selected season
1. The component snapshots the selected season and asks the adapter for
candidates in the displayed episode order.
2. The coordinator classifies the full snapshot and reserves every eligible
identity before awaiting work.
3. Invalid candidates and already-blocked items are counted without invoking
provider preparation.
4. Eligible candidates are prepared and submitted one at a time in snapshot
order.
5. A preparation or start failure increments `failed`, releases that identity,
and processing continues.
6. The global list is refreshed after processing. Successful pending keys are
released only after that refresh attempt.
7. The component shows one aggregate snackbar and recomputes `N` from the new
authoritative state.
The coordinator does not mutate `DownloadsService.downloads` optimistically.
The existing Electron broadcast remains authoritative, with an explicit final
refresh closing the acknowledgement-to-list timing gap.
## Error and Race Semantics
- Two rapid clicks for the same episode race only against the synchronous
pending reservation; the second is skipped without calling the provider.
- Separate episode clicks reserve separate keys and both reach the existing
backend queue.
- Repeating `Download season` after completion skips rows now reported as
queued, downloading, paused, or available completed.
- A backend `already-in-progress` response caused by a stale renderer snapshot
counts as skipped rather than failed.
- Stalker link resolution failures affect only their candidate.
- A missing or unauthorized download folder produces failed results through
the current service/backend path; destination selection is not bypassed.
- A failed row with a retained partial follows today's `DOWNLOADS_START`
restart behavior, including its existing safe partial cleanup.
- A paused row is never restarted by the season action, preserving Resume and
retained-partial semantics.
- A missing completed row follows today's detail-page fresh-download behavior
and targets the currently authorized download folder.
- If the final renderer refresh fails, pending state is released after the
attempt so controls cannot become permanently stuck. The backend uniqueness
and runtime-id guards remain the final duplicate defense.
## Accessibility, Responsive Layout, and Themes
- The season action uses a real Material button with visible localized text,
a keyboard focus indicator, and an accessible name that includes the
eligible count.
- The progress spinner is decorative; the visible `Adding to queue…` text
communicates the state without relying on animation or color.
- Disabled controls retain the existing native/Material disabled semantics.
- Material snackbar supplies its existing live-region announcement for the
aggregate result.
- The section header may wrap at narrow widths rather than clipping the label
or shrinking the touch target.
- Styling reuses existing `--app-*` and Material system tokens. Light and dark
themes must preserve contrast, hover, focus, pending, and disabled states.
- Grid and list presentations must expose the same action names and state
semantics.
## Test Strategy
Implementation follows test-driven development. Regression tests are written
and observed failing before production changes for each behavior slice.
### Pure and service tests
- Canonical identity and legacy coordinate matching.
- Unique Stalker identities when episodes share `originalId` or
`originalCmd`.
- Eligibility for every download status and completed file-availability value.
- Unknown completed availability fails closed.
- Per-item pending reservation and release.
- Two rapid requests for one identity dispatch once.
- Requests for distinct identities both dispatch.
- Batch order, deduplication, skip counts, missing-completed behavior, and
best-effort continuation.
- Provider preparation and IPC failures produce `failed` without aborting the
batch.
- Invalid provider candidates contribute to `skipped`, so the three result
counts always total the snapshotted season size.
- A stale backend duplicate is classified as `skipped`.
- Final authoritative refresh and pending handoff behavior.
### Component and provider tests
- `Download season (N)` visibility, count, loading, empty, initial-load, and
zero-eligible states.
- Batch pending presentation and per-episode isolation in both grid and list.
- Web and provider-only views omit download presentation.
- Accessible names and native disabled state.
- Xtream URL, title, identity, coordinates, poster, and metadata snapshot.
- Stalker link resolution, headers, metadata, canonical identity, and isolated
error handling.
- The regression test demonstrates that two Stalker episodes sharing playback
identifiers no longer share download state.
### Backend tests
- Canonical exact-match behavior remains unchanged.
- A coordinate-matched legacy active row is rejected as already in progress.
- Queued, downloading, and paused duplicate results carry the stable
`already-in-progress` reason.
- Eligible legacy failed, canceled, and completed rows are reused and upgraded
to the canonical id.
- Coordinate fallback does not apply to VOD or incomplete episode requests.
- A uniqueness conflict fails without mutating or enqueueing another task.
- Existing destination authorization and runtime FIFO tests stay green.
### Electron E2E and manual verification
Extend the closest existing Electron series/download coverage to prove:
- two consecutive episode clicks produce one downloading and one queued row;
- `Download season` submits eligible episodes in display order;
- a repeated action does not create duplicate managed rows;
- the aggregate snackbar shows correct counts;
- only one transfer is active at a time.
Use the existing Xtream mock flow for the complete UI-to-queue journey. Add a
Stalker UI E2E when the existing deterministic mock exposes downloadable
episode streams without expanding fixture scope; otherwise retain Stalker
collision coverage at adapter, component, and backend integration levels and
record that reason in the final summary.
Web E2E does not require a new download workflow because the feature is
Electron-only. Component coverage explicitly verifies that Web does not render
the action.
Perform CDP/manual checks in light and dark themes at desktop and narrow
content widths, including keyboard focus, button names, disabled states, and
snackbar announcement text.
## Documentation and Release Note
Implementation updates:
- `docs/architecture/download-manager.md` with season orchestration, canonical
episode identity, legacy Stalker compatibility, and per-item pending rules;
- the relevant Download Manager description in `CLAUDE.md` so repository
bootstrap guidance remains current;
- one user-facing `.changes/downloads-<short-slug>.md` feature note following
`.changes/README.md`.
Translation keys for the season action, in-progress label, accessible name,
and aggregate result are added to every locale file using the repository's
current localization convention.
## Validation
The implementation plan must name exact targets after Nx discovery. The
expected validation ladder is:
1. focused tests for shared util/data-access, `components`, Xtream feature,
Stalker feature, `services`, and Electron backend;
2. full affected-project tests;
3. affected-project lint;
4. the atomized Electron E2E target covering the new series download flow;
5. `pnpm run release:notes:validate`;
6. light/dark and keyboard/CDP verification.
The fresh-worktree baseline already passes the nearest existing suites:
- `components`: 128 tests;
- `portal-xtream-feature` serial details: 14 tests;
- `portal-stalker-feature` series view: 16 tests;
- `services` downloads: 21 tests;
- Electron backend download requests: 20 tests.
## Acceptance Criteria
1. Starting one episode immediately blocks only that episode's Download
action; a different episode can be clicked and enqueued before the first
transfer completes.
2. The backend runs one transfer and preserves FIFO ordering for queued tasks.
3. `Download season (N)` applies only to the selected loaded season and uses
the displayed episode order.
4. Pending, queued, downloading, paused, and available completed episodes are
skipped without duplicate provider or IPC work.
5. Failed, canceled, and missing-completed episodes are eligible.
6. Batch failures do not stop later episodes and the final counts partition
the entire snapshotted season truthfully.
7. Xtream and Stalker use the same shared orchestration path while retaining
provider-specific request preparation.
8. Stalker episodes with shared playback identifiers have independent
canonical download identities.
9. Legacy Stalker download rows remain recognized by episode coordinates and
are reused rather than duplicated when eligible.
10. The season action is absent from Web and provider-only details.
11. Grid/List, light/dark, keyboard focus, and accessible names remain usable.
12. No batch IPC, transfer concurrency, queue reordering, full-series action,
or multi-season selection is introduced.