From 2dae54b6d775f075d7b6c72faebb1eab063c43a2 Mon Sep 17 00:00:00 2001 From: AmriteshKhare Date: Mon, 9 Mar 2026 14:25:01 +0530 Subject: [PATCH] feat(media): auto-detect Pear Desktop for seamless YouTube Music sync When Pear Desktop (com.github.th-ch.youtube-music) is running, MusicManager now automatically switches to YouTubeMusicController for seamless media sync without requiring manual settings changes. Changes: - Added NSWorkspace notification observers for app launch/terminate - Auto-switches to YouTubeMusicController when Pear Desktop launches - Reverts to user's preferred controller when Pear Desktop terminates - Checks for Pear Desktop at startup - Respects manual settings changes (won't override user preference) --- DynamicIsland/managers/MusicManager.swift | 56 ++++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift index 418e01b..b4d7363 100644 --- a/DynamicIsland/managers/MusicManager.swift +++ b/DynamicIsland/managers/MusicManager.swift @@ -71,6 +71,10 @@ class MusicManager: ObservableObject { // Active controller private var activeController: (any MediaControllerProtocol)? + // Pear Desktop auto-detection + private static let pearDesktopBundleID = YouTubeMusicConfiguration.default.bundleIdentifier + private var isPearDesktopAutoSwitched: Bool = false + // Published properties for UI @Published var songTitle: String = "I'm Handsome" @Published var artistName: String = "Me" @@ -126,10 +130,14 @@ class MusicManager: ObservableObject { // Listen for changes to the default controller preference NotificationCenter.default.publisher(for: Notification.Name.mediaControllerChanged) .sink { [weak self] _ in + self?.isPearDesktopAutoSwitched = false self?.setActiveControllerBasedOnPreference() } .store(in: &cancellables) + // Observe Pear Desktop launch/terminate for auto-detection + setupPearDesktopAutoDetection() + // Initialize deprecation check asynchronously Task { @MainActor in do { @@ -140,11 +148,55 @@ class MusicManager: ObservableObject { self.isNowPlayingDeprecated = false } - // Initialize the active controller after deprecation check - self.setActiveControllerBasedOnPreference() + // Check if Pear Desktop is already running at startup + let pearDesktopRunning = NSWorkspace.shared.runningApplications.contains { + $0.bundleIdentifier == Self.pearDesktopBundleID + } + + if pearDesktopRunning { + print("[MusicManager] Pear Desktop detected at startup, auto-switching to YouTubeMusicController") + self.isPearDesktopAutoSwitched = true + if let controller = self.createController(for: .youtubeMusic) { + self.setActiveController(controller) + } + } else { + // Initialize the active controller after deprecation check + self.setActiveControllerBasedOnPreference() + } } } + // MARK: - Pear Desktop Auto-Detection + private func setupPearDesktopAutoDetection() { + NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didLaunchApplicationNotification) + .sink { [weak self] notification in + guard let self = self, + let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, + app.bundleIdentifier == Self.pearDesktopBundleID else { return } + + print("[MusicManager] Pear Desktop launched, auto-switching to YouTubeMusicController") + self.isPearDesktopAutoSwitched = true + if let controller = self.createController(for: .youtubeMusic) { + self.setActiveController(controller) + } + } + .store(in: &cancellables) + + NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didTerminateApplicationNotification) + .sink { [weak self] notification in + guard let self = self, + let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, + app.bundleIdentifier == Self.pearDesktopBundleID else { return } + + print("[MusicManager] Pear Desktop terminated, reverting to preferred controller") + if self.isPearDesktopAutoSwitched { + self.isPearDesktopAutoSwitched = false + self.setActiveControllerBasedOnPreference() + } + } + .store(in: &cancellables) + } + deinit { destroy() }