From 52fddc89ce49eefb68a171147210506ef09a8b7e Mon Sep 17 00:00:00 2001 From: Nathanial Henniges <19924836+nathanialhenniges@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:49:26 -0500 Subject: [PATCH 1/5] feat: add multi-source music support for v1.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract PlaybackSource protocol + PlaybackSourceMode enum - Refactor MusicPlaybackMonitor → AppleMusicSource (same logic, new abstraction) - Add SystemNowPlayingSource using private MediaRemote framework (dlopen/dlsym) captures playback from Spotify, browsers, and any system Now Playing app - Add PlaybackSourceManager to switch sources and forward delegate callbacks - Add Music Source segmented picker to MusicMonitorSettingsView (Apple Events warning hidden when System mode is selected) - Update AppDelegate to use PlaybackSourceManager; Apple Music remains default - Add AppConstants for playbackSourceMode, PlaybackSourceModeChanged, SystemNowPlaying - Add PlaybackSourceManagerTests and SystemNowPlayingSourceTests (693 tests, 0 failures) - Add FEATURE_IDEAS.md with 8 future feature ideas - Sync docs changelog with v1.0.2, v1.1.0, and v1.2.0 entries - Bump version to 1.2.0, build number to 5 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 6 + FEATURE_IDEAS.md | 75 +++++++ apps/docs/content/docs/changelog.mdx | 24 +++ .../MusicPlaybackMonitorTests.swift | 8 +- .../PlaybackSourceManagerTests.swift | 122 +++++++++++ .../SystemNowPlayingSourceTests.swift | 84 ++++++++ .../native/wolfwave.xcodeproj/project.pbxproj | 16 +- apps/native/wolfwave/Core/AppConstants.swift | 16 ++ ...ckMonitor.swift => AppleMusicSource.swift} | 162 +++----------- .../wolfwave/Monitors/PlaybackSource.swift | 34 +++ .../Monitors/PlaybackSourceManager.swift | 86 ++++++++ .../Monitors/SystemNowPlayingSource.swift | 202 ++++++++++++++++++ .../MusicMonitorSettingsView.swift | 40 +++- apps/native/wolfwave/WolfWaveApp.swift | 26 +-- 14 files changed, 744 insertions(+), 157 deletions(-) create mode 100644 FEATURE_IDEAS.md create mode 100644 apps/native/WolfWaveTests/PlaybackSourceManagerTests.swift create mode 100644 apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift rename apps/native/wolfwave/Monitors/{MusicPlaybackMonitor.swift => AppleMusicSource.swift} (57%) create mode 100644 apps/native/wolfwave/Monitors/PlaybackSource.swift create mode 100644 apps/native/wolfwave/Monitors/PlaybackSourceManager.swift create mode 100644 apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index c22de94..fd207d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [1.2.0] - 2026-04-01 + +### Added + +- **Multi-source music support** — choose between Apple Music (direct ScriptingBridge connection) and Any App (System), which uses the macOS system Now Playing API to capture playback from Spotify, browsers, and any other app. Setting lives in Music Monitor preferences. + ## [1.1.0] - 2026-03-31 ### Added diff --git a/FEATURE_IDEAS.md b/FEATURE_IDEAS.md new file mode 100644 index 0000000..c288e40 --- /dev/null +++ b/FEATURE_IDEAS.md @@ -0,0 +1,75 @@ +# WolfWave — Feature Ideas + +Future feature ideas for WolfWave. These are not committed to any release — just a running list of things worth building. + +--- + +## Song Request Queue + +Let Twitch viewers request songs via `!request `. Requests appear in a queue visible from the menu bar. Streamer can approve, skip, or clear. + +**Complexity**: Medium +**Builds on**: Existing `BotCommand` protocol, `TwitchChatService` + +--- + +## Listening History & Stats + +Track play counts, listening time, and most-played artists locally. Store in a lightweight SQLite database. Add a "Stats" section to the settings sidebar. + +**Complexity**: Medium +**Builds on**: `MusicPlaybackMonitor` delegate callbacks, existing settings sidebar pattern + +--- + +## Custom Bot Commands + +Let users define their own Twitch bot command responses via the settings UI. For example: `!dj` → "DJ WolfWave in the house 🎧". Stored in UserDefaults as a key-value map. + +**Complexity**: Low +**Builds on**: `BotCommand` protocol, `BotCommandDispatcher.registerDefaultCommands()` + +--- + +## Overlay Themes + +Ship multiple pre-built WebSocket overlay themes (minimal, retro, neon, glassmorphism). Users pick a theme in settings. Overlay clients receive the theme name and render accordingly. + +**Complexity**: Medium +**Builds on**: Existing WebSocket server, browser source overlay + +--- + +## Global Hotkeys + +Global keyboard shortcuts to toggle tracking, skip song, or show/hide the overlay — even when WolfWave is in the background. + +**Complexity**: Low–Medium +**Approach**: `CGEvent` tap or a lightweight hotkey library + +--- + +## Last.fm Scrobbling + +Optional Last.fm integration to automatically scrobble tracks as they play. Uses Last.fm API with OAuth device flow (same pattern as Twitch auth). + +**Complexity**: Medium +**Builds on**: `TwitchDeviceAuth` OAuth pattern, `KeychainService` + +--- + +## Menu Bar Now-Playing Ticker + +Optionally scroll the current track name in the macOS menu bar status item, like a marquee. Configurable max width. Falls back to static icon when nothing is playing. + +**Complexity**: Low +**Builds on**: Existing `NSStatusItem` setup in `AppDelegate` + +--- + +## Multi-Platform Chat Support + +Extend the bot beyond Twitch to YouTube Live chat and Kick. Same `BotCommand` protocol, different transport layer per platform. + +**Complexity**: High +**Builds on**: `BotCommand` protocol, `BotCommandDispatcher` diff --git a/apps/docs/content/docs/changelog.mdx b/apps/docs/content/docs/changelog.mdx index 8e6a246..951d73f 100644 --- a/apps/docs/content/docs/changelog.mdx +++ b/apps/docs/content/docs/changelog.mdx @@ -7,6 +7,30 @@ description: Release history for WolfWave All notable changes to WolfWave are documented here. +## v1.2.0 — April 1, 2026 + +### Added +- **Multi-source music support** — choose between Apple Music (direct, most accurate) and Any App (System), which picks up whatever's playing — Spotify, browsers, anything. Switch in the Music Monitor settings. + +## v1.1.0 — March 31, 2026 + +### Added +- **Discord buttons** — Rich Presence now shows two clickable buttons: Open in Apple Music (direct track link) and song.link (opens on Spotify, YouTube Music, Tidal, and more) +- **Launch at Login** — new toggle in Settings → App Visibility. Uses SMAppService, appears in System Settings → General → Login Items +- **Custom DMG background** — installer window has a polished dark background with WolfWave brand colors +- **Homebrew auto-update** — GitHub Actions now automatically opens a pull request on the Homebrew tap on new releases + +### Fixed +- iTunes Search API URL encoding — track/artist names with &, +, or = no longer break artwork lookups +- Launch at Login toggle now reverts if SMAppService registration fails + +## v1.0.2 — March 31, 2026 + +### Fixed +- App icon missing in CI-built releases +- Sparkle updater unable to detect new versions (build number now incremented per release) +- Sparkle initialization race condition fixed + ## v1.0.1 — March 30, 2026 ### Changed diff --git a/apps/native/WolfWaveTests/MusicPlaybackMonitorTests.swift b/apps/native/WolfWaveTests/MusicPlaybackMonitorTests.swift index 1833071..a4b3242 100644 --- a/apps/native/WolfWaveTests/MusicPlaybackMonitorTests.swift +++ b/apps/native/WolfWaveTests/MusicPlaybackMonitorTests.swift @@ -1,5 +1,5 @@ // -// MusicPlaybackMonitorTests.swift +// AppleMusicSourceTests.swift // WolfWaveTests // // Created by MrDemonWolf, Inc. on 2/27/26. @@ -8,12 +8,12 @@ import XCTest @testable import WolfWave -final class MusicPlaybackMonitorTests: XCTestCase { - var monitor: MusicPlaybackMonitor! +final class AppleMusicSourceTests: XCTestCase { + var monitor: AppleMusicSource! override func setUp() { super.setUp() - monitor = MusicPlaybackMonitor() + monitor = AppleMusicSource() } override func tearDown() { diff --git a/apps/native/WolfWaveTests/PlaybackSourceManagerTests.swift b/apps/native/WolfWaveTests/PlaybackSourceManagerTests.swift new file mode 100644 index 0000000..6c59473 --- /dev/null +++ b/apps/native/WolfWaveTests/PlaybackSourceManagerTests.swift @@ -0,0 +1,122 @@ +// +// PlaybackSourceManagerTests.swift +// WolfWaveTests + +import XCTest +@testable import WolfWave + +final class PlaybackSourceManagerTests: XCTestCase { + + override func setUp() { + super.setUp() + UserDefaults.standard.removeObject(forKey: "playbackSourceMode") + } + + override func tearDown() { + UserDefaults.standard.removeObject(forKey: "playbackSourceMode") + super.tearDown() + } + + // MARK: - Default Mode + + func testDefaultModeIsAppleMusic() { + let manager = PlaybackSourceManager() + XCTAssertEqual(manager.currentMode, .appleMusic) + } + + func testDefaultModePersistedModeIsRestored() { + UserDefaults.standard.set("systemNowPlaying", forKey: "playbackSourceMode") + let manager = PlaybackSourceManager() + XCTAssertEqual(manager.currentMode, .systemNowPlaying) + } + + func testInvalidPersistedModeFallsBackToAppleMusic() { + UserDefaults.standard.set("invalidMode", forKey: "playbackSourceMode") + let manager = PlaybackSourceManager() + XCTAssertEqual(manager.currentMode, .appleMusic) + } + + // MARK: - Mode Switching + + func testSwitchModeUpdatesCurrentMode() { + let manager = PlaybackSourceManager() + manager.switchMode(.systemNowPlaying) + XCTAssertEqual(manager.currentMode, .systemNowPlaying) + } + + func testSwitchModeToSameModeIsNoOp() { + let manager = PlaybackSourceManager() + // Should not crash or change anything + manager.switchMode(.appleMusic) + XCTAssertEqual(manager.currentMode, .appleMusic) + } + + func testSwitchModePersistsToUserDefaults() { + let manager = PlaybackSourceManager() + manager.switchMode(.systemNowPlaying) + let stored = UserDefaults.standard.string(forKey: "playbackSourceMode") + XCTAssertEqual(stored, "systemNowPlaying") + } + + // MARK: - Delegate Forwarding + + func testDelegateReceivesTrackUpdate() { + let manager = PlaybackSourceManager() + let spy = PlaybackSourceDelegateSpy() + manager.delegate = spy + + // Simulate a source calling back into the manager + manager.playbackSource(MockPlaybackSource(), didUpdateTrack: "Song", artist: "Artist", album: "Album", duration: 200, elapsed: 30) + + XCTAssertTrue(spy.didReceiveTrackUpdate) + XCTAssertEqual(spy.lastTrack, "Song") + XCTAssertEqual(spy.lastArtist, "Artist") + } + + func testDelegateReceivesStatusUpdate() { + let manager = PlaybackSourceManager() + let spy = PlaybackSourceDelegateSpy() + manager.delegate = spy + + manager.playbackSource(MockPlaybackSource(), didUpdateStatus: "No track playing") + + XCTAssertTrue(spy.didReceiveStatusUpdate) + XCTAssertEqual(spy.lastStatus, "No track playing") + } + + // MARK: - updateCheckInterval + + func testUpdateCheckIntervalDoesNotCrashWhenNotTracking() { + let manager = PlaybackSourceManager() + // Should not crash when no active source + manager.updateCheckInterval(10.0) + } +} + +// MARK: - Test Helpers + +private class PlaybackSourceDelegateSpy: PlaybackSourceDelegate { + var didReceiveTrackUpdate = false + var lastTrack: String? + var lastArtist: String? + var didReceiveStatusUpdate = false + var lastStatus: String? + + func playbackSource(_ source: any PlaybackSource, didUpdateTrack track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval) { + didReceiveTrackUpdate = true + lastTrack = track + lastArtist = artist + } + + func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) { + didReceiveStatusUpdate = true + lastStatus = status + } +} + +private class MockPlaybackSource: PlaybackSource { + weak var delegate: PlaybackSourceDelegate? + func startTracking() {} + func stopTracking() {} + func updateCheckInterval(_ interval: TimeInterval) {} +} diff --git a/apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift b/apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift new file mode 100644 index 0000000..86f6e6e --- /dev/null +++ b/apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift @@ -0,0 +1,84 @@ +// +// SystemNowPlayingSourceTests.swift +// WolfWaveTests + +import XCTest +@testable import WolfWave + +final class SystemNowPlayingSourceTests: XCTestCase { + + // MARK: - Initialization + + func testInitializationDoesNotCrash() { + // MediaRemote may or may not be available in the test environment + let source = SystemNowPlayingSource() + XCTAssertNotNil(source) + } + + func testConformsToPlaybackSource() { + let source = SystemNowPlayingSource() + XCTAssertTrue((source as AnyObject) is any PlaybackSource) + } + + // MARK: - Start / Stop + + func testStartTrackingDoesNotCrash() { + let source = SystemNowPlayingSource() + source.startTracking() + source.stopTracking() + } + + func testDoubleStartIsIdempotent() { + let source = SystemNowPlayingSource() + source.startTracking() + source.startTracking() // second call should be a no-op + source.stopTracking() + } + + func testDoubleStopIsIdempotent() { + let source = SystemNowPlayingSource() + source.startTracking() + source.stopTracking() + source.stopTracking() // second call should be a no-op + } + + // MARK: - Interval + + func testUpdateCheckIntervalDoesNotCrashWhenNotTracking() { + let source = SystemNowPlayingSource() + source.updateCheckInterval(10.0) + } + + func testUpdateCheckIntervalDoesNotCrashWhenTracking() { + let source = SystemNowPlayingSource() + source.startTracking() + source.updateCheckInterval(10.0) + source.stopTracking() + } + + // MARK: - Graceful Degradation + + func testDelegateIsNotifiedIfFrameworkUnavailable() { + // We can't force dlopen to fail, but we can verify the delegate interface works + let source = SystemNowPlayingSource() + let spy = StatusSpy() + source.delegate = spy + // If framework loaded, startTracking works silently. + // If it didn't load, delegate should have been called with an error status synchronously in init (not tested here). + // Just verify no crash. + source.startTracking() + source.stopTracking() + } +} + +// MARK: - Helpers + +private class StatusSpy: PlaybackSourceDelegate { + var statuses: [String] = [] + + func playbackSource(_ source: any PlaybackSource, didUpdateTrack track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval) {} + + func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) { + statuses.append(status) + } +} diff --git a/apps/native/wolfwave.xcodeproj/project.pbxproj b/apps/native/wolfwave.xcodeproj/project.pbxproj index d859531..f0bb98a 100644 --- a/apps/native/wolfwave.xcodeproj/project.pbxproj +++ b/apps/native/wolfwave.xcodeproj/project.pbxproj @@ -363,7 +363,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = HBB7T99U79; ENABLE_APP_SANDBOX = YES; @@ -394,7 +394,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 26.0; - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.mrdemonwolf.wolfwave.dev; PRODUCT_MODULE_NAME = WolfWave; PRODUCT_NAME = "WolfWave Dev"; @@ -424,7 +424,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = HBB7T99U79; ENABLE_APP_SANDBOX = YES; @@ -455,7 +455,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 26.0; - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.mrdemonwolf.wolfwave; PRODUCT_NAME = WolfWave; REGISTER_APP_GROUPS = YES; @@ -479,12 +479,12 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = HBB7T99U79; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.mrdemonwolf.wolfwave.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -501,12 +501,12 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = HBB7T99U79; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; - MARKETING_VERSION = 1.1.0; + MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.mrdemonwolf.wolfwave.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_APPROACHABLE_CONCURRENCY = YES; diff --git a/apps/native/wolfwave/Core/AppConstants.swift b/apps/native/wolfwave/Core/AppConstants.swift index eece069..8798072 100644 --- a/apps/native/wolfwave/Core/AppConstants.swift +++ b/apps/native/wolfwave/Core/AppConstants.swift @@ -65,6 +65,9 @@ enum AppConstants { /// Posted when Twitch chat connection state changes. UserInfo contains "isConnected" Bool. static let twitchConnectionStateChanged = "TwitchChatConnectionStateChanged" + + /// Posted when the user changes the playback source mode. + static let playbackSourceModeChanged = "PlaybackSourceModeChanged" } // MARK: - UserDefaults Keys @@ -151,6 +154,9 @@ enum AppConstants { /// Whether the widget HTTP server is enabled (Bool, default: false) static let widgetHTTPEnabled = "widgetHTTPEnabled" + + /// The user's chosen playback source mode: "appleMusic" or "systemNowPlaying" (String, default: "appleMusic") + static let playbackSourceMode = "playbackSourceMode" } // MARK: - Dock Visibility Modes @@ -388,6 +394,8 @@ enum AppConstants { /// Queue for WebSocket server operations static let websocketServer = "com.mrdemonwolf.wolfwave.websocketserver" + + static let systemNowPlaying = "com.mrdemonwolf.wolfwave.systemnowplaying" } // MARK: - UI Dimensions @@ -476,6 +484,14 @@ enum AppConstants { static let reducedProgressBroadcastInterval: TimeInterval = 3.0 } + // MARK: - System Now Playing + + /// Constants for the System Now Playing (MediaRemote) source. + enum SystemNowPlaying { + static let frameworkPath = "/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemote" + static let nowPlayingInfoDidChangeNotification = "kMRMediaRemoteNowPlayingInfoDidChangeNotification" + } + // MARK: - Onboarding UI /// Onboarding wizard window configuration. diff --git a/apps/native/wolfwave/Monitors/MusicPlaybackMonitor.swift b/apps/native/wolfwave/Monitors/AppleMusicSource.swift similarity index 57% rename from apps/native/wolfwave/Monitors/MusicPlaybackMonitor.swift rename to apps/native/wolfwave/Monitors/AppleMusicSource.swift index d04dd23..9edc605 100644 --- a/apps/native/wolfwave/Monitors/MusicPlaybackMonitor.swift +++ b/apps/native/wolfwave/Monitors/AppleMusicSource.swift @@ -1,61 +1,27 @@ -// -// MusicPlaybackMonitor.swift -// wolfwave -// -// Created by MrDemonWolf, Inc. on 1/17/26. -// - import Foundation import AppKit import ScriptingBridge -/// Delegate protocol for receiving music playback updates. -protocol MusicPlaybackMonitorDelegate: AnyObject { - /// Called when a new track starts playing. - func musicPlaybackMonitor(_ monitor: MusicPlaybackMonitor, didUpdateTrack track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval) - - /// Called when the playback status changes (not running, not playing, etc.). - func musicPlaybackMonitor(_ monitor: MusicPlaybackMonitor, didUpdateStatus status: String) -} +class AppleMusicSource: PlaybackSource { -/// Monitors Apple Music playback using ScriptingBridge and distributed notifications. -/// -/// Uses ScriptingBridge to communicate with Music.app directly without spawning osascript. -/// Subscribes to distributed notifications for real-time updates. -/// Delegate callbacks are delivered on the main thread. -/// -/// Usage: -/// ```swift -/// let monitor = MusicPlaybackMonitor() -/// monitor.delegate = self -/// monitor.startTracking() -/// ``` -/// -/// Requirements: -/// - Entitlements: `com.apple.security.automation.apple-events` -/// - Info.plist: `NSAppleEventsUsageDescription` -class MusicPlaybackMonitor { - private enum Constants { static let musicBundleIdentifier = "com.apple.Music" static let notificationName = "com.apple.Music.playerInfo" static let queueLabel = "com.mrdemonwolf.wolfwave.musicplaybackmonitor" - // Fallback polling interval. Distributed notifications handle real-time - // changes; this only catches missed events, so 5s is sufficient. static let checkInterval: TimeInterval = 5.0 static let trackSeparator = " | " static let notificationDedupWindow: TimeInterval = 0.75 static let idleGraceWindow: TimeInterval = 2.0 static let playerStatePlaying: UInt32 = 1800426320 - + enum Status { static let notRunning = "NOT_RUNNING" static let notPlaying = "NOT_PLAYING" static let errorPrefix = "ERROR:" } } - - weak var delegate: MusicPlaybackMonitorDelegate? + + weak var delegate: PlaybackSourceDelegate? private var currentCheckInterval: TimeInterval = Constants.checkInterval private var timer: DispatchSourceTimer? @@ -65,37 +31,25 @@ class MusicPlaybackMonitor { private var isTracking = false private var pendingDuration: TimeInterval = 0 private var pendingElapsed: TimeInterval = 0 - - private let backgroundQueue = DispatchQueue( - label: Constants.queueLabel, - qos: .utility - ) - + + private let backgroundQueue = DispatchQueue(label: Constants.queueLabel, qos: .utility) + func startTracking() { guard !isTracking else { return } isTracking = true - subscribeToMusicNotifications() performInitialTrackCheck() setupFallbackTimer() } - + func stopTracking() { - guard isTracking else { - return - } + guard isTracking else { return } DistributedNotificationCenter.default().removeObserver(self) timer?.cancel() timer = nil isTracking = false } - /// Updates the fallback polling interval and reschedules the timer. - /// - /// Distributed notifications still provide real-time updates; this only - /// affects how often the fallback timer fires to catch missed events. - /// - /// - Parameter interval: New polling interval in seconds. func updateCheckInterval(_ interval: TimeInterval) { guard isTracking else { return } currentCheckInterval = max(interval, 1.0) @@ -103,27 +57,21 @@ class MusicPlaybackMonitor { timer = nil setupFallbackTimer() } - + @objc private func musicPlayerInfoChanged(_ notification: Notification) { let now = Date() - guard now.timeIntervalSince(lastNotificationAt) >= Constants.notificationDedupWindow else { - return - } + guard now.timeIntervalSince(lastNotificationAt) >= Constants.notificationDedupWindow else { return } lastNotificationAt = now - Log.debug("MusicPlaybackMonitor: Music notification received", category: "Music") + Log.debug("AppleMusicSource: Music notification received", category: "Music") scheduleTrackCheck(reason: "notification") } - + private func checkCurrentTrack() { - let isRunning = NSRunningApplication - .runningApplications(withBundleIdentifier: Constants.musicBundleIdentifier) - .first != nil - + let isRunning = NSRunningApplication.runningApplications(withBundleIdentifier: Constants.musicBundleIdentifier).first != nil guard isRunning else { handleTrackInfo(Constants.Status.notRunning) return } - guard let musicApp = SBApplication(bundleIdentifier: Constants.musicBundleIdentifier) else { notifyDelegate(status: "No track info") return @@ -132,17 +80,12 @@ class MusicPlaybackMonitor { notifyDelegate(status: "No track info") return } - - // Step 4: Check if currently playing - // playerState returns a FourCharCode: 'kPSP' (0x6b505350 = 1800426320) = playing let isPlaying: Bool if let stateNum = stateObj as? NSNumber { isPlaying = (stateNum.uint32Value == Constants.playerStatePlaying) } else { isPlaying = false } - - // Step 5: If playing, retrieve track information if isPlaying { if let track = musicApp.value(forKey: "currentTrack") as? SBObject { let name = track.value(forKey: "name") as? String ?? "" @@ -150,7 +93,6 @@ class MusicPlaybackMonitor { let album = track.value(forKey: "album") as? String ?? "" let duration = (track.value(forKey: "duration") as? Double) ?? 0 let elapsed = (musicApp.value(forKey: "playerPosition") as? Double) ?? 0 - let combined = name + Constants.trackSeparator + artist + Constants.trackSeparator + album + Constants.trackSeparator + String(duration) + Constants.trackSeparator + String(elapsed) handleTrackInfo(combined) } else { @@ -160,46 +102,34 @@ class MusicPlaybackMonitor { handleTrackInfo(Constants.Status.notPlaying) } } - - // MARK: - Delegate Notifications - - /// Notifies the delegate of a status change on the main thread. + private func notifyDelegate(status: String) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } - self.delegate?.musicPlaybackMonitor(self, didUpdateStatus: status) + self.delegate?.playbackSource(self, didUpdateStatus: status) } } - - /// Notifies the delegate of a track change on the main thread. + private func notifyDelegate(track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } - self.delegate?.musicPlaybackMonitor(self, didUpdateTrack: track, artist: artist, album: album, duration: duration, elapsed: elapsed) + self.delegate?.playbackSource(self, didUpdateTrack: track, artist: artist, album: album, duration: duration, elapsed: elapsed) } } - - // MARK: - Track Info Processing - - /// Processes track info string received from Music.app and notifies the delegate. + private func processTrackInfoString(_ trackInfo: String) { let components = trackInfo.components(separatedBy: Constants.trackSeparator) - guard components.count >= 3 else { - return - } - + guard components.count >= 3 else { return } let trackName = components[0] let artist = components[1] let album = components[2] let duration = components.count > 3 ? (Double(components[3]) ?? 0) : 0 let elapsed = components.count > 4 ? (Double(components[4]) ?? 0) : 0 - lastTrackSeenAt = Date() notifyDelegate(track: trackName, artist: artist, album: album, duration: duration, elapsed: elapsed) logTrackIfNew(trackInfo, trackName: trackName, artist: artist, album: album) } - - /// Handles track info string and routes to appropriate handler based on status. + private func handleTrackInfo(_ trackInfo: String) { if trackInfo.hasPrefix(Constants.Status.errorPrefix) { notifyDelegate(status: "Script error") @@ -211,8 +141,7 @@ class MusicPlaybackMonitor { processTrackInfoString(trackInfo) } } - - /// Handles the "not playing" state with grace period to avoid transient stops. + private func handleNotPlayingState() { let idleDuration = Date().timeIntervalSince(lastTrackSeenAt) if idleDuration < Constants.idleGraceWindow { @@ -221,36 +150,22 @@ class MusicPlaybackMonitor { } notifyDelegate(status: "No track playing") } - - /// Logs track information if it's different from the last logged track. + private func logTrackIfNew(_ trackInfo: String, trackName: String, artist: String, album: String) { let dedupKey = trackName + Constants.trackSeparator + artist + Constants.trackSeparator + album guard lastLoggedTrack != dedupKey else { return } - Log.debug("MusicPlaybackMonitor: Now Playing → \(trackName) — \(artist) [\(album)]", category: "Music") + Log.debug("AppleMusicSource: Now Playing → \(trackName) — \(artist) [\(album)]", category: "Music") lastLoggedTrack = dedupKey } - - // MARK: - Setup & Scheduling - /// Subscribes to distributed notifications from Music.app. private func subscribeToMusicNotifications() { - DistributedNotificationCenter.default().addObserver( - self, - selector: #selector(musicPlayerInfoChanged), - name: NSNotification.Name(Constants.notificationName), - object: nil - ) + DistributedNotificationCenter.default().addObserver(self, selector: #selector(musicPlayerInfoChanged), name: NSNotification.Name(Constants.notificationName), object: nil) } - - /// Performs an initial track check when monitoring starts. + private func performInitialTrackCheck() { scheduleTrackCheck(reason: "initial") } - - /// Sets up a fallback timer that periodically checks track status. - /// - /// The timer acts as a fallback in case distributed notifications are missed. - /// It fires every `checkInterval` seconds on the background queue. + private func setupFallbackTimer() { let timer = DispatchSource.makeTimerSource(queue: backgroundQueue) timer.schedule(deadline: .now() + currentCheckInterval, repeating: currentCheckInterval) @@ -262,28 +177,13 @@ class MusicPlaybackMonitor { self.timer = timer } - /// Schedules an immediate track check on the background queue. - /// - Parameter reason: A descriptive reason for logging purposes (e.g., "notification", "timer"). private func scheduleTrackCheck(reason: String) { - backgroundQueue.async { [weak self] in - self?.checkCurrentTrack() - } + backgroundQueue.async { [weak self] in self?.checkCurrentTrack() } } - /// Schedules a delayed track check on the background queue. - /// - Parameters: - /// - delay: The delay in seconds before executing the check. - /// - reason: A descriptive reason for logging purposes (e.g., "idle-grace-recheck"). private func scheduleTrackCheck(after delay: TimeInterval, reason: String) { - backgroundQueue.asyncAfter(deadline: .now() + delay) { [weak self] in - self?.checkCurrentTrack() - } - } - - // MARK: - Lifecycle - - /// Automatically stops tracking when the monitor is deallocated. - deinit { - stopTracking() + backgroundQueue.asyncAfter(deadline: .now() + delay) { [weak self] in self?.checkCurrentTrack() } } + + deinit { stopTracking() } } diff --git a/apps/native/wolfwave/Monitors/PlaybackSource.swift b/apps/native/wolfwave/Monitors/PlaybackSource.swift new file mode 100644 index 0000000..b54b347 --- /dev/null +++ b/apps/native/wolfwave/Monitors/PlaybackSource.swift @@ -0,0 +1,34 @@ +import Foundation + +// MARK: - PlaybackSourceMode + +/// The user's chosen music source mode. +enum PlaybackSourceMode: String { + case appleMusic = "appleMusic" + case systemNowPlaying = "systemNowPlaying" +} + +// MARK: - PlaybackSourceDelegate + +/// Delegate protocol for receiving playback updates from any music source. +protocol PlaybackSourceDelegate: AnyObject { + func playbackSource( + _ source: any PlaybackSource, + didUpdateTrack track: String, + artist: String, + album: String, + duration: TimeInterval, + elapsed: TimeInterval + ) + func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) +} + +// MARK: - PlaybackSource + +/// Contract that every music source must satisfy. +protocol PlaybackSource: AnyObject { + var delegate: PlaybackSourceDelegate? { get set } + func startTracking() + func stopTracking() + func updateCheckInterval(_ interval: TimeInterval) +} diff --git a/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift b/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift new file mode 100644 index 0000000..ba6f431 --- /dev/null +++ b/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift @@ -0,0 +1,86 @@ +import Foundation + +/// Manages the active music playback source and switches between them based on user preference. +/// +/// AppDelegate owns a single PlaybackSourceManager and interacts with it instead of +/// individual sources directly. The manager persists the chosen mode to UserDefaults +/// and handles clean start/stop transitions when switching. +class PlaybackSourceManager: PlaybackSourceDelegate { + + // MARK: - Properties + + /// The delegate that receives forwarded playback callbacks. + weak var delegate: PlaybackSourceDelegate? + + /// The currently active playback mode. + private(set) var currentMode: PlaybackSourceMode + + private let appleMusicSource = AppleMusicSource() + private lazy var systemNowPlayingSource = SystemNowPlayingSource() + private var activeSource: (any PlaybackSource)? + private var isTracking = false + + // MARK: - Init + + init() { + let stored = UserDefaults.standard.string(forKey: "playbackSourceMode") + currentMode = PlaybackSourceMode(rawValue: stored ?? "") ?? .appleMusic + } + + // MARK: - Public Methods + + /// Starts tracking with the current mode's source. + func startTracking() { + stopTracking() + let source: any PlaybackSource = (currentMode == .appleMusic) ? appleMusicSource : systemNowPlayingSource + source.delegate = self // NOTE: this requires a workaround — see below + activeSource = source + isTracking = true + source.startTracking() + } + + /// Stops the active source. + func stopTracking() { + activeSource?.stopTracking() + // Clear delegate to avoid retain issues + // (set via the concrete type helpers below) + clearActiveSourceDelegate() + activeSource = nil + isTracking = false + } + + /// Switches to a new mode. If currently tracking, restarts with the new source. + func switchMode(_ mode: PlaybackSourceMode) { + guard mode != currentMode else { return } + let wasTracking = isTracking + stopTracking() + currentMode = mode + UserDefaults.standard.set(mode.rawValue, forKey: "playbackSourceMode") + if wasTracking { startTracking() } + } + + /// Updates the fallback polling interval on the active source. + func updateCheckInterval(_ interval: TimeInterval) { + activeSource?.updateCheckInterval(interval) + } + + // MARK: - PlaybackSourceDelegate (forwarding) + + func playbackSource(_ source: any PlaybackSource, didUpdateTrack track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval) { + delegate?.playbackSource(source, didUpdateTrack: track, artist: artist, album: album, duration: duration, elapsed: elapsed) + } + + func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) { + delegate?.playbackSource(source, didUpdateStatus: status) + } + + // MARK: - Private Helpers + + private func clearActiveSourceDelegate() { + if let source = activeSource as? AppleMusicSource { + source.delegate = nil + } else if let source = activeSource as? SystemNowPlayingSource { + source.delegate = nil + } + } +} diff --git a/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift b/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift new file mode 100644 index 0000000..e744b6e --- /dev/null +++ b/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift @@ -0,0 +1,202 @@ +import Foundation +import AppKit + +// MARK: - SystemNowPlayingSource + +/// A playback source that uses the private MediaRemote framework to capture +/// now-playing info from any app (Spotify, Chrome, web players, etc.). +class SystemNowPlayingSource: PlaybackSource { + + // MARK: - Constants + + private enum Constants { + static let frameworkPath = "/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemote" + static let nowPlayingChangedNotification = "kMRMediaRemoteNowPlayingInfoDidChangeNotification" + static let queueLabel = "com.mrdemonwolf.wolfwave.systemnowplaying" + static let checkInterval: TimeInterval = 5.0 + static let notificationDedupWindow: TimeInterval = 0.75 + static let idleGraceWindow: TimeInterval = 2.0 + + static let keyTitle = "kMRMediaRemoteNowPlayingInfoTitle" + static let keyArtist = "kMRMediaRemoteNowPlayingInfoArtist" + static let keyAlbum = "kMRMediaRemoteNowPlayingInfoAlbum" + static let keyDuration = "kMRMediaRemoteNowPlayingInfoDuration" + static let keyElapsedTime = "kMRMediaRemoteNowPlayingInfoElapsedTime" + static let keyPlaybackRate = "kMRMediaRemoteNowPlayingInfoPlaybackRate" + } + + // MARK: - Properties + + weak var delegate: PlaybackSourceDelegate? + + private var currentCheckInterval: TimeInterval = Constants.checkInterval + private var timer: DispatchSourceTimer? + private var lastTrackSeenAt: Date = .distantPast + private var lastNotificationAt: Date = .distantPast + private var lastLoggedTrack: String? + private var isTracking = false + + private let backgroundQueue = DispatchQueue(label: Constants.queueLabel, qos: .utility) + + // MARK: - Framework Loading + + private typealias MRMediaRemoteGetNowPlayingInfoFunction = + @convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void + private typealias MRMediaRemoteRegisterForNowPlayingNotificationsFunction = + @convention(c) (DispatchQueue) -> Void + + private var getNowPlayingInfo: MRMediaRemoteGetNowPlayingInfoFunction? + private var registerForNotifications: MRMediaRemoteRegisterForNowPlayingNotificationsFunction? + private var frameworkLoaded = false + + // MARK: - Lifecycle + + init() { + guard let handle = dlopen(Constants.frameworkPath, RTLD_LAZY) else { + Log.warn("SystemNowPlayingSource: MediaRemote framework unavailable", category: "Music") + return + } + getNowPlayingInfo = unsafeBitCast( + dlsym(handle, "MRMediaRemoteGetNowPlayingInfo"), + to: MRMediaRemoteGetNowPlayingInfoFunction.self + ) + registerForNotifications = unsafeBitCast( + dlsym(handle, "MRMediaRemoteRegisterForNowPlayingNotifications"), + to: MRMediaRemoteRegisterForNowPlayingNotificationsFunction.self + ) + frameworkLoaded = getNowPlayingInfo != nil + if !frameworkLoaded { + Log.warn("SystemNowPlayingSource: Failed to resolve MediaRemote symbols", category: "Music") + } + } + + deinit { stopTracking() } + + // MARK: - Public Methods + + /// Begins tracking system-wide now-playing state via MediaRemote. + func startTracking() { + guard !isTracking else { return } + guard frameworkLoaded else { + delegate?.playbackSource(self, didUpdateStatus: "System Now Playing unavailable") + return + } + isTracking = true + registerForNotifications?(backgroundQueue) + NotificationCenter.default.addObserver( + self, + selector: #selector(nowPlayingInfoChanged), + name: NSNotification.Name(Constants.nowPlayingChangedNotification), + object: nil + ) + fetchNowPlayingInfo() + setupFallbackTimer() + } + + /// Stops tracking and tears down the notification observer and timer. + func stopTracking() { + guard isTracking else { return } + NotificationCenter.default.removeObserver( + self, + name: NSNotification.Name(Constants.nowPlayingChangedNotification), + object: nil + ) + timer?.cancel() + timer = nil + isTracking = false + } + + /// Updates the fallback polling interval while tracking is active. + func updateCheckInterval(_ interval: TimeInterval) { + guard isTracking else { return } + currentCheckInterval = max(interval, 1.0) + timer?.cancel() + timer = nil + setupFallbackTimer() + } + + // MARK: - Private Helpers + + @objc private func nowPlayingInfoChanged(_ notification: Notification) { + let now = Date() + guard now.timeIntervalSince(lastNotificationAt) >= Constants.notificationDedupWindow else { return } + lastNotificationAt = now + Log.debug("SystemNowPlayingSource: Now Playing notification received", category: "Music") + fetchNowPlayingInfo() + } + + private func fetchNowPlayingInfo() { + getNowPlayingInfo?(backgroundQueue) { [weak self] info in + guard let self = self else { return } + self.processNowPlayingInfo(info) + } + } + + private func processNowPlayingInfo(_ info: [String: Any]) { + let playbackRate = (info[Constants.keyPlaybackRate] as? Double) ?? 0 + let title = (info[Constants.keyTitle] as? String) ?? "" + + guard playbackRate != 0, !title.isEmpty else { + handleNotPlayingState() + return + } + + let artist = (info[Constants.keyArtist] as? String) ?? "" + let album = (info[Constants.keyAlbum] as? String) ?? "" + let duration = (info[Constants.keyDuration] as? Double) ?? 0 + let elapsed = (info[Constants.keyElapsedTime] as? Double) ?? 0 + + lastTrackSeenAt = Date() + notifyDelegate(track: title, artist: artist, album: album, duration: duration, elapsed: elapsed) + logTrackIfNew(title, trackName: title, artist: artist, album: album) + } + + private func handleNotPlayingState() { + let idleDuration = Date().timeIntervalSince(lastTrackSeenAt) + if idleDuration < Constants.idleGraceWindow { + scheduleCheck(after: 0.5, reason: "idle-grace-recheck") + return + } + notifyDelegate(status: "No track playing") + } + + private func notifyDelegate(status: String) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.delegate?.playbackSource(self, didUpdateStatus: status) + } + } + + private func notifyDelegate(track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.delegate?.playbackSource(self, didUpdateTrack: track, artist: artist, album: album, duration: duration, elapsed: elapsed) + } + } + + private func logTrackIfNew(_ key: String, trackName: String, artist: String, album: String) { + let dedupKey = trackName + " | " + artist + " | " + album + guard lastLoggedTrack != dedupKey else { return } + Log.debug("SystemNowPlayingSource: Now Playing → \(trackName) — \(artist) [\(album)]", category: "Music") + lastLoggedTrack = dedupKey + } + + // MARK: - Setup & Scheduling + + private func setupFallbackTimer() { + let timer = DispatchSource.makeTimerSource(queue: backgroundQueue) + timer.schedule(deadline: .now() + currentCheckInterval, repeating: currentCheckInterval) + timer.setEventHandler { [weak self] in + guard let self = self, self.isTracking else { return } + self.fetchNowPlayingInfo() + } + timer.activate() + self.timer = timer + } + + private func scheduleCheck(after delay: TimeInterval, reason: String) { + backgroundQueue.asyncAfter(deadline: .now() + delay) { [weak self] in + self?.fetchNowPlayingInfo() + } + } +} diff --git a/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift b/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift index e7149ba..7274012 100644 --- a/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift +++ b/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift @@ -23,6 +23,8 @@ struct MusicMonitorSettingsView: View { @AppStorage(AppConstants.UserDefaults.trackingEnabled) private var trackingEnabled = true + @AppStorage("playbackSourceMode") private var playbackSourceMode: String = "appleMusic" + @State private var permissionDenied = false @State private var currentTrack: String? @State private var currentArtist: String? @@ -48,8 +50,44 @@ struct MusicMonitorSettingsView: View { .fixedSize(horizontal: false, vertical: true) } + // Music Source picker + VStack(alignment: .leading, spacing: 6) { + Text("Music Source") + .font(.headline) + + Picker("", selection: $playbackSourceMode) { + Text("Apple Music").tag("appleMusic") + Text("Any App (System)").tag("systemNowPlaying") + } + .pickerStyle(.segmented) + .onChange(of: playbackSourceMode) { _, newValue in + NotificationCenter.default.post( + name: NSNotification.Name(AppConstants.Notifications.playbackSourceModeChanged), + object: nil, + userInfo: ["mode": newValue] + ) + } + + Text(playbackSourceMode == "appleMusic" + ? "Connects directly to Apple Music for accurate track info." + : "Picks up whatever's playing — Spotify, browsers, anything.") + .font(.caption) + .foregroundStyle(.secondary) + + if playbackSourceMode == "systemNowPlaying" { + HStack(spacing: 6) { + Image(systemName: "info.circle") + .foregroundStyle(.secondary) + Text("Uses macOS system media info. No Apple Music access needed.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + .padding(.bottom, 4) + // Permission warning - if permissionDenied { + if permissionDenied && playbackSourceMode == "appleMusic" { HStack(spacing: 10) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 14)) diff --git a/apps/native/wolfwave/WolfWaveApp.swift b/apps/native/wolfwave/WolfWaveApp.swift index 814325a..56881b4 100644 --- a/apps/native/wolfwave/WolfWaveApp.swift +++ b/apps/native/wolfwave/WolfWaveApp.swift @@ -40,7 +40,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele // MARK: - Properties var statusItem: NSStatusItem? - var musicMonitor: MusicPlaybackMonitor? + var playbackSourceManager: PlaybackSourceManager? var settingsWindow: NSWindow? var onboardingWindow: NSWindow? var whatsNewWindow: NSWindow? @@ -186,12 +186,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele @objc func trackingSettingChanged(_ notification: Notification) { guard let enabled = notification.userInfo?["enabled"] as? Bool else { return } - enabled ? musicMonitor?.startTracking() : stopTrackingAndUpdate() + enabled ? playbackSourceManager?.startTracking() : stopTrackingAndUpdate() } /// Stops the music monitor and clears the now-playing display. private func stopTrackingAndUpdate() { - musicMonitor?.stopTracking() + playbackSourceManager?.stopTracking() postNowPlayingUpdate(song: nil, artist: nil, album: nil) } @@ -686,7 +686,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele @objc func powerStateChanged(_ notification: Notification) { let reduced = PowerStateMonitor.shared.isReducedMode - musicMonitor?.updateCheckInterval( + playbackSourceManager?.updateCheckInterval( reduced ? AppConstants.PowerManagement.reducedMusicCheckInterval : 5.0 ) discordService?.updatePollInterval( @@ -703,10 +703,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele // MARK: - Service Initialization - /// Creates the music playback monitor and sets this delegate. + /// Creates the playback source manager and sets this delegate. private func setupMusicMonitor() { - musicMonitor = MusicPlaybackMonitor() - musicMonitor?.delegate = self + playbackSourceManager = PlaybackSourceManager() + playbackSourceManager?.delegate = self } /// Creates the Twitch chat service and wires up song info callbacks. @@ -932,7 +932,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele } if isTrackingEnabled() { - musicMonitor?.startTracking() + playbackSourceManager?.startTracking() } else { postNowPlayingUpdate(song: nil, artist: nil, album: nil) } @@ -1183,12 +1183,12 @@ extension AppDelegate { } } -// MARK: - Music Playback Monitor Delegate +// MARK: - Playback Source Delegate -extension AppDelegate: MusicPlaybackMonitorDelegate { +extension AppDelegate: PlaybackSourceDelegate { /// Updates track history, broadcasts to all services, and fetches artwork. - func musicPlaybackMonitor( - _ monitor: MusicPlaybackMonitor, didUpdateTrack track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval + func playbackSource( + _ source: any PlaybackSource, didUpdateTrack track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval ) { if currentSong != track { lastSong = currentSong @@ -1223,7 +1223,7 @@ extension AppDelegate: MusicPlaybackMonitorDelegate { } /// Clears track state and notifies services when playback stops. - func musicPlaybackMonitor(_ monitor: MusicPlaybackMonitor, didUpdateStatus status: String) { + func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) { if status == "No track playing" { currentSong = nil currentArtist = nil From d521ec147b7e04142540412e4062aa20c8ea3d91 Mon Sep 17 00:00:00 2001 From: Nathanial Henniges <19924836+nathanialhenniges@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:18:47 -0500 Subject: [PATCH 2/5] fix: address CodeRabbit nitpicks on multi-source music PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SystemNowPlayingSource: use AppConstants for frameworkPath, notification name, and queue label instead of local duplicates - SystemNowPlayingSource: check dlsym result for NULL before unsafeBitCast - SystemNowPlayingSource: dispatch unavailable status via notifyDelegate (main thread) - PlaybackSourceManager: use AppConstants.UserDefaults.playbackSourceMode key in both init() and switchMode(_:) instead of hardcoded string - MusicMonitorSettingsView: use AppConstants.UserDefaults.playbackSourceMode in @AppStorage; add accessibilityLabel/accessibilityIdentifier to picker and accessibilityElement to system mode info row - SystemNowPlayingSourceTests: rename misleading test to testDelegateWiringDoesNotCrash - CHANGELOG: fix "macOS system Now Playing API" → "macOS Now Playing API" Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 +- .../SystemNowPlayingSourceTests.swift | 8 +++---- .../Monitors/PlaybackSourceManager.swift | 4 ++-- .../Monitors/SystemNowPlayingSource.swift | 22 +++++++++---------- .../MusicMonitorSettingsView.swift | 6 ++++- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd207d4..2051c0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file. ### Added -- **Multi-source music support** — choose between Apple Music (direct ScriptingBridge connection) and Any App (System), which uses the macOS system Now Playing API to capture playback from Spotify, browsers, and any other app. Setting lives in Music Monitor preferences. +- **Multi-source music support** — choose between Apple Music (direct ScriptingBridge connection) and Any App (System), which uses the macOS Now Playing API to capture playback from Spotify, browsers, and any other app. Setting lives in Music Monitor preferences. ## [1.1.0] - 2026-03-31 diff --git a/apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift b/apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift index 86f6e6e..d00c2d0 100644 --- a/apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift +++ b/apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift @@ -58,14 +58,12 @@ final class SystemNowPlayingSourceTests: XCTestCase { // MARK: - Graceful Degradation - func testDelegateIsNotifiedIfFrameworkUnavailable() { - // We can't force dlopen to fail, but we can verify the delegate interface works + func testDelegateWiringDoesNotCrash() { let source = SystemNowPlayingSource() let spy = StatusSpy() source.delegate = spy - // If framework loaded, startTracking works silently. - // If it didn't load, delegate should have been called with an error status synchronously in init (not tested here). - // Just verify no crash. + // If framework unavailable, delegate receives "System Now Playing unavailable" on main thread. + // If available, startTracking proceeds silently. Either way, no crash. source.startTracking() source.stopTracking() } diff --git a/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift b/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift index ba6f431..a69e1f2 100644 --- a/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift +++ b/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift @@ -23,7 +23,7 @@ class PlaybackSourceManager: PlaybackSourceDelegate { // MARK: - Init init() { - let stored = UserDefaults.standard.string(forKey: "playbackSourceMode") + let stored = UserDefaults.standard.string(forKey: AppConstants.UserDefaults.playbackSourceMode) currentMode = PlaybackSourceMode(rawValue: stored ?? "") ?? .appleMusic } @@ -55,7 +55,7 @@ class PlaybackSourceManager: PlaybackSourceDelegate { let wasTracking = isTracking stopTracking() currentMode = mode - UserDefaults.standard.set(mode.rawValue, forKey: "playbackSourceMode") + UserDefaults.standard.set(mode.rawValue, forKey: AppConstants.UserDefaults.playbackSourceMode) if wasTracking { startTracking() } } diff --git a/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift b/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift index e744b6e..47ce068 100644 --- a/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift +++ b/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift @@ -10,9 +10,9 @@ class SystemNowPlayingSource: PlaybackSource { // MARK: - Constants private enum Constants { - static let frameworkPath = "/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemote" - static let nowPlayingChangedNotification = "kMRMediaRemoteNowPlayingInfoDidChangeNotification" - static let queueLabel = "com.mrdemonwolf.wolfwave.systemnowplaying" + static let frameworkPath = AppConstants.SystemNowPlaying.frameworkPath + static let nowPlayingChangedNotification = AppConstants.SystemNowPlaying.nowPlayingInfoDidChangeNotification + static let queueLabel = AppConstants.DispatchQueues.systemNowPlaying static let checkInterval: TimeInterval = 5.0 static let notificationDedupWindow: TimeInterval = 0.75 static let idleGraceWindow: TimeInterval = 2.0 @@ -56,14 +56,12 @@ class SystemNowPlayingSource: PlaybackSource { Log.warn("SystemNowPlayingSource: MediaRemote framework unavailable", category: "Music") return } - getNowPlayingInfo = unsafeBitCast( - dlsym(handle, "MRMediaRemoteGetNowPlayingInfo"), - to: MRMediaRemoteGetNowPlayingInfoFunction.self - ) - registerForNotifications = unsafeBitCast( - dlsym(handle, "MRMediaRemoteRegisterForNowPlayingNotifications"), - to: MRMediaRemoteRegisterForNowPlayingNotificationsFunction.self - ) + if let sym = dlsym(handle, "MRMediaRemoteGetNowPlayingInfo") { + getNowPlayingInfo = unsafeBitCast(sym, to: MRMediaRemoteGetNowPlayingInfoFunction.self) + } + if let sym = dlsym(handle, "MRMediaRemoteRegisterForNowPlayingNotifications") { + registerForNotifications = unsafeBitCast(sym, to: MRMediaRemoteRegisterForNowPlayingNotificationsFunction.self) + } frameworkLoaded = getNowPlayingInfo != nil if !frameworkLoaded { Log.warn("SystemNowPlayingSource: Failed to resolve MediaRemote symbols", category: "Music") @@ -78,7 +76,7 @@ class SystemNowPlayingSource: PlaybackSource { func startTracking() { guard !isTracking else { return } guard frameworkLoaded else { - delegate?.playbackSource(self, didUpdateStatus: "System Now Playing unavailable") + notifyDelegate(status: "System Now Playing unavailable") return } isTracking = true diff --git a/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift b/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift index 7274012..eeaaa91 100644 --- a/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift +++ b/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift @@ -23,7 +23,7 @@ struct MusicMonitorSettingsView: View { @AppStorage(AppConstants.UserDefaults.trackingEnabled) private var trackingEnabled = true - @AppStorage("playbackSourceMode") private var playbackSourceMode: String = "appleMusic" + @AppStorage(AppConstants.UserDefaults.playbackSourceMode) private var playbackSourceMode: String = "appleMusic" @State private var permissionDenied = false @State private var currentTrack: String? @@ -60,6 +60,8 @@ struct MusicMonitorSettingsView: View { Text("Any App (System)").tag("systemNowPlaying") } .pickerStyle(.segmented) + .accessibilityLabel("Music Source") + .accessibilityIdentifier("musicSourcePicker") .onChange(of: playbackSourceMode) { _, newValue in NotificationCenter.default.post( name: NSNotification.Name(AppConstants.Notifications.playbackSourceModeChanged), @@ -82,6 +84,8 @@ struct MusicMonitorSettingsView: View { .font(.caption) .foregroundStyle(.secondary) } + .accessibilityElement(children: .combine) + .accessibilityLabel("Uses macOS system media info. No Apple Music access needed.") } } .padding(.bottom, 4) From 2b7b4f6131f74702f961d5407bf03afbeba5c704 Mon Sep 17 00:00:00 2001 From: Nathanial Henniges <19924836+nathanialhenniges@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:59:12 -0500 Subject: [PATCH 3/5] fix: lazy-initialize AppleMusicSource to prevent CI malloc crash PlaybackSourceManagerTests crashed on every test with "pointer being freed was not allocated" on a background thread. Root cause: AppleMusicSource was eagerly created as a stored property in PlaybackSourceManager.init, racing with a still-in-flight backgroundQueue async block from the previous AppleMusicSourceTests teardown on CI runners. Making appleMusicSource (and systemNowPlayingSource) lazy defers allocation until startTracking() is called. No tests in PlaybackSourceManagerTests call startTracking(), so no AppleMusicSource is ever created during those tests, eliminating the race. Co-Authored-By: Claude Sonnet 4.6 --- apps/native/wolfwave/Monitors/PlaybackSourceManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift b/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift index a69e1f2..89f8937 100644 --- a/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift +++ b/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift @@ -15,7 +15,7 @@ class PlaybackSourceManager: PlaybackSourceDelegate { /// The currently active playback mode. private(set) var currentMode: PlaybackSourceMode - private let appleMusicSource = AppleMusicSource() + private lazy var appleMusicSource = AppleMusicSource() private lazy var systemNowPlayingSource = SystemNowPlayingSource() private var activeSource: (any PlaybackSource)? private var isTracking = false From 75da81935bb4f25171e3563259e489757a307563 Mon Sep 17 00:00:00 2001 From: Nathanial Henniges <19924836+nathanialhenniges@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:42:54 -0500 Subject: [PATCH 4/5] feat: fix System Now Playing source switching + source-aware branding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL BUG FIX: playbackSourceModeChanged notification had no observer in AppDelegate, so switching the Music Source picker never took effect at runtime. Added the missing observer — switchMode() is now called immediately when the user changes the picker. SOURCE DETECTION: SystemNowPlayingSource now resolves MRMediaRemoteGet- NowPlayingApplicationPID via dlsym to detect which app is playing (Spotify, Chrome, etc.). Bundle ID is surfaced via new PlaybackSourceDelegate method playbackSource(_:didDetectSourceApp:) with a default no-op extension for backward compatibility. SOURCE-AWARE DISCORD RPC: - Small icon + tooltip now show the actual playing app (Spotify logo, Apple Music logo, YouTube/Chrome icon, or generic music note) - "Open in Apple Music" button only appears when Apple Music is the source - Large image fallback uses the source-specific asset instead of always showing apple_music TWITCH COMMANDS: getCurrentSongInfo/getLastSongInfo no longer gate on isMusicAppOpen() when in system mode — Spotify/Chrome tracks now respond correctly. Non-Apple-Music sources append "via Spotify" etc. UI TEXT: Updated settings and onboarding descriptions to be source-agnostic. DISCORD ASSETS: Added discord-assets/ folder with placeholder PNGs for spotify, youtube, and music_generic. Upload these to Discord Developer Portal → Rich Presence → Art Assets (see discord-assets/README.md). Added KnownMusicApps helper in AppConstants with displayName(for:), discordAssetName(for:), isAppleMusic(_:), isBrowser(_:). Tested in KnownMusicAppsTests.swift (16 new tests). Co-Authored-By: Claude Sonnet 4.6 --- .../WolfWaveTests/KnownMusicAppsTests.swift | 105 ++++++++++++++++++ apps/native/wolfwave/Core/AppConstants.swift | 52 +++++++++ .../wolfwave/Monitors/PlaybackSource.swift | 7 ++ .../Monitors/PlaybackSourceManager.swift | 4 + .../Monitors/SystemNowPlayingSource.swift | 22 ++++ .../Services/Discord/DiscordRPCService.swift | 37 +++--- .../MusicMonitorSettingsView.swift | 2 +- .../OnboardingWelcomeStepView.swift | 2 +- apps/native/wolfwave/WolfWaveApp.swift | 57 ++++++++-- discord-assets/README.md | 27 +++++ discord-assets/music_generic.png | Bin 0 -> 1496 bytes discord-assets/spotify.png | Bin 0 -> 1496 bytes discord-assets/youtube.png | Bin 0 -> 167643 bytes 13 files changed, 290 insertions(+), 25 deletions(-) create mode 100644 apps/native/WolfWaveTests/KnownMusicAppsTests.swift create mode 100644 discord-assets/README.md create mode 100644 discord-assets/music_generic.png create mode 100644 discord-assets/spotify.png create mode 100644 discord-assets/youtube.png diff --git a/apps/native/WolfWaveTests/KnownMusicAppsTests.swift b/apps/native/WolfWaveTests/KnownMusicAppsTests.swift new file mode 100644 index 0000000..adfe204 --- /dev/null +++ b/apps/native/WolfWaveTests/KnownMusicAppsTests.swift @@ -0,0 +1,105 @@ +// +// KnownMusicAppsTests.swift +// WolfWaveTests + +import XCTest +@testable import WolfWave + +final class KnownMusicAppsTests: XCTestCase { + + // MARK: - displayName + + func testDisplayNameAppleMusic() { + XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "com.apple.Music"), "Apple Music") + } + + func testDisplayNameSpotify() { + XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "com.spotify.client"), "Spotify") + } + + func testDisplayNameiTunes() { + XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "com.apple.iTunes"), "iTunes") + } + + func testDisplayNameChrome() { + XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "com.google.Chrome"), "Chrome") + } + + func testDisplayNameFirefox() { + XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "org.mozilla.firefox"), "Firefox") + } + + func testDisplayNameUnknownFallsBackToMusic() { + XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "com.unknown.app"), "Music") + } + + func testDisplayNameNilFallsBackToMusic() { + XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: nil), "Music") + } + + // MARK: - discordAssetName + + func testDiscordAssetAppleMusic() { + XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.apple.Music"), "apple_music") + } + + func testDiscordAssetiTunes() { + XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.apple.iTunes"), "apple_music") + } + + func testDiscordAssetSpotify() { + XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.spotify.client"), "spotify") + } + + func testDiscordAssetChrome() { + XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.google.Chrome"), "youtube") + } + + func testDiscordAssetFirefox() { + XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "org.mozilla.firefox"), "youtube") + } + + func testDiscordAssetBrave() { + XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.brave.Browser"), "youtube") + } + + func testDiscordAssetUnknownFallsBackToGeneric() { + XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.unknown.app"), "music_generic") + } + + func testDiscordAssetNilFallsBackToGeneric() { + XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: nil), "music_generic") + } + + // MARK: - isAppleMusic + + func testIsAppleMusicTrue() { + XCTAssertTrue(AppConstants.KnownMusicApps.isAppleMusic("com.apple.Music")) + } + + func testIsAppleMusicFalseForSpotify() { + XCTAssertFalse(AppConstants.KnownMusicApps.isAppleMusic("com.spotify.client")) + } + + func testIsAppleMusicFalseForNil() { + XCTAssertFalse(AppConstants.KnownMusicApps.isAppleMusic(nil)) + } + + // MARK: - isBrowser + + func testIsBrowserChrome() { + XCTAssertTrue(AppConstants.KnownMusicApps.isBrowser("com.google.Chrome")) + } + + func testIsBrowserFirefox() { + XCTAssertTrue(AppConstants.KnownMusicApps.isBrowser("org.mozilla.firefox")) + } + + func testIsBrowserFalseForSpotify() { + XCTAssertFalse(AppConstants.KnownMusicApps.isBrowser("com.spotify.client")) + } + + func testIsBrowserFalseForNil() { + XCTAssertFalse(AppConstants.KnownMusicApps.isBrowser(nil)) + } +} diff --git a/apps/native/wolfwave/Core/AppConstants.swift b/apps/native/wolfwave/Core/AppConstants.swift index 8798072..047cf4f 100644 --- a/apps/native/wolfwave/Core/AppConstants.swift +++ b/apps/native/wolfwave/Core/AppConstants.swift @@ -492,6 +492,58 @@ enum AppConstants { static let nowPlayingInfoDidChangeNotification = "kMRMediaRemoteNowPlayingInfoDidChangeNotification" } + // MARK: - Known Music Apps + + /// Maps source app bundle IDs to display names and Discord asset names. + enum KnownMusicApps { + private static let browserBundleIDs: Set = [ + "com.google.Chrome", + "org.mozilla.firefox", + "com.brave.Browser", + "com.microsoft.edgemac", + "com.apple.Safari", + ] + + /// Human-readable name for the given source app bundle ID. + static func displayName(for bundleID: String?) -> String { + switch bundleID { + case "com.apple.Music": return "Apple Music" + case "com.spotify.client": return "Spotify" + case "com.apple.iTunes": return "iTunes" + case "com.google.Chrome": return "Chrome" + case "org.mozilla.firefox": return "Firefox" + case "com.brave.Browser": return "Brave" + case "com.microsoft.edgemac": return "Edge" + case "tv.plex.plexamp": return "Plexamp" + case "com.tidal.desktop": return "TIDAL" + case "com.amazon.music": return "Amazon Music" + default: return "Music" + } + } + + /// Discord Developer Portal asset name for the given source app bundle ID. + static func discordAssetName(for bundleID: String?) -> String { + switch bundleID { + case "com.apple.Music", "com.apple.iTunes": return "apple_music" + case "com.spotify.client": return "spotify" + default: + if let id = bundleID, browserBundleIDs.contains(id) { return "youtube" } + return "music_generic" + } + } + + /// Returns `true` when the bundle ID is Apple Music. + static func isAppleMusic(_ bundleID: String?) -> Bool { + bundleID == "com.apple.Music" + } + + /// Returns `true` when the bundle ID belongs to a known browser. + static func isBrowser(_ bundleID: String?) -> Bool { + guard let id = bundleID else { return false } + return browserBundleIDs.contains(id) + } + } + // MARK: - Onboarding UI /// Onboarding wizard window configuration. diff --git a/apps/native/wolfwave/Monitors/PlaybackSource.swift b/apps/native/wolfwave/Monitors/PlaybackSource.swift index b54b347..4bf57de 100644 --- a/apps/native/wolfwave/Monitors/PlaybackSource.swift +++ b/apps/native/wolfwave/Monitors/PlaybackSource.swift @@ -21,6 +21,13 @@ protocol PlaybackSourceDelegate: AnyObject { elapsed: TimeInterval ) func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) + /// Called when the source detects which app is currently playing media. + /// - Parameter bundleIdentifier: The bundle ID of the playing app, or `nil` if unknown. + func playbackSource(_ source: any PlaybackSource, didDetectSourceApp bundleIdentifier: String?) +} + +extension PlaybackSourceDelegate { + func playbackSource(_ source: any PlaybackSource, didDetectSourceApp bundleIdentifier: String?) {} } // MARK: - PlaybackSource diff --git a/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift b/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift index 89f8937..976e6ec 100644 --- a/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift +++ b/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift @@ -74,6 +74,10 @@ class PlaybackSourceManager: PlaybackSourceDelegate { delegate?.playbackSource(source, didUpdateStatus: status) } + func playbackSource(_ source: any PlaybackSource, didDetectSourceApp bundleIdentifier: String?) { + delegate?.playbackSource(source, didDetectSourceApp: bundleIdentifier) + } + // MARK: - Private Helpers private func clearActiveSourceDelegate() { diff --git a/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift b/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift index 47ce068..695f6d4 100644 --- a/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift +++ b/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift @@ -44,9 +44,12 @@ class SystemNowPlayingSource: PlaybackSource { @convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void private typealias MRMediaRemoteRegisterForNowPlayingNotificationsFunction = @convention(c) (DispatchQueue) -> Void + private typealias MRMediaRemoteGetNowPlayingApplicationPIDFunction = + @convention(c) (DispatchQueue, @escaping (Int32) -> Void) -> Void private var getNowPlayingInfo: MRMediaRemoteGetNowPlayingInfoFunction? private var registerForNotifications: MRMediaRemoteRegisterForNowPlayingNotificationsFunction? + private var getNowPlayingApplicationPID: MRMediaRemoteGetNowPlayingApplicationPIDFunction? private var frameworkLoaded = false // MARK: - Lifecycle @@ -62,6 +65,9 @@ class SystemNowPlayingSource: PlaybackSource { if let sym = dlsym(handle, "MRMediaRemoteRegisterForNowPlayingNotifications") { registerForNotifications = unsafeBitCast(sym, to: MRMediaRemoteRegisterForNowPlayingNotificationsFunction.self) } + if let sym = dlsym(handle, "MRMediaRemoteGetNowPlayingApplicationPID") { + getNowPlayingApplicationPID = unsafeBitCast(sym, to: MRMediaRemoteGetNowPlayingApplicationPIDFunction.self) + } frameworkLoaded = getNowPlayingInfo != nil if !frameworkLoaded { Log.warn("SystemNowPlayingSource: Failed to resolve MediaRemote symbols", category: "Music") @@ -128,6 +134,15 @@ class SystemNowPlayingSource: PlaybackSource { guard let self = self else { return } self.processNowPlayingInfo(info) } + fetchNowPlayingApplicationBundleID() + } + + private func fetchNowPlayingApplicationBundleID() { + getNowPlayingApplicationPID?(backgroundQueue) { [weak self] pid in + guard let self = self, pid > 0 else { return } + let bundleID = NSRunningApplication(processIdentifier: pid_t(pid))?.bundleIdentifier + self.notifyDelegateSourceApp(bundleID) + } } private func processNowPlayingInfo(_ info: [String: Any]) { @@ -165,6 +180,13 @@ class SystemNowPlayingSource: PlaybackSource { } } + private func notifyDelegateSourceApp(_ bundleID: String?) { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.delegate?.playbackSource(self, didDetectSourceApp: bundleID) + } + } + private func notifyDelegate(track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } diff --git a/apps/native/wolfwave/Services/Discord/DiscordRPCService.swift b/apps/native/wolfwave/Services/Discord/DiscordRPCService.swift index 960bf72..159eaf6 100644 --- a/apps/native/wolfwave/Services/Discord/DiscordRPCService.swift +++ b/apps/native/wolfwave/Services/Discord/DiscordRPCService.swift @@ -212,7 +212,8 @@ final class DiscordRPCService: @unchecked Sendable { artist: String, album: String, duration: TimeInterval = 0, - elapsed: TimeInterval = 0 + elapsed: TimeInterval = 0, + sourceAppBundleID: String? = nil ) { ipcQueue.async { [weak self] in guard let self, self.state == .connected else { return } @@ -224,7 +225,8 @@ final class DiscordRPCService: @unchecked Sendable { artworkURL: cached.artworkURL, duration: duration, elapsed: elapsed, appleMusicURL: cached.trackViewURL, - songLinkURL: cached.songLinkURL + songLinkURL: cached.songLinkURL, + sourceAppBundleID: sourceAppBundleID ) // Fetch track links asynchronously on cache miss @@ -243,7 +245,8 @@ final class DiscordRPCService: @unchecked Sendable { artworkURL: links.artworkURL, duration: duration, elapsed: elapsed, appleMusicURL: links.trackViewURL, - songLinkURL: links.songLinkURL + songLinkURL: links.songLinkURL, + sourceAppBundleID: sourceAppBundleID ) } // Notify listeners (e.g., WebSocket server) only when artwork is resolved @@ -296,10 +299,11 @@ final class DiscordRPCService: @unchecked Sendable { /// - track: Song title. /// - artist: Artist name. /// - album: Album name (used as large image tooltip). - /// - artworkURL: Optional iTunes artwork URL. If nil, falls back to the - /// static "apple_music" asset uploaded in the Discord Developer Portal. + /// - artworkURL: Optional iTunes artwork URL. If nil, falls back to a source-specific + /// asset uploaded in the Discord Developer Portal. /// - duration: Total track duration in seconds (0 if unknown). /// - elapsed: Elapsed time in seconds (0 if unknown). + /// - sourceAppBundleID: Bundle ID of the playing app for source-aware branding. private func sendPresenceActivity( track: String, artist: String, @@ -308,25 +312,28 @@ final class DiscordRPCService: @unchecked Sendable { duration: TimeInterval, elapsed: TimeInterval, appleMusicURL: String? = nil, - songLinkURL: String? = nil + songLinkURL: String? = nil, + sourceAppBundleID: String? = nil ) { + let sourceAsset = AppConstants.KnownMusicApps.discordAssetName(for: sourceAppBundleID) + let sourceName = AppConstants.KnownMusicApps.displayName(for: sourceAppBundleID) + let isAppleMusic = AppConstants.KnownMusicApps.isAppleMusic(sourceAppBundleID) + var activity: [String: Any] = [ "type": AppConstants.Discord.listeningActivityType, "details": track, "state": "by \(artist)", ] - // Assets — prefer dynamic artwork URL from iTunes, fall back to static asset - let largeImage = artworkURL ?? "apple_music" + // Assets — prefer dynamic artwork URL, fall back to source-specific Discord asset + let largeImage = artworkURL ?? sourceAsset var assets: [String: Any] = [ "large_image": largeImage, "large_text": album, ] - // Show Apple Music branding as small icon when we have album art - if artworkURL != nil { - assets["small_image"] = "apple_music" - assets["small_text"] = "Apple Music" - } + // Always show the source app logo as small overlay icon + assets["small_image"] = sourceAsset + assets["small_text"] = sourceName activity["assets"] = assets // Timestamps — show a progress bar if duration is known @@ -340,9 +347,9 @@ final class DiscordRPCService: @unchecked Sendable { ] } - // Buttons — shown on the Discord profile card (max 2) + // Buttons — "Open in Apple Music" only for Apple Music source; song.link when available var buttons: [[String: String]] = [] - if let appleMusicURL { + if isAppleMusic, let appleMusicURL { buttons.append(["label": "Open in Apple Music", "url": appleMusicURL]) } if let songLinkURL { diff --git a/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift b/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift index eeaaa91..2292f9f 100644 --- a/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift +++ b/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift @@ -44,7 +44,7 @@ struct MusicMonitorSettingsView: View { .sectionSubHeader() .accessibilityLabel("Music Playback Monitor") - Text("Connects to Apple Music and shares what's playing everywhere.") + Text("Detects what's playing and shares it everywhere.") .font(.system(size: 13)) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) diff --git a/apps/native/wolfwave/Views/Onboarding/OnboardingWelcomeStepView.swift b/apps/native/wolfwave/Views/Onboarding/OnboardingWelcomeStepView.swift index 988162c..f07233f 100644 --- a/apps/native/wolfwave/Views/Onboarding/OnboardingWelcomeStepView.swift +++ b/apps/native/wolfwave/Views/Onboarding/OnboardingWelcomeStepView.swift @@ -27,7 +27,7 @@ struct OnboardingWelcomeStepView: View { Feature( icon: .brand(name: "AppleMusicLogo", renderOriginal: true), title: "Music Sync", - description: "Automatically detects what's playing in Apple Music." + description: "Detects what's playing in Apple Music, Spotify, or any app." ), Feature( icon: .brand(name: "TwitchLogo", renderOriginal: true), diff --git a/apps/native/wolfwave/WolfWaveApp.swift b/apps/native/wolfwave/WolfWaveApp.swift index 56881b4..6a3155f 100644 --- a/apps/native/wolfwave/WolfWaveApp.swift +++ b/apps/native/wolfwave/WolfWaveApp.swift @@ -57,6 +57,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele private var currentElapsed: TimeInterval = 0 private var lastSong: String? private var lastArtist: String? + private(set) var currentSourceBundleID: String? private var currentDockVisibilityMode: String { UserDefaults.standard.string(forKey: AppConstants.UserDefaults.dockVisibility) @@ -371,25 +372,37 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele /// Returns a formatted string with the current track for Twitch bot commands. func getCurrentSongInfo() -> String { - guard isMusicAppOpen() else { - return "🐺 Please open Apple Music" + let isSystemMode = playbackSourceManager?.currentMode == .systemNowPlaying + if !isSystemMode { + guard isMusicAppOpen() else { + return "🐺 Please open Apple Music" + } } - guard let song = currentSong, let artist = currentArtist else { return "🐺 No music playing" } + if isSystemMode && !AppConstants.KnownMusicApps.isAppleMusic(currentSourceBundleID) { + let name = AppConstants.KnownMusicApps.displayName(for: currentSourceBundleID) + return "🐺 Playing: \(song) by \(artist) via \(name)" + } return "🐺 Playing: \(song) by \(artist)" } /// Returns a formatted string with the previously played track for Twitch bot commands. func getLastSongInfo() -> String { - guard isMusicAppOpen() else { - return "🐺 Please open Apple Music" + let isSystemMode = playbackSourceManager?.currentMode == .systemNowPlaying + if !isSystemMode { + guard isMusicAppOpen() else { + return "🐺 Please open Apple Music" + } } - guard let song = lastSong, let artist = lastArtist else { return "🐺 No previous tracks yet, keep the music flowing!" - } + } + if isSystemMode && !AppConstants.KnownMusicApps.isAppleMusic(currentSourceBundleID) { + let name = AppConstants.KnownMusicApps.displayName(for: currentSourceBundleID) + return "🐺 Previous: \(song) by \(artist) via \(name)" + } return "🐺 Previous: \(song) by \(artist)" } @@ -921,6 +934,24 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele self?.handleUpdateStateChanged(notification) } ) + + notificationObservers.append( + nc.addObserver( + forName: NSNotification.Name(AppConstants.Notifications.playbackSourceModeChanged), + object: nil, + queue: .main + ) { [weak self] notification in + guard let modeString = notification.userInfo?["mode"] as? String, + let mode = PlaybackSourceMode(rawValue: modeString) else { return } + self?.playbackSourceManager?.switchMode(mode) + // Reset source bundle ID when switching modes + if mode == .appleMusic { + self?.currentSourceBundleID = AppConstants.Music.bundleIdentifier + } else { + self?.currentSourceBundleID = nil + } + } + ) } // MARK: - Tracking State @@ -1213,15 +1244,25 @@ extension AppDelegate: PlaybackSourceDelegate { fetchArtworkForWidget(track: track, artist: artist) + // For Apple Music mode the source never calls didDetectSourceApp, so set it here. + if playbackSourceManager?.currentMode == .appleMusic { + currentSourceBundleID = AppConstants.Music.bundleIdentifier + } + discordService?.updatePresence( track: track, artist: artist, album: album, duration: duration, - elapsed: elapsed + elapsed: elapsed, + sourceAppBundleID: currentSourceBundleID ) } + func playbackSource(_ source: any PlaybackSource, didDetectSourceApp bundleIdentifier: String?) { + currentSourceBundleID = bundleIdentifier + } + /// Clears track state and notifies services when playback stops. func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) { if status == "No track playing" { diff --git a/discord-assets/README.md b/discord-assets/README.md new file mode 100644 index 0000000..c9c732e --- /dev/null +++ b/discord-assets/README.md @@ -0,0 +1,27 @@ +# Discord Rich Presence Assets + +Upload these PNG files to your Discord Developer Portal as Rich Presence Art Assets. + +## How to upload + +1. Go to https://discord.com/developers/applications +2. Select your WolfWave application +3. Click **Rich Presence** → **Art Assets** +4. Upload each file below with the exact asset name shown + +## Assets + +| File | Asset Name | Description | Status | +|------|-----------|-------------|--------| +| `apple_music.png` | `apple_music` | Apple Music logo | Already uploaded ✅ | +| `spotify.png` | `spotify` | Spotify logo | **Upload needed** ⬆️ | +| `youtube.png` | `youtube` | YouTube/browser icon | **Upload needed** ⬆️ | +| `music_generic.png` | `music_generic` | Generic music fallback | **Upload needed** ⬆️ | + +## Notes + +- `spotify.png` is a **placeholder** (solid Spotify green). Replace it with the official Spotify logo from https://developer.spotify.com/documentation/design#using-our-logo +- `youtube.png` was extracted from your installed Chrome app icon — replace with YouTube's official logo from https://www.youtube.com/howyoutubeworks/resources/brand-resources/ if you prefer +- `music_generic.png` is a placeholder dark square — replace with any music note icon you like +- All assets should be at least **512×512px PNG** (square) +- Asset names must match **exactly** (lowercase, underscores) — the app references them by these exact names diff --git a/discord-assets/music_generic.png b/discord-assets/music_generic.png new file mode 100644 index 0000000000000000000000000000000000000000..bf57c09034d17da9e51c72350db562deb340f855 GIT binary patch literal 1496 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&t&wwUqN(1_svoo-U3d6}R4AHRNSr;5ob@ zNo&$acVoWl^phN)RNgXt2vA`#07@`0Ff$xrAd5+{c2SC~2fCUP*Mqb>u#)e4NT87& zcvR8OLvGYTY^PW|F!U%+1QbQ00RtyR`ay|+f*b(SKJaNMfDsm*WTxhU)jr^fhzf~- d)HL)Ujgk4WmSMg!XAuJsc)I$ztaD0e0stWmmskJ* literal 0 HcmV?d00001 diff --git a/discord-assets/spotify.png b/discord-assets/spotify.png new file mode 100644 index 0000000000000000000000000000000000000000..5289e86cf671fbb364ad7686793bd1915df1c0cf GIT binary patch literal 1496 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&t&wwUqN(1_svoo-U3d6}R4AHRNSr;9%Jp z-eeY&%+9~YWMxydo+Mn8{*NtUZJx>w&JO#Pwk9qU8G?5@=)x z9#ymh{ZC%h!L?DSorkQ11Gk+D+Epk|1QewK1BL-lLk_G+0ZJB=!c;(^b}FQT0Z#-Z drlE$}tp6(g-}VZbrvXcB22WQ%mvv4FO#n-Xm~Q|8 literal 0 HcmV?d00001 diff --git a/discord-assets/youtube.png b/discord-assets/youtube.png new file mode 100644 index 0000000000000000000000000000000000000000..5fff448979383ce6f6a189c86bfa74a623890055 GIT binary patch literal 167643 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�Z^V?=lnA#$Th^h6@b* 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 yH6xtio Date: Thu, 2 Apr 2026 16:41:23 -0500 Subject: [PATCH 5/5] revert: remove System Now Playing source, revert to Apple Music only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The private MediaRemote framework is blocked by kMRMediaRemoteFrameworkErrorDomain Code=3 on macOS 26 (Tahoe). The mediaremoted daemon enforces private Apple entitlements (com.apple.private.mediaremote.nowplayinginfo) that third-party apps cannot obtain — no public API exists to read now-playing info from other apps. - Delete SystemNowPlayingSource.swift and all related tests - Remove PlaybackSourceMode.systemNowPlaying, didDetectSourceApp delegate method - Simplify PlaybackSourceManager to always use AppleMusicSource - Remove KnownMusicApps, SystemNowPlaying enums from AppConstants - Remove source picker from MusicMonitorSettingsView - Remove source-aware branding from DiscordRPCService and WolfWaveApp - Revert docs (index, features, usage, changelog) to Apple Music language - Revert entitlements: remove com.apple.mediaremoted mach-lookup exception - Update WhatsNewView with v1.1.0 highlights (pending v1.2.0 feature set) - Fix @MainActor isolation for Twitch bot callbacks (MainActor.assumeIsolated) - Move CHANGELOG v1.2.0 entry to [Unreleased] — more features coming Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 6 +- apps/docs/content/docs/changelog.mdx | 4 - .../WolfWaveTests/KnownMusicAppsTests.swift | 105 --------- .../PlaybackSourceManagerTests.swift | 36 --- .../SystemNowPlayingSourceTests.swift | 82 ------- .../native/wolfwave.xcodeproj/project.pbxproj | 15 +- apps/native/wolfwave/Core/AppConstants.swift | 64 ----- .../wolfwave/Monitors/PlaybackSource.swift | 8 - .../Monitors/PlaybackSourceManager.swift | 39 +-- .../Monitors/SystemNowPlayingSource.swift | 222 ------------------ .../Services/Discord/DiscordRPCService.swift | 30 +-- .../MusicMonitorSettingsView.swift | 44 +--- .../OnboardingWelcomeStepView.swift | 2 +- .../wolfwave/Views/Shared/WhatsNewView.swift | 12 +- apps/native/wolfwave/WolfWaveApp.swift | 61 +---- 15 files changed, 46 insertions(+), 684 deletions(-) delete mode 100644 apps/native/WolfWaveTests/KnownMusicAppsTests.swift delete mode 100644 apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift delete mode 100644 apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 2051c0d..3cfa0a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -## [1.2.0] - 2026-04-01 - -### Added +### Fixed -- **Multi-source music support** — choose between Apple Music (direct ScriptingBridge connection) and Any App (System), which uses the macOS Now Playing API to capture playback from Spotify, browsers, and any other app. Setting lives in Music Monitor preferences. +- `@MainActor` isolation for `getCurrentSongInfo()` / `getLastSongInfo()` Twitch bot callbacks — replaced `DispatchQueue.main.sync` with `MainActor.assumeIsolated` to satisfy Swift strict concurrency. ## [1.1.0] - 2026-03-31 diff --git a/apps/docs/content/docs/changelog.mdx b/apps/docs/content/docs/changelog.mdx index 951d73f..9e2776e 100644 --- a/apps/docs/content/docs/changelog.mdx +++ b/apps/docs/content/docs/changelog.mdx @@ -7,10 +7,6 @@ description: Release history for WolfWave All notable changes to WolfWave are documented here. -## v1.2.0 — April 1, 2026 - -### Added -- **Multi-source music support** — choose between Apple Music (direct, most accurate) and Any App (System), which picks up whatever's playing — Spotify, browsers, anything. Switch in the Music Monitor settings. ## v1.1.0 — March 31, 2026 diff --git a/apps/native/WolfWaveTests/KnownMusicAppsTests.swift b/apps/native/WolfWaveTests/KnownMusicAppsTests.swift deleted file mode 100644 index adfe204..0000000 --- a/apps/native/WolfWaveTests/KnownMusicAppsTests.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// KnownMusicAppsTests.swift -// WolfWaveTests - -import XCTest -@testable import WolfWave - -final class KnownMusicAppsTests: XCTestCase { - - // MARK: - displayName - - func testDisplayNameAppleMusic() { - XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "com.apple.Music"), "Apple Music") - } - - func testDisplayNameSpotify() { - XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "com.spotify.client"), "Spotify") - } - - func testDisplayNameiTunes() { - XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "com.apple.iTunes"), "iTunes") - } - - func testDisplayNameChrome() { - XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "com.google.Chrome"), "Chrome") - } - - func testDisplayNameFirefox() { - XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "org.mozilla.firefox"), "Firefox") - } - - func testDisplayNameUnknownFallsBackToMusic() { - XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: "com.unknown.app"), "Music") - } - - func testDisplayNameNilFallsBackToMusic() { - XCTAssertEqual(AppConstants.KnownMusicApps.displayName(for: nil), "Music") - } - - // MARK: - discordAssetName - - func testDiscordAssetAppleMusic() { - XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.apple.Music"), "apple_music") - } - - func testDiscordAssetiTunes() { - XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.apple.iTunes"), "apple_music") - } - - func testDiscordAssetSpotify() { - XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.spotify.client"), "spotify") - } - - func testDiscordAssetChrome() { - XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.google.Chrome"), "youtube") - } - - func testDiscordAssetFirefox() { - XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "org.mozilla.firefox"), "youtube") - } - - func testDiscordAssetBrave() { - XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.brave.Browser"), "youtube") - } - - func testDiscordAssetUnknownFallsBackToGeneric() { - XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: "com.unknown.app"), "music_generic") - } - - func testDiscordAssetNilFallsBackToGeneric() { - XCTAssertEqual(AppConstants.KnownMusicApps.discordAssetName(for: nil), "music_generic") - } - - // MARK: - isAppleMusic - - func testIsAppleMusicTrue() { - XCTAssertTrue(AppConstants.KnownMusicApps.isAppleMusic("com.apple.Music")) - } - - func testIsAppleMusicFalseForSpotify() { - XCTAssertFalse(AppConstants.KnownMusicApps.isAppleMusic("com.spotify.client")) - } - - func testIsAppleMusicFalseForNil() { - XCTAssertFalse(AppConstants.KnownMusicApps.isAppleMusic(nil)) - } - - // MARK: - isBrowser - - func testIsBrowserChrome() { - XCTAssertTrue(AppConstants.KnownMusicApps.isBrowser("com.google.Chrome")) - } - - func testIsBrowserFirefox() { - XCTAssertTrue(AppConstants.KnownMusicApps.isBrowser("org.mozilla.firefox")) - } - - func testIsBrowserFalseForSpotify() { - XCTAssertFalse(AppConstants.KnownMusicApps.isBrowser("com.spotify.client")) - } - - func testIsBrowserFalseForNil() { - XCTAssertFalse(AppConstants.KnownMusicApps.isBrowser(nil)) - } -} diff --git a/apps/native/WolfWaveTests/PlaybackSourceManagerTests.swift b/apps/native/WolfWaveTests/PlaybackSourceManagerTests.swift index 6c59473..40255bd 100644 --- a/apps/native/WolfWaveTests/PlaybackSourceManagerTests.swift +++ b/apps/native/WolfWaveTests/PlaybackSourceManagerTests.swift @@ -7,16 +7,6 @@ import XCTest final class PlaybackSourceManagerTests: XCTestCase { - override func setUp() { - super.setUp() - UserDefaults.standard.removeObject(forKey: "playbackSourceMode") - } - - override func tearDown() { - UserDefaults.standard.removeObject(forKey: "playbackSourceMode") - super.tearDown() - } - // MARK: - Default Mode func testDefaultModeIsAppleMusic() { @@ -24,12 +14,6 @@ final class PlaybackSourceManagerTests: XCTestCase { XCTAssertEqual(manager.currentMode, .appleMusic) } - func testDefaultModePersistedModeIsRestored() { - UserDefaults.standard.set("systemNowPlaying", forKey: "playbackSourceMode") - let manager = PlaybackSourceManager() - XCTAssertEqual(manager.currentMode, .systemNowPlaying) - } - func testInvalidPersistedModeFallsBackToAppleMusic() { UserDefaults.standard.set("invalidMode", forKey: "playbackSourceMode") let manager = PlaybackSourceManager() @@ -38,26 +22,6 @@ final class PlaybackSourceManagerTests: XCTestCase { // MARK: - Mode Switching - func testSwitchModeUpdatesCurrentMode() { - let manager = PlaybackSourceManager() - manager.switchMode(.systemNowPlaying) - XCTAssertEqual(manager.currentMode, .systemNowPlaying) - } - - func testSwitchModeToSameModeIsNoOp() { - let manager = PlaybackSourceManager() - // Should not crash or change anything - manager.switchMode(.appleMusic) - XCTAssertEqual(manager.currentMode, .appleMusic) - } - - func testSwitchModePersistsToUserDefaults() { - let manager = PlaybackSourceManager() - manager.switchMode(.systemNowPlaying) - let stored = UserDefaults.standard.string(forKey: "playbackSourceMode") - XCTAssertEqual(stored, "systemNowPlaying") - } - // MARK: - Delegate Forwarding func testDelegateReceivesTrackUpdate() { diff --git a/apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift b/apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift deleted file mode 100644 index d00c2d0..0000000 --- a/apps/native/WolfWaveTests/SystemNowPlayingSourceTests.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// SystemNowPlayingSourceTests.swift -// WolfWaveTests - -import XCTest -@testable import WolfWave - -final class SystemNowPlayingSourceTests: XCTestCase { - - // MARK: - Initialization - - func testInitializationDoesNotCrash() { - // MediaRemote may or may not be available in the test environment - let source = SystemNowPlayingSource() - XCTAssertNotNil(source) - } - - func testConformsToPlaybackSource() { - let source = SystemNowPlayingSource() - XCTAssertTrue((source as AnyObject) is any PlaybackSource) - } - - // MARK: - Start / Stop - - func testStartTrackingDoesNotCrash() { - let source = SystemNowPlayingSource() - source.startTracking() - source.stopTracking() - } - - func testDoubleStartIsIdempotent() { - let source = SystemNowPlayingSource() - source.startTracking() - source.startTracking() // second call should be a no-op - source.stopTracking() - } - - func testDoubleStopIsIdempotent() { - let source = SystemNowPlayingSource() - source.startTracking() - source.stopTracking() - source.stopTracking() // second call should be a no-op - } - - // MARK: - Interval - - func testUpdateCheckIntervalDoesNotCrashWhenNotTracking() { - let source = SystemNowPlayingSource() - source.updateCheckInterval(10.0) - } - - func testUpdateCheckIntervalDoesNotCrashWhenTracking() { - let source = SystemNowPlayingSource() - source.startTracking() - source.updateCheckInterval(10.0) - source.stopTracking() - } - - // MARK: - Graceful Degradation - - func testDelegateWiringDoesNotCrash() { - let source = SystemNowPlayingSource() - let spy = StatusSpy() - source.delegate = spy - // If framework unavailable, delegate receives "System Now Playing unavailable" on main thread. - // If available, startTracking proceeds silently. Either way, no crash. - source.startTracking() - source.stopTracking() - } -} - -// MARK: - Helpers - -private class StatusSpy: PlaybackSourceDelegate { - var statuses: [String] = [] - - func playbackSource(_ source: any PlaybackSource, didUpdateTrack track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval) {} - - func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) { - statuses.append(status) - } -} diff --git a/apps/native/wolfwave.xcodeproj/project.pbxproj b/apps/native/wolfwave.xcodeproj/project.pbxproj index f0bb98a..cfd4596 100644 --- a/apps/native/wolfwave.xcodeproj/project.pbxproj +++ b/apps/native/wolfwave.xcodeproj/project.pbxproj @@ -21,7 +21,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - D40965CA2F0F906C00B292B6 /* WolfWave.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WolfWave.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D40965CA2F0F906C00B292B6 /* WolfWave Dev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WolfWave Dev.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D4A1B2012F0F906C00B292B6 /* WolfWaveTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WolfWaveTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D4CFGXCC00000000000000F1 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Config.xcconfig; path = wolfwave/Config.xcconfig; sourceTree = ""; }; D4CFGXCC00000000000000F2 /* Config.Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Config.Debug.xcconfig; path = wolfwave/Config.Debug.xcconfig; sourceTree = ""; }; @@ -81,18 +81,27 @@ D4A1B2022F0F906C00B292B6 /* WolfWaveTests */, D40965CB2F0F906C00B292B6 /* Products */, D4CFGXCC00000000000000F1 /* Config.xcconfig */, + D46E2E062F7F05E800089923 /* Recovered References */, ); sourceTree = ""; }; D40965CB2F0F906C00B292B6 /* Products */ = { isa = PBXGroup; children = ( - D40965CA2F0F906C00B292B6 /* WolfWave.app */, + D40965CA2F0F906C00B292B6 /* WolfWave Dev.app */, D4A1B2012F0F906C00B292B6 /* WolfWaveTests.xctest */, ); name = Products; sourceTree = ""; }; + D46E2E062F7F05E800089923 /* Recovered References */ = { + isa = PBXGroup; + children = ( + D4CFGXCC00000000000000F2 /* Config.Debug.xcconfig */, + ); + name = "Recovered References"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -116,7 +125,7 @@ D4FB5E3E2F6808870085EB41 /* Sparkle */, ); productName = WolfWave; - productReference = D40965CA2F0F906C00B292B6 /* WolfWave.app */; + productReference = D40965CA2F0F906C00B292B6 /* WolfWave Dev.app */; productType = "com.apple.product-type.application"; }; D4A1B2082F0F906C00B292B6 /* WolfWaveTests */ = { diff --git a/apps/native/wolfwave/Core/AppConstants.swift b/apps/native/wolfwave/Core/AppConstants.swift index 047cf4f..bfcb3ec 100644 --- a/apps/native/wolfwave/Core/AppConstants.swift +++ b/apps/native/wolfwave/Core/AppConstants.swift @@ -66,8 +66,6 @@ enum AppConstants { /// Posted when Twitch chat connection state changes. UserInfo contains "isConnected" Bool. static let twitchConnectionStateChanged = "TwitchChatConnectionStateChanged" - /// Posted when the user changes the playback source mode. - static let playbackSourceModeChanged = "PlaybackSourceModeChanged" } // MARK: - UserDefaults Keys @@ -155,8 +153,6 @@ enum AppConstants { /// Whether the widget HTTP server is enabled (Bool, default: false) static let widgetHTTPEnabled = "widgetHTTPEnabled" - /// The user's chosen playback source mode: "appleMusic" or "systemNowPlaying" (String, default: "appleMusic") - static let playbackSourceMode = "playbackSourceMode" } // MARK: - Dock Visibility Modes @@ -484,66 +480,6 @@ enum AppConstants { static let reducedProgressBroadcastInterval: TimeInterval = 3.0 } - // MARK: - System Now Playing - - /// Constants for the System Now Playing (MediaRemote) source. - enum SystemNowPlaying { - static let frameworkPath = "/System/Library/PrivateFrameworks/MediaRemote.framework/MediaRemote" - static let nowPlayingInfoDidChangeNotification = "kMRMediaRemoteNowPlayingInfoDidChangeNotification" - } - - // MARK: - Known Music Apps - - /// Maps source app bundle IDs to display names and Discord asset names. - enum KnownMusicApps { - private static let browserBundleIDs: Set = [ - "com.google.Chrome", - "org.mozilla.firefox", - "com.brave.Browser", - "com.microsoft.edgemac", - "com.apple.Safari", - ] - - /// Human-readable name for the given source app bundle ID. - static func displayName(for bundleID: String?) -> String { - switch bundleID { - case "com.apple.Music": return "Apple Music" - case "com.spotify.client": return "Spotify" - case "com.apple.iTunes": return "iTunes" - case "com.google.Chrome": return "Chrome" - case "org.mozilla.firefox": return "Firefox" - case "com.brave.Browser": return "Brave" - case "com.microsoft.edgemac": return "Edge" - case "tv.plex.plexamp": return "Plexamp" - case "com.tidal.desktop": return "TIDAL" - case "com.amazon.music": return "Amazon Music" - default: return "Music" - } - } - - /// Discord Developer Portal asset name for the given source app bundle ID. - static func discordAssetName(for bundleID: String?) -> String { - switch bundleID { - case "com.apple.Music", "com.apple.iTunes": return "apple_music" - case "com.spotify.client": return "spotify" - default: - if let id = bundleID, browserBundleIDs.contains(id) { return "youtube" } - return "music_generic" - } - } - - /// Returns `true` when the bundle ID is Apple Music. - static func isAppleMusic(_ bundleID: String?) -> Bool { - bundleID == "com.apple.Music" - } - - /// Returns `true` when the bundle ID belongs to a known browser. - static func isBrowser(_ bundleID: String?) -> Bool { - guard let id = bundleID else { return false } - return browserBundleIDs.contains(id) - } - } - // MARK: - Onboarding UI /// Onboarding wizard window configuration. diff --git a/apps/native/wolfwave/Monitors/PlaybackSource.swift b/apps/native/wolfwave/Monitors/PlaybackSource.swift index 4bf57de..2ecd324 100644 --- a/apps/native/wolfwave/Monitors/PlaybackSource.swift +++ b/apps/native/wolfwave/Monitors/PlaybackSource.swift @@ -5,7 +5,6 @@ import Foundation /// The user's chosen music source mode. enum PlaybackSourceMode: String { case appleMusic = "appleMusic" - case systemNowPlaying = "systemNowPlaying" } // MARK: - PlaybackSourceDelegate @@ -21,13 +20,6 @@ protocol PlaybackSourceDelegate: AnyObject { elapsed: TimeInterval ) func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) - /// Called when the source detects which app is currently playing media. - /// - Parameter bundleIdentifier: The bundle ID of the playing app, or `nil` if unknown. - func playbackSource(_ source: any PlaybackSource, didDetectSourceApp bundleIdentifier: String?) -} - -extension PlaybackSourceDelegate { - func playbackSource(_ source: any PlaybackSource, didDetectSourceApp bundleIdentifier: String?) {} } // MARK: - PlaybackSource diff --git a/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift b/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift index 976e6ec..73211f8 100644 --- a/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift +++ b/apps/native/wolfwave/Monitors/PlaybackSourceManager.swift @@ -16,15 +16,13 @@ class PlaybackSourceManager: PlaybackSourceDelegate { private(set) var currentMode: PlaybackSourceMode private lazy var appleMusicSource = AppleMusicSource() - private lazy var systemNowPlayingSource = SystemNowPlayingSource() private var activeSource: (any PlaybackSource)? private var isTracking = false // MARK: - Init init() { - let stored = UserDefaults.standard.string(forKey: AppConstants.UserDefaults.playbackSourceMode) - currentMode = PlaybackSourceMode(rawValue: stored ?? "") ?? .appleMusic + currentMode = .appleMusic } // MARK: - Public Methods @@ -32,33 +30,20 @@ class PlaybackSourceManager: PlaybackSourceDelegate { /// Starts tracking with the current mode's source. func startTracking() { stopTracking() - let source: any PlaybackSource = (currentMode == .appleMusic) ? appleMusicSource : systemNowPlayingSource - source.delegate = self // NOTE: this requires a workaround — see below - activeSource = source + appleMusicSource.delegate = self + activeSource = appleMusicSource isTracking = true - source.startTracking() + appleMusicSource.startTracking() } /// Stops the active source. func stopTracking() { activeSource?.stopTracking() - // Clear delegate to avoid retain issues - // (set via the concrete type helpers below) - clearActiveSourceDelegate() + appleMusicSource.delegate = nil activeSource = nil isTracking = false } - /// Switches to a new mode. If currently tracking, restarts with the new source. - func switchMode(_ mode: PlaybackSourceMode) { - guard mode != currentMode else { return } - let wasTracking = isTracking - stopTracking() - currentMode = mode - UserDefaults.standard.set(mode.rawValue, forKey: AppConstants.UserDefaults.playbackSourceMode) - if wasTracking { startTracking() } - } - /// Updates the fallback polling interval on the active source. func updateCheckInterval(_ interval: TimeInterval) { activeSource?.updateCheckInterval(interval) @@ -73,18 +58,4 @@ class PlaybackSourceManager: PlaybackSourceDelegate { func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) { delegate?.playbackSource(source, didUpdateStatus: status) } - - func playbackSource(_ source: any PlaybackSource, didDetectSourceApp bundleIdentifier: String?) { - delegate?.playbackSource(source, didDetectSourceApp: bundleIdentifier) - } - - // MARK: - Private Helpers - - private func clearActiveSourceDelegate() { - if let source = activeSource as? AppleMusicSource { - source.delegate = nil - } else if let source = activeSource as? SystemNowPlayingSource { - source.delegate = nil - } - } } diff --git a/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift b/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift deleted file mode 100644 index 695f6d4..0000000 --- a/apps/native/wolfwave/Monitors/SystemNowPlayingSource.swift +++ /dev/null @@ -1,222 +0,0 @@ -import Foundation -import AppKit - -// MARK: - SystemNowPlayingSource - -/// A playback source that uses the private MediaRemote framework to capture -/// now-playing info from any app (Spotify, Chrome, web players, etc.). -class SystemNowPlayingSource: PlaybackSource { - - // MARK: - Constants - - private enum Constants { - static let frameworkPath = AppConstants.SystemNowPlaying.frameworkPath - static let nowPlayingChangedNotification = AppConstants.SystemNowPlaying.nowPlayingInfoDidChangeNotification - static let queueLabel = AppConstants.DispatchQueues.systemNowPlaying - static let checkInterval: TimeInterval = 5.0 - static let notificationDedupWindow: TimeInterval = 0.75 - static let idleGraceWindow: TimeInterval = 2.0 - - static let keyTitle = "kMRMediaRemoteNowPlayingInfoTitle" - static let keyArtist = "kMRMediaRemoteNowPlayingInfoArtist" - static let keyAlbum = "kMRMediaRemoteNowPlayingInfoAlbum" - static let keyDuration = "kMRMediaRemoteNowPlayingInfoDuration" - static let keyElapsedTime = "kMRMediaRemoteNowPlayingInfoElapsedTime" - static let keyPlaybackRate = "kMRMediaRemoteNowPlayingInfoPlaybackRate" - } - - // MARK: - Properties - - weak var delegate: PlaybackSourceDelegate? - - private var currentCheckInterval: TimeInterval = Constants.checkInterval - private var timer: DispatchSourceTimer? - private var lastTrackSeenAt: Date = .distantPast - private var lastNotificationAt: Date = .distantPast - private var lastLoggedTrack: String? - private var isTracking = false - - private let backgroundQueue = DispatchQueue(label: Constants.queueLabel, qos: .utility) - - // MARK: - Framework Loading - - private typealias MRMediaRemoteGetNowPlayingInfoFunction = - @convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void - private typealias MRMediaRemoteRegisterForNowPlayingNotificationsFunction = - @convention(c) (DispatchQueue) -> Void - private typealias MRMediaRemoteGetNowPlayingApplicationPIDFunction = - @convention(c) (DispatchQueue, @escaping (Int32) -> Void) -> Void - - private var getNowPlayingInfo: MRMediaRemoteGetNowPlayingInfoFunction? - private var registerForNotifications: MRMediaRemoteRegisterForNowPlayingNotificationsFunction? - private var getNowPlayingApplicationPID: MRMediaRemoteGetNowPlayingApplicationPIDFunction? - private var frameworkLoaded = false - - // MARK: - Lifecycle - - init() { - guard let handle = dlopen(Constants.frameworkPath, RTLD_LAZY) else { - Log.warn("SystemNowPlayingSource: MediaRemote framework unavailable", category: "Music") - return - } - if let sym = dlsym(handle, "MRMediaRemoteGetNowPlayingInfo") { - getNowPlayingInfo = unsafeBitCast(sym, to: MRMediaRemoteGetNowPlayingInfoFunction.self) - } - if let sym = dlsym(handle, "MRMediaRemoteRegisterForNowPlayingNotifications") { - registerForNotifications = unsafeBitCast(sym, to: MRMediaRemoteRegisterForNowPlayingNotificationsFunction.self) - } - if let sym = dlsym(handle, "MRMediaRemoteGetNowPlayingApplicationPID") { - getNowPlayingApplicationPID = unsafeBitCast(sym, to: MRMediaRemoteGetNowPlayingApplicationPIDFunction.self) - } - frameworkLoaded = getNowPlayingInfo != nil - if !frameworkLoaded { - Log.warn("SystemNowPlayingSource: Failed to resolve MediaRemote symbols", category: "Music") - } - } - - deinit { stopTracking() } - - // MARK: - Public Methods - - /// Begins tracking system-wide now-playing state via MediaRemote. - func startTracking() { - guard !isTracking else { return } - guard frameworkLoaded else { - notifyDelegate(status: "System Now Playing unavailable") - return - } - isTracking = true - registerForNotifications?(backgroundQueue) - NotificationCenter.default.addObserver( - self, - selector: #selector(nowPlayingInfoChanged), - name: NSNotification.Name(Constants.nowPlayingChangedNotification), - object: nil - ) - fetchNowPlayingInfo() - setupFallbackTimer() - } - - /// Stops tracking and tears down the notification observer and timer. - func stopTracking() { - guard isTracking else { return } - NotificationCenter.default.removeObserver( - self, - name: NSNotification.Name(Constants.nowPlayingChangedNotification), - object: nil - ) - timer?.cancel() - timer = nil - isTracking = false - } - - /// Updates the fallback polling interval while tracking is active. - func updateCheckInterval(_ interval: TimeInterval) { - guard isTracking else { return } - currentCheckInterval = max(interval, 1.0) - timer?.cancel() - timer = nil - setupFallbackTimer() - } - - // MARK: - Private Helpers - - @objc private func nowPlayingInfoChanged(_ notification: Notification) { - let now = Date() - guard now.timeIntervalSince(lastNotificationAt) >= Constants.notificationDedupWindow else { return } - lastNotificationAt = now - Log.debug("SystemNowPlayingSource: Now Playing notification received", category: "Music") - fetchNowPlayingInfo() - } - - private func fetchNowPlayingInfo() { - getNowPlayingInfo?(backgroundQueue) { [weak self] info in - guard let self = self else { return } - self.processNowPlayingInfo(info) - } - fetchNowPlayingApplicationBundleID() - } - - private func fetchNowPlayingApplicationBundleID() { - getNowPlayingApplicationPID?(backgroundQueue) { [weak self] pid in - guard let self = self, pid > 0 else { return } - let bundleID = NSRunningApplication(processIdentifier: pid_t(pid))?.bundleIdentifier - self.notifyDelegateSourceApp(bundleID) - } - } - - private func processNowPlayingInfo(_ info: [String: Any]) { - let playbackRate = (info[Constants.keyPlaybackRate] as? Double) ?? 0 - let title = (info[Constants.keyTitle] as? String) ?? "" - - guard playbackRate != 0, !title.isEmpty else { - handleNotPlayingState() - return - } - - let artist = (info[Constants.keyArtist] as? String) ?? "" - let album = (info[Constants.keyAlbum] as? String) ?? "" - let duration = (info[Constants.keyDuration] as? Double) ?? 0 - let elapsed = (info[Constants.keyElapsedTime] as? Double) ?? 0 - - lastTrackSeenAt = Date() - notifyDelegate(track: title, artist: artist, album: album, duration: duration, elapsed: elapsed) - logTrackIfNew(title, trackName: title, artist: artist, album: album) - } - - private func handleNotPlayingState() { - let idleDuration = Date().timeIntervalSince(lastTrackSeenAt) - if idleDuration < Constants.idleGraceWindow { - scheduleCheck(after: 0.5, reason: "idle-grace-recheck") - return - } - notifyDelegate(status: "No track playing") - } - - private func notifyDelegate(status: String) { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.delegate?.playbackSource(self, didUpdateStatus: status) - } - } - - private func notifyDelegateSourceApp(_ bundleID: String?) { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.delegate?.playbackSource(self, didDetectSourceApp: bundleID) - } - } - - private func notifyDelegate(track: String, artist: String, album: String, duration: TimeInterval, elapsed: TimeInterval) { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.delegate?.playbackSource(self, didUpdateTrack: track, artist: artist, album: album, duration: duration, elapsed: elapsed) - } - } - - private func logTrackIfNew(_ key: String, trackName: String, artist: String, album: String) { - let dedupKey = trackName + " | " + artist + " | " + album - guard lastLoggedTrack != dedupKey else { return } - Log.debug("SystemNowPlayingSource: Now Playing → \(trackName) — \(artist) [\(album)]", category: "Music") - lastLoggedTrack = dedupKey - } - - // MARK: - Setup & Scheduling - - private func setupFallbackTimer() { - let timer = DispatchSource.makeTimerSource(queue: backgroundQueue) - timer.schedule(deadline: .now() + currentCheckInterval, repeating: currentCheckInterval) - timer.setEventHandler { [weak self] in - guard let self = self, self.isTracking else { return } - self.fetchNowPlayingInfo() - } - timer.activate() - self.timer = timer - } - - private func scheduleCheck(after delay: TimeInterval, reason: String) { - backgroundQueue.asyncAfter(deadline: .now() + delay) { [weak self] in - self?.fetchNowPlayingInfo() - } - } -} diff --git a/apps/native/wolfwave/Services/Discord/DiscordRPCService.swift b/apps/native/wolfwave/Services/Discord/DiscordRPCService.swift index 159eaf6..c2a45b5 100644 --- a/apps/native/wolfwave/Services/Discord/DiscordRPCService.swift +++ b/apps/native/wolfwave/Services/Discord/DiscordRPCService.swift @@ -212,8 +212,7 @@ final class DiscordRPCService: @unchecked Sendable { artist: String, album: String, duration: TimeInterval = 0, - elapsed: TimeInterval = 0, - sourceAppBundleID: String? = nil + elapsed: TimeInterval = 0 ) { ipcQueue.async { [weak self] in guard let self, self.state == .connected else { return } @@ -225,8 +224,7 @@ final class DiscordRPCService: @unchecked Sendable { artworkURL: cached.artworkURL, duration: duration, elapsed: elapsed, appleMusicURL: cached.trackViewURL, - songLinkURL: cached.songLinkURL, - sourceAppBundleID: sourceAppBundleID + songLinkURL: cached.songLinkURL ) // Fetch track links asynchronously on cache miss @@ -245,8 +243,7 @@ final class DiscordRPCService: @unchecked Sendable { artworkURL: links.artworkURL, duration: duration, elapsed: elapsed, appleMusicURL: links.trackViewURL, - songLinkURL: links.songLinkURL, - sourceAppBundleID: sourceAppBundleID + songLinkURL: links.songLinkURL ) } // Notify listeners (e.g., WebSocket server) only when artwork is resolved @@ -303,7 +300,6 @@ final class DiscordRPCService: @unchecked Sendable { /// asset uploaded in the Discord Developer Portal. /// - duration: Total track duration in seconds (0 if unknown). /// - elapsed: Elapsed time in seconds (0 if unknown). - /// - sourceAppBundleID: Bundle ID of the playing app for source-aware branding. private func sendPresenceActivity( track: String, artist: String, @@ -312,28 +308,22 @@ final class DiscordRPCService: @unchecked Sendable { duration: TimeInterval, elapsed: TimeInterval, appleMusicURL: String? = nil, - songLinkURL: String? = nil, - sourceAppBundleID: String? = nil + songLinkURL: String? = nil ) { - let sourceAsset = AppConstants.KnownMusicApps.discordAssetName(for: sourceAppBundleID) - let sourceName = AppConstants.KnownMusicApps.displayName(for: sourceAppBundleID) - let isAppleMusic = AppConstants.KnownMusicApps.isAppleMusic(sourceAppBundleID) - var activity: [String: Any] = [ "type": AppConstants.Discord.listeningActivityType, "details": track, "state": "by \(artist)", ] - // Assets — prefer dynamic artwork URL, fall back to source-specific Discord asset - let largeImage = artworkURL ?? sourceAsset + // Assets — prefer dynamic artwork URL, fall back to Apple Music Discord asset + let largeImage = artworkURL ?? "apple_music" var assets: [String: Any] = [ "large_image": largeImage, "large_text": album, ] - // Always show the source app logo as small overlay icon - assets["small_image"] = sourceAsset - assets["small_text"] = sourceName + assets["small_image"] = "apple_music" + assets["small_text"] = "Apple Music" activity["assets"] = assets // Timestamps — show a progress bar if duration is known @@ -347,9 +337,9 @@ final class DiscordRPCService: @unchecked Sendable { ] } - // Buttons — "Open in Apple Music" only for Apple Music source; song.link when available + // Buttons — "Open in Apple Music" and song.link when available var buttons: [[String: String]] = [] - if isAppleMusic, let appleMusicURL { + if let appleMusicURL { buttons.append(["label": "Open in Apple Music", "url": appleMusicURL]) } if let songLinkURL { diff --git a/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift b/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift index 2292f9f..e2e6ec2 100644 --- a/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift +++ b/apps/native/wolfwave/Views/MusicMonitor/MusicMonitorSettingsView.swift @@ -23,8 +23,6 @@ struct MusicMonitorSettingsView: View { @AppStorage(AppConstants.UserDefaults.trackingEnabled) private var trackingEnabled = true - @AppStorage(AppConstants.UserDefaults.playbackSourceMode) private var playbackSourceMode: String = "appleMusic" - @State private var permissionDenied = false @State private var currentTrack: String? @State private var currentArtist: String? @@ -50,48 +48,8 @@ struct MusicMonitorSettingsView: View { .fixedSize(horizontal: false, vertical: true) } - // Music Source picker - VStack(alignment: .leading, spacing: 6) { - Text("Music Source") - .font(.headline) - - Picker("", selection: $playbackSourceMode) { - Text("Apple Music").tag("appleMusic") - Text("Any App (System)").tag("systemNowPlaying") - } - .pickerStyle(.segmented) - .accessibilityLabel("Music Source") - .accessibilityIdentifier("musicSourcePicker") - .onChange(of: playbackSourceMode) { _, newValue in - NotificationCenter.default.post( - name: NSNotification.Name(AppConstants.Notifications.playbackSourceModeChanged), - object: nil, - userInfo: ["mode": newValue] - ) - } - - Text(playbackSourceMode == "appleMusic" - ? "Connects directly to Apple Music for accurate track info." - : "Picks up whatever's playing — Spotify, browsers, anything.") - .font(.caption) - .foregroundStyle(.secondary) - - if playbackSourceMode == "systemNowPlaying" { - HStack(spacing: 6) { - Image(systemName: "info.circle") - .foregroundStyle(.secondary) - Text("Uses macOS system media info. No Apple Music access needed.") - .font(.caption) - .foregroundStyle(.secondary) - } - .accessibilityElement(children: .combine) - .accessibilityLabel("Uses macOS system media info. No Apple Music access needed.") - } - } - .padding(.bottom, 4) - // Permission warning - if permissionDenied && playbackSourceMode == "appleMusic" { + if permissionDenied { HStack(spacing: 10) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 14)) diff --git a/apps/native/wolfwave/Views/Onboarding/OnboardingWelcomeStepView.swift b/apps/native/wolfwave/Views/Onboarding/OnboardingWelcomeStepView.swift index f07233f..c121ea7 100644 --- a/apps/native/wolfwave/Views/Onboarding/OnboardingWelcomeStepView.swift +++ b/apps/native/wolfwave/Views/Onboarding/OnboardingWelcomeStepView.swift @@ -27,7 +27,7 @@ struct OnboardingWelcomeStepView: View { Feature( icon: .brand(name: "AppleMusicLogo", renderOriginal: true), title: "Music Sync", - description: "Detects what's playing in Apple Music, Spotify, or any app." + description: "Detects what's playing in Apple Music." ), Feature( icon: .brand(name: "TwitchLogo", renderOriginal: true), diff --git a/apps/native/wolfwave/Views/Shared/WhatsNewView.swift b/apps/native/wolfwave/Views/Shared/WhatsNewView.swift index fdfd2f4..789d8b7 100644 --- a/apps/native/wolfwave/Views/Shared/WhatsNewView.swift +++ b/apps/native/wolfwave/Views/Shared/WhatsNewView.swift @@ -21,12 +21,12 @@ struct WhatsNewView: View { private static let discordIndigo = Color(red: 0.35, green: 0.40, blue: 0.95) private let features: [(icon: String, iconColor: Color, title: String, description: String)] = [ - ("apple.logo", .primary, "macOS Tahoe", "Built exclusively for macOS 26"), - ("cpu", .blue, "Apple Silicon Only", "Optimized for M-series chips"), - ("shield.checkered", .green, "Security Hardened", "Tighter entitlements and token validation"), - ("figure.stand", twitchPurple, "Accessibility", "Full VoiceOver support across all settings"), - ("swift", .orange, "Modern Swift", "Async/await, @Observable, and actor isolation"), - ("testtube.2", .mint, "Better Testing", "End-to-end test coverage for all major flows") + ("music.note.2", .pink, "Discord Buttons", "Open in Apple Music or jump to song.link from your Discord status"), + ("arrow.up.right.circle", .blue, "Launch at Login", "WolfWave starts automatically when your Mac does"), + ("sparkle", twitchPurple, "Homebrew Auto-Update", "Homebrew tap stays in sync whenever a new release ships"), + ("rectangle.and.arrow.up.right.and.arrow.down.left", .green, "Custom DMG", "Polished installer background with WolfWave branding"), + ("paintbrush", discordIndigo, "Artwork & Links", "Album art and song.link resolved automatically for every track"), + ("checkmark.shield", .orange, "Stability", "Tighter entitlements, fixed Sparkle updates, and smarter reconnects") ] // MARK: - Body diff --git a/apps/native/wolfwave/WolfWaveApp.swift b/apps/native/wolfwave/WolfWaveApp.swift index 6a3155f..8cc7303 100644 --- a/apps/native/wolfwave/WolfWaveApp.swift +++ b/apps/native/wolfwave/WolfWaveApp.swift @@ -57,8 +57,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele private var currentElapsed: TimeInterval = 0 private var lastSong: String? private var lastArtist: String? - private(set) var currentSourceBundleID: String? - private var currentDockVisibilityMode: String { UserDefaults.standard.string(forKey: AppConstants.UserDefaults.dockVisibility) ?? AppConstants.DockVisibility.default @@ -372,37 +370,23 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele /// Returns a formatted string with the current track for Twitch bot commands. func getCurrentSongInfo() -> String { - let isSystemMode = playbackSourceManager?.currentMode == .systemNowPlaying - if !isSystemMode { - guard isMusicAppOpen() else { - return "🐺 Please open Apple Music" - } + guard isMusicAppOpen() else { + return "🐺 Please open Apple Music" } guard let song = currentSong, let artist = currentArtist else { return "🐺 No music playing" } - if isSystemMode && !AppConstants.KnownMusicApps.isAppleMusic(currentSourceBundleID) { - let name = AppConstants.KnownMusicApps.displayName(for: currentSourceBundleID) - return "🐺 Playing: \(song) by \(artist) via \(name)" - } return "🐺 Playing: \(song) by \(artist)" } /// Returns a formatted string with the previously played track for Twitch bot commands. func getLastSongInfo() -> String { - let isSystemMode = playbackSourceManager?.currentMode == .systemNowPlaying - if !isSystemMode { - guard isMusicAppOpen() else { - return "🐺 Please open Apple Music" - } + guard isMusicAppOpen() else { + return "🐺 Please open Apple Music" } guard let song = lastSong, let artist = lastArtist else { return "🐺 No previous tracks yet, keep the music flowing!" } - if isSystemMode && !AppConstants.KnownMusicApps.isAppleMusic(currentSourceBundleID) { - let name = AppConstants.KnownMusicApps.displayName(for: currentSourceBundleID) - return "🐺 Previous: \(song) by \(artist) via \(name)" - } return "🐺 Previous: \(song) by \(artist)" } @@ -732,21 +716,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele twitchService?.getCurrentSongInfo = { [weak self] in if Thread.isMainThread { - return self?.getCurrentSongInfo() ?? "Nothing playing right now" + return MainActor.assumeIsolated { self?.getCurrentSongInfo() ?? "Nothing playing right now" } } var result = "Nothing playing right now" DispatchQueue.main.sync { - result = self?.getCurrentSongInfo() ?? "Nothing playing right now" + result = MainActor.assumeIsolated { self?.getCurrentSongInfo() ?? "Nothing playing right now" } } return result } twitchService?.getLastSongInfo = { [weak self] in if Thread.isMainThread { - return self?.getLastSongInfo() ?? "No previous track yet" + return MainActor.assumeIsolated { self?.getLastSongInfo() ?? "No previous track yet" } } var result = "No previous track yet" DispatchQueue.main.sync { - result = self?.getLastSongInfo() ?? "No previous track yet" + result = MainActor.assumeIsolated { self?.getLastSongInfo() ?? "No previous track yet" } } return result } @@ -935,23 +919,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDele } ) - notificationObservers.append( - nc.addObserver( - forName: NSNotification.Name(AppConstants.Notifications.playbackSourceModeChanged), - object: nil, - queue: .main - ) { [weak self] notification in - guard let modeString = notification.userInfo?["mode"] as? String, - let mode = PlaybackSourceMode(rawValue: modeString) else { return } - self?.playbackSourceManager?.switchMode(mode) - // Reset source bundle ID when switching modes - if mode == .appleMusic { - self?.currentSourceBundleID = AppConstants.Music.bundleIdentifier - } else { - self?.currentSourceBundleID = nil - } - } - ) } // MARK: - Tracking State @@ -1244,25 +1211,15 @@ extension AppDelegate: PlaybackSourceDelegate { fetchArtworkForWidget(track: track, artist: artist) - // For Apple Music mode the source never calls didDetectSourceApp, so set it here. - if playbackSourceManager?.currentMode == .appleMusic { - currentSourceBundleID = AppConstants.Music.bundleIdentifier - } - discordService?.updatePresence( track: track, artist: artist, album: album, duration: duration, - elapsed: elapsed, - sourceAppBundleID: currentSourceBundleID + elapsed: elapsed ) } - func playbackSource(_ source: any PlaybackSource, didDetectSourceApp bundleIdentifier: String?) { - currentSourceBundleID = bundleIdentifier - } - /// Clears track state and notifies services when playback stops. func playbackSource(_ source: any PlaybackSource, didUpdateStatus status: String) { if status == "No track playing" {