docs(downloads): design missing-file recovery

This commit is contained in:
4gray committed 2026-08-01 16:29:50 +02:00
1 parent 55a009c1fe
commit 830f50dfcd
1 file changed
+264
@@ -0,0 +1,264 @@
# Download Manager File Availability and Card Cleanup
**Status:** Approved
**Date:** 2026-07-30
## Context
The Download Manager currently treats every persisted `completed` row as
ready to watch. That is only historical transfer state: the finalized file may
have been deleted or its external volume may be unavailable. The main process
checks the filesystem only after Play or Show in folder, so an unavailable
item remains in `Ready to watch` until an action fails.
The completed-library cards also repeat information that is already implied by
the section:
- every card carries an `Offline` badge under the `Ready to watch` heading;
- the source label permanently occupies the narrow metadata row.
This follow-up makes the library reflect current file availability, simplifies
the cards, and preserves source information in the existing overflow menus.
It supersedes the missing-file non-goal in the original Download Manager MVP
spec without adding a background filesystem watcher or database migration.
## Decisions
- `Ready to watch` contains only completed downloads whose finalized files are
currently available as regular, non-symbolic-link files.
- A completed row whose file is unavailable appears in `Needs attention` with
a `File missing` status.
- File availability is derived by the Electron main process and is not
persisted as a replacement download status.
- `Download again` revalidates the file and requeues the existing download row
at its retained destination when the file is still unavailable.
- The Download Manager removes the `Offline` badge from completed cards.
Provider detail pages retain their `Offline` indicator for available local
files.
- The source label moves from always-visible card/row metadata into the top of
the overflow menu.
- Existing Stalker series detail navigation remains unchanged and gains
regression coverage for both ordinary series and VOD-series routes.
## Goals
- Never describe an unavailable local file as ready to watch.
- Keep temporary volume disconnection recoverable without permanently
rewriting transfer history.
- Offer an immediate, understandable recovery action.
- Keep available members of a partially missing series playable.
- Remove redundant card chrome while keeping file size and source provenance
reachable.
- Ensure missing files cannot make detail views advertise local playback.
- Reuse existing Material components, theme tokens, queue rows, dialogs,
pending-state handling, and download destination rules.
## Non-goals
This change does not add:
- a continuous filesystem watcher;
- persisted availability columns or a download-table migration;
- automatic redownload without an explicit user action;
- folder relocation or a search-for-file workflow;
- offline metadata caching or provider-independent detail pages;
- bulk redownload for a partially missing series;
- automatic deletion of a missing download record;
- changes to provider playback or Stalker/Xtream route structure.
## Availability contract
`DownloadItem` gains a renderer-visible availability value:
```ts
type DownloadFileAvailability = 'available' | 'missing' | 'not-applicable';
```
The Electron main process decorates rows returned by `DOWNLOADS_GET_LIST` and
`DOWNLOADS_GET`:
- `available` — status is `completed`, `filePath` is present, and `lstat`
identifies a regular non-symbolic-link file;
- `missing` — status is `completed`, but the path is absent, inaccessible, a
directory, or a symbolic link;
- `not-applicable` — the download is not completed.
The value is computed for each list request and never written into SQLite.
This distinction matters for removable volumes: reconnecting a volume and
refreshing the list restores `available` without repairing database state.
Filesystem inspection stays in the trusted main process. Renderer code does
not receive a new arbitrary-path probe and cannot request checks for unmanaged
files.
## View-model partitioning
The pure Download Manager view model partitions rows in this order:
1. `queued`, `downloading`, and `paused` enter `Downloading now`;
2. `failed` and `canceled` enter `Needs attention`;
3. `completed + missing` also enters `Needs attention` with a derived
missing-file presentation;
4. only `completed + available` enters `Ready to watch`.
Missing completed items retain their persisted `completed` status. The
attention-row view model carries a distinct presentation reason so the queue
component does not pretend that the transfer itself failed.
For series:
- available episodes continue to group by `(playlistId, seriesXtreamId)` in
`Ready to watch`;
- each unavailable episode appears as a concrete `File missing` row in
`Needs attention`, because recovery operates on one finalized file;
- if every episode is unavailable, the series has no ready card;
- a partially available series card reports only its currently available
members and aggregate available bytes.
Filter and search semantics remain unchanged. Missing VOD rows match Movies,
missing episode rows match Series, and source name remains searchable even
though it is no longer always visible. Category counts reflect the entities
the page renders; each missing episode is an attention entity.
## Missing-file row
The existing queue-row component renders a derived missing-file variant:
- poster, title, episode label when applicable, and tracked size;
- amber `File missing` status;
- restrained, token-based muted treatment without reducing text contrast
below the normal queue-row contract;
- `Download again` as the primary recovery action;
- overflow menu containing source provenance, Copy URL, and Remove from
manager.
Artwork and title may still open provider details when provider navigation is
available. They never attempt local playback. Play and Show in folder are not
rendered for a missing item.
If the source playlist is no longer available, the existing source-missing
navigation behavior remains authoritative. Local recovery can still use the
stored download request metadata.
## Download-again flow
Recovery uses an explicit main-process command rather than overloading the
existing failed-download Retry contract:
1. The renderer sends the managed download ID, never a caller-selected file
path.
2. The main process reloads the row and requires persisted status
`completed`.
3. It rechecks the finalized path.
4. If the file has reappeared, it performs no network request, returns a
successful recovered result, and causes the renderer to refresh.
5. If the file is still unavailable, it requeues the existing row using the
retained destination and existing request URL/header metadata.
6. Existing destination reservation, no-overwrite, authorization, partial
cleanup, queue serialization, broadcast, and structured-error rules remain
authoritative.
The retained destination follows the existing retry/resume ownership rule:
changing the preferred download folder does not relocate a previously owned
row. If the retained directory itself is unavailable, recovery returns a
structured failure and the row remains in `Needs attention`.
The item participates in the existing pending-ID set, so repeated clicks are
disabled until the operation settles. Successful requeueing moves it to
`Downloading now` after the backend broadcast. A failed Play or Reveal caused
by a file deleted after the last list load triggers a list refresh, so the row
moves to `Needs attention` immediately after that failure.
## Completed-card cleanup
Ready cards keep the shared cover-size and content-grid contracts.
- Remove the `Offline` artwork badge from movie, grouped-series, and
standalone-episode cards.
- Remove the source label and separator from the permanent metadata row.
- Keep tracked size visible.
- Keep movie Play and Show in folder actions unchanged for available files.
- Put a small non-interactive `Source` label and the resolved playlist title at
the top of the movie overflow menu, followed by Copy URL and Remove from
manager.
- Give grouped-series cards an overflow menu with the same source header and
an action to open downloaded episodes. Per-episode file actions remain in
the existing series dialog.
- Put source provenance at the top of missing-file and other queue-row overflow
menus instead of keeping it in the row metadata.
The source header truncates long values, exposes the full value through
accessible text or tooltip, and uses existing typography/color tokens. It is
informational rather than a disabled Material menu item, so screen readers do
not announce it as an unavailable command.
## Detail playback
`DownloadsService.isDownloaded()` and `getDownloadedFilePath()` require
`fileAvailability === 'available'`. Therefore:
- an available completed item keeps the `Offline` detail indicator and local
primary Play action;
- a missing completed item does not expose local playback or identify itself
as offline on a detail page;
- provider playback remains available when the provider item is usable.
This prevents the detail page from reintroducing the same misleading state
after the item has correctly moved out of `Ready to watch`.
## Series navigation regression
Manual Electron reproduction on the current build confirmed that Stalker
series cards no longer navigate to Recently viewed:
- a VOD-series download opened the canonical Stalker `vod/vod` detail route;
- an ordinary series download opened the canonical Stalker `series/series`
detail route.
The implementation therefore changes no routing production code unless a
failing regression test reveals a separate case. Focused tests must prove that
both series shapes call the canonical detail target and never navigate to a
recent route.
## Error handling and refresh
- A filesystem inspection error is treated as `missing` and does not crash or
reject the entire list.
- A recovery race in which the file reappears is resolved in favor of the
existing file; IPTVnator never overwrites it.
- A deleted file detected by Play or Reveal shows the existing file-not-found
feedback and refreshes the list.
- A network, folder, or queue error during Download again uses the existing
action-error snackbar and leaves the item in `Needs attention`.
- Removing a missing entry removes only the manager row and any owned partial;
there is no finalized media file to delete.
## Testing
The implementation follows red-green TDD and adds coverage at the closest
ownership boundaries:
- Electron event tests for list decoration, regular-file validation,
unavailable paths, reappeared-file races, and Download again;
- `DownloadsService` tests for the availability-aware local-playback contract
and refresh after file-action failure;
- pure view-model tests for missing movies, fully missing series, partially
missing series, search, filters, counts, and input immutability;
- queue component tests for File missing presentation, actions, source menu,
pending state, and accessibility;
- library component tests proving Offline/source removal and source placement
in movie and series menus;
- container tests for recovery wiring and file-not-found refresh;
- Stalker navigation regressions for both canonical series detail shapes;
- Electron E2E covering an unavailable finalized file moving to
`Needs attention`, a successful Download again transition, and available
cards remaining in `Ready to watch`;
- affected project tests, lint, Electron web build, release-note validation,
and manual light/dark Electron inspection.
## Documentation and release note
The implementation updates `docs/architecture/download-manager.md` to make
filesystem-derived readiness canonical and adds a user-facing release note.
The original MVP spec remains historical context; this approved follow-up is
the source of truth for missing-file presentation and card metadata cleanup.