自分の好きなアーティストと似ているアーティストを出力するWebアプリケーションの開発 part1

## はじめに
大学の授業で、入力されたアーティストと近しいアーティストのネットワーク図を作成するシステムを作ったので、今回は個人的な趣味で、そのシステムを発展させてWebアプリケーションを開発しました。その際の備忘録を残します。
part1では、バックエンド側の実装についてまとめます。

## Webアプリケーションの大まかな流れ
1. 初期画面で入力フォームを表示し、ユーザにアーティスト名を入力させる
2. 入力されたアーティストに対して、Spotipyの`artist_related_artists()`を使用して、そのアーティストと関連のあるアーティスト情報を取得する
3. `networkx`ライブラリを使用して、関連のあるアーティストを芋づる式にノードで結んでネットワーク図を作り、**ページランクの値が高い順に**取り出す（この時、人気曲の情報も取得する）
4. リザルト画面で関連のあるアーティスト10人と取得した人気曲の情報を表示する

今回の備忘録では、2.と3.のバックエンド側の実装についてまとめます。

## バックエンドの開発
### 初期設定
```python:app.py
from flask import Flask, render_template, request, redirect, url_for
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
import networkx as nx
import pandas as pd
import os
from dotenv import load_dotenv

app = Flask(__name__)

# .envファイルから環境変数を読み込む
load_dotenv()

# 環境変数からSpotify APIのクライアントIDとシークレットを取得
client_id = os.getenv('SPOTIFY_CLIENT_ID')
client_secret = os.getenv('SPOTIFY_CLIENT_SECRET')

auth_manager = SpotifyClientCredentials(client_id=client_id, client_secret=client_secret)
sp = spotipy.Spotify(auth_manager=auth_manager)
```

1. 必要なモジュールをインポートします
2. `app`変数にFlaskクラスのインスタンスを格納します
3. `.env`ファイルから環境変数を取得し、Spotipyを使えるようにします。.envの具体的な扱い方は次回以降の記事で書きます

### ネットワーク図作成にあたり必要な関数の作成
```python:app.py
# アーティスト名のURIを取得
def get_artist_uri(name):
    results = sp.search(q='artist:' + name, type='artist')
    if results['artists']['items']:
        artist = results['artists']['items'][0]
        uri = artist['uri']
        return uri
    else:
        return None

# アーティストのURIを入力し，そのアーティストと関連があるアーティスト情報を出力
def get_related_artist_info(uri):
    df = pd.DataFrame()
    for artist in sp.artist_related_artists(uri)['artists']:
        tmp = pd.Series([], name=artist['name'])
        for key in ['popularity', 'uri']:
            tmp[key] = artist[key]
        # アーティストのトップトラックを取得
        top_tracks = sp.artist_top_tracks(artist['uri'])['tracks']
        top_track_names = [track['name'] for track in top_tracks]
        # トップトラックをデータフレームに追加
        tmp['top_tracks'] = ', '.join(top_track_names)
        df = pd.concat([df, pd.DataFrame(tmp).T])           
    return df
```
Spotipyでは、**アーティスト情報はURIを使って表される**ので、

1. アーティスト名のURIを取得する関数
2. 上記のURIを使って、そのアーティストと関連がある（似ている）アーティスト情報を取得する関数

を作成します。これらの関数を使ってネットワーク図を作ります。

### ネットワーク図の作成、関連アーティストと人気曲情報の取得
```python:app.py
# 以下，ページランクをもとに上位10個のアーティストを取得する関数
#最初にアーティストネットワークを作成する
#次に，そのネットワークに対してpagerankを計算し，pagerankの上位10個を出力する．
def get_top_artists(artist_name):
    G = nx.Graph()  # ページランク計算用の新しいグラフオブジェクトを生成
    uri = get_artist_uri(artist_name)
    if not uri:
        return None, None
    df = get_related_artist_info(uri)
    G.add_node(artist_name)
    for name in df.index.tolist():
        G.add_node(name)
        G.add_edge(artist_name, name)
    pr = nx.pagerank(G)
    sorted_pr = sorted(pr.items(), key=lambda x: x[1], reverse=True) #sorted_prリストは(アーティスト名, PageRank値)のタプル構造になっている．
    top_artists_list = [artist[0] for artist in sorted_pr[:11]] #artist[0]はsorted_prリストの「アーティスト名」を参照している．
    if artist_name in top_artists_list:  #上位10人の中で入力されたアーティスト名と同じ名前のものが入っていた場合は，除去する
        top_artists_list.remove(artist_name)
    else:   #top_artistリストの1番目は入力されたアーティスト名と同じなので，else文に来たときはpopして除去する．
        top_artists_list.pop()
    
    # アーティストごとに有名曲を5曲取得
    top_tracks_dict = {}
    for artist in top_artists_list:
        uri = get_artist_uri(artist)
        if uri:
            top_tracks = sp.artist_top_tracks(uri)['tracks'] #人気順になってる
            top_tracks_info = []
            for track in top_tracks[:6]:
                track_info = {
                    'name': track['name'],  # 曲名を取得
                    'release_date': track['album']['release_date'].split('-')[0],  # リリース年を取得
                    'image_url': track['album']['images'][0]['url']  # アルバムの画像URLを取得
                }
                top_tracks_info.append(track_info)
            # トップトラック情報をアーティスト名をキーにして辞書に追加
            top_tracks_dict[artist] = top_tracks_info
    
    return top_artists_list, top_tracks_dict
```

