読者です 読者をやめる 読者になる 読者になる

ちなみに

火曜日の空は僕を押しつぶした。

いま聴いている曲の iTunes Store での trackId を取得する旅

f:id:Sixeight:20160419034911p:plain

2016年3月30日に京都で開催された 関西モバイルアプリ研究会 #12 で、iOS 9.3 に追加された、Apple Music 関連のAPIについて紹介しました。 Apple Music は月額料金を払えば、ストリーミングで音楽を聴きまくれる最高のサービスなので、使ってない人はいますぐ契約しましょう。

今回の更新で主に以下のようなことが出来るようになりました。

  • SKCloudServiceController
    • Apple Music へのアクセス権限の確認
    • Apple Music へのアクセス権限の取得
    • 再生、またはライブラリの追加が可能かどうかの確認
    • ユーザーがApple Musicを契約している国のStorefront IDの取得
  • MPMusicPlayerController
    • Apple Music の曲を再生する
  • MPMediaLibrary / MPMediaPlaylist
    • Apple Music の曲をライブラリに追加する
    • 権限の周りのAPIも追加されてるけど SKCloudServiceController と全く同じ動きでした

権限の取得や、Storefront ID を確認したあとは、おもむろに以下のようにして曲を再生できます。

let player = MPMusicPlayerController.systemMusicPlayer()
player.setQueueWithStoreIDs([ "1017194380", "771599829" ])

非常に簡単ですね。この StoreID は同じ曲でも国ごとに違うので Storefront ID を確認しておく必要があります

詳しくは僕の資料や、以下の記事が参考になるかと思います。 発表したあとブログに書こうと思って追加でもうちょっと調べてたら、先にこの記事が出たので書くのをやめたという負け惜しみをひとつ。

dev.classmethod.jp

ところで、この StoreID とやらはどうやって取得出来るのでしょうか。 残念ながら iOSAPI だけでは取得することが出来ません。悲しい。 なんか思っていた夢の世界とはちょっと違う。

iTunes Search API を使う

affiliate.itunes.apple.com

ところで iTunes Search API を使うと iTunes Store 上の曲などの情報を取得できます。

curl https://itunes.apple.com/search?term=ESNO+とか。&country=JP&media=music&entiry=song&isStreamable=true

たとえばこのようなクエリで Search API を叩くと以下のようなJSONが得られます。

{
  "resultCount": 1,
  "results": [
    {
      "wrapperType": "track",
      "kind": "song",
      "artistId": 555290400,
      "collectionId": 1017193906,
      "trackId": 1017194380,
      "artistName": "ESNO",
      "collectionName": "Release",
      "trackName": "とか。 feat. きゃべこ - remix -",
      "collectionCensoredName": "Release",
      "trackCensoredName": "とか。 feat. きゃべこ - remix -",
      "artistViewUrl": "https://itunes.apple.com/jp/artist/esno/id555290400?uo=4",
      "collectionViewUrl": "https://itunes.apple.com/jp/album/toka-feat.-kyabeko-remix/id1017193906?i=1017194380&uo=4",
      "trackViewUrl": "https://itunes.apple.com/jp/album/toka-feat.-kyabeko-remix/id1017193906?i=1017194380&uo=4",
      "previewUrl": "http://a1896.phobos.apple.com/us/r20/Music5/v4/7c/06/ae/7c06aefa-3c4b-3916-20a3-660589df15dd/mzaf_2523222398560687944.plus.aac.p.m4a",
      "artworkUrl30": "http://is3.mzstatic.com/image/thumb/Music7/v4/8a/4e/42/8a4e42c8-0638-163e-bcb9-ec2a980999dc/source/30x30bb.jpg",
      "artworkUrl60": "http://is3.mzstatic.com/image/thumb/Music7/v4/8a/4e/42/8a4e42c8-0638-163e-bcb9-ec2a980999dc/source/60x60bb.jpg",
      "artworkUrl100": "http://is3.mzstatic.com/image/thumb/Music7/v4/8a/4e/42/8a4e42c8-0638-163e-bcb9-ec2a980999dc/source/100x100bb.jpg",
      "collectionPrice": 1800,
      "trackPrice": 200,
      "releaseDate": "2015-07-22T07:00:00Z",
      "collectionExplicitness": "notExplicit",
      "trackExplicitness": "notExplicit",
      "discCount": 1,
      "discNumber": 1,
      "trackCount": 12,
      "trackNumber": 9,
      "trackTimeMillis": 215507,
      "country": "JPN",
      "currency": "JPY",
      "primaryGenreName": "エレクトロニック",
      "isStreamable": true
    }
  ]
}

このJSONtrackIdcollectionId が今回求めている StoreID で、この ID さえあればAPIから再生やライブラリに追加することが出来ます。 呼び方がいろいろあってややこしいので、以下は trackId に統一します。 ちなみに isStreamabletrue の曲のみ Apple Music で有効です。

しかし、この Search API、意外と融通が効かない。このアーティストのこのアルバムなどのようなピンポイントの検索が出来ない。つまりいま聴いている曲から機械的に正しい trackId を取得することが困難です。変わった曲名や、変わったアルバム名なら項目指定で一発で引けそうですが、一般的な名前なら絶望的。

では、人間が検索して正しいのを選ぶとうのはどうでしょうか。良さそうですか? いやいや、我々はそういったことが出来る最高のアプリを知っているはずです。iTunes という最高のアプリがあるので使いましょう。 そう、つまりこの trackId が自動的に分からなければ特になんのメリットもないとうことです。

再生中の曲を得る

ひとまず現在、再生中の曲をのぞいてみましょう。

一番簡単な方法は MPMusicPlayerControllernowPlayingItem プロパティを見ることです。これは現在 Music アプリで再生中の曲情報を MPMediaItemインスタンスとして取得出来ます。 取得した MPMediaItemvalueForProperty(_:) メソッドを使って各種情報を取得できます。ドキュメントにそう書かれているけど、ヘッダーを眺めているとそれぞれプロパティが用意されているようなのでそれを使うと良さそうです。

let player = MPMusicPlayerController.systemMusicPlayer()
let currentTrack = player.nowPlayingItem
print(currentTrack.valueForProperty(MPMediaItemPropertyTitle))
// => とか。 feat. きゃべこ - remix -

MPMusicPlayController に指定することで、再生状態が変わったとき (MPMusicPlayerControllerPlaybackStateDidChangeNotification) や、曲が変わったとき (MPMusicPlayerControllerNowPlayingItemDidChangeNotification) に通知が発行することが出来るので、これを監視することで常にいま聴いている曲の情報を取得し続ける事ができます。

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

     // 通知を監視する
    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(updateTrackTitle), name: MPMusicPlayerControllerPlaybackStateDidChangeNotification, object: self.player)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(updateTrackTitle), name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: self.player)

    // 通知を送るようにする
    player.beginGeneratingPlaybackNotifications()
}

override func viewWillDisappear(animated: Bool) {
    // 通知を止める
    player.endGeneratingPlaybackNotifications()

    // 通知の監視を止める
    NSNotificationCenter.defaultCenter().removeObserver(self)

    super.viewWillDisappear(animated)
}

これで聴いてる曲を共有出来るかと思いきや、残念ながら iTunes Store に関する情報は皆無です。(もしかしてなんか取得方法あったら教えてください!) 曲名とかは共有出来るけど、いま欲しいのは trackId であって、曲名などどうでもいいのです。むしろ trackId さえあれば Lookup API で各種情報は取得できる。

Music アプリから iTunes Store の URL を共有出来るのだから取得出来てもおかしくないのに。

一方 Mac では

だんだんと iPhone で聴いている曲を自動で共有するということをあきらめつつあり、ひとまず trackId を集めて、API を試したいという気持ちになってきました。 IDを集めるだけだったらふだん仕事中に Mac で音楽を聴いているわけで、Mac で取得出来たら得だな、なにか方法はないのかなと調べ始めました。

再生中の曲を得る

Mac には Scripting Bridge という仕組みがあって、アプリのインターフェースを定義しておくと、別のアプリから操作することが出来るというものです。

sdef コマンドで定義されているインターフェースを取得することができ、さらに sdp コマンドでヘッダーファイルなどに変換できます。

sdef /Applications/iTunes.app | sdp -fh --basename iTunes

iTunes だとこういう感じ。

この生成されたヘッダーファイルを使うと使うといいのですが、Swift だとうまくいきません。

あきらめて適当なプロトコルを定義して、SBObject を拡張することで欲しい値を取得します。

@objc protocol iTunesTrack {
    optional var name: String { get }
    optional var album:  String { get }
    optional var artist: String { get }
}
extension SBObject: iTunesTrack {
}

let iTunesApplication = SBApplication(bundleIdentifier: "com.apple.iTunes")!
let currentTrack = iTunesApplication.valueForKey("currentTrack") as! iTunesTrack
print(currentTrack.name)

// => 夕暮れパラレリズム (feat. daoko)

しかし、残念ながら生成した iTunes.h を眺めても iTunes Store に関する情報は取得できそうにありませんでした。 そして、さらに Stream 再生している曲の場合は、currentTrack は取得出来るものに、name などが空文字列になってしまって、正常に取得出来ないように見えました。

