fix(download): remove deleted/cancelled items from the Downloads list in real time (#1227)#2598
fix(download): remove deleted/cancelled items from the Downloads list in real time (#1227)#2598LeC-D wants to merge 4 commits into
Conversation
fire-light42
left a comment
There was a problem hiding this comment.
Please review our AI Policy https://github.com/recloudstream/cloudstream/blob/master/AI-POLICY.md
|
Hi @fire-light42, thanks for pointing to the AI policy. To be transparent: this fix was developed with AI assistance (GitHub Copilot / Claude for context exploration). I'm fully responsible for understanding and verifying the code — the approach directly mirrors the existing Let me know if you have any concerns about the implementation itself! |
|
Updated the PR description to include the AI usage disclosure directly in the body, as required by the AI Policy. Thanks for flagging this, @fire-light42! |
|
Rebased on upstream |
|
Thank you for your contribution! I will test and review soon. The code looks good, but getting these things right can be tricky. Do not concern yourself with merge conflicts or keeping the branch up to date, I will resolve any conflicts if necessary 👍 |
|
I can not replicate the stale download state. |
…ecloudstream#1227) Issue recloudstream#1227 reports that cancelled downloads keep showing as episode/movie cards on the Downloads page. The previous change only hooked downloadDeleteEvent in BaseFetchButton, which resets the download *button* on the result page but never touches the Downloads list itself. Observe VideoDownloadManager.downloadDeleteEvent at the activity-scoped DownloadViewModel so both DownloadFragment (headers) and DownloadChildFragment (children) drop the deleted item in real time, with no manual refresh. The handler also keeps multi-select state consistent. Uses thread-safe LiveData postValue since the event fires on the downloader's background thread. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Quick status update to clear up confusion about what this PR actually does — the original title/description undersold its scope. The code already covers the full #1227 fix (2 files, the diff hasn't changed):
Together this hooks @fire-light43 — I think the earlier difficulty reproducing came from the old description, which pointed at the button rather than the Downloads list. The #1227 symptom is on the list. Repro:
I've updated the title and description to match. Happy to attach a before/after recording if that helps. |
| // filter() returns null only while the list is still Loading; postHeaders/postChildren | ||
| // treat null as "no change", so there is nothing to update in that case. | ||
| postHeaders(_headerCards.success?.filter { it.data.id != id }) | ||
| postChildren(_childCards.success?.filter { it.data.id != id }) |
There was a problem hiding this comment.
This does not work for TV shows because _childCards are 'Loading' when this gets triggered.
…episodes vanish (recloudstream#1227) The previous real-time refresh filtered the in-memory header/child lists by id. That fails for TV shows, as the maintainer noted: _childCards is often in the Loading state when the delete event arrives (deletes triggered from outside the child screen), so filtering a null success list is a no-op. Filtering also can't update a series header when one of its episodes is deleted, since the event carries the child id, not the header id. Instead, rebuild the affected lists from disk in onDownloadDeleted. After a delete the file (and KEY_DOWNLOAD_INFO) is already gone, so getDownloadFileInfo returns null and updateChildList/updateHeaderList naturally drop the episode/movie and recompute header aggregates (count, size). updateChildList gains a pushLoading flag so the live refresh doesn't flicker the list. The application context and the current child folder are retained for the reload; currentChildFolder is cleared when the child screen closes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for taking the time to reproduce this and for the precise pointer — you were right. Filtering the in-memory lists by id was the wrong approach for TV shows, for the two reasons your comment exposed:
I've reworked Implementation notes: I retain the application Ready for another look when you have a moment 🙏 |
fire-light42
left a comment
There was a problem hiding this comment.
It should be mergeable when the comments are addressed.
If you have time it would be very nice if you could add a corresponding feature for when new downloads are started.
| val currentSelection = selectedItemIds.value | ||
| if (currentSelection?.contains(id) == true) { | ||
| _selectedItemIds.postValue(currentSelection - id) | ||
| updateSelectedBytes() | ||
| } |
There was a problem hiding this comment.
| val currentSelection = selectedItemIds.value | |
| if (currentSelection?.contains(id) == true) { | |
| _selectedItemIds.postValue(currentSelection - id) | |
| updateSelectedBytes() | |
| } | |
| updateSelectedItems { it?.minus(id) } |
Same logic, much cleaner.
There was a problem hiding this comment.
Applied in 60cfebd — collapsed to updateSelectedItems { it?.minus(id) }.
| // Refresh the child screen too, without flashing a loading state. currentChildFolder is | ||
| // cleared once the child screen closes (clearChildren), so this is a no-op afterwards. | ||
| currentChildFolder?.let { folder -> | ||
| updateChildList(context, folder, pushLoading = false) | ||
| } |
There was a problem hiding this comment.
| // Refresh the child screen too, without flashing a loading state. currentChildFolder is | |
| // cleared once the child screen closes (clearChildren), so this is a no-op afterwards. | |
| currentChildFolder?.let { folder -> | |
| updateChildList(context, folder, pushLoading = false) | |
| } | |
| postChildren(_childCards.success?.filterNot { it.data.id == id }) |
Unnecessary to save a folder or re-fetch children if you just need to delete ids.
There was a problem hiding this comment.
Applied in 60cfebd — the child list now just drops the deleted id via postChildren(_childCards.success?.filterNot { it.data.id == id }), and the pushLoading parameter on updateChildList is reverted since nothing re-fetches the folder anymore. The header list is still rebuilt from disk so series counts/sizes stay correct when an episode (child id) is deleted.
| // Nothing has been shown yet (no list ever loaded), so there is nothing to refresh. | ||
| val context = appContext ?: return | ||
| updateHeaderList(context) |
There was a problem hiding this comment.
| // Nothing has been shown yet (no list ever loaded), so there is nothing to refresh. | |
| val context = appContext ?: return | |
| updateHeaderList(context) | |
| val context = CloudStreamApp.context ?: return | |
| updateHeaderList(context) |
We use the CloudStreamApp global context here, otherwise the context variable will cause memory leaks.
There was a problem hiding this comment.
Applied in 60cfebd — now uses CloudStreamApp.context, and the retained appContext/currentChildFolder fields are gone entirely.
…mpler selection and child updates
- Use CloudStreamApp.context in onDownloadDeleted instead of retaining a
context reference in the ViewModel, avoiding any leak risk. Drops the
appContext/currentChildFolder fields.
- Collapse the selection cleanup to updateSelectedItems { it?.minus(id) }.
- Remove the deleted id from the child list directly via postChildren
instead of re-fetching the folder from disk; the header list is still
rebuilt from disk so series aggregates stay correct.
- Revert the pushLoading parameter on updateChildList, no longer needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three suggestions applied in 60cfebd (net effect: −31 lines vs the previous revision — On the follow-up idea — live-adding items when a new download starts: agreed it would round this out nicely. I'd rather do it as a separate PR than grow this one, for two reasons: it needs its own event wiring ( |
Fixes #1227.
Problem
When a download is cancelled or deleted,
VideoDownloadManager.downloadDeleteEventfires — but until now only the download button (BaseFetchButton) listened to it. The Downloads page itself never reacted, so a deleted episode/movie card stayed on screen until the user navigated away and back. That stale card is the actual bug reported in #1227 ("UI still showing episodes/movies that were deleted").Fix
DownloadViewModel— the core #1227 fix. Subscribes todownloadDeleteEventat the activity-scoped (shared) ViewModel level and removes the deleted id from the header + child lists live, so bothDownloadFragment(headers) andDownloadChildFragment(children) refresh with no manual reload. It also clears the id from multi-select state. UsespostValueonly, since the event fires on the downloader's background thread. This hooks the event "to update the UI in case of deletion from any source" (result page, queue, notification, …), as the issue asks.BaseFetchButton— resets the button viaresetView()on a matchingpersistentId; subscription wired inonAttached/DetachedFromWindow, mirroring the existingdownloadStatusEventhandling. This keeps an on-screen download button from showing a stale state after a deletion elsewhere.How to reproduce (the #1227 symptom)
I'm happy to attach a before/after screen recording if useful.