Skip to content

Add a Troubleshooting screen to remove orphaned/duplicate episodes#4697

Open
kean wants to merge 5 commits into
release/8.16from
kean/add-remove-orphaned-episodes-option-for-support
Open

Add a Troubleshooting screen to remove orphaned/duplicate episodes#4697
kean wants to merge 5 commits into
release/8.16from
kean/add-remove-orphaned-episodes-option-for-support

Conversation

@kean

@kean kean commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Fixes PCIOS-806.

RCA

The issue reproduces for podcasts that have duplicated (and orphaned*) episodes.

When you open the episode list, it shows the live episode (resolved via the local podcast id). Most operations on episodes act on uuid (the cross-platform foreign id) and pick up the orphaned row instead. The Episode Details screen also reads by uuid, so it shows the orphaned episode - which is why it displays the "correct" state while the list doesn't.

*An orphaned episode points at the id of a podcast row that no longer exists.

Timeline

  • Prior to Aug 2022, unsubscribing from a podcast could leave orphaned episodes behind.
  • In Aug 2022 the issue was fixed and a migration was added (Prevent "ghost" episodes from displaying in the filters #137) to remove existing orphaned episodes. That migration missed users who subsequently resubscribed to a podcast they'd previously deleted, which left duplicated episodes in their database.

Solution

Running another healing migration for every user is high risk for a problem that only affects a tiny portion of power users carrying duplicate episodes from 2022 or earlier. Instead of running it unconditionally, this adds a self-serve "Troubleshooting" screen under Help & Feedback's "..." menu that:

  • proactively counts orphaned episodes and explains the symptoms they can cause (incorrect download/playback status)
  • lets you view them grouped by podcast
  • lets you remove them on demand, behind a confirmation that it's destructive

Removal repoints the surviving row at the real podcast instead of deleting blindly, so playback progress, download status, and archive state are preserved on a best-effort basis. Tested against the DB export from the user who originally reported the issue.

To test

  1. Go to Profile > Help & Feedback, tap the "..." menu, tap "Troubleshooting".
  2. On a normal account it should show "No orphaned episodes were found" and the Remove button should be disabled.
  3. To exercise the repair flow, load a database containing duplicate SJEpisode rows that share a uuid with different podcast_id values (one pointing at a real podcast row, one dangling). Reopen Troubleshooting - it should show a count, and tapping it lists the episodes grouped by podcast.
  4. Tap "Remove Orphaned Episodes", confirm the warning alert, and verify the count drops and playback progress/download status/archived state is preserved on the surviving episode.
  5. Run PocketCastsDataModelTests/OrphanedEpisodeDataManagerTests and PocketCastsTests/Tests/Utilities/PodcastManagerTests - all should pass.

Here's the recoding of the issues, the troublshooting, and the fix.

Screen.Recording.2026-07-07.at.4.12.59.PM.mov

Screenshot

Screenshot 2026-07-07 at 4 31 58 PM

Checklist

  • I have considered if this change warrants user-facing release notes and have added them to CHANGELOG.md if necessary.
  • I have considered adding unit tests for my changes.
  • I have updated (or requested that someone edit) the Event Horizon schema to reflect any new or changed analytics.

Some users end up with duplicate episode rows sharing a uuid where one
row's podcast_id points at a podcast that no longer exists (an
"orphaned" episode). Episode lists resolve by podcast_id and show the
pristine "live" row, while most other operations (and the Episode
Details screen) key off uuid and hit the orphaned row instead, so
download/playback state looks wrong or stuck.

Rather than re-running a healing migration for every user - risky, and
only relevant to a small number of long-time users with leftover
duplicates from 2022 - add a self-serve "Troubleshooting" screen under
Help & Feedback's "..." menu that counts orphaned episodes, lets you
view them grouped by podcast, and removes them on demand. Removal
repoints the surviving row at the real podcast instead of deleting
blindly, so playback progress, download status, and archive state are
preserved on a best-effort basis.
@kean kean requested a review from a team as a code owner July 7, 2026 20:18
@kean kean requested review from Copilot and removed request for a team July 7, 2026 20:18
@kean kean added this to the 8.16 ❄️ milestone Jul 7, 2026
@kean kean requested a review from SergioEstevao July 7, 2026 20:18
@kean kean added [Type] Bug Used for issues where something is not functioning as intended. [Area] Settings labels Jul 7, 2026
@dangermattic

Copy link
Copy Markdown
Collaborator
2 Warnings
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
⚠️ This PR is assigned to the milestone 8.16 ❄️. The due date for this milestone has already passed.
Please assign it to a milestone with a later deadline or check whether the release for this milestone has already been finished.

Generated by 🚫 Danger

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a self-serve Troubleshooting flow under Help & Feedback to detect and repair a rare “duplicate/orphaned episodes” database state (duplicate SJEpisode rows sharing a uuid, with one row pointing at a non-existent podcast_id). This fits into the existing iOS app support tooling by surfacing diagnostics in UI and performing best-effort reconciliation/deletion on demand, rather than running a risky unconditional migration.

Changes:

  • Adds new SwiftUI Troubleshooting screens to count, list (grouped by podcast), and confirm removal of orphaned episode rows.
  • Implements DataModel support for finding, deleting, and reconciling orphaned episodes (including a targeted cleanup path in PodcastManager).
  • Adds analytics events, localization strings, tests, and release notes for the new troubleshooting feature.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
podcasts/TroubleshootingViewModel.swift Main-actor view model that loads orphan count and triggers the cleanup flow + analytics.
podcasts/TroubleshootingView.swift SwiftUI UI for status, explanation, and destructive “Remove” confirmation.
podcasts/OrphanedEpisodesListView.swift Lists orphaned episodes grouped by podcast for diagnostic visibility.
podcasts/OnlineSupportController.swift Wires Troubleshooting entry point into the Help & Feedback “…” menu.
podcasts/PodcastManager+Cleanup.swift Adds deleteOrphanedEpisodesIfNeeded() reconciliation/deletion logic.
Modules/Sources/PocketCastsDataModel/Public/DataManager.swift Exposes orphaned-episode operations via DataManager public API.
Modules/Sources/PocketCastsDataModel/Private/Managers/EpisodeDataManager.swift Implements orphan detection + delete/reconcile SQL helpers.
podcasts/Analytics/AnalyticsEvent.swift Adds new analytics event identifiers for the troubleshooting actions.
podcasts/en.lproj/Localizable.strings Adds user-facing copy for the troubleshooting screens and alerts.
PocketCastsTests/Tests/Utilities/PodcastManagerTests.swift Adds regression test for PodcastManager reconciliation behavior.
Modules/Tests/PocketCastsDataModelTests/OrphanedEpisodeDataManagerTests.swift Adds focused DataManager-level regression tests for orphan handling.
CHANGELOG.md Adds a release note entry for the new Troubleshooting screen.
podcasts.xcodeproj/project.pbxproj Includes new Swift files in the Xcode project.

Comment on lines +1284 to +1288
dbQueue.write { db in
let query = "DELETE FROM \(DataManager.episodeTableName) WHERE id IN (\(ids.map(String.init).joined(separator: ",")))"

try? db.executeUpdate(query, values: nil)
}
}
}

FileLog.shared.addMessage("Deleted \(deletedCount) Orphaned Episodes")
kean added 3 commits July 7, 2026 16:22
…pty state

Adds an icon + explanation above the fold so the screen reads as a
diagnostic tool, and gives the "no orphaned episodes" case a more
conclusive checkmark treatment instead of a plain list row.
Icon-only header with tighter top padding, the explanation text moves
back to the section footer, and the new decorative icons use standard
system colors instead of the app theme.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Area] Settings [Type] Bug Used for issues where something is not functioning as intended.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants