[iOS 11] Apple Music APIを使用してアルバム検索アプリを作る

はじめに

こんにちは。モバイルアプリサービス部の平屋です。

本記事では、Apple Music APIを使用してアルバム情報を取得し、収録曲を再生する実装を紹介します。

Apple Music APIはWWDC17で紹介されたMusicKitに含まれるWeb APIです。MusicKitの概要は以下の記事にまとめています。

[iOS 11] Apple Musicとの連携を行うための「MusicKit」について #WWDC17

サンプルアプリについて

本記事を書くにあたって、以下の機能をもったサンプルアプリを作成しました。

  • トップ画面
    • キーワードでアルバムを検索する
    • アルバムを選択すると、詳細画面を表示
  • 詳細画面
    • 収録曲情報などを表示する
    • 曲をタップすると再生される

本記事では、上記の機能を実現するのに必要な作業のうちの一部を抜粋して紹介します。

記事内で扱っていない部分については、以下のリポジトリで公開しているコードを参照してください!

検証環境

  • macOS Sierra 10.12.6
  • Xcode 9
  • 端末
    • iPhone 7, iOS 11
    • Apple Music 購読中の Apple IDでログイン済み

目次

Developer Tokenを作成する

Developer Tokenを作成するには、以下の記事の「Music IDを作成する」から「Developer Tokenを作成する」までの作業を行います。

[iOS 11] Apple Music APIにアクセスするために必要な作業について #WWDC17

Apple Music APIへのリクエストのフォーマット

Apple Music APIのURLの形式は以下の通りです。

  • Apple Music Catalogに関するAPI
    • https://api.music.apple.com/{version}/catalog/{storefront}/{api}?[params]
  • ユーザー固有のデータに関するAPI
    • https://api.music.apple.com/{version}/me/{api}
  • パスパラメータ
    • version
      • APIのバージョンを指定
        • 現在はv1のみ指定可
    • storefront
      • 国ごとに割り振られたコードを指定
        • 日本の場合はjpを指定
    • api
      • 各API用の文字列を指定
        • Search APIの場合はsearchを指定

本記事では「Apple Music Catalogに関するAPI」のうち、Search / Albums APIを扱います。

Search APIを使用してキーワードに合致するアルバムを取得する

リクエスト

キーワードに合致するリソースを取得するにはSearch APIを使用します。

パスは/catalog/{storefront}/searchで、検索ワードやリソースのタイプはクエリパラメータで指定します。

func search(term: String, completion: @escaping (SearchResult?) -> Void) {
    // ...

    // jp: 日本のストアを指定
    var components = URLComponents(string: "https://api.music.apple.com/v1/catalog/jp/search")! 

    let expectedTerms = term.replacingOccurrences(of: " ", with: "+")
    let urlParameters = ["term": expectedTerms, // 検索ワード
                         "limit": "10", // 件数
                         "types": "albums"] // リソースのタイプとしてアルバムを指定
    var queryItems = [URLQueryItem]()
    for (key, value) in urlParameters {
        queryItems.append(URLQueryItem(name: key, value: value))
    }
    components.queryItems = queryItems

    var request = URLRequest(url: components.url!)
    request.httpMethod = "GET"
    request.addValue("Bearer \(developerToken)", // Developer Tokenをヘッダに入れる
        forHTTPHeaderField: "Authorization")

    let task = URLSession.shared.dataTask(with: request) { data, response, error -> Void in
        // ...
    }
    task.resume()
}

レスポンスの形式

以下のJSONは、GET https://api.music.apple.com/v1/catalog/jp/search?term=swift&types=albumsのレスポンスです。

results > albums > dataの中にアルバムの配列が入ってきます。

{
  "results": {
    "albums": {
      "href": "/v1/catalog/jp/search?term=swift&types=albums",
      "next": "/v1/catalog/jp/search?offset=5&term=swift&types=albums",
      "data": [ // アルバムの配列
        {
          "id": "907312293",
          "type": "albums",
          "href": "/v1/catalog/jp/albums/907312293",
          "attributes": {
            "artwork": { ...},
            "artistName": "テイラー・スウィフト",
            "isSingle": false,

            // ...
          }
        },

        // ...
      ]
    }
  }
}

