feature image

2024年4月14日 | ブログ記事

Spotifyのクライアントを自作しよう

この記事はtraP新歓ブログリレー2024 38日目の記事です。

こんにちは@d_etteiu8383です。@eyemono.moeでもあります。
本記事ではSpotifyのWeb Playback SDKを使用したプレイヤーの自作方法について解説します。

Spotifyとは

image-69

Spotify - Web Player: Music for everyone
Spotify is a digital music service that gives you access to millions of songs.

Spotifyはスマートフォン/PCで利用可能な音楽ストリーミングサービスです。

Spotify API

Spotifyには楽曲の検索やメタデータ(アーティスト情報、アルバム情報など)の取得などが可能なWeb APIが提供されています。

Web API | Spotify for Developers
Retrieve metadata from Spotify content, control playback or get recommendations

↓過去に本ブログでSpotify APIを使用した記事が投稿されています。ぜひご覧ください↓

Spotify API を使ってみた
皆さんこんにちは。20BのRozelinです。この記事は新歓ブログリレー2021 10日目の記事です。 traPでは様々な部内サービスを利用することができますが、その中でも最も多くの部員に使われているのは部内SNSツール「traQ」だと思います。そこで、プログラミングの勉強と自分の娯楽を兼ねて、traQ用のBOTとSpotifyAPIを組み合わせて、traQ内でSpotifyの楽曲検索とアーティスト検索をできるようにしてみました。 最初SpotifyAPIを使うにあたって、使おうと思っている全てのリクエストのコードを全て1から書くのはコスパが悪すぎると思ったので、パッケージを使うことにしました。BOTのコードはTypeScriptで書いていたので、TypeScriptで使えるSpotifyAPIのパッケージを探したところ、spotify-web-api-ts[https://adamgrieger.github.io/spotify-web-api-ts/]というものを見つけました。これを使えばnewSpotifyWebApi({ accessToken: ACCESS
Spotifyで今年聴いた曲全部見る
はじめにこの記事はtraPアドベントカレンダー2021 [https://trap.jp/tag/advent-calendar-2021/] 43日目(12/25)の記事です。 こんにちは、@d_etteiu8383 [https://trap.jp/author/d_etteiu8383]です。最近は研究室で大腸菌とかDNAをこねこねしてます。@Uzaki [https://trap.jp/author/uzaki]さんが「今年のプレイリストを作って、1年を振り返ってみよう」という面白そうな企画をやってたので乗っかります。 今年のプレイリストを作って、1年を振り返ってみようtraP部員が自分の好きな曲を詰め込んだ「今年のプレイリスト」を紹介します。東京工業大学デジタル創作同好会traPUzaki [https://trap.jp/post/1457/]で、「今年のプレイリスト」ということで、2021年を振り返りながらプレイリストを作ってみようと思ったのですが、これがなかなか難しかったので私は徹底的にデータに頼ることにしました。本記事ではSpotifyでの楽曲データに

実はこのAPIでは、楽曲情報の取得のみならず、楽曲の再生制御も可能です。しかしこれを使用したプレイヤーを作る場合、楽曲の再生/停止等の操作をいちいちAPIリクエストで行う必要があります...

Web Playback SDK

そこでSpotifyはWeb Playback SDKを提供しています。これを使用することで、Web上でSpotifyの楽曲を再生するプレイヤーを簡単に作成することができます。

Web Playback SDK | Spotify for Developers
Create a new player and stream Spotify content inside your website application.

仕組みとしては、SDKを通してローカルのSpotify Connectデバイスが作成され、このデバイスで楽曲の再生制御を行うことができるようになっているらしいです。PCのSpotifyアプリを開きながら、同一ネットワーク上のスマホのSpotifyアプリから再生操作を行うと、PCのSpotifyアプリで再生が行われるのと同じような仕組みです。

この画像における「iPhoneで再生中」の"iPhone"に当たる部分を、Web Playback SDKを使用して作成することができます。

本記事ではこのWeb Playback SDKを使用したプレイヤーの作成方法について簡単に解説します。

Web Playback SDKを使用したプレイヤーの作成方法

「解説します」と言っていますが、実は公式の解説が非常に充実しているため、英語が読める方であれば以下の公式ドキュメントを読むことを強くお勧めします。

Getting Started with Web Playback SDK | Spotify for Developers

本記事では Vite + SolidJS の構成による、静的なWebページとしてプレイヤーを作成する方法を解説します。React...?なんですかそれは......?

リポジトリ:https://github.com/eyemono-moe/oreore-spotify

本記事内では大まかに何をやっているかの説明にとどめます。詳細なコードはリポジトリを参照してください。

必要なもの

1. Spotify OAuthアプリケーションの登録

Web Playback SDKを使用するには、SpotifyのOAuthアクセストークンが必要です。そのため、まずはSpotifyの開発者ダッシュボードからアプリケーションを登録します。

実際にSpotifyのOAuthアプリケーションを作成・公開する場合、規約デザインガイドラインに沿ったアプリケーションを作成する必要があります。Spotidyのロゴの使用に関するガイドラインはもちろん、Spotify上に存在する楽曲データやアルバムジャケット画像の扱いについても注意が必要です。(ジャケット画像を切り抜いたりオーバーレイをかけてはいけない、楽曲データを加工してはいけないなど)

OAuthアプリケーションを作成した直後は、アプリケーションはdevelopment modeとして作成され、開発者自身のSpotifyアカウント(と開発者が設定から追加したアカウント)でのみ使用可能です。
アプリケーションを公開し、全世界のSpotifyユーザーに使用可能にするには、Spotifyの審査を通過する必要があります。

詳細:https://developer.spotify.com/documentation/web-api/concepts/quota-modes

今回の記事内ではアプリケーションの公開まではせず、ローカルサーバー上でのみ動作するアプリケーションを作成します。

2. プロジェクトの作成

プロジェクトを作成します。

pnpm create vite my-spotify
cd my-spotify

私のリポジトリではpnpm, SolidJSを使用していますが、他のパッケージマネージャやフレームワークでも問題ありません。そもそもViteでなくてもいい。

3. ログイン機能実装とアクセストークンの取得

以下のように

  1. https://accounts.spotify.com/authorizeに必要情報を付けてリダイレクトしてログイン画面を表示
  2. ログイン後、リダイレクトされたURLにcodeが付与されるので、このcodeを使ってアクセストークンを取得
  3. アクセストークンを保存

します。SolidJSでの例はhttps://github.com/eyemono-moe/oreore-spotify/blob/235b16325a7eda29614ac68650573fac182a834a/src/context/auth.tsxにあります。

今回は簡単のためにアクセストークンをlocalStorageに保存していますが、実際のアプリケーションではセキュリティ上の理由からlocalStorageに保存することは避けるべきです
適当なバックエンドサーバーを用意し、sessionを使ってアクセストークンを保存するようにしましょう。

でもわざわざBFF用意するの面倒だよね。

作例のリポジトリではこのアクセストークンを使用し、Spotify APIを叩いてログインユーザーの名前を表示するようにしています。このAPIのクライアントはhttps://github.com/sonallux/spotify-web-apiで有志が作成しているOpenAPI定義から生成しています。

4. Web Playback SDKの読み込み

アクセストークンを取得できたので、さっそくWeb Playback SDKを使用しましょう。

やることは以下の4つです。

  1. sdkのスクリプトの読み込み
  2. playerの作成
  3. イベントリスナーの登録
  4. 公式クライアントへのplayerの接続

4.1 sdkのスクリプトの読み込み

以下のようにscriptタグを使ってsdkのスクリプトを読み込みます。

<script src="https://sdk.scdn.co/spotify-player.js"></script>

動的にやるなら以下のような感じ。

4.2 playerの作成

以下のようにplayerを作成します。Spotifyオブジェクトはsdkのスクリプトを読み込んだ後に使用可能になるグローバルオブジェクトです。SDKが正常に読み込まれるとonSpotifyWebPlaybackSDKReadyが自動的に呼ばれるので、その中でplayerを作成します。

window.onSpotifyWebPlaybackSDKReady = () => {
  const token = '[My access token]';
  const player = new Spotify.Player({
    name: 'Web Playback SDK Quick Start Player',
    getOAuthToken: cb => { cb(token); },
  });
}

4.3 イベントリスナーの登録

作成したplayerは接続完了時やエラー時にイベントを発火します。
UIの更新やロギングを行いたい場合これらを使用しましょう。
利用可能なイベント一覧はhttps://developer.spotify.com/documentation/web-playback-sdk/reference#eventsを参照してください。

↓使用例↓

4.4 公式クライアントへのplayerの接続

作成したplayerについて、player.connect()を実行することで、初期化が行われます。この時点で公式クライアントのデバイス一覧に自作のplayerが表示されるようになります。

ここで重要なのが、connect()をしただけではブラウザ上での再生は可能になりません。実際に再生制御をこのplayerから行うには、再生中のデバイスをこのplayerに切り替える必要があります

そのためにSpotify APIのTransfer Playback endpointを使用します。player.connect()を実行するとそのplyerのデバイスIDが取得可能になるため、このデバイスIDを使用してTransfer Playback endpointを叩きます。

これで、自作のplayerからSpotifyの楽曲を再生することができるようになります🎉

5. 楽曲データ取得と再生制御

playerを作成・接続すると、player_state_changedイベントを通じてplayback stateを取得することができます。これは以下のようなデータで構成されています(長いので一部省略しています)。

{
  "playbackState": {
    "timestamp": 1713083615136,
    "context": {
      "uri": "spotify:user:l1mq96khn4z1fg6smsp5qdyjb:collection",
      "metadata": {}
    },
    "duration": 225570,
    "paused": false,
    "shuffle": true,
    "position": 0,
    "loading": false,
    "repeat_mode": 1,
    "track_window": {
      "current_track": {
        "id": "2vpIVi4TxAtjfF7kz9wmOQ",
        "uri": "spotify:track:2vpIVi4TxAtjfF7kz9wmOQ",
        "type": "track",
        "uid": "f2d9c29c6320776ca5ed",
        "linked_from": {
          "uri": null,
          "id": null
        },
        "media_type": "audio",
        "track_type": "audio",
        "name": "走れ!うさかめ高校テニス部!!",
        "duration_ms": 225570,
        "artists": [
          {
            "name": "うさかめ高校テニス部",
            "uri": "spotify:artist:0Kw2qZ1BrMugNYlExzvcqm",
            "url": "https://api.spotify.com/v1/artists/0Kw2qZ1BrMugNYlExzvcqm"
          },
          {
            "name": "中島由貴(田中きなこCV)",
            "uri": "spotify:artist:7oFIPeQya2BSX4zCrXlte8",
            "url": "https://api.spotify.com/v1/artists/7oFIPeQya2BSX4zCrXlte8"
          },
          {
            "name": "小出ひかる(佐藤くるみCV)",
            "uri": "spotify:artist:5z9cjipgVytd6UULrUIzUv",
            "url": "https://api.spotify.com/v1/artists/5z9cjipgVytd6UULrUIzUv"
          },
          {
            "name": "新井田いづみ(鈴木あやこCV)",
            "uri": "spotify:artist:4mCaN6AiX65V00yZN8NCTg",
            "url": "https://api.spotify.com/v1/artists/4mCaN6AiX65V00yZN8NCTg"
          },
          {
            "name": "谷尻まりあ(西新井大師西CV)",
            "uri": "spotify:artist:5wml1Y1J3rBwBwZxgfRgIB",
            "url": "https://api.spotify.com/v1/artists/5wml1Y1J3rBwBwZxgfRgIB"
          }
        ],
        "album": {
          "name": "てーきゅうBEST",
          "uri": "spotify:album:19OuaVZecS5k2SUHwiRDVs",
          "images": [
            {
              "url": "https://i.scdn.co/image/ab67616d00001e02bcfb45e76a0d756da3c473a4",
              "height": 300,
              "width": 300,
              "size": "UNKNOWN"
            },
            {
              "url": "https://i.scdn.co/image/ab67616d00004851bcfb45e76a0d756da3c473a4",
              "height": 64,
              "width": 64,
              "size": "SMALL"
            },
            {
              "url": "https://i.scdn.co/image/ab67616d0000b273bcfb45e76a0d756da3c473a4",
              "height": 640,
              "width": 640,
              "size": "LARGE"
            }
          ]
        },
        "is_playable": true,
        "metadata": {}
      },
      "next_tracks": [
        {
          "id": "0IiaRSmULwHbmU1rxMvFSe",
          "uri": "spotify:track:0IiaRSmULwHbmU1rxMvFSe",
          "type": "track",
          "uid": "533d961356b613fac6b6",
          "linked_from": {
            "uri": null,
            "id": null
          },
          "media_type": "video",
          "track_type": "video",
          "name": "白金ディスコ",
          "duration_ms": 256573,
          "artists": [
            {
              "name": "物語シリーズ",
              "uri": "spotify:artist:0NT8fqhPoKJrd038u1Qumz",
              "url": "https://api.spotify.com/v1/artists/0NT8fqhPoKJrd038u1Qumz"
            }
          ],
          "album": {
            "name": "歌物語 Special Edition",
            "uri": "spotify:album:1oP65KKl98hRSjJvpKeFmQ",
            "images": [
              {
                "url": "https://i.scdn.co/image/ab67616d00001e02426fff6f6d9dbdd9a1bc3133",
                "height": 300,
                "width": 300,
                "size": "UNKNOWN"
              },
              {
                "url": "https://i.scdn.co/image/ab67616d00004851426fff6f6d9dbdd9a1bc3133",
                "height": 64,
                "width": 64,
                "size": "SMALL"
              },
              {
                "url": "https://i.scdn.co/image/ab67616d0000b273426fff6f6d9dbdd9a1bc3133",
                "height": 640,
                "width": 640,
                "size": "LARGE"
              }
            ]
          },
          "is_playable": true,
          "metadata": {}
        },
        ...
      ],
      "previous_tracks": [
        {
          "id": "65Pu62l9hucWHs3azbKYGU",
          "uri": "spotify:track:65Pu62l9hucWHs3azbKYGU",
          "type": "track",
          "uid": "31b52dc6e108d8855063",
          "linked_from": {
            "uri": null,
            "id": null
          },
          "media_type": "video",
          "track_type": "video",
          "name": "ドリームパレード",
          "duration_ms": 155946,
          "artists": [
            {
              "name": "ふれんど〜る(cv.茜屋日海夏&芹澤 優&澁谷梓希&牧野由依&渡部優衣)",
              "uri": "spotify:artist:0dippPmLv2DuHuNSi5dIVn",
              "url": "https://api.spotify.com/v1/artists/0dippPmLv2DuHuNSi5dIVn"
            }
          ],
          "album": {
            "name": "プリパラ☆ミュージックコレクション season.2",
            "uri": "spotify:album:4KJnTeFK500L0eRyvdVfjy",
            "images": [
              {
                "url": "https://i.scdn.co/image/ab67616d00001e0216683ee2672611d60129a78c",
                "height": 300,
                "width": 300,
                "size": "UNKNOWN"
              },
              {
                "url": "https://i.scdn.co/image/ab67616d0000485116683ee2672611d60129a78c",
                "height": 64,
                "width": 64,
                "size": "SMALL"
              },
              {
                "url": "https://i.scdn.co/image/ab67616d0000b27316683ee2672611d60129a78c",
                "height": 640,
                "width": 640,
                "size": "LARGE"
              }
            ]
          },
          "is_playable": true,
          "metadata": {}
        },
        ...
      ]
    },
    "restrictions": {
      "disallow_seeking_reasons": [],
      "disallow_skipping_next_reasons": [],
      "disallow_skipping_prev_reasons": [],
      "disallow_toggling_repeat_context_reasons": [],
      "disallow_toggling_repeat_track_reasons": [],
      "disallow_toggling_shuffle_reasons": [],
      "disallow_peeking_next_reasons": [],
      "disallow_peeking_prev_reasons": [],
      "undefined": [
        "not_supported_by_device",
        "not_supported_by_content_type"
      ],
      "disallow_resuming_reasons": [
        "not_paused"
      ]
    },
    "disallows": {
      "seeking": false,
      "skipping_next": false,
      "skipping_prev": false,
      "toggling_repeat_context": false,
      "toggling_repeat_track": false,
      "toggling_shuffle": false,
      "peeking_next": false,
      "peeking_prev": false,
      "undefined": true,
      "resuming": true
    },
    "playback_id": "62d373e704224da0bb404e3dc910de3e",
    "playback_quality": "VERY_HIGH",
    "playback_features": {
      "hifi_status": "NONE",
      "playback_speed": {
        "current": 1,
        "selected": 1,
        "restricted": true
      },
      "signal_ids": []
    },
    "playback_speed": 1
  },
  "player": {
    "_options": {
      "name": "オレオレクライアント",
      "volume": 1
    },
    "_eventListeners": {
      "account_error": [
        null
      ],
      "authentication_error": [
        null
      ],
      "autoplay_failed": [],
      "playback_error": [
        null
      ],
      "initialization_error": [
        null
      ],
      "ready": [
        null
      ],
      "not_ready": [
        null
      ],
      "player_state_changed": [
        null
      ],
      "progress": []
    },
    "_connectionRequests": {},
    "_getCurrentStateRequests": {},
    "_getVolumeRequests": {},
    "_messageHandlers": {},
    "isLoaded": {}
  },
  "errors": null,
  "device": {
    "device_id": "278a6c20000d406de577c59aa820f7e8870d733d",
    "isReady": true
  }
}

これらの情報からUIの更新を行いましょう。

また、再生制御を行う場合は以下のようにplayerオブジェクトのメソッドを使用します。

player.setVolume(0.5).then(() => {
  console.log('Volume updated!');
});

player.pause().then(() => {
  console.log('Paused!');
});

player.resume().then(() => {
  console.log('Resumed!');
});

player.togglePlay().then(() => {
  console.log('Toggled playback!');
});

// Seek to a minute into the track
player.seek(60 * 1000).then(() => {
  console.log('Changed position!');
});

player.previousTrack().then(() => {
  console.log('Set to previous track!');
});

player.nextTrack().then(() => {
  console.log('Skipped to next track!');
});

特定のアルバムやプレイリストの楽曲を指定して再生を開始する場合は、Playback SDKではなくAPIのStart/Resume Playbackを使用する必要があるようです。

Spotify上での複数楽曲の再生はcontextを指定して行われており、例えば"アルバムのcontext uri"や"プレイリストのcontext uri"を指定することで、そのアルバムやプレイリストの楽曲を再生することができます。そのcontext内のどの楽曲から再生するかはoffsetパラメータで指定します。

ということでSpotifyの楽曲を再生するプレイヤーが完成しました🎉

まとめ

Web Playback SDKを使用することで、Spotifyの楽曲を再生するプレイヤーを簡単に作成することができます。皆さんもぜひオリジナルのプレイヤーを作成してみてください。

おまけ

もともとは、iTunesと同様に、Spotifyで再生中の楽曲のジャケット画像を常に最前面に表示したいと思い自作プレイヤーの作成を進めていました。

↑ピクチャーインピクチャーを使用した自作プレイヤー(バグり散らかしている)

しかし、僕が気づいていなかっただけで、ブラウザ版のSpotify公式プレイヤーではとっくにこの機能が実装されていました...
しかもつい先日、ネイティブアプリ版のSpotifyにもこの機能が追加されていました。ありがとうね...


最後までお読みいただきありがとうございました。明日の新歓ブログリレー2024担当者は@cp20です。楽しみ~

d_etteiu8383 icon
この記事を書いた人
d_etteiu8383

グラフィック班とゲーム班とSysAd班所属 いろいろ活動しています

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年12月11日
DIGI-CON HACKATHON 2023『Mikage』
toshi00 icon toshi00
ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】 feature image
2018年11月3日
ERC20トークンを用いた宝探しゲーム(真)の提案【アドベントカレンダー2018 10日目】
Azon icon Azon
2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2024年3月22日
traPグラフィック班の活動紹介2024
haru10 icon haru10
2023年4月27日
Vulkanのデバイスドライバを自作してみた
kegra icon kegra
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記