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)
This commit is contained in:
AmriteshKhare 2026-03-09 14:25:01 +05:30
commit 2dae54b6d7

View file

@ -71,6 +71,10 @@ class MusicManager: ObservableObject {
// Active controller // Active controller
private var activeController: (any MediaControllerProtocol)? 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 properties for UI
@Published var songTitle: String = "I'm Handsome" @Published var songTitle: String = "I'm Handsome"
@Published var artistName: String = "Me" @Published var artistName: String = "Me"
@ -126,10 +130,14 @@ class MusicManager: ObservableObject {
// Listen for changes to the default controller preference // Listen for changes to the default controller preference
NotificationCenter.default.publisher(for: Notification.Name.mediaControllerChanged) NotificationCenter.default.publisher(for: Notification.Name.mediaControllerChanged)
.sink { [weak self] _ in .sink { [weak self] _ in
self?.isPearDesktopAutoSwitched = false
self?.setActiveControllerBasedOnPreference() self?.setActiveControllerBasedOnPreference()
} }
.store(in: &cancellables) .store(in: &cancellables)
// Observe Pear Desktop launch/terminate for auto-detection
setupPearDesktopAutoDetection()
// Initialize deprecation check asynchronously // Initialize deprecation check asynchronously
Task { @MainActor in Task { @MainActor in
do { do {
@ -140,11 +148,55 @@ class MusicManager: ObservableObject {
self.isNowPlayingDeprecated = false self.isNowPlayingDeprecated = false
} }
// Initialize the active controller after deprecation check // Check if Pear Desktop is already running at startup
self.setActiveControllerBasedOnPreference() 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 { deinit {
destroy() destroy()
} }