サーバーレス環境で構築する最新人気楽曲速報【spotipy】

この記事は、<a href='https://qiita.com/advent-calendar/2022/craftegg'>Craft Eggアドベントカレンダー2022</a>、9日目の記事です。
# ■はじめに
Spotifyのレコメンド機能は優秀だなと感じているデータアナリストの落合（@takumining）です。
今回は、Spotifyがレコメンドで使用しているデータを調べつつ人気が急上昇している楽曲をSlackに通知する仕組みを作ってみました。取得できるデータがとても面白いので、今後活用の余地も含めて備忘録として残したいと思います。

# ■今回の内容
1) SpotifyAPIで毎日更新されているSpotify公式プレイリストを取得する。
2) 取得したデータをSpreadsheetに書き残す。
3) GASでスプレッドシートのデータを見て急上昇している楽曲があればSlackに通知する。

※Lambdaだけで完結できる内容ですが、中間に非エンジニアでもデータ操作をしやすい目的でスプレッドシートをデータベースのように活用します。
（完成イメージ）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989377/2ac5bfd0-e5a3-10b4-23c8-56ca63126774.jpeg" width=50%>

# ■使用する技術・サービス
- Python
- JavaScript
- AWS Lambada
- Google App Script（GAS）
- Spotify API
- Slack API
- Spreadsheet

# ■主に利用するライブラリ
SpotifyDevloperサイトにある<a href= 'https://developer.spotify.com/documentation/web-api/libraries/#libraries'>Web API Libraries</a>にてさまざまな言語で利用できるライブラリが紹介されています。その中でPythonで記述できる<a href= 'https://developer.spotify.com/documentation/web-api/libraries/#libraries'>spotipy</a>を使用してます。

# ■前提
LambdaでSpotipyのライブラリやスプレッドシートへの書き出しに利用するライブラリは取り込み前提、GCPの認証情報は、AWSパラメータストア等で管理されている前提の記述になります。また、自動化に関する詳細はCloudWatchのトリガーやGASのトリガーを利用しているため説明を省いてます。

# ■準備
Spotify for Devloperにアプリして、client_id、client_secretの情報を取得する。

<a href= 'https://developer.spotify.com/dashboard/'>Spotify for Devlopers</a>

#### ・Devloperアカウントを作成
ページ下部の「Sign up for a free Spotify account here. 」より新規登録。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989377/49f67928-4664-0548-d37d-f471d13761dd.png" width=95%>

#### ・ダッシュボードでアプリを作成
「CREATE AN APP」から適当な名前を付けて作成。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989377/cd02d5ce-92ff-7c19-3b58-b663c4d51b9a.png" width=95%>

#### ・client_id、client_secretを取得する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989377/84b395f6-e069-4129-efd7-cfe8ac83f7c0.png" width=95%>
「SHOW CLIENT SECRET」押下でclient_secretも確認できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989377/99c826ff-1a88-85e3-3543-82d245d02c60.png" width=95%>

これでSpotify APIを利用する準備は完了。

Slackへの通知にはIncoming Webhookを利用。利用するためには<a href='https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8'>こちら</a>のサイトの手順でSlackAppを作成してIncoming Webhook URLを取得する必要があります。

# ■最新の人気楽曲データを取得してスプレッドシートに書き出す
今回の例ではSpotifyのプレイリスト<a href= 'https://open.spotify.com/playlist/37i9dQZEVXbKXQ4mDTEBXq'>Top50-日本</a>を取得します。
Lambda実行想定になります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989377/aea94cea-00cd-c67d-5812-9f6ae0f8f930.png" width=95%>

```python:Python
import logging, json, datetime, json, time
import SpreadSheet #ライブラリ追加済み想定
import aws_access #ライブラリ追加済み想定
import spotipy #ライブラリ追加済み想定
from spotipy.oauth2 import SpotifyClientCredentials

#Log出力用設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

#Spotipy認証
client_id = '取得したClientID'
client_secret = '取得したClientSecret'
client_credentials_manager = spotipy.oauth2.SpotifyClientCredentials(client_id, client_secret)
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
```

