mcpField
C
connector-spotify

connector-spotify

github.com/caffeinelabs開発27.1k 回インストール1 週間前に更新
リポジトリ

connector-spotify について

connector-spotifyは、Internet ComputerのcanisterからSpotify Web APIを呼び出す開発者向けのMotokoバインディングです。公式OpenAPI仕様から生成され、カタログ参照や再生操作に対応し、OAuth bearer tokenは呼び出し側が渡します。

主な機能

  1. 01Spotify Web API向けMotokoバインディング
  2. 02カタログとプレーヤーの操作
  3. 03呼び出し側から渡すOAuth bearer token
  4. 04既定で非複製のアウトコール
  5. 05書き込み時のHTTPステータス確認

活用シーン

  1. 01トラック、アルバム、アーティストの検索
  2. 02再生状態の取得とプレーヤー操作
  3. 03必要な認可を得てプレイリストやライブラリを編集

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/caffeinelabs/skills --skill connector-spotify

インストール後、Claude Code で/connector-spotifyを入力して読み込まれたか確認します。

詳細

spotify-clientを使ったSpotify

Spotify公式のOpenAPI仕様から生成された、Spotify Web API用のMotokoバインディングです。15個のAPIモジュール、270個の操作が含まれています。このパッケージはicp-cliモード(管理カナスターインターフェースにmo:ic/Typesを使用)で構築されているため、依存関係としてicを取り込み、[toolchain]でPocketICのバージョンを固定します。

バックエンド

カタログデータの読み取りとプレーヤーの操作を行います。トークンは常に呼び出し元から渡され、カナスターがSpotifyのクライアントシークレットを保持することはありません。

import { getTrack } "mo:spotify-client/Apis/TracksApi";
import { getInformationAboutTheUsersCurrentPlayback; skipUsersPlaybackToNextTrack } "mo:spotify-client/Apis/PlayerApi";
import { type Config; defaultConfig } "mo:spotify-client/Config";