今回のサンプルアプリでは、Codableプロトコルに適合したstructを用意しました。

このプロトコルに関するドキュメントはこちらにあります。

レスポンスJSON内のアルバムに対応するstructはResourceです。

struct SearchResult: Codable {

    let albums: [Resource]?

    // ...
}

struct Resource: Codable {

    let id: String?
    let type: String?
    let attributes: Attributes?
    let href: String?
    let next: String?
    let relationships: Relationships?
}

struct Attributes: Codable {

    let artwork: Artwork?
    let artistName: String?

    //...
}

struct Relationships: Codable {

    let tracks: [Resource]?

    // ...
}

レスポンスのパース

SearchResultのinit(from: Decoder)内にResourceの配列を取得する処理を実装します。

struct SearchResult: Codable {

    // ...

    enum RootKeys: String, CodingKey {
        case results
    }

    enum ResultsKeys: String, CodingKey {
        case albums
    }

    enum AlbumsKeys: String, CodingKey {
        case data
    }

    init(albums: [Resource]?) {
        self.albums = albums
    }

    init(from decoder: Decoder) throws {
        // [Resource]を取得する
        let values = try decoder.container(keyedBy: RootKeys.self)
        let results = try values.nestedContainer(keyedBy: ResultsKeys.self,
                                                 forKey: .results)
        let albums = try? results.nestedContainer(keyedBy: AlbumsKeys.self,
                                                  forKey: .albums)
        let data = try albums?.decode([Resource].self,
                                      forKey: .data)
        self.init(albums: data)
    }
}

レスポンス取得時にJSONDecoderを使用してSearchResultオブジェクトを作成します。

func search(term: String, completion: @escaping (SearchResult?) -> Void) {
    // ...

    let task = URLSession.shared.dataTask(with: request) { data, response, error -> Void in
        if let error = error {
            // ...
        } else {
            // JSONDecoderを使用してデコードを行う
            guard let searchResult = try? JSONDecoder().decode(SearchResult.self, from: data!) else {
                print("JSON Decode Failed");
                completion(nil)
                return
            }
            completion(searchResult)
        }
    }

    // ...
}

Albums APIを使用してアルバムの詳細を取得する

リクエスト

アルバムの詳細情報を取得するにはAlbums APIを使用します。

パスは/catalog/{storefront}/albums/{id}で、{id}にアルバムのIDを指定します。

func album(id: String, completion: @escaping (Resource?) -> Swift.Void) {
    // ...

    // jp: 日本のストアを指定
    // id: アルバムのリソースID
    let url = URL(string: "https://api.music.apple.com/v1/catalog/jp/albums/\(id)")! 

    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    request.addValue("Bearer \(developerToken)", // Developer Tokenをヘッダに入れる
        forHTTPHeaderField: "Authorization")

    let task = URLSession.shared.dataTask(with: request) { data, response, error -> Void in
        // ...
    }
    task.resume()
}

レスポンスの形式

以下のJSONは、GET https://api.music.apple.com/v1/catalog/jp/albums/907312293のレスポンスです。

ルートのdataの値はアルバムの配列で、アルバム収録曲のオブジェクトはrelationships > tracks > dataの中に入ってきます。

{
  "data": [ // アルバムの配列
    {
      "id": "907312293",
      "type": "albums",
      "href": "/v1/catalog/jp/albums/907312293",
      "attributes": {
        "artwork": { ... },
        "artistName": "テイラー・スウィフト",
        "isSingle": false,

        // ...
      },
      "relationships": {
        "artists": { ... },
        "tracks": {
          "data": [ // アルバム収録曲の配列
            {
              "id": "907312371",
              "type": "songs",
              "href": "/v1/catalog/jp/songs/907312371",
              "attributes": {
                "name": "Welcome To New York",
                "trackNumber": 1,

                // ...
              }
            },
            {
              "id": "907312376",

              // ...
            },

            // ...
          ],
          "href": "/v1/catalog/jp/albums/907312293/tracks"
        }
      }
    }
  ]
}

