iTunesで現在再生中の曲をslackに投げるMacアプリをswiftで組んだ話

仕事場でiTunesからの自動選曲を使ってBGM流してるので、再生中の曲をslackで確認できるようにするbotを作った時の話。

github.com

これを書いた前後でswift 3に移行したりしたのでコードの内容が中途半端なんだけどもそこは見なかったことに。

このネタの肝は以下の二つ。

  • DistributedNotificationCenter経由でiTunesからの再生中トラックの変更通知を受け取る
  • 受け取った通知から曲情報を取り出してslack APIを叩く

再生中のトラックの変更通知

qiita.com

にほぼまま倣い。

        DistributedNotificationCenter.default().addObserver(self,
                                                                                                selector: #selector(AppDelegate.onChangeTrack(_:)),
                                                                                                name: Notification.Name(rawValue: "com.apple.iTunes.playerInfo"),
                                                                                                object: nil)

曲情報の取得

    func onChangeTrack(_ notification: Notification?) {
        let userInfo = (notification as NSNotification?)?.userInfo
        print(userInfo ?? "")
        
        if (userInfo!["Player State"] as! String) == "Playing" {
            guard let
                displayLine0: String = userInfo!["Display Line 0"] as? String,
                let displayLine1: String = userInfo!["Display Line 1"] as? String,
                let storeURL: String = userInfo!["Store URL"] as? String else {
                    return
            }

NotificationのuserInfoに諸情報が収納されているのだが、このDictionaryオブジェクト内に「どの情報がどのキーで」収められているのかの明確な定義情報がどうやら見当たらない。なので上の実装は「とりあえずのもの」である。

userInfo["Player State"]が"Playing"かどうかで場合分けをしているのも、これ以外の状態が入った通知が飛んできた場合にも通知を受け取ってしまって予期しないslack送信にならないようにしたものだが、この辺の挙動も明確なドキュメントがないのが辛いところ。

slack APIへの送信

            let hookURL = UserDefaults.standard.string(forKey: "hookURL")!
            let params: [String: Any] = [
                "channel": UserDefaults.standard.string(forKey: "channel")! as AnyObject,
                "username": "nowplayingbot" as AnyObject,
                "text": String(format: "%@ %@", arguments: [formatter.string(from: Date()), displayLine0]) as AnyObject,
                "attachments": [[
                    "title": displayLine1,
                    "title_link": httpStoreURL // 上で取得したstoreURLのschemeを"itms://"から"http://"に差し替えたもの
                    ]]
            ]
            let request: NSMutableURLRequest = NSMutableURLRequest(url: URL(string: hookURL)!)
            
            request.httpMethod = "POST"
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            request.addValue("application/json", forHTTPHeaderField: "Accept")
            do {
                request.httpBody = try JSONSerialization.data(withJSONObject: params, options: JSONSerialization.WritingOptions.init(rawValue: 2))
            } catch {
                // Error Handling
                print("NSJSONSerialization Error")
                return
            }
            session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
                // code
            }).resume()

まず、このhookURLというのはslackで用意される「Custom Integrations」「Incoming Webhooks」を使って生成する。このAPI hookなど認証情報が絡む箇所はUserDefaultsに逃して(さらに初期plistファイルをgitignoreに入れてリポジトリに入らないようにして)ある。

で、生成したNSURLRequestインスタンスにbodyとしてJSON化したparamsを付与しPOST送信。ご覧の通り送信完了時及びエラー時処理は簡易実装のため入れてない点は許して。

qiita.com

なおparamsでご覧の通りattachmentsを使っているのでちょっとリッチなメッセージを送ることができる。

f:id:zvorak:20170704155847p:plain

たまに訳のわからない選曲がぶっこまれても安心。