🎼

Cloudflare Workers を使ってランダムなSpotifyのプレイリストを返す

2023/05/28に公開

はじめに

Cloudflare Workers を使って学習用に何か作ろうと思い、Spotifyのプレイリストをランダムで返すものを作ってみました。

処理の大まかな流れとしては以下になります。

  1. Workers KVに保存している検索ワードの中からランダムなワードを選び出す
  2. 選んだワードでプレイリストをSpotify APIで検索する
  3. 得られた複数のプレイリストからさらにランダムに一つを選び、それをResponseとして返す

あくまで学習目的で作成したため、プレイリストのURIを返すだけというシンプルなものになっています。

ゴール

返ってきたプレイリストのURIを、Spotifyのデスクトップアプリで開いてランダムなプレイリストになっていればOKというゴールに向かって進めて行きたいと思います。

image1

Cloudflare workersとは

CloudflareのCDNのエッジサーバで実行されるサーバレス実行環境(FaaS)
また、CloudFlare workersで使えるキャッシュは Workers KV、Durable Objects、Cache APIの3つ

環境構築や準備

- node: v18.16.0
- wrangler: v2.15.1

1. Spotify URIについて

Spotifyには曲やプレイリストなどに関連する以下のようなURIが存在します。

spotify:artist:xxxxxxxxxxxxxxxx

SpotifyデスクトップアプリがインストールされているMacの場合、Terminal上で open spotify:artist:xxxxxxxxxxxxxxxx と入力することで、Spotifyアプリが対象のコンテンツを開いてくれます。この手順に従い、レスポンスのURIをMacのSpotifyアプリで確認しながら作業を進めていこうと思います。

What's a Spotify URI? - The Spotify Community

2. Cloudflare Workersの環境構築

2-1. サインアップして最初のWorkerを作成してみる

早速 Cloudflare Workers の環境を構築していきたいと思います。

こちらのページからサインアップしダッシュボードの Workers ページにいくと以下の様になっているので早速ダッシュボード上でWorkerを作成してみます。

image2

作成した後IDEっぽい画面が表示されました。ここでも編集、再デプロイなんかができるみたいですね 👀

image3

一旦 Workers ページに戻ってみるとサブドメインが変更できるようになっていました ✨

image4

ここからお好みのサブドメインを設定できるようです。

2-2. wranglerを使ってデプロイしてみる

CLIツール wrangler が提供されているのでそちらを使って進めてみたいと思います。

Commands · Cloudflare Workers docs

ここからはDockerを使って環境構築していきます。
まずはプロジェクト直下に以下内容のDockerfileを作成します。

FROM node:18-alpine
WORKDIR /usr/src/app

RUN apk update && \
    apk add git vim bash curl

RUN npm install -g wrangler

次に以下内容で docker-compose.yml を作成します。

version: '3.3'

services:
  worker:
    build: .
    image: wrangler
    container_name: "wrangler"
    tty: true
    stdin_open: true
    volumes:
      - .:/usr/src/app
      - wrangler_config:/root/.config/.wrangler
    ports:
      - '8976:8976'
      - '8787:8787'
    entrypoint: bash

volumes:
  wrangler_config:

コンテナを起動させ、コンテナ内で以下コマンドを実施し Cloudflare にログインします。

wrangler login

ブラウザが開けないので Failed to open と表示されるますが、URLをコピーしてブラウザを起動させると以下の確認画面が表示され、「Allow」を選択する事でログインする事ができます。

image5

wrangler whoami コマンドを実行し自身のアカウントが表示されていればOKです。

2-3. プロジェクトの初期化

$ wrangler init hello
⛅️ wrangler 2.15.1 (update available 2.16.0)
-------------------------------------------------------
Using npm as package manager.
✨ Created hello/wrangler.toml
✔ No package.json found. Would you like to create one? … yes
✨ Created hello/package.json
✔ Would you like to use TypeScript? … yes
✨ Created hello/tsconfig.json
✔ Would you like to create a Worker at hello/src/index.ts? › Fetch handler
✨ Created hello/src/index.ts
✔ Would you like us to write your first test with Vitest? … no

↑ 今回は一旦Vitestはなしで、Fetch handlerとして作成してみました。
早速起動してみます。

cd hello
npm run start

起動後、http://localhost:8787/ にアクセスしてみると「Hello World!」が返ってくるはずです。
今度はデプロイしてみます。

cd hello
npm run deploy

↑これでサクッとデプロイされ、デプロイされたURLにアクセスすると同じ「Hello World!」が返ってくるはずかと思います。
ちなみにデプロイされたものを削除する場合は wrangler delete で削除できます。
一旦ここまででCloudflare Workersを動かす環境ができました!

実装

1. まずはCloudflare Workersから返却されたURIをアプリで開いてみる

1-1. セットアップ

先程の hello ディレクトリを削除し、以下コマンドでプロジェクトルート直下にsrcディレクトリを作成します。

wrangler init .

次に docker-compose.yml に以下を追加します。

version: '3.3'

services:
  worker:
    build: .
    image: wrangler
    container_name: "wrangler"
    command:
      - ./bin/start_dev_server.sh # 追加
    tty: true
    stdin_open: true
    environment:
      - RUN_WRANGLER_DEV=${RUN_WRANGLER_DEV:-1} # 追加
    volumes:
      - .:/usr/src/app
      - wrangler_config:/root/.config/.wrangler
      - modules_data:/usr/src/app/node_modules # 追加
    ports:
      - '8976:8976'
      - '8787:8787'

volumes:
  wrangler_config:
  modules_data: # 追加

次に bin/start_dev_server.sh を以下内容で作成します。

#!/bin/bash

# エラーで処理中断
set -ex

npm install
if [ "${RUN_WRANGLER_DEV}" = "1" ] ; then
  npm run start
else
  echo "[NOT RUN WRANGLER DEV]"
  tail -f /dev/null
fi

npm run start を行わず wrangler login などの操作を実施するケースも考慮して RUN_WRANGLER_DEV の環境変数でserver起動のON/OFFを切り替えれるようにしています。
早速docker compose up -d で起動させて http://localhost:8787 にアクセスし「Hello World」が表示されればOKです。

※ server起動させない場合は RUN_WRANGLER_DEV=0 docker compose up -d で起動させます。

1-2. 固定のSpotify URIを返す

ひとまず固定のSpotify URIを用意し、単純にURIを返すように修正してみます。
SpotifyのURIの取得方法はこちらを参考に「シェア」を選択している際にOptionボタンを押下するとURIをコピーのメニューが表示されます ↓

image6

次にsrc/index.tsResponse("Hello World!"); 部分を修正します。
Response("spotify:track:xxxxxxxxxxxxx");

起動している状態で以下をターミナルから実行し、Spotifyの指定した内容が開けばOKです。

open `curl http://localhost:8787`

2. Cloudflare Workers内でSpotify APIを使用してURIを取得する

2-1. Client IDと Client secretの取得

Spotify Clientを使用する際に必要な Client IDClient secret を以下手順で取得します。

  1. Spotifyのアカウントを使ってダッシュボードにログインします
  2. 「Create app」でアプリを作成します
    • 今回は以下の内容で作成しました
      image7
  3. appの「Settings」>「Basic Information」から Client IDClient secret を確認します

2-2.Environment variables

Environment variables · Cloudflare Workers docs

  • wranglerによる環境変数
  • Secret環境変数の場合
  • ダッシュボードから環境変数を設定

今回はSecretの環境変数として先程取得した Client IDClient secret を管理していきたいと思います。

プロジェクトルートの .dev.vars ファイルを作成し、 Client IDClient secret を設定します。

SPOTIFY_CLIENT_ID = "..."
SPOTIFY_CLIENT_SECRET = "..."

次に export interface Env 部分に↑の環境変数を定義します。

export interface Env {
  SPOTIFY_CLIENT_ID: string;
  SPOTIFY_CLIENT_SECRET: string;
}

これで、 env.SPOTIFY_CLIENT_ID のようにアクセスできるようになりました。

2-3.特定のワードで検索してトップのプレイリストを返してみる

src/index.ts を以下の様に修正します。

import { Buffer } from "buffer";

export interface Env {
  SPOTIFY_CLIENT_ID: string;
  SPOTIFY_CLIENT_SECRET: string;
}

const getAccessToken = async (clientID: string, clientSecret: string) => {
  try {
    const urlencoded = new URLSearchParams();
    urlencoded.append("grant_type", "client_credentials");

    const res = await fetch("https://accounts.spotify.com/api/token", {
      method: "POST",
      body: urlencoded,
      headers: {
        Authorization:
          "Basic " +
          Buffer.from(clientID + ":" + clientSecret).toString("base64"),
        "Content-Type": "application/x-www-form-urlencoded",
      },
    });
    const result = (await res.json()) as { access_token: string };
    return result["access_token"];
  } catch (e) {
    console.error(e);
  }
};

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    const token = await getAccessToken(
      env.SPOTIFY_CLIENT_ID,
      env.SPOTIFY_CLIENT_SECRET
    );

    try {
      const params = {
        q: "remaster",
        type: "playlist",
      };
      const query = new URLSearchParams(params);

      const result = await fetch(`https://api.spotify.com/v1/search?${query}`, {
        method: "GET",
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
      const json = (await result.json()) as any;
      const uri = json["playlists"]["items"][0]["uri"];
      return new Response(uri);
    } catch (e: any) {
      console.error(e);
      return new Response(e.stack, { status: 500 });
    }
  },
};

remaster というワードで検索した playlist の候補から最初のURIを返すような実装になっています。

以下コマンドを実行してremasterのプレイリストが開いていればOKです!

open `curl http://localhost:8787`

※ SpotifyのWeb API の詳細は以下
Web API | Spotify for Developers

3. Workers KVでプレイリストのURIを複数保存する

Workers KVに関して
サーバーレスストレージとアプリケーション | Cloudflare Workers KV | Cloudflare
Cloudflare Workersから利用できるKey-Value型のストレージサービスになります。

3-1. KV namespaceの作成

今回は KV_PLAYLISTS という名前でKV namespaceを作成します。wrangler CLIを使って作成していきます。

$ wrangler kv:namespace create "KV_PLAYLISTS"
⛅️ wrangler 3.0.0 (update available 3.0.1)
-----------------------------------------------------
🌀 Creating namespace with title "app-KV_PLAYLISTS"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
{ binding = "KV_PLAYLISTS", id = "......." }

--preview オプションをつけるとローカルから実行する際に参照されるnamespaceが作成されるのでそちらも作成しておきます。

wrangler kv:namespace create "KV_PLAYLISTS" --preview

wrangler kv:namespace list コマンドで2つのnamespaceが作成されていればOKです。

$ wrangler kv:namespace list
[
  {
    "id": "....",
    "title": "app-KV_PLAYLISTS",
    "supports_url_encoding": true
  },
  {
    "id": "....",
    "title": "app-KV_PLAYLISTS_preview",
    "supports_url_encoding": true
  }
]

上記で作成した2つのnamespaceのidを wrangler.toml に追記しときます。

kv_namespaces = [
   { binding = "KV_PLAYLISTS", id = "....", preview_id = "...." }
 ]

次に設定した KV namespace KV_PLAYLISTS が扱えるように export interface Env に以下を 追加します。

export interface Env {
  KV_PLAYLISTS: KVNamespace; // ← 追加
  SPOTIFY_CLIENT_ID: string;
  SPOTIFY_CLIENT_SECRET: string;
}

3-2. KV namespaceを使用する

試しにダッシュボード画面からPreview用のKV namespaceにKey Valueを追加してみます。

image8

次に Workers上で↑で設定した値をログに取得してみたいと思います。

const value = await env.KV_PLAYLISTS.get("test");
console.log(value);

実行して「Hello world!」が表示されればOKです。

3-3. ランダムな検索ワードを取得する

ダッシュボード画面でPreview用のKV namespaceから一度先程の「test」keyのものは削除して、Spotifyで検索するワードを以下の様な形で好きなワードを複数登録しておきます。

image9

次に以下を src/index.ts に追加し、ランダムな検索ワードでプレイリストを取得できるようにします。

// ランダムなnumber値を取得するutility
const getRandomInt = (max: number) => {
  return Math.floor(Math.random() * max);
};

// ...

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    // ↓追加
    const list = await env.KV_PLAYLISTS.list();
    const randamAt = getRandomInt(list.keys.length);
    const key = list.keys[randamAt].name;
    const queryWord = await env.KV_PLAYLISTS.get(key);
    if (!queryWord) {
      return new Response("Not found query word", { status: 404 });
    }

    const token = await getAccessToken(
      env.SPOTIFY_CLIENT_ID,
      env.SPOTIFY_CLIENT_SECRET
    );

    try {
      const params = {
        q: queryWord, // queryWord を使うように変更
        type: "playlist",
      };
      const query = new URLSearchParams(params);

      const result = await fetch(`https://api.spotify.com/v1/search?${query}`, {
        method: "GET",
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
      const json = (await result.json()) as any;
      // ↓ついでにヒットしたPlylistsからランダムなものを返すように変更
      const items = json["playlists"]["items"];
      const uri = items[getRandomInt(items.length)]["uri"];
      return new Response(uri);
    } catch (e: any) {
      console.error(e);
      return new Response(e.stack, { status: 500 });
    }
  },
};

早速試しに以下を実行し、ランダムにプレイリストが開いていればOKです。

open `curl http://localhost:8787`

4. デプロイ

早速デプロイしてみたいと思います。

yarn deploy

ダッシュボード上のWorkers Overviewで対象のWorkerがデプロイされていればOKです。

image10

次に Spotifyの Client IDClient secret の環境変数を設定してやります。

「Worker選択」>「Settings」>「Variables」の画面で「Environment Variables」を設定します。

image11

以下コマンドを実施し、ローカルと同様にランダムにPlaylistがSpotifyで開いていればOKです!

open `curl [デプロイされたURL]`

バッドノウハウ

Error: Adapter 'http' is not available in the build が発生する!

Spotify APIを使う際に axios を使っていた時に↑のエラーが発生しました。色々調べてみるとEdgeの環境だとaxiosがサポートされてない(?)ようなので Cloudflare Workersのドキュメントにもあるようにfetchを使うようにしました。

参考リンク

まとめ

実際の用途としてはあってないかもですが、Cloudflare Workersを今回ローカルで動かして実際にデプロイするまでを実施してみて、Wrangler CLIで色々設定やKVの作成、Preview環境など使いやすく、スムーズに進めたかなと思いました。
次はCloudflare D1やR2など試して見たいと思います。

この記事は以下の情報を参考にして執筆しました

Discussion