Spotify APIを使用して毎月の「いいねリスト」を作ってみた

# はじめに
元々**Apple Music**に加入していましたが、あとで聴きたい曲や刺さった曲に「いいね」できる機能はあっても、
「いいね」した曲を蓄積するプレイリストを作ってくれず、毎回自分で作成したプレイリストに入れていました。
（スマートプレイリストという機能を使えばできるそうです...当時は知りませんでした。 [apple musicのラブ済みの曲を集めるには？](https://discussionsjapan.apple.com/thread/110207764)）

それもあり「いいね」を自動で蓄積してくれる機能を持つ**Spotify**に乗り換えました。

たまに学生時代ヘビーローテーションしていた曲を聴きかえして、当時の思い出を鮮明に思い出したくなるんです。
「あの時こんな曲聴いていたなぁ〜」とか「あの時辛かったなぁ〜」とか。

そこで**Spotify**が提供する[Web API](https://developer.spotify.com/documentation/web-api/)を使用して***毎月「いいね」したリストを作るくん***を作成してみたので紹介します。

導入や初期設定等は公式がご丁寧に用意されているのでそちらを。
日本語で要点だけみたい方は以下記事が参考になるかと思います。

[Spotify Developer Platform: Spotify APIアクセスしてデータ取得してみてみた](https://qiita.com/shirok/items/ba5c45511498b75aac27)

# 仕組み
- 【準備】[Quick Start](https://developer.spotify.com/documentation/web-api/quick-start/)を使ってAppの作成と認証トークンの発行
- GASを使って以下処理（毎時実行）
  1. あらかじめ生成しておいたリフレッシュトークンを使用してアクセストークンを発行
  2. 1時間以内にいいねされた曲リストを取得
  3. 取得した曲リストを今月のプレイリストに追加（今月のプレイリストがなければ作成して追加）
- GASの設定


## 【準備】Quick Start使ってAppの作成と認証トークンの発行

Web APIにリクエストを投げるに当たり、認可されたアクセストークンが必要です。
公式の[Quick Start](https://developer.spotify.com/documentation/web-api/quick-start/)に従い、Appの作成とトークンの作成をおこないます。
App作成したら`Client ID`と`Client Secret`をメモしておきましょう。
(handle-liked-musicはApp名です。好きな名前でOKっす。)

![スクリーンショット 2020-12-26 17.22.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376573/01034d72-b52e-12ba-2c87-4edf6e7da411.png)



トークンに以下スコープを許可しましょう。

- `user-library-read` いいねした曲の取得に必要
- `playlist-modify-public` 公開プレイリスト操作（作成や更新）に必要
- `playlist-modify-private` 非公開プレイリスト操作（作成や更新）に必要
- `playlist-read-private` 追加対象のプレイリスト取得に必要

うまくいけばこんなページが表示されるので`Refresh token`をメモしておきましょう。

![スクリーンショット 2020-12-26 17.12.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376573/3cd7ae04-0518-5e0e-af9d-613479fe70a4.png)


## GASを使って以下処理（毎時実行）

全体のコードです。
サクッと作ったためミュータブルなコードですがご了承ください。

```javascript

var refresh_token = "{メモったRefresh token}"
var username = "{Idに記される名前（私の場合tuaaasa)}"

// アクセストークンの発行
function getToken() {
  var endpoint = 'https://accounts.spotify.com/api/token'
  
  var encode = Utilities.base64Encode("{メモったClient ID}:{メモったClient Secret}", Utilities.Charset.UTF_8)
  var options = {
    'method': 'post',
    'contentType': 'application/x-www-form-urlencoded',
    'headers': {
      'Authorization': 'Basic ' + encode
    },
    'payload': {
      'grant_type': 'refresh_token',
      'refresh_token': refresh_token
    },
    'muteHttpExceptions': true
  }

  var response = UrlFetchApp.fetch(endpoint, options)
  var content = response.getContentText("UTF-8")
  return JSON.parse(content).access_token
}

// 今月のプレイリストを作成
function makePlaylist(yyyymm, token) {
  var endpoint = 'https://api.spotify.com/v1/users/' + username + '/playlists';

  var data = {
    'name': yyyymm,
    'description': 'liked list in ' + yyyymm,
    'public': false
  }
  var options = {
    'method': 'post',
    'contentType': 'application/json',
    'headers': {
      'Authorization': 'Bearer ' + token
    },
    'payload': JSON.stringify(data)
  }

  var response = UrlFetchApp.fetch(endpoint, options)
  return JSON.parse(response).id
}

// 今月のプレイリストを取得
function getPlaylistThisMonth(token) {
  var endpoint = 'https://api.spotify.com/v1/users/'+ username +'/playlists?offset=0&limit=20';
  var options = {
    'method': 'get',
    'headers': {
      'Authorization': 'Bearer ' + token
    }
  }

  var response = UrlFetchApp.fetch(endpoint, options)

  var date = new Date()
  var yyyymm = String(date.getFullYear()) + String(date.getMonth()+ 1)

  var targetPlaylist = JSON.parse(response).items.filter(item => item.name === yyyymm)

  if(targetPlaylist[0]){
    return targetPlaylist[0].id
  }else{
    return makePlaylist(yyyymm, token)
  }
}

// 直近1時間以内にいいねした曲を取得
function getNewSongs(token) {
  var endpoint = 'https://api.spotify.com/v1/me/tracks?offset=0&limit=20';
  var options = {
    'method': 'get',
    'headers': {
      'Authorization': 'Bearer ' + token
    }
  }

  var response = UrlFetchApp.fetch(endpoint, options)

  var now = new Date()
  now.setMinutes(now.getHours() - 1)

  var targetItems = JSON.parse(response).items.filter(
    item => {
      var itemDate = new Date(item.added_at)
      return itemDate > now
    })

  return targetItems.map(item => item.track.uri)
}

// 曲をプレイリストに追加
function addNewSongs(playlist_id, uris, token) {
  var endpoint = 'https://api.spotify.com/v1/playlists/' + playlist_id + '/tracks';
  
  var data = {
    'uris': uris,
    'position': 0
  }
  var options = {
    'method': 'post',
    'contentType': 'application/json',
    'headers': {
      'Authorization': 'Bearer ' + token
    },
    'payload': JSON.stringify(data)
  }

  var response = UrlFetchApp.fetch(endpoint, options)
}

// メイン関数
function main() {
  var token = getToken()
  var uris = getNewSongs(token)
  if(uris.length >= 1){
    var playlistId = getPlaylistThisMonth(token)
    addNewSongs(playlistId, uris, token)
  }
}
```

### あらかじめ生成しておいたリフレッシュトークンを使用してアクセストークンを発行
本処理のコードです。

```javascript
// アクセストークンの発行
function getToken() {
  var endpoint = 'https://accounts.spotify.com/api/token'
  
  var encode = Utilities.base64Encode("{メモったClient ID}:{メモったClient Secret}", Utilities.Charset.UTF_8)
  var options = {
    'method': 'post',
    'contentType': 'application/x-www-form-urlencoded',
    'headers': {
      'Authorization': 'Basic ' + encode
    },
    'payload': {
      'grant_type': 'refresh_token',
      'refresh_token': refresh_token
    },
    'muteHttpExceptions': true
  }

  var response = UrlFetchApp.fetch(endpoint, options)
  var content = response.getContentText("UTF-8")
  return JSON.parse(content).access_token
}
```

Basic認証のため、`Client ID`と`Client Secret`をエンコードします。

```javascript
  var encode = Utilities.base64Encode("{メモったClient ID}:{メモったClient Secret}", Utilities.Charset.UTF_8)
```

### 1時間以内にいいねされた曲リストを取得
本処理のコードです。

```javascript
// 直近1時間以内にいいねした曲を取得
function getNewSongs(token) {
  var endpoint = 'https://api.spotify.com/v1/me/tracks?offset=0&limit=20';
  var options = {
    'method': 'get',
    'headers': {
      'Authorization': 'Bearer ' + token
    }
  }

  var response = UrlFetchApp.fetch(endpoint, options)

  var now = new Date()
  now.setMinutes(now.getHours() - 1)

  var targetItems = JSON.parse(response).items.filter(
    item => {
      var itemDate = new Date(item.added_at)
      return itemDate > now
    })

  return targetItems.map(item => item.track.uri)
}
```

GASで毎時実行するので、1時間以内を検索対象に指定します。

```javascript
  var now = new Date()
  now.setHours(now.getHours() - 1)
```

フィルタ部です。
曲をプレイリストに追加する際`uri`で曲を指定するため、`uri`の配列を返します。

```javascript
var targetItems = JSON.parse(response).items.filter(
    item => {
      var itemDate = new Date(item.added_at)
      return itemDate > now
    })

  return targetItems.map(item => item.track.uri)
```

### 取得した曲リストを今月のプレイリストに追加（今月のプレイリストがなければ作成して追加）
本処理のコードです。

```javascript
// 曲をプレイリストに追加
function addNewSongs(playlist_id, uris, token) {
  var endpoint = 'https://api.spotify.com/v1/playlists/' + playlist_id + '/tracks';

  var data = {
    'uris': uris,
    'position': 0
  }
  var options = {
    'method': 'post',
    'contentType': 'application/json',
    'headers': {
      'Authorization': 'Bearer ' + token
    },
    'payload': JSON.stringify(data)
  }

  var response = UrlFetchApp.fetch(endpoint, options)
}
```

ここで曲を指定しています。
`position`はプレイリスト中に曲を挿入する位置です。
`0`は先頭を指定しています。

```javascript
var data = {
    'uris': uris,
    'position': 0
  }
```

## GASの設定
[Apps Script](https://script.google.com/home)で新規プロジェクトを作成。コーディング。
実行許可を行うため一度動作確認しましょう。

`トリガー`タブ > `トリガーを追加`から以下設定。
*時間の間隔を選択（時間）* 欄を`１時間おき`にしましょう。

![スクリーンショット 2020-12-26 17.54.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376573/783edac7-070f-ca90-6566-738ac8610bd8.png)

# 結果

上手にできました！

![スクリーンショット 2020-12-26 17.56.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376573/087903e6-510f-f87e-7022-92152e73f7c9.png)

# さいごに

これで思い出すのが楽になりました。
Spotify以外のサービスで曲を聴くこともあるので、できたら他のサービスでもやってみたいです。
（SoundCloudもAPIあるっぽい、作るぞ...!!!）

最後まで読んでいただきありがとうございました〜。良い音楽ライフを〜♪