Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Sources/Kaset/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -11669,6 +11669,17 @@
}
}
},
"Autoplay" : {
"comment" : "Toggle: automatically play the next suggested video (it: 'Riproduzione automatica').",
"localizations" : {
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Riproduzione automatica"
}
}
}
},
"Available" : {
"comment" : "Reusable label shown in the Foundation Models screen, Intelligence settings.",
"localizations" : {
Expand Down Expand Up @@ -73139,6 +73150,17 @@
}
}
},
"Play now" : {
"comment" : "Tooltip on the autoplay countdown play button (it: 'Riproduci ora').",
"localizations" : {
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Riproduci ora"
}
}
}
},
"Play video" : {
"comment" : "YouTube video watch page (below the player: title, channel, subscribe/bell, description, chapters, comments, related).",
"localizations" : {
Expand Down
2 changes: 2 additions & 0 deletions Sources/Kaset/Resources/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"Auto" = "Automatico";
"Auto-skips sponsored segments in videos." = "Auto-skips sponsored segments in videos.";
"Automatically check for updates" = "Controlla automaticamente gli aggiornamenti";
"Autoplay" = "Riproduzione automatica";
"Available" = "Disponibile";
"Back 30 seconds" = "Indietro di 30 secondi";
"Background Playback" = "Riproduzione in background";
Expand Down Expand Up @@ -395,6 +396,7 @@
"Play Next" = "Riproduci dopo";
"Play a song to view its lyrics here." = "Riproduci un brano per vederne qui il testo.";
"Play songs from a playlist or album to build your queue." = "Riproduci brani da una playlist o da un album per creare la tua coda.";
"Play now" = "Riproduci ora";
"Play video" = "Riproduci video";
"Playback" = "Riproduzione";
"Playback Audio Quality" = "Qualità audio di riproduzione";
Expand Down
84 changes: 84 additions & 0 deletions Sources/Kaset/Services/Player/YouTubePlayerService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,16 @@ final class YouTubePlayerService {
/// Up-next candidates for skip-forward (related videos from the watch page).
private(set) var upNext: [YouTubeVideo] = []

/// The video queued to autoplay next, shown behind a countdown card after the
/// current video finishes (YouTube-style). Nil when no autoplay is pending.
private(set) var autoplayPendingVideo: YouTubeVideo?
/// Whole seconds left on the autoplay countdown (counts `autoplayCountdownSeconds`→0);
/// meaningful only while `autoplayPendingVideo` is set.
private(set) var autoplayCountdownRemaining = 0
@ObservationIgnored private var autoplayCountdownTask: Task<Void, Never>?
/// How long the "up next" countdown card lingers before advancing.
static let autoplayCountdownSeconds = 5

/// Navigation chapters for the current video, loaded from the watch page's
/// companion `next` response.
private(set) var chapters: [YouTubeChapter] = []
Expand Down Expand Up @@ -516,6 +526,7 @@ final class YouTubePlayerService {
/// Starts playback of a video, docked inline.
func play(video: YouTubeVideo, usesCookieFreeDataStore: Bool = false, startAt: Double? = nil) {
self.beginYouTubePlaybackIntent()
self.clearAutoplayCountdown()
self.autoplayRecoveryRequestGeneration &+= 1
self.shouldRecoverAutoplayTransitionOnResume = false
let normalizedStartAt = Self.normalizedExplicitStartAt(startAt)
Expand Down Expand Up @@ -935,6 +946,7 @@ final class YouTubePlayerService {
/// Stops playback entirely and releases the surface.
func stop() {
self.beginYouTubePlaybackIntent()
self.clearAutoplayCountdown()
self.autoplayRecoveryRequestGeneration &+= 1
self.shouldRecoverAutoplayTransitionOnResume = false
self.logger.info("YouTubePlayer: stop")
Expand Down Expand Up @@ -1076,6 +1088,7 @@ final class YouTubePlayerService {
}

private func advance(to video: YouTubeVideo, recordingHistory: Bool = true) {
self.clearAutoplayCountdown()
self.autoplayRecoveryRequestGeneration &+= 1
self.shouldRecoverAutoplayTransitionOnResume = false
self.logger.info("YouTubePlayer: advancing to another video")
Expand Down Expand Up @@ -1990,9 +2003,80 @@ extension YouTubePlayerService {
self.watchActivityGeneration += 1
self.onVideoEnded?(videoId)
}
self.autoplayNextIfEnabled()
return true
}

/// YouTube-style autoplay: when the setting is on, a finished video queues the
/// next suggested video (the first up-next candidate, or a freshly fetched
/// related video when none are known — e.g. a floating-window finish) behind a
/// countdown card, rather than switching immediately. Off by default, so
/// playback otherwise stops at the end. Mixes and playlists advance through
/// their own queue via page drift and are unaffected by this.
private func autoplayNextIfEnabled() {
guard SettingsManager.shared.youtubeAutoplayEnabled,
let current = self.currentVideo
else { return }

Comment on lines +2017 to +2020

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The autoplay setting is checked only when creating a countdown. Turning the setting off afterward does not cancel the existing countdown task, and that task never rechecks the setting before confirming playback, so the queued video can still start even though autoplay is now disabled. Invalidate or clear the countdown when the setting changes, or check the current setting before confirming. [logic error]

Severity Level: Major ⚠️
- ❌ Disabling Autoplay does not prevent queued playback.
- ⚠️ Related-header setting changes are ignored by active countdowns.
- ⚠️ Users can be unexpectedly navigated to another video.
Steps of Reproduction ✅
1. Enable the Autoplay toggle implemented by `YouTubeWatchView.autoplayToggle` at
`Sources/Kaset/Views/YouTube/YouTubeWatchView.swift:1286-1289`.

2. Finish a video with an available `upNext` candidate; `autoplayNextIfEnabled()` at
`Sources/Kaset/Services/Player/YouTubePlayerService.swift:2016-2023` passes the setting
guard and starts the five-second task at lines 2038-2056.

3. Before the five-second task reaches zero, disable the toggle.
`SettingsManager.youtubeAutoplayEnabled` at
`Sources/Kaset/Services/SettingsManager.swift:576-580` only persists the value and does
not clear the service's countdown.

4. Allow the task to finish. Its zero-countdown branch at
`YouTubePlayerService.swift:2049-2053` calls `confirmAutoplayNow()` without rechecking the
setting, and lines 2061-2065 advance to the queued video even though autoplay is disabled.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** Sources/Kaset/Services/Player/YouTubePlayerService.swift
**Line:** 2017:2020
**Comment:**
	*Logic Error: The autoplay setting is checked only when creating a countdown. Turning the setting off afterward does not cancel the existing countdown task, and that task never rechecks the setting before confirming playback, so the queued video can still start even though autoplay is now disabled. Invalidate or clear the countdown when the setting changes, or check the current setting before confirming.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

if let next = self.upNext.first {
self.startAutoplayCountdown(to: next)
return
}
let expectedVideoId = current.videoId
Task { @MainActor in
guard let client = self.youtubeClient,
let next = try? await client.getWatchNext(videoId: expectedVideoId, playlistId: nil)
.related.first(where: { !$0.isShort }),
SettingsManager.shared.youtubeAutoplayEnabled,
self.currentVideo?.videoId == expectedVideoId,
self.autoplayPendingVideo == nil
else { return }
self.startAutoplayCountdown(to: next)
}
Comment on lines +2026 to +2035

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The fallback getWatchNext task is not tied to a playback-intent generation or cancelled by cancelAutoplay(). If the user cancels while this request is in flight, autoplayPendingVideo is nil, so a late successful response passes the final guard and recreates the countdown. Capture a generation before starting the request and require it to remain current before calling startAutoplayCountdown. [race condition]

Severity Level: Major ⚠️
- ❌ Cancelled autoplay can reappear after delayed network responses.
- ⚠️ Floating-window finishes use this fallback request path.
- ⚠️ Users may see an unexpected later video transition.
Steps of Reproduction ✅
1. Enable autoplay and finish a YouTube video through `handleVideoEnded(videoId:)` at
`Sources/Kaset/Services/Player/YouTubePlayerService.swift:1960-2007` when `upNext` is
empty, causing the fallback task at line 2026 to call `getWatchNext`.

2. Keep `getWatchNext` suspended in the client while the fallback request is in flight; no
task handle is stored, so `cancelAutoplay()` at lines 2069-2071 cannot cancel this
request.

3. Invoke the countdown card's cancel action, which calls `cancelAutoplay()` and clears
`autoplayPendingVideo` at lines 2073-2078.

4. Resume the suspended request and return a related video. The guards at lines 2027-2033
still pass because autoplay remains enabled, the current video is unchanged, and
`autoplayPendingVideo` is now nil; line 2034 recreates the countdown despite the user's
cancellation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** Sources/Kaset/Services/Player/YouTubePlayerService.swift
**Line:** 2026:2035
**Comment:**
	*Race Condition: The fallback `getWatchNext` task is not tied to a playback-intent generation or cancelled by `cancelAutoplay()`. If the user cancels while this request is in flight, `autoplayPendingVideo` is nil, so a late successful response passes the final guard and recreates the countdown. Capture a generation before starting the request and require it to remain current before calling `startAutoplayCountdown`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}

private func startAutoplayCountdown(to next: YouTubeVideo) {
self.autoplayCountdownTask?.cancel()
self.autoplayPendingVideo = next
self.autoplayCountdownRemaining = Self.autoplayCountdownSeconds
self.autoplayCountdownTask = Task { [weak self] in
while true {
do {
try await Task.sleep(for: .seconds(1))
} catch {
return
}
guard let self, self.autoplayPendingVideo?.videoId == next.videoId else { return }
self.autoplayCountdownRemaining -= 1
if self.autoplayCountdownRemaining <= 0 {
self.confirmAutoplayNow()
return
}
Comment on lines +2042 to +2054

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The countdown task remains active when the user invokes pause or resume after the video has finished. Resuming the current video therefore does not dismiss the pending autoplay transition, and the task can still call confirmAutoplayNow() seconds later and replace the video the user resumed. Clear or invalidate the countdown on explicit pause/resume intent, consistent with the existing playback-intent generation handling. [stale reference]

Severity Level: Major ⚠️
- ❌ Resuming after a finish can unexpectedly replace the current video.
- ⚠️ Explicit pause/resume leaves stale autoplay state active.
- ⚠️ Playback controls conflict with countdown navigation.
Steps of Reproduction ✅
1. Enable autoplay, finish a video, and leave the resulting countdown active. The task is
created at `Sources/Kaset/Services/Player/YouTubePlayerService.swift:2042-2056`.

2. Invoke the service's explicit `pause()` at `YouTubePlayerService.swift:909-913`; it
begins a playback intent and pauses the controller, but does not call
`clearAutoplayCountdown()`.

3. Invoke `resume()` at `YouTubePlayerService.swift:843-847`. `performResume()` resumes
the current video at lines 849-880, while the countdown task remains active.

4. Wait for the task's remaining seconds to expire. The task at lines 2049-2053 calls
`confirmAutoplayNow()`, and lines 2061-2065 advance from the video the user resumed to the
queued video.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** Sources/Kaset/Services/Player/YouTubePlayerService.swift
**Line:** 2042:2054
**Comment:**
	*Stale Reference: The countdown task remains active when the user invokes pause or resume after the video has finished. Resuming the current video therefore does not dismiss the pending autoplay transition, and the task can still call `confirmAutoplayNow()` seconds later and replace the video the user resumed. Clear or invalidate the countdown on explicit pause/resume intent, consistent with the existing playback-intent generation handling.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}
}
}

/// Plays the queued autoplay video right away (the countdown card's play
/// button — "speed it up").
func confirmAutoplayNow() {
guard let next = self.autoplayPendingVideo else { return }
self.clearAutoplayCountdown()
self.advance(to: next)
}

/// Dismisses the autoplay countdown without advancing (the card's cancel
/// button, or any explicit playback the user starts instead).
func cancelAutoplay() {
self.clearAutoplayCountdown()
}

private func clearAutoplayCountdown() {
self.autoplayCountdownTask?.cancel()
self.autoplayCountdownTask = nil
self.autoplayPendingVideo = nil
self.autoplayCountdownRemaining = 0
}

nonisolated static func isEndEventStale(
eventIssuedAtMilliseconds: Double,
lastResumeIssuedAtMilliseconds: Double
Expand Down
11 changes: 11 additions & 0 deletions Sources/Kaset/Services/SettingsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ final class SettingsManager {
static let dearrowEnabled = "settings.dearrow.enabled"
static let hasCompletedInitialOnboarding = "settings.onboarding.completed"
static let distractionFreeEnabled = "settings.distractionFree.enabled"
static let youtubeAutoplayEnabled = "settings.youtube.autoplay.enabled"
static let accentColorHex = "settings.accentColorHex"
#if DEBUG
static let useLegacyMacOS15UI = "settings.debug.useLegacyMacOS15UI"
Expand Down Expand Up @@ -569,6 +570,15 @@ final class SettingsManager {
}
}

/// YouTube-style autoplay: when a video finishes, automatically play the next
/// suggested (up-next) video. Off by default; the fork otherwise stops at the
/// end (YouTube's own autonav is always disabled).
var youtubeAutoplayEnabled: Bool {
didSet {
UserDefaults.standard.set(self.youtubeAutoplayEnabled, forKey: Keys.youtubeAutoplayEnabled)
}
}

/// Whether the one-time first-launch onboarding (the addons walkthrough)
/// has been completed. The full onboarding — changelog → Continue → Addons →
/// Done — is shown only once, on first launch. Later versions surface just the
Expand Down Expand Up @@ -665,6 +675,7 @@ final class SettingsManager {
self.dearrowEnabled = UserDefaults.standard.object(forKey: Keys.dearrowEnabled) as? Bool ?? false
self.hasCompletedInitialOnboarding = UserDefaults.standard.object(forKey: Keys.hasCompletedInitialOnboarding) as? Bool ?? false
self.distractionFreeEnabled = UserDefaults.standard.object(forKey: Keys.distractionFreeEnabled) as? Bool ?? false
self.youtubeAutoplayEnabled = UserDefaults.standard.object(forKey: Keys.youtubeAutoplayEnabled) as? Bool ?? false
#if DEBUG
self.useLegacyMacOS15UI = UserDefaults.standard.object(forKey: Keys.useLegacyMacOS15UI) as? Bool ?? false
#endif
Expand Down
79 changes: 79 additions & 0 deletions Sources/Kaset/Views/YouTube/YouTubeAutoplayCountdownOverlay.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import SwiftUI

// MARK: - YouTubeAutoplayCountdownOverlay

/// YouTube-style "up next" countdown card, shown over the video surface after a
/// video finishes when autoplay is on: it names the next video and counts down
/// inside a ring the user can click to play now ("speed it up"), or cancel.
///
/// Reads the shared `YouTubePlayerService`, so both the inline watch view and the
/// floating window can host it; it renders nothing when no autoplay is pending.
struct YouTubeAutoplayCountdownOverlay: View {
@Environment(YouTubePlayerService.self) private var youtubePlayer

var body: some View {
if let next = self.youtubePlayer.autoplayPendingVideo {
self.card(next: next)
.transition(.opacity)
}
}

private func card(next: YouTubeVideo) -> some View {
let total = YouTubePlayerService.autoplayCountdownSeconds
let remaining = max(self.youtubePlayer.autoplayCountdownRemaining, 0)
let fraction = CGFloat(remaining) / CGFloat(max(total, 1))

return ZStack {
Rectangle().fill(.black.opacity(0.62))

VStack(spacing: 14) {
Text("Up Next", comment: "Label above the autoplay countdown card")
.font(.system(size: 12, weight: .semibold))
.textCase(.uppercase)
.foregroundStyle(.white.opacity(0.7))

Text(next.title)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(.white)
.multilineTextAlignment(.center)
.lineLimit(2)

// The ring counts down; clicking it plays the next video now.
Button {
self.youtubePlayer.confirmAutoplayNow()
} label: {
ZStack {
Circle().stroke(.white.opacity(0.25), lineWidth: 3)
Circle()
.trim(from: 0, to: fraction)
.stroke(.white, style: StrokeStyle(lineWidth: 3, lineCap: .round))
.rotationEffect(.degrees(-90))
Text("\(remaining)")
.font(.system(size: 22, weight: .semibold).monospacedDigit())
.foregroundStyle(.white)
}
.frame(width: 62, height: 62)
.contentShape(Circle())
}
.buttonStyle(.plain)
.help(Text("Play now", comment: "Tooltip on the autoplay countdown play button"))

Button {
self.youtubePlayer.cancelAutoplay()
} label: {
Text("Cancel", comment: "Button to cancel autoplay to the next video")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.white)
.padding(.horizontal, 16)
.frame(height: 30)
.compatGlass(interactive: true, tint: nil, in: Capsule())
.contentShape(Capsule())
}
.buttonStyle(.plain)
}
.padding(24)
.frame(maxWidth: 440)
}
.animation(.linear(duration: 1), value: remaining)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ private struct YouTubeVideoWindowContent: View {
ZStack(alignment: .bottom) {
YouTubeWatchSurfaceView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay {
YouTubeAutoplayCountdownOverlay()
}
.animation(.easeInOut(duration: 0.25), value: self.youtubePlayer.autoplayPendingVideo == nil)

if self.isHovering {
// The full player bar — same items as the main window.
Expand Down
31 changes: 29 additions & 2 deletions Sources/Kaset/Views/YouTube/YouTubeWatchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/// The surface is the singleton `YouTubeWatchWebView`, docked here while
/// this view owns it. Navigating away while playing hands the surface off
/// to the floating window (`YouTubeVideoWindowController`).
struct YouTubeWatchView: View {

Check failure on line 11 in Sources/Kaset/Views/YouTube/YouTubeWatchView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Struct body should span 1000 lines or less excluding comments and whitespace: currently spans 1365 lines (type_body_length)
@MainActor fileprivate static var brandAccent: Color { SettingsManager.shared.accentColor }

let video: YouTubeVideo
Expand Down Expand Up @@ -260,6 +260,12 @@
}
.animation(.easeInOut(duration: 0.25), value: self.youtubePlayer.isPlaybackLoading)
.animation(.easeInOut(duration: 0.25), value: self.viewModel.membersGate == nil)
// YouTube-style "up next" countdown card when a finished video is
// about to autoplay the next one.
.overlay {
YouTubeAutoplayCountdownOverlay()
}
.animation(.easeInOut(duration: 0.25), value: self.youtubePlayer.autoplayPendingVideo == nil)
// "Ad" badge: a server-side (SSAI) ad can still slip past the
// blocker; label it so it's clear this isn't the content.
.overlay(alignment: .topLeading) {
Expand Down Expand Up @@ -1202,12 +1208,12 @@
let separator = index == collaborators.count - 1
? " \(String(localized: "and")) "
: ", "
result = result + Text(separator).foregroundColor(.secondary)

Check failure on line 1211 in Sources/Kaset/Views/YouTube/YouTubeWatchView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Prefer shorthand operators (+=, -=, *=, /=) over doing the operation and assigning (shorthand_operator)

Check failure on line 1211 in Sources/Kaset/Views/YouTube/YouTubeWatchView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Use .foregroundStyle() instead of .foregroundColor() (no_foreground_color)
}
result = result + Text(collaborator.name).fontWeight(.semibold)

Check failure on line 1213 in Sources/Kaset/Views/YouTube/YouTubeWatchView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Prefer shorthand operators (+=, -=, *=, /=) over doing the operation and assigning (shorthand_operator)
if collaborator.isVerified {
result = result + Text(" ")
+ Text(Image(systemName: "checkmark.seal.fill")).foregroundColor(.secondary)

Check failure on line 1216 in Sources/Kaset/Views/YouTube/YouTubeWatchView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Use .foregroundStyle() instead of .foregroundColor() (no_foreground_color)
}
}
return result
Expand Down Expand Up @@ -1265,8 +1271,29 @@
}

private var relatedHeader: some View {
Text("Related", comment: "Related videos section header")
.font(.title3.bold())
HStack(alignment: .firstTextBaseline) {
Text("Related", comment: "Related videos section header")
.font(.title3.bold())
Spacer(minLength: 12)
self.autoplayToggle

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The autoplay switch is added to relatedHeader, but that header is also rendered for mix and playlist playback. Enabling this global setting while viewing a mix exposes the autoplay path to the mix queue, despite the feature contract that mixes and playlists should advance through their own page-drift queue. Hide or disable this control when mixVideos is non-empty. [api mismatch]

Severity Level: Major ⚠️
- ❌ Mix playback receives the unrelated Autoplay countdown.
- ❌ Mix tracks can advance through `confirmAutoplayNow()`.
- ⚠️ The control is shown whenever mix data is loaded.
Steps of Reproduction ✅
1. Open a watch page whose loaded data contains mix videos.
`YouTubeWatchView.relatedColumn` still renders `relatedHeader` at
`Sources/Kaset/Views/YouTube/YouTubeWatchView.swift:1255-1260`, and `relatedHeader` always
renders `self.autoplayToggle` at `YouTubeWatchView.swift:1273-1279`.

2. Enable that toggle. The setting is global and is persisted by
`SettingsManager.youtubeAutoplayEnabled` at
`Sources/Kaset/Services/SettingsManager.swift:576-580`.

3. For the mix page, `upNextQueue` returns the mix videos mapped with mix context at
`Sources/Kaset/Views/YouTube/YouTubeWatchView.swift:1239-1242`; `startOrAdoptPlayback()`
supplies that queue to `YouTubePlayerService.setUpNext()` at
`YouTubeWatchView.swift:413-416`.

4. Let the current mix video finish. `handleVideoEnded()` calls `autoplayNextIfEnabled()`
at `Sources/Kaset/Services/Player/YouTubePlayerService.swift:2003-2005`, which takes
`self.upNext.first` at `YouTubePlayerService.swift:2021-2024` and starts the autoplay
countdown.

5. The countdown overlay then offers confirmation and advances through `advance(to:)` at
`Sources/Kaset/Services/Player/YouTubePlayerService.swift:2061-2064`, so the mix advances
through the new autoplay path rather than remaining exclusively under its intended
page-drift queue.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** Sources/Kaset/Views/YouTube/YouTubeWatchView.swift
**Line:** 1278:1278
**Comment:**
	*Api Mismatch: The autoplay switch is added to `relatedHeader`, but that header is also rendered for mix and playlist playback. Enabling this global setting while viewing a mix exposes the autoplay path to the mix queue, despite the feature contract that mixes and playlists should advance through their own page-drift queue. Hide or disable this control when `mixVideos` is non-empty.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}
}

/// YouTube-style "Autoplay" switch: when on, a finished video advances to the
/// next suggested video. Off by default. Lives in the related header, like
/// YouTube's up-next autoplay toggle.
private var autoplayToggle: some View {
Toggle(isOn: Binding(
get: { self.settings.youtubeAutoplayEnabled },
set: { self.settings.youtubeAutoplayEnabled = $0 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Turning the toggle off only updates SettingsManager; it does not cancel an already-running countdown. The countdown task does not re-check this setting before calling confirmAutoplayNow(), so autoplay can still advance after the UI reports that autoplay is disabled. Cancel the pending countdown when the value changes to false, or make the service observe the setting before advancing. [logic error]

Severity Level: Major ⚠️
- ❌ Disabling Autoplay cannot stop an active countdown.
- ❌ The next suggested video starts against current user preference.
- ⚠️ The issue requires changing the setting during five seconds.
Steps of Reproduction ✅
1. Open a YouTube watch page and enable the Autoplay switch;
`YouTubeWatchView.autoplayToggle` writes only `settings.youtubeAutoplayEnabled` at
`Sources/Kaset/Views/YouTube/YouTubeWatchView.swift:1286-1288`.

2. Let the current video finish through
`YouTubeWatchWebView.Coordinator.handleVideoEnded()` at
`Sources/Kaset/Views/YouTube/YouTubeWatchWebView+Coordinator.swift:191-210`;
`YouTubePlayerService.handleVideoEnded()` then calls `autoplayNextIfEnabled()` at
`Sources/Kaset/Services/Player/YouTubePlayerService.swift:2003-2005`.

3. With a related candidate available, `startAutoplayCountdown(to:)` creates the
five-second countdown task at
`Sources/Kaset/Services/Player/YouTubePlayerService.swift:2017-2053`.

4. Turn the Autoplay switch off before the five seconds elapse. The setter at
`YouTubeWatchView.swift:1288` does not call `cancelAutoplay()`, and the countdown task
only checks the pending video at `YouTubePlayerService.swift:2049`; it does not re-check
the setting.

5. When the countdown reaches zero, `YouTubePlayerService.swift:2051-2052` calls
`confirmAutoplayNow()`, which advances to the queued video at
`YouTubePlayerService.swift:2061-2064` despite the UI now showing Autoplay disabled.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** Sources/Kaset/Views/YouTube/YouTubeWatchView.swift
**Line:** 1288:1288
**Comment:**
	*Logic Error: Turning the toggle off only updates `SettingsManager`; it does not cancel an already-running countdown. The countdown task does not re-check this setting before calling `confirmAutoplayNow()`, so autoplay can still advance after the UI reports that autoplay is disabled. Cancel the pending countdown when the value changes to false, or make the service observe the setting before advancing.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

)) {
Text("Autoplay", comment: "Toggle: automatically play the next suggested video")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(.secondary)
}
.toggleStyle(.switch)
.controlSize(.mini)
.fixedSize()
}

private var relatedSkeleton: some View {
Expand Down Expand Up @@ -2358,4 +2385,4 @@
}
)
}
}

Check failure on line 2388 in Sources/Kaset/Views/YouTube/YouTubeWatchView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

File should contain 1500 lines or less: currently contains 2388 (file_length)
Loading
Loading