【GAS】Spotifyの曲のリンクをLINEに送ったらプレイリストに追加されるようにしたい

友達からLINEでSpotifyの曲リンクが送られてくるけど、あとでまとめて聴きたい。

# 目次
[1. 構成](#1-構成)
[2. 参考にしたサイト](#2-参考にしたサイト)
[3. コード解説](#4-コード解説)
[4. ハマったところ](#5-ハマったところ)

# 1. 構成
使用するサービスは以下の3つを使用します。

- LINE bot（[LINE Developers](https://developers.line.biz/ja/)）
- Google Apps Script（[Googleスプレッドシート](https://www.google.com/intl/ja_jp/sheets/about/)）
- [Spotify Web API](https://developer.spotify.com/documentation/web-api/)

▼イメージ図
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1643466/7915ad3f-d6a1-ec89-39ac-926c13823dc8.png)
①友達がグループにメッセージ投稿
②LINE botが、友達のメッセージをGASに転送
③GASが、メッセージにSpotifyの曲のリンクが含まれているかチェック、含まれていればSpotify APIに送信するメッセージを作成
④GASがSpotify APIに対し、友達が送ったおすすめ曲をプレイリストに追加するよう指示するメッセージを送信

# 2. 参考にしたサイト
LINE botの作り方、メッセージの編集方法は以下。

- [Google Apps ScriptでLINE BOTつくったら30分で動かせた件](https://qiita.com/hakshu/items/55c2584cf82718f47464)

Spotify APIの始め方は以下。

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

Spotify APIでプレイリストに追加する指示を出す方法は以下。

- [Spotify APIを使用して毎月の「いいねリスト」を作ってみた](https://qiita.com/tuaaasa/items/ee218e2f42bef0e01998)

# 3. コード解説
「line2gas.gs」の記述から解説していきます。

まずはLINEからのメッセージを受け取る部分。
userMessageという変数にメッセージが格納されます。

```js:line_receive_response.gs
// ━━━━━━━━━━━━━━━
// メッセージ受信
// ━━━━━━━━━━━━━━━
function doPost(e) {
  var ACCESS_TOKEN = 'e5mz*******************************************FU=';
  var DEBUGID = 'Ud************fc';

  var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  var userMessage = JSON.parse(e.postData.contents).events[0].message.text;
  var fetchurl = 'https://api.line.me/v2/bot/message/reply';

  var spotifyUrl = 'open.spotify.com/track/';
  var sorenaPhrase = 'それな' //本題とは関係ありませんが、「それな」と入力すると、可愛い女の子の画像が送られる機能を入れています。

  if (userMessage.indexOf(spotifyUrl) != -1) {
    var index = userMessage.indexOf(spotifyUrl)
    var trackMusicId = userMessage.substr(index, 45)
    var musicId = trackMusicId.replace(spotifyUrl, "")
    var message = 'プレイリストに曲を追加しました。\nhttps://open.spotify.com/playlist/1oW******M7';
    operateSpotify(musicId);
    replyMessage(fetchurl, ACCESS_TOKEN, replyToken, message);

  } else if (userMessage.indexOf(sorenaPhrase) != -1) {
    let imageUrl = 'https://storage.googleapis.com/***********.jpg';
    replyPicture(fetchurl, ACCESS_TOKEN, DEBUGID, replyToken, imageUrl);
  }
}

// ━━━━━━━━━━━━━━━
// メッセージ返信
// ━━━━━━━━━━━━━━━
function replyMessage(fetchurl, ACCESS_TOKEN, replyToken, message) {
  UrlFetchApp.fetch(fetchurl, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [{
      'type': 'text',
      'text': message,
      }],
    }),
  });
  return ContentService.
    createTextOutput(JSON.stringify({'content': 'post ok'})).
      setMimeType(ContentService.MimeType.JSON);
}

function replyPicture(fetchurl, ACCESS_TOKEN, DEBUGID, replyToken, imageUrl) {
  UrlFetchApp.fetch(fetchurl, {
    'headers': {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'to': DEBUGID,
      'replyToken': replyToken,
      'messages': [{
        'type': 'image',
        'originalContentUrl': imageUrl, 
        'previewImageUrl': imageUrl
      }],
      'notificationDesabled': false
    }),
  });
}
```

```js:operateSpotify.gs
// ━━━━━━━━━━━━━━━
// Spotify操作メイン
// ━━━━━━━━━━━━━━━
function operateSpotify(musicId) {
  var token = getToken()
  var uris = "spotify:track:" + musicId
  if(uris.length >= 1){
    var playlistId = "1oW*********ZM7"
    addNewSongs(playlistId, uris, token)
  }
}

// ━━━━━━━━━━━━━━━
// Spotifyトークン取得
// ━━━━━━━━━━━━━━━
function getToken() {
  var refresh_token = "AQD7******************************BDk"
  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
}

// ━━━━━━━━━━━━━━━
// Spotifyプレイリストに追加
// ━━━━━━━━━━━━━━━
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': data
  }
  UrlFetchApp.fetch(endpoint, options)
}
```


# 4. ハマったところ
参考にしたサイトに細かい説明がなく、ハマったところを解説します。

「[Spotify Developer Platform: Spotify APIアクセスしてデータ取得してみてみた](https://qiita.com/shirok/items/ba5c45511498b75aac27)」の作業過程で、app.jsファイルに認証情報を記載する部分があるのですが、参考ページの記載に加えてscope部分の設定追加をする必要があります。

追加が必要なscopeは、「[Spotify APIを使用して毎月の「いいねリスト」を作ってみた](https://qiita.com/tuaaasa/items/ee218e2f42bef0e01998)」の【準備】の章で紹介されています。

実際の記述は以下のようになります。

```js:app.js
  // your application requests authorization
  var scope = 'user-read-private user-read-email user-library-read playlist-modify-public playlist-modify-private';
```

Spotify APIに対して、曲情報を「読み取る」操作だけであればあまり気にしなくていいのですが、今回のようにプレイリストに曲追加するといった「書き込み」の動作の場合に注意が必要です。