fix(downloads): preserve offline library ownership

This commit is contained in:
4gray committed 2026-08-01 16:29:51 +02:00
1 parent 81e098d52c
commit d35c255157
25 files changed
+846 -227

No files matched your search

+20 -9
View File
@@ -115,14 +115,17 @@ variants, contextual buttons, and theme-aware styling.
targets that row's local file. The provider's other seasons and episodes are
deliberately absent from this view.
- `View in portal` resolves an exact category/item route for Xtream. For
Stalker it preserves the raw mode and item shape when a matching
recently-viewed snapshot is available; otherwise it builds a regular VOD or
series target from the persisted identity and title without first proving
that the provider still serves it. The handoff opens the normal provider
detail host in explicit `provider-only` presentation: provider content and
playback remain available when that host resolves them, while local,
Offline, and download actions are hidden. Regular provider navigation does
not inherit this one-shot presentation state.
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 virtual `vod`/`series` category. 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-shot
`provider-only` presentation: 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
@@ -132,6 +135,10 @@ variants, contextual buttons, and theme-aware styling.
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
@@ -162,11 +169,15 @@ variants, contextual buttons, and theme-aware styling.
`/workspace/stalker/:id/downloads/:downloadId`. The workspace shell treats
all three as focused content: route search is disabled and the context panel
is `none`, 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.ts` emits a `downloads` section 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 `downloads` table with statuses (`queued`, `downloading`, `paused`, `completed`, `failed`, `canceled`) plus metadata such as `bytesDownloaded`, `totalBytes`, `errorMessage`, `requestHeaders`, `resumeValidator`, the offline-detail metadata snapshot, and Xtream identifiers. Existing SQLite tables are rebuilt on startup when their status CHECK still lacks `paused`; additive columns are applied through the idempotent column migrations.
- Every download row writes to the shared `downloads` table with statuses (`queued`, `downloading`, `paused`, `completed`, `failed`, `canceled`) plus metadata such as `bytesDownloaded`, `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.
- On startup, `download-recovery.ts` converts stale `downloading` rows with a non-empty `.part` file to `paused`, converts stale `queued` rows to `paused` while keeping any retained `.part` (a resumed download waiting behind an active one persists as `queued` with its partial), and marks stale `downloading` rows without recoverable partial bytes as `failed`.
- 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 `paused` and keeps the `.part`. Retries reuse the same database entry: a failed row with a retained `filePath` resumes its `.part` through HTTP Range, otherwise the retry starts from zero. Resume appends to the existing `.part` through HTTP Range with `If-Range` validation.
- 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.
+12 -7
View File
@@ -161,13 +161,18 @@ Behavior to preserve:
Download handoff behavior:
- `View in portal` preserves Stalker's inline/store-state architecture. A
matching recently-viewed snapshot is carried as `openStalkerItem` into the
normal category host, preserving regular series, embedded VOD `series[]`, or
lazy Ministra VOD `is_series=1` shape when that raw snapshot exists.
- When no matching snapshot exists, a movie download falls back to a regular
VOD item and an episode download to a regular-series item derived from the
persisted provider identity and title. This fallback is not existence-checked
before navigation and cannot reconstruct embedded or `is_series=1` mode.
type-compatible recently-viewed snapshot is carried as `openStalkerItem`
into the normal category host, preserving regular series, embedded VOD
`series[]`, or lazy Ministra VOD `is_series=1` shape. Candidate filtering
rejects live items and the opposite movie/series namespace even when ids
overlap. When the download snapshot carries an exact numeric provider
category, that category wins over the recent record's virtual `vod` or
`series` marker while the raw mode fields stay intact.
- When no compatible recent snapshot exists, only a movie download with an
exact numeric provider category can form a metadata-only VOD target. A
legacy movie without that category and every episode without a recoverable
raw series mode leave `View in portal` unavailable instead of fabricating an
unverified provider target.
- The provider-only marker is scoped to the resulting selected item. Its normal
provider host supplies the seasons, episodes, and playback it can resolve,
while the shared VOD or series UI suppresses local Offline and download
+7 -5
View File
@@ -245,11 +245,13 @@ counts only content and category deletion candidates; favorite,
recently-viewed, and hidden-category user data is timed but is not added to
that count. The matching write count uses the same deletion-candidate
definition. Playlist-delete collection counts every collected favorite,
recently-viewed, playback-position, download, content, and category ID. Its
write count adds the final playlist row. Both write spans include every
existing cooperative checkpoint, 100-row transaction, progress callback, and,
for playlist deletion, the final playlist-row autocommit; they are not exact
SQLite commit-time measurements.
recently-viewed, playback-position, content, and category ID. Download rows are
intentionally excluded: they own local offline files independently of the
source playlist and survive source deletion, with provider handoff disabled
while that source is absent. The write count adds the final playlist row. Both
write spans include every existing cooperative checkpoint, 100-row
transaction, progress callback, and, for playlist deletion, the final
playlist-row autocommit; they are not exact SQLite commit-time measurements.
Successful end markers carry only row/item counts. Error or cancellation still
closes the active phase without metadata and preserves the original error.
+11 -9
View File
@@ -369,15 +369,17 @@ The VOD-series contract is cross-surface:
Manager detail uses only locally available episode rows; it does not reuse
the provider season resource as an offline availability list.
- `View in portal` first looks for a matching recently-viewed Stalker snapshot.
When found, it hands the raw shape to the normal category/inline detail host,
so regular series, embedded `series[]`, and lazy `is_series=1` mode are
preserved with identity-scoped provider-only presentation.
- Without that matching snapshot, the episode download supplies only persisted
identity/title metadata and falls back to a regular-series item. The fallback
is not existence-checked before navigation and cannot reconstruct embedded or
`is_series=1` mode. In either path, the provider host renders whatever
seasons, episodes, and playback it can resolve while Offline/local/download
controls stay hidden. A normal Stalker item open remains unchanged.
Candidates must match both identity and the requested movie/series mode, so
overlapping provider ids cannot bind an episode to a movie or vice versa.
When found, the handoff preserves the raw regular-series, embedded
`series[]`, or lazy `is_series=1` shape; an exact numeric category from the
download snapshot replaces a virtual `vod`/`series` collection category.
- Without that compatible snapshot, only a movie with an exact persisted
numeric category can form a metadata-only VOD target. Episodes and legacy
movies without that proof leave the provider handoff unavailable rather than
inventing a regular-series or generic VOD target. When a target does resolve,
the provider host renders its content in identity-scoped provider-only
presentation while Offline/local/download controls stay hidden.
Core decision logic and normalization are centralized in: