Add a Troubleshooting screen to remove orphaned/duplicate episodes#4697
Open
kean wants to merge 5 commits into
Open
Add a Troubleshooting screen to remove orphaned/duplicate episodes#4697kean wants to merge 5 commits into
kean wants to merge 5 commits into
Conversation
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.
Collaborator
Generated by 🚫 Danger |
Contributor
There was a problem hiding this comment.
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") |
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 byuuid, so it shows the orphaned episode - which is why it displays the "correct" state while the list doesn't.Timeline
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:
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
SJEpisoderows that share auuidwith differentpodcast_idvalues (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.PocketCastsDataModelTests/OrphanedEpisodeDataManagerTestsandPocketCastsTests/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
Checklist
CHANGELOG.mdif necessary.