actor {
  func config(accessToken : Text) : Config = { defaultConfig with auth = ?#bearer accessToken };

  // Catalog read — a client-credentials token suffices. `market = ""` omits the
  // parameter; empty strings and zeroes are how optional query params are
  // dropped, there is no `?Text` to leave null.
  public func trackName(accessToken : Text, id : Text) : async ?Text {
    let track = await* getTrack(config accessToken, id, "");
    track.name;
  };

  // User read — needs a user token with `user-read-playback-state`. An idle
  // player answers 204 with an empty body, which the generated decoder cannot
  // parse, so it rejects; treat that as "nothing playing", not as an error.
  public func nowPlaying(accessToken : Text) : async ?Bool {
    try {
      let state = await* getInformationAboutTheUsersCurrentPlayback(config accessToken, "", "");
      state.is_playing;
    } catch (_err) {
      null;
    };
  };

  // User write — needs `user-modify-playback-state`. Returns `()`, and since
  // 0.3.0 a non-2xx status rejects, so a missing scope or an expired token
  // surfaces here instead of looking like success.
  public func skip(accessToken : Text) : async () {
    await* skipUsersPlaybackToNextTrack(config accessToken, "");
  };
}

認証:カナスターがクライアントシークレットを見ることはありません

すべての呼び出しには、オフチェーンで発行され、渡されるOAuth 2.0ベアラーアクセストークンが必要です。重要なフローは2つあります。

  1. クライアントクレデンシャル — サーバー間通信で、ユーザーは不要です。読み取り専用の公開カタログにのみアクセスします。対象は、search、getTrack、getAnAlbum、getAnArtist、getNewReleases、getCategories、getAvailableMarkets、公開プレイリスト、公開番組です。/me/*エンドポイント、ライブラリ、プレーヤーにはアクセスできません。
  2. PKCEを使った認証コード — ユーザー向けのフローです。すべての/me/*呼び出し、プレイリストの変更、ライブラリへの書き込み、プレーヤーコマンドに必要です。それぞれに独自のスコープ(user-read-playback-state、user-modify-playback-state、playlist-modify-public、user-library-readなど)が設定されています。トークンに含まれていないスコープは、検証エラーではなく403として返されます。

トークンは1時間後に期限切れとなり、更新もオフチェーンで行います。401が返された場合は、恒久的な失敗とせず、「クライアントにトークンを更新して再試行するよう依頼する」ものとして扱ってください。クライアントシークレットをカナスターに保存してはいけません。

呼び出しはデフォルトで非レプリケートです

このパッケージではdefaultConfigにis_replicated = ?falseが設定されており、135個の操作のうち91個がこれに依存します。ICで必須となる27個のPUT操作と17個のDELETE操作については、ジェネレーターがリクエストごとに非レプリケートを指定済みです。そのため、プレイリストの編集やライブラリへの保存が危険にさらされることはありません。残りをカバーするのがデフォルト設定です。

  • addToQueueと2つのスキップエンドポイントを含む7個のPOST。これらは冪等ではないため、レプリケートされるとトラックが約13回キューに追加されたり、約13曲スキップされたりします。
  • 84個すべてのGET。プレーヤーのデータは非決定的です。現在再生中の情報にはtimestampとprogress_msが含まれ、ノードごとに異なります。また、結果が一致する場合でもサイクルのコストが約13倍になります。

このフラグを自分で設定する必要はありません。デフォルト設定が適切です。is_replicated = ?trueで上書きするのは、揮発性フィールドを取り除くtransformと組み合わせる場合に限ってください。

0.2.xからのアップグレード — 以前の案内は逆でした。 以前のスキルでは「コンセンサスが重要」という理由で、変更操作に対して呼び出し元がis_replicated = nullを維持するよう案内し、let userCfg = { cfg with is_replicated = null }という例を示していました。この設定を引き継ぐと、かえって問題が生じます。nullはレプリケートを意味するため、すべてのaddToQueueとスキップがレプリカごとに1回ずつ実行されます。このような上書きはすべて削除し、defaultConfigをそのまま使用してください。

レスポンス内のすべてのフィールドはオプショナルです

Spotifyでは必須とされるレスポンスフィールドがほとんどないため、すべてのモデルでフィールドはオプショナルです。例:TrackObject.name : ?Text、.artists : ?[SimplifiedArtistObject]、CurrentlyPlayingContextObject.is_playing : ?Bool。ネストしたswitchではなくdo ?ブロックで値をたどり、値が存在しない場合にどう扱うかを呼び出し元側で決めてください。Spotifyは、トークンのスコープで許可されていないフィールドを省略します。

URIではなくIDを使用

エンドポイントのパラメーターには、Spotifyのbase-62 ID(11dFghVXANMlKmJXsNCbNl)を指定します。URI(spotify:track:11dFghVXANMlKmJXsNCbNl)やURLではありません。ユーザーがSpotifyのリンクを貼り付けた場合は、最後の/の後から?の前までの部分を取り出してください。例外として、プレイリスト操作のurisパラメーターには、spotify:track:…形式の完全なURIを指定します。

再生の開始

PlayerApi.startAUsersPlayback は4つのフィールドをスカラーとして受け取ります — context_uri : ?Text、uris : ?[Text]、position_ms : ?Int — また、transferAUsersPlayback は play : ?Bool を受け取ります。uris(明示的なトラック)か context_uri(アルバム、アーティスト、プレイリスト)のどちらか一方を選び、両方を指定しないでください。

例外は offset です。仕様では自由形式のオブジェクトとして定義されているため、ジェネレーターはこれを ?Map<Text, Text> にマッピングし、すべての値を文字列としてシリアライズします。そのため、{"uri": "spotify:track:…"} は機能しますが、Spotify がドキュメントに記載しているもう一つの形式 {"position": 5} は {"position": "5"} として送信され、拒否されます。コンテキスト内の位置は、インデックスではなく URI で指定してください。

動作しない操作が1つあります

PlaylistsApi.uploadCustomPlaylistCover — 仕様では本文が image/jpeg(format: byte)と宣言されていますが、ジェネレーターは常に JSON リクエスト本文を出力するため、呼び出しでは base64 を JSON の引用符で囲んだ状態で Content-Type: application/json とともに送信します。Spotify はこれを拒否します。これは 0.3.0 で新たに発生した問題ではありません。0.2.2 では生の Blob を JSON でラップしていましたが、通信上は同様に誤りでした。カスタムプレイリストカバーのアップロードを提供しないでください。また、ic.http_request で独自に実装しないでください。 caffeinelabs/skills-internal に報告してください。

このサーフェスのその他の部分は問題ありません。一部のコネクターとは異なり、スタブの oneOf コンバーターはなく、リクエスト本文を黙って破棄する操作もなく、JSON デコーダーを通じて JSON 以外の本文を返すエンドポイントもありません。

アイドル状態のプレーヤーは「再生中の曲なし」を返さず、拒否します

再生がアクティブでない場合、PlayerApi.getInformationAboutTheUsersCurrentPlayback と getTheUsersCurrentlyPlayingTrack は 本文が空の 204 を返します。生成されたコードはすべての 2xx を 200 のスキーマとして扱い、その本文に JSON デコーダーを適用するため、アイドル状態のプレーヤーでは値が存在しない状態になるのではなく、Error.reject(… Failed to parse JSON …) が発生します。§Backend のサンプルのように、両方を try/catch で囲み、拒否された場合は「再生中の曲なし」として扱ってください。

これは Spotify 固有の挙動ではなく、コード生成の不足です。最初の操作では仕様に '204': Playback not available or active と宣言されていますが(ジェネレーターは no-content の 2xx レスポンスを無視し、それに対応する ?T の戻り値の形式もありません)、2つ目の操作では Spotify は実際には 204 を返すものの、仕様には宣言されていません。宣言済みの no-content レスポンスをモデル化すると、生成されるシグネチャが ?CurrentlyPlayingContextObject に変わるため、これはプラグイン側で別途変更として対応すべきです。

すべての書き込み操作でステータスが報告されるようになりました

0.3.0 より前は、async* () を返す43個の操作 — skip、pause、addToQueue、ライブラリとプレイリストのすべての変更操作 — が HTTP レスポンスを完全に破棄していたため、401、403、429 を成功と区別できませんでした。現在は、2xx 以外のステータスで拒否されます。書き込み操作を囲んだ try/catch が実際に動作するようになりました。スコープが不足している場合、以前は成功した no-op のように見えていましたが、403 として通知されます。

サイクルとレスポンスサイズ

cycles は未設定(null)のままにしてください。呼び出しごとに、IC がそのリクエストに課す料金分のサイクルが付加されます。アウトコールが無料のサブネットでは、これはゼロです。?n を指定すると、ちょうど n サイクルが付加されます。n が料金を下回る場合、IC は呼び出しを拒否し、キャニスターが n 未満のサイクルしか保持していないサブネットでは呼び出しがトラップします。 limit=50 の search、トラックリストが長い場合の getAnAlbum、または非常に大きなオブジェクトを返す getAudioAnalysis など、大きなページでは、max_response_bytes = ?2_000_000 を明示的に指定してください。

エラー

2xx 以外のレスポンスとデコード失敗は throw Error.reject(…) を発生させます。クライアントは diagnostics を有効にして生成されるため、メッセージは HTTP <status> body[<n>B]=<first 100 chars>: <reason> となります。追加のログを取らなくても、401(トークンの期限切れ)、403(スコープ不足)、404(不正な ID)を区別するのに十分な情報が得られます。try/catch で捕捉し、Error.message(err) を提示してください。

コメント