公開不可な Spotify プレイリストの内容をなんとかして公開した話

## はじめに
最近実装された Spotify の Blend 機能はご存知でしょうか？2人のユーザーの嗜好を加味してしてお互いに気に入りそうなプレイリストを自動で生成してくれるサービス、みたいな感じです。ひょんなところからそのプレイリストを公開しようとしたところ、Blend　で生成されたプレイリストはその2人のユーザーしか閲覧できないような仕様になっているようでした。本記事はそこをなんとかして公開できるようにしよう、ということでいろいろやった結果のメモです。ざっくりやったこととしては

1. 公開可能な空のプレイリストを別途作成
1. Blend で生成されたプレイリストの内容を取得し、前述の公開可能プレイリストに全部コピーするプログラムを実装
1. そのスクリプトを定期的に実行するように設定

という感じです。

### できたもの
以下のプレイリストが Blend で生成されたプレイリストの内容に追従して定期的に更新されています。よろしければ記事閲覧のお供にお聞きください。

https://open.spotify.com/playlist/1NRDPQ0B5IMhWpiilXPpCt?si=2122e9574ab8460e

-----

## 使用したツール
- Google Apps Script
    - 以前に使用したことがあり定時で実行とかを割とカジュアルにできるので使いやすいかなと思い選定しました。
- `clasp` + TypeScript
    - GAS のブラウザエディタでの開発はかなり厳しいのでローカルで、しかも TypeScript で開発ができる `clasp` を採用しました。TypeScript で書いたソースを `clasp push` するだけでいい感じに .gs ナイズしたコードに変換してくれます。
- [spotify\-types](https://www.npmjs.com/package/spotify-types)
    - せっかくTypeScriptで書くので投げるオブジェクトやレスポンスにも型推定させたい、ということでSpotifyのAPIで使える型ライブラリを探し、`spotify-types`を採用しました。なんか他にもいろいろありそうだけど違いはよく見てないです。[spotify types \- npm search](https://www.npmjs.com/search?q=spotify%20types)

## 書いたプログラム
全体ソースは以下を参照ください。
[SyncSpotifyPlaylist/index\.ts at master · Dance\-At\-Kamogawa/SyncSpotifyPlaylist](https://github.com/Dance-At-Kamogawa/SyncSpotifyPlaylist/blob/master/src/index.ts)

### ハマったポイント
#### `fetch` が使えない
これはGAS側の制約なんですがREST APIを叩くときに JavaScript ネイティブの `fetch` が使えないみたいです。（デプロイして走らせてからエラーが出て気がつきました）。API コールにはGAS上で使える `UrlFetchApp.fetch(url, option)` を使用するといいっぽいです。`fetch`と違って特にawaitとか書かずに同期処理的に書けるのでこれはこれで便利ですね。

```httpリクエスト部分.ts
/**
 * HTTP リクエストを実施
 *
 * @param fetchApiUrl URL
 * @returns レスポンスのJSON
 */
const requestHttp = (
  fetchApiUrl: string,
  options?: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions
) => {
  const commonOptions: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: "Bearer " + accessToken,
    },
  };

  const option = Object.assign({}, { ...commonOptions }, { ...options });

  const response = UrlFetchApp.fetch(fetchApiUrl, option);

  return JSON.parse(response.getContentText());
};
```

#### API のコールで body に生のオブジェクトを渡すとコケる
前述の `UrlFetchApp.fetch`  の `options` の `payload` にbody相当のオブジェクトを入れるのですがその場合生のobjectをそのまま入れるとパースの仕方が変になってBad Requestが帰ってきてしまいました。payloadに入れるときに object を `JSON.stringify()` してあげればOKでした。

```HTTPリクエスト処理にオブジェクトを渡す部分.ts
/**
 * プレイリストのトラックを更新
 * @param playlist プレイリスト
 */
const updateTargetPlaylist = (playlist: Paging<PlaylistTrack>) => {
  const updateApiUrl = API_BASE_URL + Env.TARGET_PLAYLIST_ID + "/tracks";

  const tracks = {
    uris: playlist.items.map((el) => {
      return el.track.uri;
    }),
  };

  const options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
    method: "post",
    payload: JSON.stringify(tracks),
  }; 

  requestHttp(updateApiUrl, options);
};
```



## Google Apps Script の定期実行設定
デプロイして動くことを確認したら、あとはそれを定期的に実行するように設定します。元のプレイリストが更新されるタイミングに応じて適度に設定してあげます。以下の画像はとりあえず一日一回更新される感じにしてますが、元のプレイリストの更新タイミングと微妙に噛み合ってないのでもう少し更新してもいい気がしてます。

![script.google.com_u_6_home_projects_1i2VmZE7-8UxmveNNGYQ03UGzRjn9qTLYr6QJr7zWItonABag_dYLjqlm_triggers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641384/49861560-9f86-5c7b-12e0-3da7cfc63a65.png)


## ソースコード
ソースは以下に公開しています。そんなに複雑なことはしていませんが参考になれば。必要なAPIキーやプレイリストの値等各種環境変数は`src/env.ts`の中の値を埋めてもらえれば使えるはずです。
[Dance\-At\-Kamogawa/SyncSpotifyPlaylist: A Google Apps Script to sync one Spotify playlist to another\.](https://github.com/Dance-At-Kamogawa/SyncSpotifyPlaylist)

---

## 参考にしたサイト
- [Spotify APIを使ってアプリケーションを作ろう \- Qiita](https://qiita.com/tasogarei/items/9d1b1a2304e0dac14aa8)
    - このサイトの手法を参考に RefreshToken を手作業で取得してそれを`env.ts`の中に持たせてます