通知を眺めていると

iTunes の曲情報を取得するのは若干面倒だったのですが、再生状況が変わったときに iTunes から通知が発行されていてこれを拾うことは簡単です。

# どこかで監視を追加
NSDistributedNotificationCenter.defaultCenter().addObserver(self, selector: #selector(trackChanged(_:)), name: iTunesNotificationName, object: nil)

// ...

func trackChanged(notification: NSNotification) {
  print(notification)
}

iOSとは違って通知は1種類なのですが、NSNotification に各種情報が格納されています 再生中かどうか、現在選択されている曲の情報などが含まれます。 こいつを眺めてみると以下のようになっています。

__CFNotification 0x608000040360 {name = com.apple.iTunes.playerInfo; object = com.apple.iTunes.player; userInfo = {
    Album = "#501";
    "Album Artist" = MIDICRONICA;
    "Album Rating" = 0;
    "Album Rating Computed" = 1;
    Artist = MIDICRONICA;
    "Artwork Count" = 1;
    "Back Button State" = Prev;
    "Disc Count" = 1;
    "Disc Number" = 1;
    "Display Line 0" = "San Francisco";
    "Display Line 1" = "MIDICRONICA \U2014 #501";
    Genre = "Hip Hop/Rap";
    "Library PersistentID" = 4494643586137931510;
    "Like Status" = None;
    Name = "San Francisco";
    PersistentID = "-1983237849806433000";
    "Play Count" = 12;
    "Play Date" = "2016-03-18 02:06:26 +0000";
    "Player State" = Playing;
    "Playlist PersistentID" = 5800905515721368577;
    "Rating Computed" = 1;
    "Skip Count" = 0;
    "Store URL" = "itms://itunes.com/album?p=500044680&i=500044854";
    "Total Time" = 206733;
    "Track Count" = 15;
    "Track Number" = 9;
    Year = 2005;
    elapsedStr = "-3:27";
}}

( ゚д゚)

(つд⊂)ゴシゴシ

(;゚д゚)

Store URL !!!!!!!!!!!!!!

ということで、iTunes Store での URL が取得出来ることが分かります。 よく見ると i=500044854 というパラメータがついており、これが求めていた trackId です。

お疲れ様でした。

安心はできない

ここで Apple Music で再生出来るのは isStreamable な曲だけだったことを思い出してください。 今回取得出来た情報だけだと、iTunes Store で販売されているということは分かりますが、Apple Music で配信されているかまでは分かりません。

そこでふたたび登場するのが iTunes Search API です。このAPIには Lookup というのもあって、これは id を指定して商品(ここでは曲!)の情報を得られます。

以下のようにします。ドキュメントを読んでいると country パラメータは不要に見えるのですが、指定しないと日本のストアで検索できないのがハマりポイントです。

curl https://itunes.apple.com/lookup?country=JP&id=500044854

すると以下のような曲情報が取得できて、isStreamabletrue なので Apple Music で再生出来ることが分かりました。

{
  "resultCount": 1,
  "results": [
    {
      "trackId": 500044854,
      # (略) ...
      "isStreamable": true
    }
  ]
}

結局

本来の目的は iOS 9.3 の Apple Music 関連APIを活用することだったのですが、trackId が分からないと何も出来ないし、Search API を検索するくらいなら iTunes 使うでしょということで、まずは trackId を集まることに注目して調べてみました。結果、Mac アプリから取得できる iTunes の通知からとれる Store URL。そこから分かる id と lookup API を使って、自動的に trackId と正確な曲情報を取得出来ることが分かりました。

trackId さえ集まれば再生やライブラリへの追加はできますし、プレイリストを Web から作れて、それを iOS で誰でも再生出来るとかのサービスは作れそうですが、どっちみち Web サービス前提となってしまって気軽には出来ません。今回の API 追加は夢だけは広がりましたが、どちらかというと既存の音楽系サービス向けのもので、一般ユーザー向けには iOS 上で再生中の曲の trackId を取得する手段が公開されるまでは使いどころが難しいのではないでしょうか。

おまけ

調べる過程で以下のようなのが出来ました。

聴いてる

サイトはどうでもよくてここで trackId を収集しつつ、Slack に Webhook を投げたりしていてちょっとだけべんり。

iOSAPIをちょっと試してみたい人向けに ここApple Music で再生出来る僕(か知り合い)が聴いた曲の直近50件の trackId を返すようにしています。(予告なく消す可能性あります)

Twitter にも一部流しています。Twitter にはアフィリエイトリンク張っていないので安心してクリックください。

twitter.com