```python:①指定プレイリストの楽曲ID一覧を取得する関数
def getTrackIDs(playlist_ids):
  track_ids = []
  for playlist_id in playlist_ids:
    playlist = sp.playlist(playlist_id)
    while playlist['tracks']['next']:
      for item in playlist['tracks']['items']:
        track = item['track']
        if not track['id'] in track_ids:
          track_ids.append(track['id'])
      playlist['tracks'] = sp.next(playlist['tracks'])
    else:
      for item in playlist['tracks']['items']:
        track = item['track']
        if not track['id'] in track_ids:
          track_ids.append(track['id'])
  return track_ids
```
```python:②楽曲の詳細データを取得する関数
def getTrackFeatures(id,rank):
  meta = sp.track(id)
  features = sp.audio_features(id)

  name = meta['name']
  album = meta['album']['name']
  artist = meta['album']['artists'][0]['name']
  release_date = meta['album']['release_date']
  length = meta['duration_ms']
  popularity = meta['popularity']
  key = features[0]['key']
  mode = features[0]['mode']
  danceability = features[0]['danceability']
  acousticness = features[0]['acousticness']
  energy = features[0]['energy']
  instrumentalness = features[0]['instrumentalness']
  liveness = features[0]['liveness']
  loudness = features[0]['loudness']
  speechiness = features[0]['speechiness']
  tempo = features[0]['tempo']
  time_signature = features[0]['time_signature']
  valence = features[0]['valence']
  type = features[0]['type']
  href = features[0]['uri']
  
  #実行日付もスプレッドシートに残すため取得
  delta = datetime.timedelta(hours=9)  # 9時間
  JST = datetime.timezone(delta, 'JST')  # UTCから9時間差の「JST」タイムゾーン
  dt_now = datetime.datetime.now(JST)
  today = dt_now.strftime('%Y/%m/%d')

  #スプレッドシートに記述する項目順に格納
  track = [today, rank, name, album, artist, release_date, length, popularity, key, mode, danceability, acousticness, energy, instrumentalness, liveness, loudness, speechiness, tempo, time_signature, valence,type,id]
  return track
```

```python:③スプレッドシートに書き込む関数　
def postSpreadSheet_top50(track):    
    try:
        gs = SpreadSheet()
        aws = Aws()
        #Systems Managerに登録されたgcpServiceaccountKeyを取得して使用
        ssmClient = aws.getClient("ssm")
        sheet = gs.sheet_open(aws.getSystemsManager(ssmClient,"gcp-serviceaccount-key"), "スプレッドシートID", "書き込むシート名", "スプレッドシートに書き込み権限あるアカウントID")
        #スプレッドシートに追記するテキスト作成
        gs.append_cell(track,sheet)
    except Exception as e:
        logger.error(e)

```

上記で記述した関数をまとめて実行する。

```python:関数を実行
def lambda_handler(event, context):
    rank = 0;
    #SpotifyのプレイリストIDを代入して①の関数を実行
    playlist_ids =[ '37i9dQZEVXbKXQ4mDTEBXq' ]
    track_ids = getTrackIDs(playlist_ids)
    #①の関数で返却された楽曲リストを使って楽曲の詳細情報を取得する②の関数を実行
    for track_id in track_ids:
     rank = rank + 1 #楽曲のランキング情報を付与
     track = getTrackFeatures(track_id,rank)
     #②で返却されるデータをスプレッドシートに書き込む③の関数を実行
     postSpreadSheet_top50(track)
```
こちらをトリガーにセットして毎日実行することで、下記のように取得したデータをスプレッドシートに毎日追記することができました。
![スクリーンショット 2022-12-02 16.07.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989377/b3a44e61-7650-ad80-8342-56a8dd515e24.png)



### ●今回取得している楽曲のデータ詳細
とても面白いデータを取得することができます！
Spotifyでは他にも色々なデータを活用していると思いますが、楽曲にこの粒度まで細かいデータを付与した上で学習させてレコメンドに使用してるみたいです！