`get_artist_uri(name)`、`get_related_artist_info(uri)`、`networkx`ライブラリを使用してネットワークを作り、その中からページランクの高い順に関連のあるアーティストを10人取り出して、`top_artists_list`辞書に格納しています。

一般的に、`G = nx.Graph()`はグローバル変数として関数の外で定義しますが、今回のアプリケーションでそのように定義してしまうと、**ユーザが1回目に入力したアーティスト情報のネットワーク結果が2回目以降の入力にも影響を与えてしまいます**。今回はそれを防ぐため、関数内に定義しました。これにより、ユーザの入力ごとに`get_top_artists(artist_name)`を利用してネットワーク図を作成することができ、以降の入力時に影響を与えないようにしています。
 
また、今回は取り出したアーティストの人気曲を6曲取り出し、それも出力するようにしました。具体的には、`artist_top_tracks(uri)['tracks']`で人気曲の情報を取得し、「曲名」・「アルバム収録年」・「アルバム画像」を`top_tracks_dict`辞書に格納しています。

### Webアプリケーションの初期画面とリザルト画面の設定
```python:app.py
#初期画面
@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        artist_name = request.form['artist_name']
        return redirect(url_for('result', artist_name=artist_name))
    return render_template('index.html')

#リザルト画面
@app.route('/result/<artist_name>')
def result(artist_name):
    top_artists_list, top_tracks_dict = get_top_artists(artist_name)
    if not top_artists_list:
        return render_template('error.html', artist_name=artist_name)
    return render_template('result.html', artist_name=artist_name, top_artists_list=top_artists_list, top_tracks_dict=top_tracks_dict)
```

初期画面（`index.html`）とリザルト画面（`result.html`と`error.html`）のバックエンド側を上記のように実装しました。HTMLテンプレートの中身は次回以降の記事で書きます。

　初期画面は、以下のように実装しています。
 ①ユーザーが初めてページにアクセスしたときや、フォームを送信せずにページをリロードしたとき`index.html`テンプレートをレンダリングして表示する。
 ②クライアント側からPOSTリクエストを受け取ったら`artist_name`パラメータを使用してresultという名前のルート(/)にリダイレクトする
 
 　一方で、リザルト画面は、以下のように実装しています。
  ①`artist_name`はURLパスから取得され、関数の引数として渡す
  ②`top_artists_list`が空（つまり、関連アーティストが見つからない）場合、`error.html`テンプレートをレンダリングして表示する。その際、`artist_name`をコンテキストとしてテンプレートに渡す。
  ③`top_artists_list`が空でない場合、`result.html`テンプレートをレンダリングして表示する。その際、`artist_name`、`top_artists_list`、`top_tracks_dict`をコンテキストとしてテンプレートに渡す。

## 終わりに
今回は、Webアプリケーションのバックエンド側の実装についてまとめました。次回以降では、[HTMLテンプレートの中身やCSSの定義](https://qiita.com/k-suna/items/3b6e5aba24e0861deb25)、[.envファイルについて](https://qiita.com/k-suna/items/ef782da10e66f642ddbc)を書こうと思います。

## 参考文献
以下の記事を参考にしました。ありがとうございました。

https://developer.spotify.com/documentation/web-api

https://hitori-sekai.com/python/spotify-network/

https://zenn.dev/nishimoto/articles/3222f1d753c5f3	

https://qiita.com/keijipoon/items/1a863be6ec056c46611c