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: 21 additions & 1 deletion Sources/Kaset/Views/MiniPlayerWebView+Coordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ extension SingletonPlayerWebView {
return decoded
}

/// Sanitizes the thumbnail URL reported by the playback bridge.
///
/// `img.src` resolves relative to the document, so a player-bar image that has not
/// yet been assigned a source reports the YouTube Music page URL. That value fetches
/// successfully as HTML and then fails to decode, so it must never reach `currentTrack`.
nonisolated static func playbackBridgeThumbnailURLString(from value: Any?) -> String {
guard let raw = (value as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty,
let components = URLComponents(string: raw),
let scheme = components.scheme?.lowercased(),
scheme == "https" || scheme == "http",
let host = components.host,
!host.isEmpty,
!components.path.isEmpty,
components.path != "/"
else { return "" }

return raw
}

// MARK: - Coordinator

final class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
Expand Down Expand Up @@ -601,7 +621,7 @@ private extension SingletonPlayerWebView.Coordinator {
let hasReadyMedia = body["hasReadyMedia"] as? Bool ?? false
let title = body["title"] as? String ?? ""
let artist = body["artist"] as? String ?? ""
let thumbnailUrl = body["thumbnailUrl"] as? String ?? ""
let thumbnailUrl = SingletonPlayerWebView.playbackBridgeThumbnailURLString(from: body["thumbnailUrl"])
let trackChanged = body["trackChanged"] as? Bool ?? false
let likeStatus = Self.likeStatus(from: body["likeStatus"] as? String)
let hasVideo = body["hasVideo"] as? Bool ?? false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,11 @@ extension SingletonPlayerWebView {

// Get the thumbnail URL from the image element
if (thumbEl) {
thumbnailUrl = thumbEl.src || thumbEl.getAttribute('src') || '';
// `img.src` resolves against the document base URL, so an empty or
// missing attribute reports the YouTube Music page URL instead of ''.
// Gate on the literal attribute before trusting the resolved value.
const rawThumbSrc = (thumbEl.getAttribute('src') || '').trim();
thumbnailUrl = rawThumbSrc ? (thumbEl.src || rawThumbSrc) : '';
}

// Extract like status from the like button renderer
Expand Down
24 changes: 24 additions & 0 deletions Tests/KasetTests/AutoplayRecoveryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -778,4 +778,28 @@ struct MusicPlaybackBridgeDecodingTests {
#expect(SingletonPlayerWebView.finitePlaybackBridgeDouble(from: true) == nil)
#expect(SingletonPlayerWebView.finitePlaybackBridgeDouble(from: "1.75") == nil)
}

@Test("Playback bridge rejects the document URL reported by an unset thumbnail element")
func rejectsDocumentURLThumbnail() {
#expect(SingletonPlayerWebView.playbackBridgeThumbnailURLString(from: "https://music.youtube.com/").isEmpty)
#expect(SingletonPlayerWebView.playbackBridgeThumbnailURLString(from: "https://music.youtube.com").isEmpty)
}

@Test("Playback bridge rejects empty and malformed thumbnail sources")
func rejectsInvalidThumbnails() {
#expect(SingletonPlayerWebView.playbackBridgeThumbnailURLString(from: "").isEmpty)
#expect(SingletonPlayerWebView.playbackBridgeThumbnailURLString(from: " ").isEmpty)
#expect(SingletonPlayerWebView.playbackBridgeThumbnailURLString(from: nil).isEmpty)
#expect(SingletonPlayerWebView.playbackBridgeThumbnailURLString(from: 42).isEmpty)
#expect(SingletonPlayerWebView.playbackBridgeThumbnailURLString(from: "data:image/png;base64,AAAA").isEmpty)
}

@Test("Playback bridge preserves real artwork sources")
func preservesArtworkThumbnails() {
let artwork = "https://lh3.googleusercontent.com/abc=w544-h544-l90-rj"
#expect(SingletonPlayerWebView.playbackBridgeThumbnailURLString(from: artwork) == artwork)

let fallback = "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg"
#expect(SingletonPlayerWebView.playbackBridgeThumbnailURLString(from: fallback) == fallback)
}
}
Loading