import SwiftUI import MusicKit struct AppleMusicTrack: Identifiable, Equatable { let id: String let title: String let artist: String let album: String let duration: Double let artwork: URL? } struct AppleMusicPlaylist: Identifiable, Equatable { let id: String;let name: String } struct AppleMusicPage { let tracks: [AppleMusicTrack];let hasMore: Bool } enum AppleMusicAccess { case notDetermined,authorized,denied,restricted } struct AppleMusicQueueItem: Identifiable, Equatable { let id: String;let title: String;let artist: String;let current: Bool } struct AppleMusicPlayback: Equatable { var title = "";var artist = "";var playing = false;var position: Double = 0;var duration: Double = 0;var queue = [AppleMusicQueueItem]() } @MainActor protocol AppleMusicClient: AnyObject { var access: AppleMusicAccess { get } func authorize() async -> AppleMusicAccess func subscribed() async throws -> Bool func playlists() async throws -> [AppleMusicPlaylist] func browse(query: String,playlist: String?,offset: Int) async throws -> AppleMusicPage func play(_ id: String) async throws func enqueue(_ id: String) async throws func resume() async throws func pause() func stop() func seek(_ seconds: Double) func next() async throws var playback: AppleMusicPlayback { get } } /// Public MusicKit playback stays inside Apple's player; protected streams are /// never passed to AVAudioFile, waveform analysis or the local deck DSP graph. @available(macOS 14.0, *) @MainActor final class NativeAppleMusicClient: AppleMusicClient { private var tracks = [String:MusicKit.Song]() private var playlistItems = [String:MusicKit.Playlist]() private var musicPlayer: ApplicationMusicPlayer { .shared } var access: AppleMusicAccess { switch MusicAuthorization.currentStatus { case .authorized:return .authorized case .denied:return .denied case .restricted:return .restricted default:return .notDetermined } } func authorize() async -> AppleMusicAccess { _ = await MusicAuthorization.request();return access } func subscribed() async throws -> Bool { try await MusicSubscription.current.canPlayCatalogContent } private func remember(_ songs: [MusicKit.Song]) -> [AppleMusicTrack] { songs.map { song in tracks[song.id.rawValue] = song return AppleMusicTrack(id:song.id.rawValue,title:song.title,artist:song.artistName,album:song.albumTitle ?? "",duration:song.duration ?? 0,artwork:song.artwork?.url(width:80,height:80)) } } func playlists() async throws -> [AppleMusicPlaylist] { var request = MusicLibraryRequest();request.limit = 100 var result = [AppleMusicPlaylist]() while true { try Task.checkCancellation() let response = try await request.response() for item in response.items { playlistItems[item.id.rawValue] = item;result.append(.init(id:item.id.rawValue,name:item.name)) } if !response.items.hasNextBatch { break };request.offset += response.items.count } return result } func browse(query: String,playlist: String?,offset: Int) async throws -> AppleMusicPage { if !query.isEmpty { var request = MusicCatalogSearchRequest(term:query,types:[MusicKit.Song.self]);request.limit = 50;request.offset = offset let response = try await request.response() return .init(tracks:remember(Array(response.songs)),hasMore:response.songs.hasNextBatch) } if let playlist,let item = playlistItems[playlist] { let detailed = try await item.with([.tracks]) guard var collection = detailed.tracks else { return .init(tracks:[],hasMore:false) } var all = Array(collection) while all.count < offset+50,collection.hasNextBatch,let next = try await collection.nextBatch(limit:50) { try Task.checkCancellation();collection = next;all.append(contentsOf:next) } let songs = all.dropFirst(offset).prefix(50).compactMap { track -> MusicKit.Song? in if case .song(let song) = track { return song };return nil } return .init(tracks:remember(songs),hasMore:all.count > offset+50 || collection.hasNextBatch) } var request = MusicLibraryRequest();request.limit = 50;request.offset = offset let response = try await request.response() return .init(tracks:remember(Array(response.items)),hasMore:response.items.hasNextBatch) } private func song(_ id: String) throws -> MusicKit.Song { guard let song = tracks[id] else { throw LibraryError.message("Refresh Apple Music and select the track again.") };return song } func play(_ id: String) async throws { musicPlayer.queue = .init(for:[try song(id)]);try await musicPlayer.play() } func enqueue(_ id: String) async throws { try await musicPlayer.queue.insert(song(id),position:.tail) } func resume() async throws { try await musicPlayer.play() } func pause() { musicPlayer.pause() } func stop() { musicPlayer.stop() } func seek(_ seconds: Double) { musicPlayer.playbackTime = max(0,seconds) } func next() async throws { try await musicPlayer.skipToNextEntry() } var playback: AppleMusicPlayback { let entry = musicPlayer.queue.currentEntry var duration: Double = 0 if case .song(let song) = entry?.item { duration = song.duration ?? 0 } let queue = musicPlayer.queue.entries.map { AppleMusicQueueItem(id:$0.id,title:$0.title,artist:$0.subtitle ?? "",current:$0.id == entry?.id) } return .init(title:entry?.title ?? "",artist:entry?.subtitle ?? "",playing:musicPlayer.state.playbackStatus == .playing,position:musicPlayer.playbackTime,duration:duration,queue:queue) } } @available(macOS 14.0, *) @MainActor final class AppleMusicTransportState: ObservableObject { @Published var value = AppleMusicPlayback() } @available(macOS 14.0, *) @MainActor final class AppleMusicSession: ObservableObject { @Published private(set) var access: AppleMusicAccess @Published private(set) var canPlay = false @Published private(set) var tracks = [AppleMusicTrack]() @Published private(set) var playlists = [AppleMusicPlaylist]() @Published private(set) var busy = false @Published private(set) var loadingMore = false @Published private(set) var hasMore = false let transport = AppleMusicTransportState() var playback: AppleMusicPlayback { get { transport.value } set { if newValue != transport.value { transport.value = newValue } } } @Published var error: String? @Published var query = "" @Published var playlist: String? private let client: AppleMusicClient private var browseTask: Task? private var playbackTask: Task? private var generation = UUID() init(client: AppleMusicClient? = nil) { let client = client ?? NativeAppleMusicClient();self.client = client;access = client.access } deinit { browseTask?.cancel();playbackTask?.cancel() } func connect() async { guard !busy else { return };browseTask?.cancel();generation = UUID();canPlay = false;tracks = [];busy = true;error = nil access = await client.authorize() guard access == .authorized else { busy = false;error = access == .restricted ? "Apple Music access is restricted on this Mac." : "Allow KJMix Pro in System Settings → Privacy & Security → Media & Apple Music.";return } do { canPlay = try await client.subscribed();playlists = try await client.playlists() } catch { self.error = "Apple Music could not connect. Check your internet connection, Apple Music subscription and this app’s MusicKit App Service configuration. \(error.localizedDescription)" } busy = false if error == nil { reload() } } func reload() { browseTask?.cancel();generation = UUID();let token = generation tracks = [];hasMore = false;loadingMore = false guard access == .authorized else { return } busy = true;error = nil let query = query.trimmingCharacters(in:.whitespacesAndNewlines),playlist = playlist browseTask = Task { [weak self] in guard let self else { return } do { let page = try await client.browse(query:query,playlist:playlist,offset:0) guard !Task.isCancelled,generation == token else { return } tracks = page.tracks;hasMore = page.hasMore;busy = false } catch { guard generation == token,!Task.isCancelled else { return };self.error = error.localizedDescription;busy = false } } } func more() { guard hasMore,!busy,!loadingMore else { return };loadingMore = true let token = generation,offset = tracks.count,query = query.trimmingCharacters(in:.whitespacesAndNewlines),playlist = playlist browseTask = Task { [weak self] in guard let self else { return } do { let page = try await client.browse(query:query,playlist:playlist,offset:offset) guard !Task.isCancelled,generation == token else { return } let existing = Set(tracks.map(\.id));tracks += page.tracks.filter{!existing.contains($0.id)};hasMore = page.hasMore;loadingMore = false } catch { guard generation == token,!Task.isCancelled else { return };self.error = error.localizedDescription;loadingMore = false } } } func play(_ track: AppleMusicTrack) async { guard canPlay else { error = "An active Apple Music subscription is required for full playback.";return } do { try await client.play(track.id);observePlayback() } catch { self.error = error.localizedDescription } } func enqueue(_ track: AppleMusicTrack) async { guard canPlay else { error = "An active Apple Music subscription is required for full playback.";return } do { try await client.enqueue(track.id);observePlayback() } catch { self.error = error.localizedDescription } } func toggle() async { do { if playback.playing { client.pause() } else { try await client.resume() };observePlayback() } catch { self.error = error.localizedDescription } } func pause() { client.pause();playback = client.playback } func stop() { client.stop();playback = client.playback;playbackTask?.cancel();playbackTask = nil } func seek(_ value: Double) { client.seek(min(playback.duration,max(0,value)));playback = client.playback } func next() async { do { try await client.next();observePlayback() } catch { self.error = error.localizedDescription } } private func observePlayback() { playback = client.playback guard playbackTask == nil else { return } playbackTask = Task { [weak self] in while !Task.isCancelled { do { try await Task.sleep(nanoseconds:250_000_000) } catch { return } guard let self else { return };self.playback = self.client.playback } } } } @available(macOS 14.0, *) struct AppleMusicBrowser: View { @ObservedObject var session: AppleMusicSession @State private var pendingPlay: AppleMusicTrack? @State private var pendingResume = false let hasPlayingDeck: () -> Bool let stopDecks: () -> Void private func requestPlay(_ track: AppleMusicTrack) { if hasPlayingDeck() { pendingPlay = track } else { Task { await session.play(track) } } } var body: some View { VStack(spacing:0) { HStack(spacing:14) { Label("Apple Music",systemImage:"music.note").font(.title3.bold()).foregroundStyle(.pink) Picker("Apple Music library",selection:$session.playlist) { Text("My Library").tag(nil as String?) ForEach(session.playlists) { Text($0.name).tag(Optional($0.id)) } }.labelsHidden().frame(maxWidth:200).disabled(session.access != .authorized) Spacer() StudioSearchField(placeholder:"Search Apple Music catalog",text:$session.query).frame(maxWidth:320) .onSubmit { session.reload() } Button("Search") { session.reload() }.buttonStyle(LibraryActionButtonStyle(tint:.pink)).disabled(session.access != .authorized) Button(session.access == .authorized ? "Refresh Connection" : "Connect Apple Music") { Task { await session.connect() } } .buttonStyle(LibraryActionButtonStyle(tint:.pink,prominent:true)).disabled(session.busy) }.padding(14) if let error = session.error { HStack { Image(systemName:"exclamationmark.circle.fill").foregroundStyle(.orange);Text(error).font(.callout);Spacer();Button("Dismiss") { session.error = nil } }.padding(12).background(Color.orange.opacity(0.1)) } if session.busy { ProgressView("Loading Apple Music…").padding() } if session.access != .authorized { VStack(spacing:12) { Image(systemName:"music.note.list").font(.system(size:34)).foregroundStyle(.pink) Text("Your Apple Music library and playlists").font(.headline) Text("Connect to browse your library, search the catalog and play through Apple Music.").foregroundStyle(.secondary) }.frame(maxWidth:.infinity,maxHeight:.infinity) } else { Table(session.tracks) { TableColumn("Track") { track in HStack(spacing:10) { AsyncImage(url:track.artwork) { image in image.resizable().scaledToFill() } placeholder: { Image(systemName:"music.note").frame(maxWidth:.infinity,maxHeight:.infinity).background(Color.pink.opacity(0.12)) }.frame(width:32,height:32).clipShape(RoundedRectangle(cornerRadius:5)) Text(track.title).lineLimit(1) }.padding(.vertical,3).onTapGesture(count:2) { requestPlay(track) } .contextMenu { Button("Play in Apple Music") { requestPlay(track) };Button("Add to Apple Music Queue") { Task { await session.enqueue(track) } } } }.width(min:180,ideal:300) TableColumn("Artist",value:\.artist).width(min:100,ideal:180) TableColumn("Album",value:\.album).width(min:100,ideal:180) TableColumn("Time") { Text(Self.time(track:$0.duration)).monospacedDigit() }.width(60) TableColumn("Playback") { track in HStack { Button { requestPlay(track) } label: { Image(systemName:"play.fill") };Button { Task { await session.enqueue(track) } } label: { Image(systemName:"text.badge.plus") } }.disabled(!session.canPlay) }.width(90) } if session.hasMore { Button(session.loadingMore ? "Loading…" : "Load More") { session.more() }.disabled(session.loadingMore).padding(8) } } Divider() AppleMusicTransportBar(session:session,toggle:{ if hasPlayingDeck(),!session.playback.playing { pendingResume = true } else { Task { await session.toggle() } } }) Text("MusicKit playback uses the system audio output. Deck EQ, sync, loops, waveform analysis and recording apply to local tracks.").font(.caption).foregroundStyle(.secondary).padding(.horizontal,14).padding(.bottom,10) }.background(Color(red:0.095,green:0.11,blue:0.135)).clipShape(RoundedRectangle(cornerRadius:10)) .onChange(of:session.playlist) { _ in session.query = "";session.reload() } .onChange(of:session.query) { value in if value.isEmpty { session.reload() } } .alert("Stop deck playback?",isPresented:Binding(get:{pendingPlay != nil || pendingResume},set:{if !$0 { pendingPlay = nil;pendingResume = false }})) { Button("Cancel",role:.cancel) { pendingPlay = nil;pendingResume = false } Button("Stop Decks & Play") { stopDecks() let track = pendingPlay;pendingPlay = nil;pendingResume = false Task { if let track { await session.play(track) } else { await session.toggle() } } } } message: { Text("Apple Music uses its own player. Stop the playing decks before starting Apple Music?") } } static func time(track: Double) -> String { guard track.isFinite else { return "0:00" };let seconds = max(0,Int(track));return String(format:"%d:%02d",seconds/60,seconds%60) } } @available(macOS 14.0, *) private struct AppleMusicTransportBar: View { let session: AppleMusicSession let toggle: () -> Void @State private var queueVisible = false @ObservedObject var transport: AppleMusicTransportState init(session: AppleMusicSession,toggle: @escaping () -> Void) { self.session = session;self.toggle = toggle;transport = session.transport } var body: some View { HStack(spacing:14) { Button(action:toggle) { Image(systemName:session.playback.playing ? "pause.fill" : "play.fill") }.disabled(session.playback.title.isEmpty) Button { session.stop() } label: { Image(systemName:"stop.fill") }.disabled(session.playback.title.isEmpty) Button { Task { await session.next() } } label: { Image(systemName:"forward.end.fill") }.disabled(session.playback.title.isEmpty) VStack(alignment:.leading,spacing:3) { Text(session.playback.title.isEmpty ? "Choose an Apple Music track" : session.playback.title).fontWeight(.semibold).lineLimit(1);Text(session.playback.artist).font(.caption).foregroundStyle(.secondary).lineLimit(1) }.frame(minWidth:160,maxWidth:300,alignment:.leading) Text(AppleMusicBrowser.time(track:session.playback.position)).monospacedDigit() Slider(value:Binding(get:{min(session.playback.duration,max(0,session.playback.position))},set:{session.seek($0)}),in:0...max(1,session.playback.duration)).tint(.pink).disabled(session.playback.duration <= 0) Text(AppleMusicBrowser.time(track:session.playback.duration)).monospacedDigit() Button { queueVisible = true } label: { Label("Queue",systemImage:"text.line.first.and.arrowtriangle.forward") } }.buttonStyle(.bordered).padding(14) .sheet(isPresented:$queueVisible) { VStack(alignment:.leading,spacing:14) { HStack { Text("Apple Music Queue").font(.title2.bold());Spacer();Button("Done") { queueVisible = false } } List(transport.value.queue) { item in HStack { Image(systemName:item.current ? "speaker.wave.2.fill" : "music.note").foregroundStyle(item.current ? .pink : .secondary);VStack(alignment:.leading) { Text(item.title);Text(item.artist).font(.caption).foregroundStyle(.secondary) } } } }.padding(22).frame(width:620,height:420).preferredColorScheme(.dark) } } }