mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
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)
10 KiB
10 KiB
Download Manager Architecture
The download manager is a desktop-only feature that layers a curated queue, progress tracking, storage configuration, and playback controls on top of the existing Xtream (libs/portal/xtream) + Stalker (libs/portal/stalker) portal views. Backend work is handled in the Electron process while the Angular renderer surface exposes a dedicated /downloads route, contextual buttons, and theme-aware styling.
Backend responsibilities
- Queue control (
apps/electron-backend/src/app/events/database/download-runtime.ts)DownloadTaskmirrors a row of the shareddownloadstable (typeDownloadinlibs/shared/database/src/lib/schema.ts) plus transient cancel/pause/progress helpers (shared task types live indownload-task.ts). Request validation and row creation live indownload-requests.ts, whiledownloads.events.tsstays focused on IPC registration.enqueueDownload()pushes the task ontodownloadQueueand triggersprocessQueue().processQueue()keeps one active download, updates the row todownloading, and callsstartDownload(). The byte transfer itself lives indownload-transfer.ts, finalization and retained-partial persistence indownload-finalize.ts, and the renderer update broadcast indownload-broadcast.ts. - Range-aware transfer (
download-transfer.ts) The transfer streams the response through the backend's validated Axios redirect helper instead ofelectron-dl. Headers (user agent, referer, origin) are persisted inrequest_headersand re-applied through the same allowlist when read back on retry/resume. Active pause/cancel operations abort the current request withAbortController; pause keeps the partial file and cancel removes it. Resume checks the existing.partsize (rejecting anything that is not a regular file, so a symlink planted while paused is never followed) and sendsRange: bytes=<offset>-plusIf-Rangewith the stored entity validator. The first response's strongETag(orLast-Modified) is persisted inresume_validatorfor exactly this purpose. A206 Partial Contentanswer must start at the requested offset (Content-Rangeis verified) before bytes are appended; any other 2xx answer — the server ignoringRange, orIf-Rangedetecting that the remote file changed — restarts the transfer from byte zero over the same.partinstead 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
numbered
.partpath while leaving the final destination path absent. The selected finalfilePathandfileNameare persisted before transfer begins. When a retained download's recorded destination got occupied while it was paused or failed (for example by a file the user created), the retained.partis renamed aside and finalized to the next free numbered destination (Movie (1).mp4) instead of resolving the collision by size orunlink(). Completion creates the finalfilePathfrom the.partwithout overwriting an existing file; cancel and ordinary transfer failures remove the.part, while finalization failures and completed-partial failures deliberately retain it (the row keepsfilePathso 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 (DOWNLOADS_START) deletes the retained.partbefore the row is reset. - IPC surface
The backend exposesDOWNLOADS_*handlers for list retrieval, start/pause/resume/cancel/retry/remove operations, folder selection/reveal, and theDOWNLOADS_UPDATE_EVENTemitter that the renderer listens to in order to refresh its signal store.
Renderer architecture
- Downloads service (
libs/services/src/lib/downloads.service.ts) Signals back the current download list whilehasDownloadsandisAvailablegates UI rendering. Before each download/resume the service asks the main process for the authorized folder and calls the download IPC command. The backend extracts the file extension from the URL or falls back tomp4.onDownloadsUpdateupdates the signal, while helper methodspauseDownload,resumeDownload,retryDownload,removeDownload,cancelDownload, andplayDownloadtalk to the corresponding IPC commands so retries reuse existing rows and completed items can open the recorded path. - Downloads view (
libs/portal/downloads/feature)
A standalone page exposes the queue, desktop-only messaging, folder picker, and action buttons.downloads.component.htmlwraps the list inside a scrollable panel (downloads__list-wrapper) so long queues stay reachable, anddownloads.component.scssdrives gradient cards with theme-aware styling through Angular Material system CSS variables (var(--mat-sys-*),var(--app-*),color-mix) — theming tracks the active Material theme rather than abody.dark-themehook. Failed/canceled cards show retry/delete controls, queued/downloading cards show pause/cancel controls, paused cards show resume/cancel/delete controls, and completed cards render inline play/open buttons withmat-iconcues. Pause/resume/cancel/retry surface backendsuccess: falseresults in a snackbar instead of failing silently. The header also shows the resolved download folder and aCHANGE FOLDERaction. VOD and episode detail views render a paused download as an active "Resume" button (DownloadsService.isPaused()/resumeDownloadByContent()) rather than a disabled "Downloading" state.
Global API surface
- Preload + types
apps/electron-backend/src/app/api/main.preload.tswires every download IPC command plus theonDownloadsUpdatelistener towindow.electron. The sharedElectronBridgeApicontract inlibs/shared/interfaces/src/lib/electron-api.interface.tsowns the download and playback-position method types;global.d.tsandapps/web/src/typings.d.tsreference that contract instead of redeclaring the bridge.
Routing and navigation
/downloadsis available under both portal flavors: the Xtream routes already loadDownloadsComponent, and the Stalker routes now import the same component so the sidebar link can target/stalker/:id/downloadswithout returning to the startup screen.- Downloads navigation is data-driven:
libs/portal/shared/util/src/lib/navigation/portal-rail-links.tsemits adownloadssection link (path: [...root, 'downloads']) for both portals, so they reuse the same download page.
Queuing, persistence, and UX notes
- Every download row writes to the shared
downloadstable with statuses (queued,downloading,paused,completed,failed,canceled) plus metadata such asbytesDownloaded,totalBytes,errorMessage,requestHeaders,resumeValidator, and Xtream identifiers. Existing SQLite tables are rebuilt on startup when their status CHECK still lackspaused; theresume_validatorcolumn is added through the idempotent column migrations. - On startup,
download-recovery.tsconverts staledownloadingrows with a non-empty.partfile topaused, converts stalequeuedrows topausedwhile keeping any retained.part(a resumed download waiting behind an active one persists asqueuedwith its partial), and marks staledownloadingrows without recoverable partial bytes asfailed. - Queue cancellation removes a queued task or records an active cancellation request and aborts the request when available. Pausing follows the same abort path but persists
pausedand keeps the.part. Retries reuse the same database entry: a failed row with a retainedfilePathresumes its.partthrough HTTP Range, otherwise the retry starts from zero. Resume appends to the existing.partthrough HTTP Range withIf-Rangevalidation. - A
.partthat cannot be deleted (locked, permission denied) never loses its database path: cancel persistscanceledwhile retainingfilePathfor later cleanup, andDOWNLOADS_REMOVEkeeps the row and answerssuccess: false(surfaced as a snackbar) so retrying the remove re-attempts the deletion once the lock is released. - Resume claims the row atomically (
paused→queuedas 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 sizewhile retaining the.partandfilePath, so a retry continues via Range from where it stopped. - Retained
filePaths 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 (
downloadingrow, no partial, final file present with the recorded size) and marks itcompletedinstead 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.parton disk, theRange/If-Rangeresume request, and byte-exact assembly of the final file. - The OS downloads path is always authorized. A custom folder becomes
authorized only after native folder selection, and the main process persists
that selection under Electron
userData. Renderer settings may display the path, but they are not trusted as authorization. - The new UI leverages CSS variables for theme-specific backgrounds/borders, ensures
.downloads__listcan scroll inside its panel, and brings consistent badge/typography treatments to each card.
Keeping the backend queue, IPC handlers, shared schema, and renderer signals synchronized minimizes drift between platform rules and the UI. Future work might cover download list filters, cancel-all actions, or integration with upcoming playback analytics.