レスポンスのパース

Relationshipsinit(from: Decoder)内にResourceの配列を取得する処理を実装します。

struct Relationships: Codable {

    // ...

    enum RelationshipsKeys: String, CodingKey {
        case tracks
    }

    enum TracksKeys: String, CodingKey {
        case data
    }

    init(tracks: [Resource]?) {
        self.tracks = tracks
    }

    init(from decoder: Decoder) throws {
        // [Resource]を取得する
        let values = try decoder.container(keyedBy: RelationshipsKeys.self)
        let tracks = try values.nestedContainer(keyedBy: TracksKeys.self,
                                                forKey: .tracks)
        let data = try tracks.decode([Resource].self,
                                     forKey: .data)
        self.init(tracks: data)
    }
}

レスポンス取得時にJSONDecoderを使用してResourceオブジェクトを作成します。

func album(id: String, completion: @escaping (Resource?) -> Swift.Void) {
    // ...

    let task = URLSession.shared.dataTask(with: request) { data, response, error -> Void in
        if let error = error {
            // ...
        } else {
            guard
                // アルバムを取り出す 
                let jsonData = try? JSONSerialization.jsonObject(with: data!),
                let dictionary = jsonData as? [String: Any],
                let dataArray = dictionary["data"] as? [[String: Any]],
                let albumDictionary = dataArray.first,
                let albumData = try? JSONSerialization.data(withJSONObject: albumDictionary),

                // JSONDecoderを使用してデコードを行う
                let album = try? JSONDecoder().decode(Resource.self, from: albumData) else {
                    print("JSON Decode Failed");
                    completion(nil)
                    return
            }
            completion(album)
        }
    }
    task.resume()
}

アルバムを再生する

再生可能かどうかを判別する

Apple Musicで提供されている曲を再生するには以下を満たす必要があります。

  • Apple Music購読中のApple IDで端末にログインしていること

StoreKitSKCloudServiceControllerrequestCapabilities(completionHandler:)を使用すれば、再生可能かどうかを判別できます。

self.cloudServiceController.requestCapabilities { capabilities, error in
    guard capabilities.contains(.musicCatalogPlayback) else { return }

    // 再生可能!
}

再生する

曲を再生するにはMediaPlayerMPMusicPlayerControllerを使用します。

setQueue(with:)を呼ぶと再生キューに曲を追加できます。play()を呼ぶと、再生キューに入っている曲が再生されます。

以下の実装は指定曲からアルバムを再生する例です。

class AlbumViewController: UITableViewController {

    // ...

    let musicPlayer = MPMusicPlayerController.systemMusicPlayer
    var album: Resource?

    // ...
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // ...

    // Descriptorを作成
    let tracks = album!.relationships!.tracks!
    let trackIDs = tracks.map { $0.id! }
    let startTrackID = tracks[indexPath.row].id!
    let descriptor = MPMusicPlayerStoreQueueDescriptor(storeIDs: trackIDs)
    descriptor.startItemID = startTrackID

    // 再生キューに曲を追加
    musicPlayer.setQueue(with: descriptor)

    // 再生
    musicPlayer.play()
}

動作確認

最後に、サンプルアプリの動作例を紹介します。

サンプルアプリを起動します。ナビゲーションバー下部にサーチバーが表示されます。

apple-music-api-search-1

キーワードを入力すると、Search APIから取得したアルバムの一覧が表示されます。

apple-music-api-search-2

アルバムを選択すると、詳細画面が表示されます。この画面にはAlbums APIから取得したアルバム詳細情報が表示されます。

apple-music-api-search-3

任意の曲をタップすると、その曲からアルバムが再生されます。

apple-music-api-search-4

アプリがバックグラウンドになっても再生が継続されます。

apple-music-api-search-5

さいごに

本記事では、Apple Music APIを使用してアルバム情報を取得し、収録曲を再生する実装を紹介しました。

今回作成したサンプルアプリのソースコードは以下のリポジトリで公開してますので参考にしてみてください。

参考資料

  • てつん

    標準アプリが使用者のことを考えてないくらい使いにくいのでapikitが公開されたことに凄く期待できますね!