| 取得できるデータ | 内容 |
| --- | --- | 
| popularity|相対的人気度（0～100）<br / >The popularity of the artist. The value will be between 0 and 100, with 100 being the most popular. The artist’s popularity is calculated from the popularity of all the artist’s tracks.| 
| acousticness| アコースティック感（0～1） <br /> A confidence measure from 0.0 to 1.0 of whether the track is acoustic. 1.0 represents high confidence the track is acoustic.| 
| danceability| 踊りやすさ（0～1）<br /> Danceability describes how suitable a track is for dancing based on a combination of musical elements including tempo, rhythm stability, beat strength, and overall regularity. A value of 0.0 is least danceable and 1.0 is most danceable.| 
| duration_ms| 楽曲の秒数(ms)<br />The duration of the track in milliseconds.| 
| energy| 楽曲の激しさ（0～1）<br />Energy is a measure from 0.0 to 1.0 and represents a perceptual measure of intensity and activity. Typically, energetic tracks feel fast, loud, and noisy. For example, death metal has high energy, whilea Bach prelude scores low on the scale. Perceptual features contributing to this attribute include dynamic range, perceived loudness, timbre, onset rate, and general entropy.| 
| instrumentalness| インスト感（0～1）<br />Predicts whether a track contains no vocals. “Ooh” and “aah” sounds are treated as instrumental in this context. Rap or spoken word tracks are clearly “vocal”. The closer the instrumentalness value is to 1.0, the greater likelihood the track contains no vocal content. Values above 0.5 are intended to represent instrumental tracks, but confidence is higher as the value approaches 1.0.| 
| liveness| ライブ感（0～1）<br />Detects the presence of an audience in the recording. Higher liveness values represent an increased probability that the track was performed live. A value above 0.8 provides strong likelihood that the track is live.| 
| loudness| 音量・音圧（dB／-60～0db）<br />The overall loudness of a track in decibels (dB). Loudness values are averaged across the entire track and are useful for comparing relative loudness of tracks. Loudness is the quality of a sound that is the primary psychological correlate of physical strength (amplitude). Values typical range between -60 and 0 db.| 
| mode| マイナーコード（0）／メジャーコード（1）<br />Mode indicates the modality (major or minor) of a track, the type of scale from which its melodic content is derived. Major is represented by 1 and minor is 0.| 
| speechiness| スピーチ感（0～1）<br />Speechiness detects the presence of spoken words in a track. The more exclusively speech-like the recording (e.g. talk show, audio book, poetry), the closer to 1.0 the attribute value. Values above 0.66 describe tracks that are probably made entirely of spoken words. Values between 0.33 and 0.66 describe tracks that may contain both music and speech, either in sections or layered, including such cases as rap music. Values below 0.33 most likely represent music and other non-speech-like tracks.| 
| tempo|テンポ（40-200bpm）<br />The overall estimated tempo of a track in beats per minute (BPM). In musical terminology, tempo is the speed or pace of a given piece and derives directly from the average beat duration.| 
| key|キー（0がC／0～11）<br />The key the track is in. Integers map to pitches using standard Pitch Class notation . E.g. 0 = C, 1 = C♯/D♭, 2 = D, and so on.|
| time_signature|拍子（1小節あたりの拍子）<br />An estimated overall time signature of a track. The time signature (meter) is a notational convention to specify how many beats are in each bar (or measure).| 
| valence|悲観的（0）～楽観的（1）<br />A measure from 0.0 to 1.0 describing the musical positiveness conveyed by a track. Tracks with high valence sound more positive (e.g. happy, cheerful, euphoric), while tracks with low valence sound more negative (e.g. sad, depressed, angry).| 


# ■スプレッドシートに溜まったデータからSlackへ通知する
毎日スプレッドシートに追記される楽曲一覧から今日追加された楽曲が初めてTop50に入っていたらSlackに通知するような仕組みにします。GoogleAppScriptを利用する想定になります。

