Slackに投稿したアーティストの最新アルバムをSpotifyのプレイリストへ自動登録する

普段、TV・ラジオ・CDショップ・音楽雑誌・店頭BGMなどで発売されたばかりの良い音楽に出会った際に、メモに残しておいて後からSpotifyで検索して聞いています。しかし、メモに残したアーティスト名を１つ１つ検索していくのは面倒だなあと感じていました。

ということで、Slackにアーティスト名を投稿するだけで、入力したアーティストの最新アルバムがSpotifyのプレイリストに追加されるようにします。

# 前提
* 実行環境にAmazon Web ServiceのLambdaを利用します。
* Pythonのバージョンは3.6を利用しています。

# 1. 準備
## Amazon Web Service(AWS)
SlackからAPI Gatewayに対してPOSTしたアーティスト名を、Lambdaで処理する構成とします。勉強がてら、取得した最新のtokenをDynamoDBに保管する構成としています。
<img width="752" alt="構成概要.png" src="https://qiita-image-store.s3.amazonaws.com/0/79258/b7a79c1a-c569-e651-5ee3-2ee438a191c6.png">


以下を参考としました。LambdaやAPI Gatewayの設定方法は、本稿では割愛します。
* [【AWS】Lambdaでpipしたいと思ったときにすべきこと](https://qiita.com/Hironsan/items/0eb5578f3321c72637b4)
* [SlackからAWS API Gatewayを通してLambdaを起動するまで](https://qiita.com/G-awa/items/fc803c285cb560dab796)
* [Python で DynamoDBを使う](https://qiita.com/inouet/items/64535fb89a2c46edf367)

## Spotify
* https://developer.spotify.com/my-applications でCREATE AN APPを行い、`CLIENT_ID`と`CLIENT_PW`を取得します。
* 楽曲を追加するプレイリストの`PLAYLIST_ID`を取得します。プレイリストURLの`{playlist_id}`の部分です。
    * `https://open.spotify.com/user/{user_id}/playlist/{playlist_id}`
* `pip install spotipy` でPython用のSpotifyライブラリを取得しておきます。

## Slack
* 利用するチャンネルに対して、Outgoing Webhooksを適用します。URL(s)にはPOSTを受け取ることができるよう設定したAPI GatewayのAPIエンドポイントを入力します。
* アーティスト名を検索して見つからなかった際に、チャンネルに「見つかりませんでした。」のメッセージを返すためのAppを作っておき、Webhook URLを取得します。
* `pip install slackweb` でPython用のSlackライブラリを取得しておきます。

# 2. token取得とDB格納
Spotifyのプレイリスト操作を行うために、`access_token`を取得する必要があります。Spotifyの`access_token`は1時間でリセットされてしまうため、Slackでアーティスト名を投稿をする度に、新しい`access_token`を取得するような設計にします。

## tokenの取得
初めてのtoken取得時には、確認画面が立ち上がり確認を求められるため、最初だけローカル環境でOAuth認証を通します。以後はtoken再発行用の文字列である`refresh_token`が与えられるため、こちらを利用して`access_token`を再取得します。

先ずは、以下をローカル環境にて実行します。



```python:gettoken.py
import spotipy.util as util


# SpotifyTokenに必要となるパラメータ情報を全て格納します
class SpotifyToken(object):
    def __init__(self):
        self.username = 'username'　　# Spotifyに登録しているユーザーネーム
        self.client_id = 'CLIENT_ID'
        self.client_secret = 'CLIENT_PW'
        self.redirect_url = 'http://localhost:8080/'
        self.scope = 'playlist-modify-public'
        self.playlist_id = 'PLAYLIST_ID'
        self.slack_channel_url = "https://hooks.slack.com/services/XXX~" # Slackに「見つかりませんでした」を返すためのWebhook　URL
        

user = SpotifyToken()
token = util.prompt_for_user_token(username=user.username, scope=user.scope,
                                   client_id=user.client_id, client_secret=user.client_secret,
                                   redirect_uri=uesr.redirect_uri)
```


上記を実施後、確認画面が立ち上がるので、OKをクリックしてください。

## tokenをDynamoDBに格納

その後、tokenの内容を確認します。

```json:token.json
{
  "access_token": "XXXXXX",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": null,
  "expires_at": XXXXXX,
  "refresh_token": "XXXXXXX"
}
```

tokenの再取得に必要となるのは、`refresh_token`だけなので、DBを利用する必要はないのですが、今回は勉強のためDynamoDBにtokenを格納してみます。

プライマリパーテションキーを`token`と定め、`"token":'key'`を要素の１つとして加えて、jsonファイル内のデータを１つずつ１行でAWSコンソールから格納していきます。

<img width="1062" alt="DynamoDB.png" src="https://qiita-image-store.s3.amazonaws.com/0/79258/dd055ade-e426-8759-c713-6ba395546fa8.png">

次に、DynamoDBから`refresh_token`を読み込み、新しい`access_token`を取得するコードを記述します。また、DynamoDBに格納されている内容も、新しいtokenの内容に書き換えます。

```python:addtracks.py(1)
import boto3
from spotipy.oauth2 import SpotifyOAuth


# AWSコンソールから作成したテーブルを指定します。
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('your_table_name')


class SpotifyToken(object):
    # --- getToken.pyと同内容のため省略 ---


def lambda_handler(event, context):
    user = SpotifyToken()
    oauth = SpotifyOAuth(client_id=user.client_id, client_secret=user.client_secret,
                        redirect_uri=None)
    access_token = get_new_access_token(oauth)


# 新しいtokenを取得する関数
def get_new_access_token(oauth):
    refresh_token = load_dynamodb()
    new_token = oauth.refresh_access_token(refresh_token)
    update_dynamodb(new_token)
    access_token = new_token['access_token']
    return access_token


# DynamoDBからrefresh_token読み込む関数
def load_dynamodb():
    response = table.scan()
    refresh_token = response['Items'][0]['refresh_token']
    return refresh_token


# DynamoDBの内容を更新する関数
def update_dynamodb(new_token):
    table.put_item(
        Item={
            'token':'key',
            'access_token': new_token['access_token'],
            'token_type': new_token['token_type'],
            'expires_in': new_token['expires_in'],
            'scope': new_token['scope'],
            'expires_at': new_token['expires_at'],
            'refresh_token': new_token['refresh_token']
        })
```

# 3. プレイリスト操作
ここからは、本題のプレイリストへ楽曲を登録する操作の部分です。

Slackから投稿されたアーティスト名は、API Gatewayを経由して、lambda_handler関数の引数である`event`に渡されます。

`event['text']`に格納されたアーティスト名をSpotifyで検索を行い、アーティストの最新作を取得して、プレイリストへ加えます。

また、該当のアーティストが見つからない場合は、Slackへ「見つかりませんでした」と投稿します。

```python:addtracks.py(2)
import spotipy
import slackweb


def lambda_handler(event, context):
    # --- (1)の内容は省略 ---
    # (1)で取得したaccess_tokenからSporifyオブジェクトを生成
    sp = spotipy.Spotify(auth=access_token)
    sp.trace = False
    # API Gatewayから渡されたアーティスト名を取り出す
    search_word = event['text']
    add_tracks(sp, user.username, user.playlist_id, user.slack_channel_url, search_word)


# アーティストの最新のアルバムをプレイリストに投稿する関数
def add_tracks(sp, username, playlist_id, slack_channel_url, search_word):
    # アーティスト名で検索を実施
    try:
        result = sp.search(q='artist:' + search_word, type='artist')
        # アーティストIDの取得
        artist_id = result['artists']['items'][0]['id']
        # アーティストの最新のアルバムを取得
        albums = sp.artist_albums(artist_id)
        most_latest_album = albums['items'][0]['id']
        # 最新アルバムに収録されている全曲のURIを取得
        album_tracks = sp.album_tracks(most_latest_album)
        track_uris =[]
        for i in range(len(album_tracks['items'])):
            track_id = album_tracks['items'][i]['uri']
            track_uris.append(track_id)
        # プレイリストへ楽曲を登録
        sp.user_playlist_add_tracks(username, playlist_id, track_uris)
    # アーティスト名が見つからない場合、Slackへ通知
    except:
        post_to_slack(slack_channel_url)


# Slackに「見つかりませんでした」を投稿する関数
def post_to_slack(slack_channel_url):
    slack = slackweb.Slack(url=slack_channel_uri)
    slack.notify(text="見つかりませんでした")
```

# 4. 完成
出来上がったコードを利用するライブラリと一緒に、zip形式でLambdaにアップロードしたら完成です。

Slackからアーティスト名を投稿すると、
<img width="516" alt="slack1.png" src="https://qiita-image-store.s3.amazonaws.com/0/79258/b2b9d1a1-158d-4d62-c59b-1e4f26202053.png">

プレイリストに反映されています。
<img width="808" alt="spotifyplaylist.png" src="https://qiita-image-store.s3.amazonaws.com/0/79258/e65bbf2c-6a57-d8da-ed9e-2e7332ce6b94.png">

アーティスト名が見つからない場合、「見つかりませんでした」と帰ってきます。
<img width="513" alt="slack2.png" src="https://qiita-image-store.s3.amazonaws.com/0/79258/0c5557e0-7253-21c9-0bbe-7694f123184a.png">

これで、お出掛け中に街で出会った新しい音楽をSlackから投稿しておくだけで、帰りの電車の中で既に出来あがったプレイリストを楽しむことができます。