JohnCoates/Aerial

View on GitHub
Aerial/Source/Models/Music/Music.swift

Summary

Maintainability
C
7 hrs
Test Coverage
//
//  Music.swift
//  Aerial
//
//  Created by Guillaume Louel on 29/06/2021.
//  Copyright © 2021 Guillaume Louel. All rights reserved.
//

import Foundation
import AppKit

typealias MusicCallback = (SongInfo) -> Void

struct SongInfo {
    let name: String
    let artist: String
    let album: String
    let artwork: NSImage?
}

// swiftlint:disable:next type_body_length
class Music {
    static let instance: Music = Music()
    var callbacks = [MusicCallback]()
    var wasSetup = false

    // This is called once at init to set our observer
    func setup() {
        if !wasSetup {
            debugLog("🎧 registering private callback")

            // Load framework
            let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework"))
            
            // Get a Swift function for MRMediaRemoteRegisterForNowPlayingNotifications
            guard let MRMediaRemoteRegisterForNowPlayingNotificationsPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteRegisterForNowPlayingNotifications" as CFString) else { return }
            typealias MRMediaRemoteRegisterForNowPlayingNotificationsFunction = @convention(c) (DispatchQueue) -> Void
            let MRMediaRemoteRegisterForNowPlayingNotifications = unsafeBitCast(MRMediaRemoteRegisterForNowPlayingNotificationsPointer, to: MRMediaRemoteRegisterForNowPlayingNotificationsFunction.self)
            
            // Call the register function
            MRMediaRemoteRegisterForNowPlayingNotifications(DispatchQueue.main)
            
            DispatchQueue.main.async {
                // Register App state change callback
                NotificationCenter.default.addObserver(self,
                                                       selector: #selector(Music.mediaRemoteAppStateChange(_:)),
                                                       name: NSNotification.Name("kMRMediaRemoteNowPlayingApplicationIsPlayingDidChangeNotification"), object: nil)
                
                // Register playback info change callback
                NotificationCenter.default.addObserver(self,
                                                       selector: #selector(Music.mediaRemoteCallback(_:)),
                                                       name: NSNotification.Name("kMRMediaRemoteNowPlayingInfoDidChangeNotification"), object: nil)
            }

            wasSetup = true
        }
    }

    // Callback to get paused status from some apps that may not update info pause on change
    @objc func mediaRemoteAppStateChange(_ aNotification: Notification) {
        debugLog("🎧 app state change")
        
        if let userInfo = aNotification.userInfo {
            if let rate = userInfo["kMRMediaRemoteNowPlayingApplicationIsPlayingUserInfoKey"] as? Double {
                
                if rate == 0 {
                    debugLog("🎧 playback is paused, clearing")
                    // Pause the thing
                    for callback in self.callbacks {
                        callback(SongInfo(name: "", artist: "", album: "", artwork: nil))
                    }
                }
            }
        }
    }
    
    // General info change callback
    @objc func mediaRemoteCallback(_ aNotification: Notification?) {
        var album = ""
        var name = ""
        var artist = ""
        var artwork: NSImage?
        
        debugLog("🎧 media remote callback")
        // Load framework
        let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework"))

        // Get a Swift function for MRMediaRemoteGetNowPlayingInfo
        guard let MRMediaRemoteGetNowPlayingInfoPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingInfo" as CFString) else { return }
        typealias MRMediaRemoteGetNowPlayingInfoFunction = @convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void
        let MRMediaRemoteGetNowPlayingInfo = unsafeBitCast(MRMediaRemoteGetNowPlayingInfoPointer, to: MRMediaRemoteGetNowPlayingInfoFunction.self)
        
        // Get song info
        MRMediaRemoteGetNowPlayingInfo(DispatchQueue.main, { (information) in
            debugLog("🎧 audio info")

            
            if let info = information["kMRMediaRemoteNowPlayingInfoPlaybackRate"] as? Double {
                if (info != 0.0) {
                    // Player is running
                    if let info = information["kMRMediaRemoteNowPlayingInfoArtist"] as? String {
                        artist = info
                    }
                    if let info = information["kMRMediaRemoteNowPlayingInfoTitle"] as? String {
                        name = info
                    }
                    if let info = information["kMRMediaRemoteNowPlayingInfoAlbum"] as? String {
                        album = info
                    }


                    // try to grab image from the keys
                    if information.keys.contains("kMRMediaRemoteNowPlayingInfoArtworkData") {
                        if let _artwork = NSImage(data: information["kMRMediaRemoteNowPlayingInfoArtworkData"] as! Data) {
                            artwork = _artwork
                        }
                    }
                    
                    debugLog("🎧 " + artist + " - " + name + " (" + album + ")" + ((artwork != nil) ? " with artwork " : " without artwork"))
                } else {
                    debugLog("🎧 Player is paused")
                }
            }
            
            // Let everyone who wants to know that we have a new song playing !
            for callback in self.callbacks {
                callback(SongInfo(name: name, artist: artist, album: album, artwork: artwork))
            }
        })
    }
    
    // MARK: - Callbacks
    func addCallback(_ callback:@escaping MusicCallback) {
        debugLog("🎧 Adding music callback")
        callbacks.append(callback)
    }
}