mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
19 KiB
19 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 exposes the global /workspace/downloads page, source-scoped route
variants, 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. - Derived file readiness and recovery
DOWNLOADS_GET_LISTandDOWNLOADS_GETinspect completed destinations on every read. Only regular, non-symbolic-link files are reported as available; missing paths, directories, symlinks, and inspection errors are reported as missing without changing the persistedcompletedtransfer status.DOWNLOADS_REDOWNLOAD_MISSINGaccepts only the managed row id, rechecks the file, and returns a recovered result without network access when it has reappeared. Otherwise it conditionally claims the completed row, preserves its owned destination, re-applies the stored header allowlist and URL safety policy, and enqueues a fresh transfer without overwriting an existing file. - IPC surface
The backend exposesDOWNLOADS_*handlers for list retrieval, start/pause/resume/cancel/retry/missing-file recovery/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)DownloadsService.downloadsis 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. TheonDownloadsUpdatebroadcast triggers a new global load. - Series season queueing
SeasonDownloadCoordinatorowns synchronous, per-identity pending reservations and submits an individual episode or selected-season snapshot throughDownloadsService.startDownload(). Season batches are sequential and best-effort: one candidate failing does not stop later candidates. After accepted submissions, one authoritative list refresh closes the pending-to- queued handoff, and the coordinator returnsadded,skipped, andfailedcounts. Xtream and Stalker adapters own provider URL, request header, and metadata preparation; the coordinator owns only provider-neutral orchestration. Both providers use normalizedepisode.idas the canonical episodextreamId; StalkeroriginalCmdandoriginalIdparticipate only in URL resolution. The exact(playlistId, contentType, xtreamId)identity is authoritative. Complete(playlistId, seriesXtreamId, seasonNumber, episodeNumber)coordinates are a legacy episode-compatibility fallback; ambiguous or conflicting matches fail closed, while restarting an eligible legacy row migrates it to the canonical id. 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 row-less episodes are eligible; a completed-missing row is restarted as a fresh download.DOWNLOADS_STARTremains the only start IPC and may return the stablereason: 'already-in-progress'for an active match, which the coordinator counts as skipped. There is no batch IPC, schema migration, 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.tsanddownload-library.viewmodel.ts) derives the current route scope, search/category filtering, queue partitions, stable ordering, counts, and tracked byte total without mutating service state.queued,downloading, andpausedrows form the active surface;failedandcanceledrows form the attention surface. Acompletedrow whose derived file availability is missing also enters attention with a dedicated recovery reason; only available completed rows enter the offline library. Completed episodes with a validseriesXtreamIdare grouped by(playlistId, seriesXtreamId), ordered by season and episode, and represented by one poster card. Episodes without a usable series id remain standalone cards so they never disappear. - Downloads workspace (
libs/portal/downloads/feature) keeps orchestration inDownloadsComponent, async mutations in the component-scopedDownloadManagerActionsService, source-route resolution inDownloadLibraryNavigationService, and rendering in the presentationalDownloadQueueComponent,DownloadLibraryComponent, andDownloadedSeriesDialogComponent. Presentational children emit typedDownloadItemActionvalues and do not inject the download service, router, dialogs, or snackbars. The fixed header and filter row sit above one vertical scroll owner. The queue uses compact progress rows with status text and icons; completed movies and grouped series reuse the portal's canonical content grid. Both completed cards and their loading skeleton consume the global--cover-grid-min-width/--cover-gaptokens, so the Small, Medium, and Large cover preference behaves like it does elsewhere in the app. All surfaces use the existing--app-*and Material system tokens. Ready cards do not repeat an Offline badge; source provenance is available at the top of their overflow menus. The global workspace download shortcut displays the service's active count, while the page badge and filter counts reflect the current route scope. - Honest interactions
Every asynchronous item command owns a pending id until the IPC result
settles, preventing duplicate dispatch without optimistically changing a
status. Service failures surface through the established snackbar path.
Removing a completed entry explicitly says the finalized media file remains
on disk. Removing a paused, failed, or canceled entry says retained partial
data is deleted. “Clear finished” communicates both outcomes and preserves
playlist scope when the page is opened under a source route. VOD and episode
detail views continue to render a paused download as an active Resume button
(
DownloadsService.isPaused()/resumeDownloadByContent()). Artwork and titles on completed movie and grouped-series cards open the focused offline detail for that download; the explicit card Play action still starts the local file. A legacy standalone episode without a usable series id stays directly playable because there is no reliable series detail to build. Missing completed rows showFile missingunder Needs attention withDownload again; Play and Show in folder are withheld. A file-action race that returnsFile not foundrefreshes the authoritative list and returns the user from focused details to the manager.
Focused offline details
- Ready movies and grouped series open a local-focused detail view rather than a provider catalog page. A movie exposes local Play and Show in folder. A series projects only completed episode rows whose finalized files are still available, grouped into seasons; every episode Play and Show in folder action targets that row's local file. The provider's other seasons and episodes are deliberately absent from this view.
View in portalresolves an exact category/item route for Xtream. For Stalker it accepts a matching recently-viewed snapshot only when its raw movie/regular-series/VOD-series markers agree with the download type, so overlapping movie and series ids cannot select the wrong item. An exact numeric category stored in the download snapshot wins over the recent collection's virtualvod/seriescategory. Without a matching recent shape, only a movie with that exact numeric category can form a metadata-only target; episodes and legacy movies without one leave the handoff unavailable. The normal provider detail opens in one-shotprovider-onlypresentation: provider content and playback remain available when that host resolves them, while local, Offline, and download actions are hidden.- A completed row that is no longer locally available is never rendered as a ready offline detail. Direct or stale detail URLs return to the manager; if navigation fails, the detail shell shows the missing-file error with Back and Retry. Invalid or removed download ids render a focused not-found state.
- Movie and episode downloads capture a versioned, display-only metadata snapshot at start time from the already-rendered Xtream or Stalker detail, including any TMDB fields already present. The snapshot keeps provider identity/category separately from presentation metadata and lets the offline view render even when the source portal is unavailable.
- A grouped series selects its newest valid parent snapshot, then fills only missing parent metadata from older valid member snapshots. Newer values, per-episode metadata, language, and freshness identity remain authoritative. Each episode row still uses its own stored episode metadata.
- Legacy rows and sparse, stale, or wrong-language snapshots are backfilled when focused details open: row metadata supplies a safe local fallback, provider data is merged when it can be resolved, and opt-in TMDB enrichment uses the same app language and merge rules as provider details. Successful refreshes persist to the representative row; transient failures keep safe improvements without falsely marking the snapshot fresh, and concurrent refreshes are request-ordered so only the latest generation may write.
- Snapshot artwork accepts only safe HTTP(S) image URLs. Renderer normalization
and main-process persistence independently reject credential-shaped path and
query keys, including
username, so provider credentials cannot be cached in offline metadata.
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
- The global workspace is
/workspace/downloads. Source-scoped variants are available under both portal flavors:/workspace/xtreams/:id/downloadsand/workspace/stalker/:id/downloads. All three routes render the same global store; the:idroutes derive a view-only playlist scope. - Each scope exposes the same focused child route:
/workspace/downloads/:downloadId,/workspace/xtreams/:id/downloads/:downloadId, and/workspace/stalker/:id/downloads/:downloadId. The workspace shell treats all three as focused content: route search is disabled and the context panel isnone, so provider categories are not shown beside a local-only item. - The manager persists its selected All/Movies/Series/In progress filter in
the current route query with
replaceUrl. Opening a focused item then keeps that exact scoped URL as its validated return target, so Back restores the manager's scope, search query, filter, and browser-history position. - 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, the offline-detail metadata snapshot, and Xtream identifiers. Downloads are locally owned records rather than playlist children: deleting a source retains its rows and local files, keeps them visible in the global library, and disables provider handoff until that source exists again. Startup rebuilds older tables that still carry the playlist foreign key, while additive columns use the idempotent column migrations. - Download-list and focused-detail reads verify completed files with asynchronous
lstatprobes in the main process. One shared probe queue permits at most four filesystem calls at once and coalesces only in-flight checks for the same path; it does not cache completed results, so external file deletion is visible on the next refresh without letting frequent progress broadcasts block Electron's main thread. - 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 manager's search and All/Movies/Series/In progress filters affect visible queue and library entities only. The tracked-byte summary deliberately ignores search/category filtering while honoring route scope, so hiding a card never makes its disk footprint appear to vanish.
Keeping the backend queue, IPC handlers, shared schema, and renderer signals synchronized minimizes drift between platform rules and the UI. Future work might cover recordings, queue reordering, bulk pause/cancel actions, disk-free space telemetry, or playback analytics.