mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
fix(downloads): resume interrupted Xtream VOD transfers (#1329)
* fix(downloads): resume interrupted Xtream VOD transfers * fix(downloads): validate partials before resuming * fix(downloads): propagate headers to episode transfers
This commit is contained in:
1 parent
d44045de31
commit
aba89d64cf
29 files changed
+894
-92
No files matched your search
@@ -12,7 +12,7 @@ variants, contextual buttons, and theme-aware styling.
|
||||
- **Queue control (`apps/electron-backend/src/app/events/database/download-runtime.ts`)**
|
||||
`DownloadTask` mirrors a row of the shared `downloads` table (type `Download` in `libs/shared/database/src/lib/schema.ts`) plus transient cancel/pause/progress helpers (shared task types live in `download-task.ts`). Request validation and row creation live in `download-requests.ts`, while `downloads.events.ts` stays focused on IPC registration. `enqueueDownload()` pushes the task onto `downloadQueue` and triggers `processQueue()`. `processQueue()` keeps one active download, updates the row to `downloading`, and calls `startDownload()`. The byte transfer itself lives in `download-transfer.ts`, finalization and retained-partial persistence in `download-finalize.ts`, and the renderer update broadcast in `download-broadcast.ts`.
|
||||
- **Range-aware transfer (`download-transfer.ts`)**
|
||||
The transfer streams the response through the backend's validated Axios redirect helper instead of `electron-dl`. Headers (user agent, referer, origin) are persisted in `request_headers` and re-applied through the same allowlist when read back on retry/resume. Active pause/cancel operations abort the current request with `AbortController`; pause keeps the partial file and cancel removes it. Resume checks the existing `.part` size (rejecting anything that is not a regular file, so a symlink planted while paused is never followed) and sends `Range: bytes=<offset>-` plus `If-Range` with the stored entity validator. The first response's strong `ETag` (or `Last-Modified`) is persisted in `resume_validator` for exactly this purpose. A `206 Partial Content` answer must start at the requested offset (`Content-Range` is verified) before bytes are appended; any other 2xx answer — the server ignoring `Range`, or `If-Range` detecting that the remote file changed — restarts the transfer from byte zero over the same `.part` instead of failing the download.
|
||||
The transfer streams the response through the backend's validated Axios redirect helper instead of `electron-dl`. Headers (user agent, referer, origin) are persisted in `request_headers` and re-applied through the same allowlist when read back on retry/resume. Fresh Xtream movie and series-episode downloads propagate the playlist's configured headers, using its User-Agent when present and otherwise sharing the provider-compatible `XTREAM_CLIENT_USER_AGENT` used by Xtream API requests and stream probes. Retry, resume, and missing-file recovery resolve the owning playlist type and add that fallback to legacy Xtream rows without a stored User-Agent; known Stalker rows are left unchanged. Download rows deliberately outlive individually deleted playlists, so a headerless legacy row whose source no longer exists receives the same IPTV-player fallback because its original provider type cannot be recovered. Active pause/cancel operations abort the current request with `AbortController`; pause keeps the partial file and cancel removes it. Resume checks the existing `.part` size (rejecting anything that is not a regular file, so a symlink planted while paused is never followed). The first response's strong `ETag` (or `Last-Modified`) is persisted in `resume_validator`; only a partial carrying that validator may send `Range: bytes=<offset>-` plus `If-Range` and append bytes. A retained partial without a validator restarts from byte zero and overwrites its `.part`, so a changed remote representation can never be joined to an unverified prefix. A `206 Partial Content` answer must start at the requested offset (`Content-Range` is verified) before bytes are appended; any other 2xx answer — the server ignoring `Range`, or `If-Range` detecting that the remote file changed — restarts the transfer from byte zero over the same `.part` instead of failing the download.
|
||||
- **Destination collision policy**
|
||||
Existing destination files are never overwritten, inspected, or deleted.
|
||||
Before starting a new transfer, the backend atomically reserves a free
|
||||
@@ -23,11 +23,14 @@ variants, contextual buttons, and theme-aware styling.
|
||||
retained `.part` is renamed aside and finalized to the next free numbered
|
||||
destination (`Movie (1).mp4`) instead of resolving the collision by size or
|
||||
`unlink()`. Completion creates the final `filePath` from the `.part` without
|
||||
overwriting an existing file; cancel and ordinary transfer failures remove
|
||||
the `.part`, while finalization failures and completed-partial failures
|
||||
deliberately retain it (the row keeps `filePath` so a later retry can finish
|
||||
without re-downloading); pause and restart recovery keep it for a later
|
||||
resume. Re-downloading such a failed row from a detail page
|
||||
overwriting an existing file; cancel and non-recoverable transfer failures
|
||||
remove the `.part`, while finalization failures, completed-partial failures,
|
||||
and allowlisted network interruptions after bytes reached disk with a stored
|
||||
representation validator deliberately retain it (the row keeps `filePath`
|
||||
so a later retry can finish without re-downloading); pause and restart
|
||||
recovery keep partials, but a later retry starts over when no validator was
|
||||
available.
|
||||
Re-downloading such a failed row from a detail page
|
||||
(`DOWNLOADS_START`) deletes the retained `.part` before the row is reset.
|
||||
- **Derived file readiness and recovery**
|
||||
`DOWNLOADS_GET_LIST` and `DOWNLOADS_GET` inspect completed destinations on
|
||||
@@ -184,6 +187,7 @@ variants, contextual buttons, and theme-aware styling.
|
||||
- A `.part` that cannot be deleted (locked, permission denied) never loses its database path: cancel persists `canceled` while retaining `filePath` for later cleanup, and `DOWNLOADS_REMOVE` keeps the row and answers `success: false` (surfaced as a snackbar) so retrying the remove re-attempts the deletion once the lock is released.
|
||||
- Resume claims the row atomically (`paused` → `queued` as a conditional update) and the runtime queue rejects duplicate ids, so two rapid Resume clicks racing the status refresh can never produce two transfers for the same download.
|
||||
- A response that ends cleanly before the advertised representation size (for example a proxy that caps each response) is never committed as completed: the transfer fails with `Transfer ended before the advertised size` while retaining the `.part` and `filePath`, so a retry continues via Range from where it stopped.
|
||||
- An allowlisted mid-response network failure such as `ECONNRESET` is recoverable only when the response advertised a larger total and the `.part` contains valid incomplete bytes. This includes a validated `206` resume that drops before adding another byte. The failed row retains that partial and exposes a stable `DOWNLOAD_NETWORK_INTERRUPTED (<code>)` message without a URL; Retry continues through the same Range/If-Range validation. Pre-response failures, unknown stream errors, filesystem errors, empty fresh failures, and responses without a trustworthy total keep the generic failure path.
|
||||
- Retained `filePath`s recorded in the database stay usable after the user switches download folders — resume/retry of a retained row does not re-require the folder to be the current selection. Fresh downloads still authorize against the currently selected folder.
|
||||
- Startup recovery recognizes a finalization that crashed between creating the final file and committing the row (`downloading` row, no partial, final file present with the recorded size) and marks it `completed` instead of failing it and orphaning the file.
|
||||
- Pause/resume is covered end to end by `apps/electron-backend-e2e/src/downloads.e2e.ts`: a throttled Range-capable mock server verifies the paused `.part` on disk, the `Range`/`If-Range` resume request, and byte-exact assembly of the final file.
|
||||
|
||||
@@ -98,9 +98,17 @@ and private-network checks.
|
||||
|
||||
## User-Agent
|
||||
|
||||
Electron's `XTREAM_REQUEST` and `XTREAM_PROBE_URL` handlers
|
||||
(`apps/electron-backend/src/app/events/xtream.events.ts`) send a shared
|
||||
`XTREAM_CLIENT_USER_AGENT` constant on every outgoing request. Some Xtream
|
||||
Electron's `XTREAM_REQUEST` and stream-probe handlers plus fresh Xtream movie
|
||||
and series-episode download requests share the exported
|
||||
`XTREAM_CLIENT_USER_AGENT` fallback. A playlist's explicit User-Agent,
|
||||
Referer, and Origin are propagated to either download kind; the explicit
|
||||
User-Agent still wins over the fallback. Legacy
|
||||
download rows without a stored User-Agent receive the fallback when retrying,
|
||||
resuming, or recovering a missing completed file. Download rows intentionally
|
||||
survive individual source deletion; when the playlist row is already gone and
|
||||
its type can no longer be recovered, a headerless legacy download receives the
|
||||
same IPTV-player fallback, while a still-identifiable Stalker row remains
|
||||
unchanged. Some Xtream
|
||||
panels sit behind a WAF (e.g. Cloudflare) configured to challenge
|
||||
generic/incomplete browser-looking User-Agents while allowlisting known IPTV
|
||||
player clients; a player-style User-Agent (currently a VLC signature) avoids
|
||||
|
||||
Reference in new issue
Block a user