Skillshub axiom-now-playing-musickit
MusicKit Now Playing integration patterns. Use when playing Apple Music content with ApplicationMusicPlayer and understanding automatic vs manual Now Playing info updates.
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/CharlesWiltgen/Axiom/axiom-now-playing-musickit" ~/.claude/skills/comeonoliver-skillshub-axiom-now-playing-musickit && rm -rf "$T"
manifest:
skills/CharlesWiltgen/Axiom/axiom-now-playing-musickit/SKILL.mdsource content
MusicKit Integration (Apple Music)
Time cost: 5-10 minutes
Key Insight
MusicKit's ApplicationMusicPlayer automatically publishes to MPNowPlayingInfoCenter. You don't need to manually update Now Playing info when playing Apple Music content.
What's Automatic
When using
ApplicationMusicPlayer:
- Track title, artist, album
- Artwork (Apple's album art)
- Duration and elapsed time
- Playback rate (playing/paused state)
The system handles all MPNowPlayingInfoCenter updates for you.
What's NOT Automatic
- Custom metadata (chapter markers, custom artist notes)
- Remote command customization beyond standard controls
- Mixing MusicKit content with your own content
Subscription and Authorization
Check Music Authorization
import MusicKit func requestMusicAccess() async -> Bool { let status = await MusicAuthorization.request() return status == .authorized } // Check current status without prompting let currentStatus = MusicAuthorization.currentStatus // .authorized, .denied, .notDetermined, .restricted
Check Apple Music Subscription
func checkSubscription() async -> Bool { do { let subscription = try await MusicSubscription.current return subscription.canPlayCatalogContent } catch { return false } } // Observe subscription changes func observeSubscription() { Task { for await subscription in MusicSubscription.subscriptionUpdates { if subscription.canPlayCatalogContent { // Full Apple Music access } else if subscription.canBecomeSubscriber { // Show subscription offer showSubscriptionOffer() } } } }
Subscription Offer Sheet
import MusicKit import StoreKit // Present Apple Music subscription offer MusicSubscriptionOffer.Options( messageIdentifier: .playMusic, itemID: song.id ) // In SwiftUI .musicSubscriptionOffer(isPresented: $showOffer, options: offerOptions)
Graceful Fallback Without Subscription
@MainActor class MusicPlayer: ObservableObject { @Published var canPlay = false func handlePlayRequest(song: Song) async { let authorized = await requestMusicAccess() guard authorized else { showAuthorizationDeniedAlert() return } do { let subscription = try await MusicSubscription.current if subscription.canPlayCatalogContent { // Full playback try await play(song: song) } else { // Preview only (30-second clips) if let previewURL = song.previewAssets?.first?.url { playPreview(url: previewURL) } } } catch { handleError(error) } } }
Playback
Basic Playback
import MusicKit @MainActor class MusicKitPlayer { private let player = ApplicationMusicPlayer.shared func play(song: Song) async throws { // ✅ Just play - MPNowPlayingInfoCenter updates automatically player.queue = [song] try await player.play() // ❌ DO NOT manually set nowPlayingInfo here // MPNowPlayingInfoCenter.default().nowPlayingInfo = [...] // WRONG! } func pause() { player.pause() } func stop() { player.stop() } }
Observing Playback State
@MainActor class PlayerViewModel: ObservableObject { private let player = ApplicationMusicPlayer.shared @Published var isPlaying = false @Published var currentEntry: ApplicationMusicPlayer.Queue.Entry? @Published var playbackTime: TimeInterval = 0 func observeState() { // Observe playback status Task { for await state in player.state.objectWillChange.values { isPlaying = player.state.playbackStatus == .playing } } // Observe current entry (track changes) Task { for await queue in player.queue.objectWillChange.values { currentEntry = player.queue.currentEntry } } } }
Queue Management
Setting the Queue
let player = ApplicationMusicPlayer.shared // Single song player.queue = [song] // Album player.queue = ApplicationMusicPlayer.Queue(album: album) // Playlist player.queue = ApplicationMusicPlayer.Queue(playlist: playlist) // Multiple items player.queue = ApplicationMusicPlayer.Queue(for: [song1, song2, song3]) // Start at specific item player.queue = ApplicationMusicPlayer.Queue(for: songs, startingAt: songs[2])
Queue Operations
// Skip to next try await player.skipToNextEntry() // Skip to previous try await player.skipToPreviousEntry() // Restart current track player.restartCurrentEntry() // Append to queue try await player.queue.insert(song, position: .afterCurrentEntry) try await player.queue.insert(song, position: .tail) // End of queue // Shuffle and repeat player.state.shuffleMode = .songs // .off, .songs player.state.repeatMode = .all // .none, .one, .all
Observing Queue Changes
// Current track info if let entry = player.queue.currentEntry { let title = entry.title let subtitle = entry.subtitle // Artist name let artwork = entry.artwork // Artwork for display // Get full Song object if needed if case .song(let song) = entry.item { let albumTitle = song.albumTitle } }
Hybrid Apps (Own Content + Apple Music)
If your app plays both Apple Music and your own content:
import MusicKit @MainActor class HybridPlayer { private let musicKitPlayer = ApplicationMusicPlayer.shared private var avPlayer: AVPlayer? private var currentSource: ContentSource = .none enum ContentSource { case none case appleMusic // MusicKit handles Now Playing case ownContent // We handle Now Playing } func playAppleMusicSong(_ song: Song) async throws { // Switch to MusicKit avPlayer?.pause() currentSource = .appleMusic musicKitPlayer.queue = [song] try await musicKitPlayer.play() // ✅ MusicKit handles Now Playing automatically } func playOwnContent(_ url: URL) { // Switch to AVPlayer musicKitPlayer.pause() currentSource = .ownContent avPlayer = AVPlayer(url: url) avPlayer?.play() // ✅ Manually update Now Playing (see axiom-now-playing) updateNowPlayingForOwnContent() } private func updateNowPlayingForOwnContent() { var nowPlayingInfo = [String: Any]() nowPlayingInfo[MPMediaItemPropertyTitle] = "My Track" // ... rest of manual setup MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } }
Common Mistake
// ❌ WRONG - Overwrites MusicKit's automatic Now Playing data func playAppleMusicSong(_ song: Song) async throws { try await ApplicationMusicPlayer.shared.play() // ❌ This clears MusicKit's Now Playing info! var nowPlayingInfo = [String: Any]() nowPlayingInfo[MPMediaItemPropertyTitle] = song.title MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } // ✅ CORRECT - Let MusicKit handle it func playAppleMusicSong(_ song: Song) async throws { try await ApplicationMusicPlayer.shared.play() // That's it! MusicKit publishes Now Playing automatically. }
When to Use Manual Updates with MusicKit
Only override MPNowPlayingInfoCenter if:
- You're mixing in additional metadata (e.g., podcast chapter markers)
- You're displaying custom content alongside Apple Music
- You have a specific reason to replace MusicKit's automatic behavior
Default: Let MusicKit manage Now Playing automatically.
Resources
Docs: /musickit, /musickit/applicationmusicplayer, /musickit/musicsubscription
Skills: axiom-now-playing, axiom-now-playing-carplay