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

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

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

今回の備忘録では、1.と4.のフロントエンド側の実装についてまとめます。

## ディレクトリ構造
[前回記事](https://qiita.com/k-suna/items/118f955b58be9105b941)で紹介した`app.py`と今回記述する`indes.html`、`result.html`、`error.html`、`style.css`のディレクトリ構造は以下の通りです。
```text:ディレクトリ構造
{プロジェクト名}/
├── app.py
├── static/
│   └── style.css
└── templates/
    └── index.html
    └── result.html
    └── error.html
```
`app.py`はルートディレクトリ直下に、`indes.html`、`result.html`、`error.html`の3つは`templates`ディレクトリに、`style.css`は`static`ディレクトリに格納しています。

## フロントエンド側の実装
### 初期画面の実装
```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <title>Artists Recommender</title>
</head>
<body>
    <div class="container">
        <h1>Welcome to Artists Recommender</h1>

        <form action="/" method="POST">
            <label for="artist_name">
                あなたの好きなアーティストと関連度(*)の高いアーティストを10人推薦します。<br>
                アーティスト名を<strong>ローマ字</strong>で入力して下さい。<br><br>
                <div class="upper-note">
                    (例)　嵐　>>　ARASHI <br>
                    　　　　　平井堅　>>　Ken Hirai<br>
                </div>
            </label>

            <div class="input-wrapper">
                <input type="text" id="artist_name" name="artist_name">
            </div>
            <div class="button-wrapper">
                <input type="submit" value="OK" class="button1">
            </div>
            <br>
            <div class="under-note">
                *「関連度」は、Spotifyコミュニティの視聴履歴の分析に基づいています。
            </div>
        </form>
    </div>
</body>
</html>
```
この画面は、ユーザーが初めてページにアクセスしたときや、フォームを送信せずにページをリロードしたときに表示するHTMLテンプレートです。

`<input type="text" id="artist_name" name="artist_name">`は、ユーザの入力フォームとなっていて、文字列を入力することができます。

`<input type="submit" value="OK" class="button1">`は、送信ボタンとなっていて、サーバにPOSTリクエストを送ります。

### リザルト画面（正常画面）
```html:result.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <title>Artist Network Result</title>
</head>
<body>
    <div class="container">
        <h1>以下が「{{ artist_name }}」と関連度の高いアーティストです</h1>
        <div class="note">※注　記載のリリース年はアルバム収録年です。</div>
        <ul>
            {% for artist in top_artists_list %}
                {% if loop.index == 1 %}
                    <li><div class="first_order">{{ loop.index }}st. {{ artist }} </div><br><br> 
                        {% for track_info in top_tracks_dict[artist] %}
                            {{ track_info['name'] }}({{ track_info['release_date'] }})<br>
                            {% if track_info['image_url'] %}
                                    <img src="{{ track_info['image_url'] }}" alt="{{ track_info['name'] }}のアルバムアート" width="100" height="100"><br>
                            {% else %}
                                <p>画像なし</p><br>
                            {% endif %}  
                        {% endfor %}</li>
                {% elif loop.index == 2 %}
                    <li><div class="second_order">{{ loop.index }}nd. {{ artist }} </div><br><br> 
                        {% for track_info in top_tracks_dict[artist] %}
                            {{ track_info['name'] }}({{ track_info['release_date'] }})<br>
                            {% if track_info['image_url'] %}
                                    <img src="{{ track_info['image_url'] }}" alt="{{ track_info['name'] }}のアルバムアート" width="100" height="100"><br>
                            {% else %}
                                <p>画像なし</p><br>
                            {% endif %}  
                        {% endfor %}</li>
                {% elif loop.index == 3 %}
                    <li><div class="third_order">{{ loop.index }}rd. {{ artist }} </div><br><br> 
                        {% for track_info in top_tracks_dict[artist] %}
                            {{ track_info['name'] }}({{ track_info['release_date'] }})<br>
                            {% if track_info['image_url'] %}
                                    <img src="{{ track_info['image_url'] }}" alt="{{ track_info['name'] }}のアルバムアート" width="100" height="100"><br>
                            {% else %}
                                <p>画像なし</p><br>
                            {% endif %}   
                        {% endfor %}</li>
                {% else %}
                    <li><div class="other_order">{{ loop.index }}th. {{ artist }} </div><br><br> 
                        {% for track_info in top_tracks_dict[artist] %}
                            {{ track_info['name'] }}({{ track_info['release_date'] }})<br>
                            {% if track_info['image_url'] %}
                                    <img src="{{ track_info['image_url'] }}" alt="{{ track_info['name'] }}のアルバムアート" width="100" height="100"><br>
                            {% else %}
                                <p>画像なし</p><br>
                            {% endif %}   
                        {% endfor %}</li>
                {% endif %}
            {% endfor %}
        </ul>
        <a href="/" class="button2">戻る</a>
    </div>
</body>
</html>
```
この画面は、初期画面でユーザがSpotifyデータベースに存在するアーティスト名を入力し、[前回記事](https://qiita.com/k-suna/items/118f955b58be9105b941)で紹介した`app.py`が正常に動作した時（`top_artists_list`が空でない場合）に表示するHTMLテンプレートです。

順位の表示の部分で、1位は1の後ろに「.st」、2位は「.nd」、3位は「.rd」、4位から10位は「.th」となるように記述しました。

`top_tracks_dict`辞書は、キーに`artist`を入れると、値としてそのアーティストの人気曲の3つの情報（「曲名」・「アルバム収録年」・「アルバム画像」）を返す辞書なので、それらを表示するように記述しています。

`<a href="/" class="button2">戻る</a>`は、初期画面に戻るボタンです。

### リザルト画面（エラー画面）
```html:error.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <title>Artist Not Found</title>
</head>
<body>
    <div class="container">
        <h1>入力されたアーティスト名は対応しておりません</h1>
        <p>「{{ artist_name }}」はSpotifyのデータベースに存在しないか、対応していません。</p>
        <p>ローマ字で入力するか、他のアーティスト名を入力してください。</p>
        <a href="/" class="button2">戻る</a>
    </div>
</body>
</html>
```
この画面は、初期画面でユーザがSpotifyデータベースに存在しないアーティスト名を入力し、[前回記事](https://qiita.com/k-suna/items/118f955b58be9105b941)で紹介した`app.py`で`top_artists_list`が空（つまり、関連アーティストが見つからない）場合に表示するHTMLテンプレートです。

### CSSの実装
```css:style.css
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');

body {
    font-family: 'Roboto', sans-serif;
    background-color: #f0f0f0;
    margin: 0;
    padding: 0;
}

.container {
    max-width: 800px;
    margin: auto;
    padding: 20px;
    padding-right: 20px;
    text-align: center;
}

.upper-note {
    margin-right: 80px;
}

.under-note {
    text-align: left;
    display: inline-block;
    margin-top: 30px;
}

input[type="text"] {
    width: 300px;
    padding: 10px;
    margin-top: 10px;
    margin-right: -200px;
}

input[type="submit"] {
    padding: 10px 20px;
    background-color: #007bff;
    color: white;
    border: none;
    cursor: pointer;
    margin-top: 10px;
    margin-left: -100px;
}

input[type="submit"]:hover {
    background-color: #0056b3;
}

.input-wrapper {
    display: inline-block;
    width: 50%; 
    margin-right: 50px;
}

.button-wrapper {
    display: inline-block;
    width: 28%;
}

ul {
    list-style-type: none;
    padding: 0;
}

li {
    margin-bottom: 10px;
    background-color: #ffffff;
    padding: 10px;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.button2 {
    padding: 10px 20px;
    background-color: #007bff;
    color: white;
    border: none;
    cursor: pointer;
}

.button2:hover {
    background-color: #0056b3;
}

.first_order{
    color:#dbb400;
    font-size: 35px;
    font-family: 'Roboto', sans-serif;
}

.second_order{
    color:#9fa0a0;
    font-size: 30px;
    font-family: 'Roboto', sans-serif;
}

.third_order{
    color:#c47022;
    font-size: 25px;
    font-family: 'Roboto', sans-serif;
}

.other_order{
    font-size: 20px;
    font-family: 'Roboto', sans-serif;
}

/* スマホ向けのスタイル */
@media (max-width: 768px) {
    .upper-note {
        margin-right: 70px;
    }
    .input-wrapper, .button-wrapper {
        display: block;
        width: 100%;
        text-align: center;
    }

    input[type="text"] {
        width: calc(100% - 20px);
        margin: 10px auto;
    }

    input[type="submit"] {
        width: calc(100% - 20px);
        margin: 10px auto;
    }
}
```
初期画面とリザルト画面の装飾部分の実装です。フォントはGoogleapiから`Roboto`をインポートして使用しています。

中央揃えで表示させたかったので、基本的に`text-align: center;`で記述しています。（一部見た目上の都合で、`text-align: left; display: inline-block;`として、左揃えになるように記述しているところもあります。）

PCだけでなく、スマホにも対応したWebアプリケーションを開発したかったので、後半部分ではスマホに対応したCSSを実装しています。

`margin`の設定方法が分からなかったので、マイナスの値になってしまっています。

## 実際の画面
以下のように表示されます。

### 初期画面（PC版）
<img width="400" alt="スクリーンショット 2024-05-26 151531.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553476/9483f987-474e-6a04-c62b-8bc905bc593e.png">

### 初期画面（スマホ版）
<img width="300" alt="スクリーンショット 2024-05-26 152837.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553476/49c6f67c-9bfb-4735-2d1f-fae460a5c594.png">

### リザルト画面（正常）
**「ARASHI」と入力した場合**
<img width="300" alt="artistsrecommender-1.onrender.com_result_ARASHI.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553476/d6d2f6f6-bd35-bae5-3598-cd5bd6455c9e.png">

### リザルト画面（エラー）
**「いきものがかり」と入力した場合**
エラー原因：ローマ字で入力しなければならない

<img width="400" alt="スクリーンショット 2024-05-26 151934.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553476/cd557c71-d095-8445-39a3-e77711c8ea4a.png">

## 最後に
今回は、Webアプリケーションのフロントエンド側の実装についてまとめました。次回以降では、[.envファイルを用いた環境変数の管理方法](https://qiita.com/k-suna/items/ef782da10e66f642ddbc)やGit、Dockerの操作方法などを書こうと思います。

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

https://adatype.co.jp/info/archives/staff_blog/76

https://www.mathpython.com/flask-loop-index

https://fonts.google.com/specimen/Roboto