Skip to content

PCIOS-176: Time left display does not factor in deselected chapters. - #4771

Draft
pocketcasts wants to merge 1 commit into
trunkfrom
pcios-176/fix-time-left-deselected-chapters
Draft

PCIOS-176: Time left display does not factor in deselected chapters.#4771
pocketcasts wants to merge 1 commit into
trunkfrom
pcios-176/fix-time-left-deselected-chapters

Conversation

@pocketcasts

Copy link
Copy Markdown
Contributor

Resolves https://linear.app/a8c/issue/PCIOS-176/time-left-display-does-not-factor-in-deselected-chapters

Summary

The time left display for the currently playing episode now subtracts the duration of deselected (skipped) chapters, so the remaining time reflects only the content that will actually be played.

Changes

  • Added ChapterManager.deselectedDuration(after:) to compute total duration of deselected chapters remaining after a given playback position
  • Added PlaybackManager.remainingDeselectedDuration() convenience wrapper
  • Adjusted time-remaining calculation in full-screen player, mini player, Up Next now-playing cell, video player, and Up Next total time label

Verification

  • Lint: PASS
  • Tests: FAIL - pre-existing GenerateCredentials build script failure, unrelated to this change. Zero Swift compilation errors.
  • Diff review: PASS

Confidence

MEDIUM - The logic is straightforward and compiles without errors. The fix adjusts the currently playing episode time remaining only (chapter data is only loaded for the active episode).

Known Issues

  • Build and tests fail due to pre-existing GenerateCredentials script failure in the worktree environment, not caused by these changes.

This PR was created autonomously by linear-solver.
Triage complexity: medium | Linear issue

@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown

Claude finished @pocketcasts's task in 3m 4s —— View job


PR Review: Time left factoring in deselected chapters

  • Read the diff and understand the change
  • Analyze ChapterManager.deselectedDuration(after:)
  • Trace all 5 call sites + scrubbing paths
  • Verify feature-gating (isPlayable() / deselectChapters)
  • Post inline comments + summary

Nice, focused change. The core deselectedDuration(after:) math is correct, and gating through ChapterInfo.isPlayable() means it's automatically a no-op when the paid "deselect chapters" feature is locked or nothing is deselected. Applying it only to the current episode is the right call since chapters are only loaded for the active episode. A few issues worth addressing before merge:

🔴 Bug: chapter-scoped call site gets an episode-wide subtraction

NowPlayingPlayerItemViewController+Update.swift:275 (in updateProvisionalChapterInfoForTime) calls updateUpTo(upTo: time, duration: chapters.duration, …) — where duration is a single chapter's length, not the episode duration. After this PR, updateUpTo now computes max(0, chapters.duration - time - remainingDeselectedDuration()), subtracting the whole-episode deselected total from a chapter-scoped duration. That mixes two different scales and will typically clamp the label to 0/blank on this path. The subtraction only belongs where duration is the full episode duration. (Couldn't attach inline — line 275 isn't in the diff; see updateUpTo at line 184.)

🟠 Reference-point mismatch while scrubbing

remainingDeselectedDuration() always measures deselected time after currentTime(), but the NowPlaying/Video updateUpTo methods compute remaining relative to their upTo argument. During a provisional scrub (sliderDidProvisionallySlide), upTo is the drag position while currentTime() is the real playhead, so the displayed remaining won't track the scrub. Cleanest fix is to thread the position through:

func remainingDeselectedDuration(after time: TimeInterval? = nil) -> TimeInterval {
    chapterManager.deselectedDuration(after: time ?? currentTime())
}

then call remainingDeselectedDuration(after: upTo) from the episode-duration call sites (and skip it on the chapter-scoped one above).

🟡 Minor

  • UpNextViewController.swift:435 isn't wrapped in max(0, …) like the other four call sites, so it can go slightly negative. Low impact (summed with the queue total) but inconsistent.
  • No test coverage. deselectedDuration(after:) is pure, branch-heavy logic and there's already a ChapterManagerTests suite — worth covering before/after/straddling cases.
  • effectiveStartTime vs startTime. For generated chapters shifted by dynamic ads, the calc uses the reference timeline. Consistent with chaptersForTime, so probably fine — just flagging.

Summary

The approach and the core computation are sound, but the chapter-duration call path (finding #1) looks like a genuine display regression and the scrubbing reference-point issue (#2) is worth fixing together — both stem from remainingDeselectedDuration() ignoring its caller's position/duration context. Adding a couple of unit tests would raise confidence from MEDIUM.

Detailed notes left as inline comments.

@dangermattic

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

Generated by 🚫 Danger


func updateUpTo(upTo: TimeInterval, duration: TimeInterval, moveSlider: Bool) {
let remaining = max(0, duration - upTo)
let remaining = max(0, duration - upTo - PlaybackManager.shared.remainingDeselectedDuration())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reference-point mismatch during scrubbing. remainingDeselectedDuration() is hard-coded to measure deselected chapters after PlaybackManager.shared.currentTime(), but this method computes remaining relative to the upTo argument. On the provisional-scrub path (sliderDidProvisionallySlide(to:)updateUpTo(upTo: time, …)), upTo is the drag position while currentTime() is still the real playhead, so the subtracted amount doesn't match the position being displayed. As the user scrubs across deselected chapters the label won't track correctly.

Consider threading the position through instead of hard-coding currentTime():

// PlaybackManager
func remainingDeselectedDuration(after time: TimeInterval? = nil) -> TimeInterval {
    chapterManager.deselectedDuration(after: time ?? currentTime())
}

and call remainingDeselectedDuration(after: upTo) here.

Comment on lines +222 to +235
func deselectedDuration(after time: TimeInterval) -> TimeInterval {
visibleChapters
.filter { !$0.isPlayable() }
.reduce(0) { total, chapter in
let chapterEnd = chapter.startTime.seconds + chapter.duration
if chapterEnd <= time {
return total
} else if chapter.startTime.seconds >= time {
return total + chapter.duration
} else {
return total + (chapterEnd - time)
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The logic here is correct and readable. Two notes:

  1. No test coverage. There's an existing ChapterManagerTests suite and this is pure, easily-testable logic. Worth adding cases for the three branches: chapter fully before time (excluded), fully after time (full duration counted), and straddling time (partial chapterEnd - time counted).
  2. Uses startTime.seconds/duration rather than effectiveStartTime. For generated chapters shifted by dynamic ads (resolvedPlaybackStartTime), this measures on the reference timeline. It's consistent with chaptersForTime above, so likely fine, but flagging in case deselected generated chapters need the resolved position.

var totalDuration = PlaybackManager.shared.queue.upNextTotalDuration(includePlayingEpisode: false)
if let episode = PlaybackManager.shared.currentEpisode() {
totalDuration += episode.duration.seconds - PlaybackManager.shared.currentTime()
totalDuration += episode.duration.seconds - PlaybackManager.shared.currentTime() - PlaybackManager.shared.remainingDeselectedDuration()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor: unlike the other four call sites, this one isn't wrapped in max(0, …). If the remaining deselected duration exceeds the current episode's remaining time, this term goes negative and slightly under-counts the Up Next total. Low impact since it's summed with the rest of the queue, but worth clamping for consistency.

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.

2 participants