Skip to content

Fix episodes queued in Up Next being auto-archived by per-podcast episode limit#5342

Draft
joashrajin wants to merge 4 commits into
mainfrom
fix/pcdroid-571-auto-archive-respect-up-next
Draft

Fix episodes queued in Up Next being auto-archived by per-podcast episode limit#5342
joashrajin wants to merge 4 commits into
mainfrom
fix/pcdroid-571-auto-archive-respect-up-next

Conversation

@joashrajin
Copy link
Copy Markdown
Contributor

Description

checkPodcastForEpisodeLimitBlocking filtered out only the currently-playing episode (playbackManager.getCurrentEpisode(), which is the top of Up Next). Any episode queued behind the current one in Up Next could still be archived when a newer episode arrived for the same podcast and tripped the per-podcast limit during a background refresh.

User-visible effect: when a user has a podcast set to Auto add to Up Next + a download/episode limit of 1, an episode queued for later is silently marked as played the next time a new episode arrives via background refresh. The Zendesk reporter perceived this as "the episode I was listening to" being marked played mid-listening.

Fix: replace the getCurrentEpisode().uuid != it.uuid filter with !playbackManager.upNextQueue.contains(it.uuid). The full-queue check is already the pattern used by episodeCanBeCleanedUp in the same file (line 886), so this aligns the over-limit path with the existing precedent. The new filter is null-safe to preserve the existing test-only playbackManager = null behaviour.

Added an androidTest regression that mirrors the bug: episode limit = 1, two episodes for the same podcast, the older one queued behind a newer one in Up Next. The legacy filter archives the older episode; with this change it stays.

Fixes PCDROID-571. User-reported via Zendesk #11253136, with the customer's debug log showing the exact Auto archiving episode over limit 1 …Download deleted … Source: UNKNOWN sequence inside a background RefreshPodcastsTask cycle.

Testing Instructions

  1. Subscribe to a podcast that publishes regularly (e.g. Marketplace Morning Report).
  2. In that podcast's settings, enable Auto Download and Auto add to Up Next.
  3. Set the podcast's Episode Limit / Auto Download limit to 1.
  4. Trigger a refresh (pull-to-refresh on the Podcasts tab) so a downloaded episode lands in Up Next.
  5. Reorder Up Next so the downloaded episode is not at the top (queue an unrelated episode in front of it).
  6. Wait for or force another refresh that brings a newer episode of the same podcast.
  7. ✅ Verify the older episode that was queued in Up Next is still present (not archived, not marked played).

Or, run the new androidTest:

./gradlew :app:connectedAndroidTest --tests "au.com.shiftyjelly.pocketcasts.models.db.AutoArchiveTest.testEpisodeLimitRespectsUpNext"

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml
  • Any jetpack compose components I added or changed are covered by compose previews
  • I have updated (or requested that someone edit) the spreadsheet to reflect any new or changed analytics.

…imit

The previous filter in checkPodcastForEpisodeLimitBlocking only excluded
the currently-playing episode (top of Up Next). Any episode queued
deeper in Up Next could be archived when a newer episode arrived and
tripped the per-podcast limit during a background refresh, silently
removing it from the queue.

Extend the filter to skip any episode the user has placed in Up Next,
matching the pattern already used in episodeCanBeCleanedUp.

Linear: PCDROID-571
Reproduces the PCDROID-571 scenario: episode limit set to 1, two
episodes for the same podcast, the older one queued in Up Next behind
a newer one. The legacy filter that only excluded `getCurrentEpisode()`
would archive the older episode here; with the upNextQueue.contains
guard it stays.

Linear: PCDROID-571
Copilot AI review requested due to automatic review settings May 25, 2026 08:27
@dangermattic
Copy link
Copy Markdown
Collaborator

1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an auto-archive edge case where episodes queued in Up Next (but not currently playing) could be archived when a per-podcast episode limit is exceeded during background refresh, causing queued episodes to disappear unexpectedly.

Changes:

  • Update per-podcast episode-limit auto-archive logic to exclude any episode currently present in the Up Next queue (not just the current episode).
  • Add an androidTest regression test to ensure queued (non-current) Up Next episodes are not archived under the per-podcast limit.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/podcast/EpisodeManagerImpl.kt Extends the episode-limit exclusion check to cover the full Up Next queue to prevent archiving queued episodes.
app/src/androidTest/java/au/com/shiftyjelly/pocketcasts/models/db/AutoArchiveTest.kt Adds a regression test reproducing the queued-episode auto-archive scenario under a per-podcast limit of 1.

Comment on lines +535 to +536
runBlocking { upNext.playLast(newerEpisode, onAdd = null) }
runBlocking { upNext.playLast(olderEpisode, onAdd = null) }
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, addressed in 549b0c7 — dropped the runBlocking wrappers and call playLast directly from the runTest coroutine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: had to revert this in 2cd7c78 — the instrumented-tests CI job failed after dropping the wrappers. upNext.playLast does withContext(Dispatchers.Default) internally, and the runTest scope here uses an UnconfinedTestDispatcher only as the ioDispatcher — it doesn't control Dispatchers.Default, so the queue can be observed not-yet-populated when the archive check runs immediately after. Matching the existing testAddingInactiveEpisodeToUpNext pattern with runBlocking keeps the ordering reliable.

@joashrajin joashrajin self-assigned this May 25, 2026
`runTest` already provides a coroutine scope, so calling the suspend
`upNext.playLast` directly is sufficient. Addresses Copilot review
feedback on PR #5342.
This reverts commit 549b0c7.

The instrumented-tests CI job started failing after that change.
`upNext.playLast` dispatches via `withContext(coroutineContext)` where
the coroutineContext is `Dispatchers.Default`, which the test's
`runTest` scope (using `UnconfinedTestDispatcher` as ioDispatcher) does
not control — so the queue can be observed not-yet-populated when the
archive check runs immediately after. Restore the `runBlocking`
wrappers to match the existing `testAddingInactiveEpisodeToUpNext`
pattern and ensure ordering.
Copilot AI review requested due to automatic review settings May 26, 2026 16:04
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines 858 to 862
val episodesToRemove = allEpisodes.drop(episodeLimit)
.filter { !it.isArchived }
.filter { (settings.autoArchiveIncludesStarred.value && it.isStarred) || !it.isStarred }
.filter { playbackManager?.getCurrentEpisode()?.uuid != it.uuid }
.filter { playbackManager == null || !playbackManager.upNextQueue.contains(it.uuid) }
if (episodesToRemove.isNotEmpty()) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants