Spotifyで聴いた曲を全部プレイリストに保存する

# はじめに
Spotifyの履歴は50曲しか保存されないし、デスクトップアプリからしか確認できません。プレイリストには1万曲まで追加できるようなので、再生した曲を全部プレイリストに自動追加してくれるスクリプトを作ってHerokuで動かしてみました。
Spotipy

https://spotipy.readthedocs.io/

でSpotify APIを叩きます。

# 今聴いている曲を取得

デベロッパーページ
https://developer.spotify.com/dashboard/applications
にアクセスしてCREATE AN APPからAPPを作成し、クライアントIDとクライアントシークレットを入手します。作ったAPPのEDIT SETTINGSから、Redirect URIsに```https://example.com/callback/``` を登録しておきます。

以下のコードの```scope```にはbotの権限を指定します。今回は現在再生している曲を読み出したいので```user-read-currently-playing```と```playlist-modify-public```を指定します。非公開プレイリストに曲を追加していきたい場合は代わりに```playlist-modify-private```を指定します。


```python

import spotipy
from spotipy.oauth2 import SpotifyOAuth
import os

# 環境変数を設定する。代わりにbashrcファイルなどで設定してもOK
os.environ["SPOTIPY_CLIENT_ID"] = "クライアントID"
os.environ["SPOTIPY_CLIENT_SECRET"] = "クライアントシークレット"
os.environ["SPOTIPY_REDIRECT_URI"] = "https://example.com/callback/"

scope = "user-read-currently-playing playlist-modify-public"
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))

current_playing = sp.current_user_playing_track()
track_name = current_playing['item']['name']
track_id = current_playing["item"]["id"]
artist_name = current_playing['item']['artists'][0]['name']
print(track_name + "/" + artist_name)
```

このコードを走らせると、Spotifyの認証ページが開くので同意ボタンを押します。"Example Domain"と書かれたページにリダイレクトされるので、そのページのURLをコピーし、

```
Enter the URL you were redirected to:
```
と表示しているコンソールにペーストします。今再生している曲名/アーティスト名が表示されれば成功です。

# プレイリストに追加

曲を追加したいプレイリストのIDを調べます。プレイリストのシェア用URLは```https://open.spotify.com/playlist/37i9dQZF1DX7HOk71GPfSw```のようになっていますが、最後の```37i9dQZF1DX7HOk71GPfSw```の部分がプレイリストのIDです。

```python
playlist_id = "プレイリストID"
sp.playlist_add_items(playlist_id, [track_id], position=0)
```
で```track_id```の曲をプレイリストの先頭に追加することができます。

# 聴いた曲をプレイリストに追加
スクリプトをwhile Trueでずっと動かし続けるか、cronなどを使って定期的に動かすのかの2通りの方針があります。ローカル環境で実行するなら前者でも大丈夫ですが、Herokuなどで動かす場合は、24時間に一度スクリプトが停止するので後者のほうが良いでしょう。

## 方法1(While True)
30秒ごとに現在再生している曲を調べて、前回と異なる場合はプレイリストに追加します。

```python
import spotipy
from spotipy.oauth2 import SpotifyOAuth
import time
import os

os.environ["SPOTIPY_CLIENT_ID"] = "クライアントID"
os.environ["SPOTIPY_CLIENT_SECRET"] = "クライアントシークレット"
os.environ["SPOTIPY_REDIRECT_URI"] = "https://example.com/callback/"
playlist_id = "プレイリストID"
scope = "user-read-currently-playing playlist-modify-public"

sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
last_track_id = ""
while True:
    try:
        current_playing = sp.current_user_playing_track()
        if current_playing is not None:
            track_name = current_playing['item']['name']
            track_id = current_playing["item"]["id"]
            artist_name = current_playing['item']['artists'][0]['name']
            if last_track_id != track_id:
                last_track_id = track_id
                print(track_name + "/" + artist_name)
                sp.playlist_add_items(playlist_id, [track_id], position=0)
        else:
            print("no song is playing...")
    except Exception as e:
        print(e)
    time.sleep(30)
```

## 方法2(定期的に実行)

直近1時間の再生履歴を調べて、プレイリストに追加します。このスクリプトは1時間ごとに定期実行する必要があります。

```python
import spotipy
from spotipy.oauth2 import SpotifyOAuth
import time
import os

os.environ["SPOTIPY_CLIENT_ID"] = "クライアントID"
os.environ["SPOTIPY_CLIENT_SECRET"] = "クライアントシークレット"
os.environ["SPOTIPY_REDIRECT_URI"] = "https://example.com/callback/"
playlist_id = "プレイリストID"
scope = "user-read-currently-playing playlist-modify-public"

sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
now_in_ms = int(time.time()*1000) # ミリ秒単位で現在のUINX時刻を取得
one_hour_in_ms = 60*60*1000 #1時間をミリ秒単位で表現

recently_played = sp.current_user_recently_played(after=now_in_ms-one_hour_in_ms)
if recently_played is not None:
	items = recently_played["items"]
	track_ids = [item["track"]["id"] for item in items]
	sp.playlist_add_items(playlist_id, track_ids, position=0)
	print("added:", track_ids)
```

# Herokuで動かす
ローカル環境で実行してもいいですが、24時間動かすならサーバーから実行したほうが便利なので、Herokuの無料枠で動かしてみます。Herokuへのデプロイについては

https://qiita.com/seigo-pon/items/ca9951dac0b7fa29cce0#%E9%96%8B%E7%99%BA%E3%81%97%E3%81%9F%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%92%E3%83%97%E3%83%83%E3%82%B7%E3%83%A5

を参考にしました。```requirement.txt```は

```bash
pip3 freeze | grep spotipy > requirements.txt
```
で作成します。また、

```
python3 -V
```
で自分のPythonのバージョンを確認すると```3.6.9```だったので、Herokuが対応しているPythonのバージョンを

https://devcenter.heroku.com/articles/python-support#supported-runtimes

で調べ、同系列の```3.6.13```を```runtime.txt```に指定しました。

```runtime.txt
python-3.6.13
```

## 認証の問題
スクリプトを実行してみると、コンソールに

```
Enter the URL you were redirected to:
```
が表示され、認証が必要なようです。しかしながら、Herokuからブラウザを開くことができないので、URLを手に入れることができず、困ってしまいました。

調べてみると、GithubのIssue

https://github.com/plamere/spotipy/issues/61

に解決策がありました。

まずローカルの環境で一度スクリプトを実行し、認証を済ませておきます。すると

```
{"access_token": "xxxxxxxx", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "xxxxxxxx", "scope": "playlist-modify-public user-read-recently-played", "expires_at": 1615812124}
```

のような中身のキャシュファイルが```.cashe```にできています。中身を適当なファイルにコピペし、Herokuにpushしておきます。

そして、スクリプトの

```python 
sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
```
を

```python 
cache_handler = spotipy.cache_handler.CacheFileHandler(cache_path="キャッシュファイルへのパス")
auth_manager = SpotifyOAuth(scope=scope, cache_handler=cache_handler,show_dialog=True)
sp = spotipy.Spotify(auth_manager=auth_manager)
```
に書き換えると、認証しなくても動くようになりました。

## Heroku Schedulerのズレ問題
「方法2」のスクリプトをHeroku Schedulerで1時間ごとに実行してみました。しかし、Heroku Schedulerの実行時間に1~2分のズレがあり、その間の履歴を取り逃してしまうという問題が生じました。そこで、毎時10分にスケジュールしておき、例えばx時11分に実行されたならx-1時0分0秒からx時0分0秒までの履歴を取得することで解決しました。
以下のスクリプトを毎時10分に自動実行しています。

```python
# 毎時10分に実行
import spotipy
from spotipy.oauth2 import SpotifyOAuth
import time, os
from datetime import datetime

os.environ["SPOTIPY_CLIENT_ID"] = "クライアントID"
os.environ["SPOTIPY_CLIENT_SECRET"] = "クライアントシークレット"
os.environ["SPOTIPY_REDIRECT_URI"] = "https://example.com/callback/"

playlist_id = "プレイリストID"

scope = "user-read-recently-played playlist-modify-public"
cache_handler = spotipy.cache_handler.CacheFileHandler(cache_path="cash_data.txt")
auth_manager = SpotifyOAuth(scope=scope, cache_handler=cache_handler,show_dialog=True)
sp = spotipy.Spotify(auth_manager=auth_manager)

now = datetime.now() # x時10分
oclock = datetime(now.year, now.month, now.day, now.hour,0) # x時0分
one_hour_in_ms = 60*60*1000 #1時間(ミリ秒単位)
before = int(oclock.timestamp()*1000) # x時0分のunix時(ミリ秒単位)
after = before - one_hour_in_ms # x-1時0分のunix時(ミリ秒単位)

# beforeとafterは同時に指定できない(泣)
recently_played = sp.current_user_recently_played(after=after)

# 日時の文字列をミリ秒単位のunix時に変換する関数
def parser(strtime):return int(datetime.strptime(strtime, \
    "%Y-%m-%dT%H:%M:%S.%fZ").timestamp()*1000)
if recently_played is not None:
    items = recently_played["items"]
    track_ids = [item["track"]["id"] for item in items \
    if parser(item["played_at"]) < before]
    sp.playlist_add_items(playlist_id, track_ids, position=0)
```

#参考にした記事

https://qiita.com/nixo/items/d1602d0817a9633831fd

https://qiita.com/seigo-pon/items/ca9951dac0b7fa29cce0#%E9%96%8B%E7%99%BA%E3%81%97%E3%81%9F%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%92%E3%83%97%E3%83%83%E3%82%B7%E3%83%A5