```javascript:javaScript （Slack通知）
function postSlack_kpi (message) {
  var postUrl = 'Incoming Webhook URL';
  var username = '';  // 通知時に表示されるユーザー名
  var icon = '';  // 通知時に表示されるアイコン
  var jsonData =
      {
        "username" : username,
        "icon_url": icon,
        "text" : message
      };
  var payload = JSON.stringify(jsonData);
  
  var options =
      {
        "method" : "post",
        "contentType" : "application/json",
        "payload" : payload
      };
  UrlFetchApp.fetch(postUrl, options);
}
```

```javascript:javaScript （スケジュールに登録して実行）
function spotify_push_app(){
  // 更新日時を記録するのスプレッドシートのID
  // https://docs.google.com/spreadsheets/d/【ここ】/edit
  var SHEET_ID = 'スプレッドシートID';
  //スプレッドシートのシート名
  var SHEET_NAME = 'シート名';
  // スプレッドシートを特定
  var spreadsheet = SpreadsheetApp.openById(SHEET_ID);
  var sheet = spreadsheet.getSheetByName(SHEET_NAME);
  
  // getvaluesの実行速度が遅いのでシート内容を一括取得
  // A列1行目が[0][0]になることに注意
  var data = sheet.getDataRange().getValues();
  
  //今日
  var date = new Date();
  var date_y = new Date(date.getYear(), date.getMonth(), date.getDate() - 1);
  var today = Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy-MM-dd');
  
  //ユニーク全曲格納リスト
  var all_music_list = [];
  
  //一括取得したリストからユニークの楽曲リストを作成
  for (var i = 1; i < data.length; i++) {
    date_sub = Utilities.formatDate(new Date(data[i][0]),"JST","yyyy-MM-dd");
    if(today > date_sub){
      count = 0;
      for(var l = 0;l < all_music_list.length; l++) {
        if(data[i][21] === all_music_list[l][0]){
          count++;
        }
      }
      if(count == 0){
        all_music_list.push([data[i][21],data[i][2],data[i][4],data[i][5]]);
      }
    }
  }

  //もし新規でTop50に入った楽曲があれば追加するリスト
  var new_music_list = [];
  
  //今日追加したTop50のリストとユニークリストを比較して
  for (var i = 1; i < data.length; i++) {
    if(today == date_sub){
      count = 0;
      for(var l = 0;l < all_music_list.length; l++) {
       if(data[i][21] === all_music_list[l][0]){
         count++;
       }
      }
      if(count == 0){
        new_music_list.push([data[i][21],data[i][2],data[i][4],data[i][5]]);
      }
    }
  }
  //新規でTop50入りした楽曲をすべてSlackで送信する
  if(new_music_list.length > 0){
    for(var s = 0;s < new_music_list.length; s++) {
      var msg = Utilities.formatString('*■%sの急上昇*\nTOP50に初めてランクイン！注目です:sparkles:\n*%s（%s）*\n`リリース日：%s`\n https://open.spotify.com/track/%s',today,new_music_list[s][1],new_music_list[s][2],new_music_list[s][3],new_music_list[s][0]);
      postSlack_kpi(msg);
    }
  }
}
```
上記をトリガー設定することで毎日決まった時間に、もし急上昇楽曲があればSlackに投稿されます。

![スクリーンショット 2022-12-02 17.33.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989377/d267ce5f-8813-235b-47db-1420626ac860.png)
これで毎日、急上昇した人気楽曲を速報としてキャッチアップできるようになりました！

# ■まとめ
多少エンジニアリングの知識が必要になる箇所もあるかもしれませんが、サーバーレスでこのような仕組みを作ることはできました。楽曲のデータの粒度がすごく細かくこのデータをさらに機械学習で人気楽曲の特徴を探ったり、どういう特徴を持った楽曲がTop50に入りやすいか？みたいな予測モデルを作っても面白そうだなと思いました。

ぜひSpotifyを利用した際は自分にあったレコメンドがされるか確認してみてください！