Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part4

## はじめに

Part3では、DailyBeatの楽曲投稿機能とタイムライン機能の実装について解説しました。Part4では、ユーザー間のつながりを強化するフォロー機能と、ユーザーエクスペリエンスを向上させる楽曲再生機能の実装について詳しく見ていきます。

## 1. フォロー機能の実装

フォロー機能により、ユーザーは興味のある他のユーザーの投稿を優先的に見ることができます。

### 1.1 フォロー関係のモデル定義

まず、ユーザー間のフォロー関係を表すモデルを定義します。

```python:models/user.py
from app import db

followers = db.Table('followers',
    db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),
    db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))
)

class User(UserMixin, db.Model):
    # 既存のフィールド...

    followed = db.relationship(
        'User', secondary=followers,
        primaryjoin=(followers.c.follower_id == id),
        secondaryjoin=(followers.c.followed_id == id),
        backref=db.backref('followers', lazy='dynamic'), lazy='dynamic')

    def follow(self, user):
        if not self.is_following(user):
            self.followed.append(user)

    def unfollow(self, user):
        if self.is_following(user):
            self.followed.remove(user)

    def is_following(self, user):
        return self.followed.filter(
            followers.c.followed_id == user.id).count() > 0
```

**コード解説：**
- `followers`: 多対多の関係を表す中間テーブルです。
- `followed`: ユーザーがフォローしている他のユーザーを表すリレーションシップです。
- `follow`, `unfollow`, `is_following`: フォロー関連の操作を行うメソッドです。

### 1.2 フォロー/アンフォロー機能の実装

次に、フォロー/アンフォロー機能を実装するビュー関数を作成します。

```python:views/follow.py
from flask import flash, redirect, url_for
from flask_login import login_required, current_user
from app import app, db
from app.models import User

@app.route('/follow/<username>')
@login_required
def follow(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash('ユーザーが見つかりません。')
        return redirect(url_for('index'))
    if user == current_user:
        flash('自分自身をフォローすることはできません。')
        return redirect(url_for('user', username=username))
    current_user.follow(user)
    db.session.commit()
    flash(f'{username}をフォローしました。')
    return redirect(url_for('user', username=username))

@app.route('/unfollow/<username>')
@login_required
def unfollow(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash('ユーザーが見つかりません。')
        return redirect(url_for('index'))
    if user == current_user:
        flash('自分自身をアンフォローすることはできません。')
        return redirect(url_for('user', username=username))
    current_user.unfollow(user)
    db.session.commit()
    flash(f'{username}のフォローを解除しました。')
    return redirect(url_for('user', username=username))
```

**コード解説：**
- `@login_required`: ログインユーザーのみがアクセスできるようにします。
- エラーチェック: ユーザーの存在確認や自分自身をフォロー/アンフォローしようとしていないかチェックします。
- `db.session.commit()`: データベースに変更を反映します。

### 1.3 フォロー/アンフォローボタンの追加

ユーザープロフィールページにフォロー/アンフォローボタンを追加します。

```html:templates/user.html
{% extends "base.html" %}

{% block content %}
    <h1>{{ user.username }}のプロフィール</h1>
    {% if user != current_user %}
        {% if current_user.is_following(user) %}
        <p><a href="{{ url_for('unfollow', username=user.username) }}">フォロー解除</a></p>
        {% else %}
        <p><a href="{{ url_for('follow', username=user.username) }}">フォローする</a></p>
        {% endif %}
    {% endif %}
    <!-- ユーザーの他の情報... -->
{% endblock %}
```

**テンプレート解説：**
- 現在のユーザーが表示されているユーザーと異なる場合のみボタンを表示します。
- `is_following()`メソッドを使用して、フォロー状態に応じたボタンを表示します。

## 2. 楽曲再生機能の実装

ユーザーエクスペリエンスを向上させるために、アプリ内で直接楽曲を再生できる機能を実装します。ここでは、Spotify Web Playback SDKを使用した実装方法を紹介します。

### 2.1 Spotify APIの設定

まず、Spotify Developer Dashboardでアプリケーションを登録し、Client IDとClient Secretを取得します。

### 2.2 サーバーサイドでのアクセストークン取得

Spotify APIを使用するためのアクセストークンを取得する関数を実装します。

```python:utils/spotify.py
import requests
from flask import current_app

def get_spotify_token():
    auth_url = 'https://accounts.spotify.com/api/token'
    auth_response = requests.post(auth_url, {
        'grant_type': 'client_credentials',
        'client_id': current_app.config['SPOTIFY_CLIENT_ID'],
        'client_secret': current_app.config['SPOTIFY_CLIENT_SECRET'],
    })
    auth_response_data = auth_response.json()
    return auth_response_data['access_token']
```

### 2.3 フロントエンドでの楽曲再生機能の実装

Spotify Web Playback SDKを使用して、フロントエンドで楽曲を再生する機能を実装します。

```html:templates/play_song.html
{% extends "base.html" %}

{% block content %}
<h1>{{ song.title }} - {{ song.artist }}</h1>
<div id="player"></div>

<script src="https://sdk.scdn.co/spotify-player.js"></script>
<script>
window.onSpotifyWebPlaybackSDKReady = () => {
    const token = '{{ spotify_token }}';
    const player = new Spotify.Player({
        name: 'DailyBeat Web Player',
        getOAuthToken: cb => { cb(token); }
    });

    // Error handling
    player.addListener('initialization_error', ({ message }) => { console.error(message); });
    player.addListener('authentication_error', ({ message }) => { console.error(message); });
    player.addListener('account_error', ({ message }) => { console.error(message); });
    player.addListener('playback_error', ({ message }) => { console.error(message); });

    // Playback status updates
    player.addListener('player_state_changed', state => { console.log(state); });

    // Ready
    player.addListener('ready', ({ device_id }) => {
        console.log('Ready with Device ID', device_id);
        playSong('{{ song.spotify_uri }}', device_id);
    });

    // Connect to the player!
    player.connect();
};

function playSong(spotify_uri, device_id) {
    fetch(`https://api.spotify.com/v1/me/player/play?device_id=${device_id}`, {
        method: 'PUT',
        body: JSON.stringify({ uris: [spotify_uri] }),
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer {{ spotify_token }}`
        },
    });
}
</script>
{% endblock %}
```

**コード解説：**
- Spotify Web Playback SDKをロードし、プレイヤーを初期化します。
- `playSong`関数: 指定された楽曲をSpotify APIを使用して再生します。

### 2.4 楽曲再生ビューの実装

```python:views/play.py
from flask import render_template
from flask_login import login_required
from app import app
from app.models import DailySong
from app.utils.spotify import get_spotify_token

@app.route('/play/<int:song_id>')
@login_required
def play_song(song_id):
    song = DailySong.query.get_or_404(song_id)
    spotify_token = get_spotify_token()
    return render_template('play_song.html', song=song, spotify_token=spotify_token)
```

**コード解説：**
- `get_or_404`: 指定されたIDの楽曲が存在しない場合、404エラーを返します。
- `get_spotify_token()`: Spotify APIのアクセストークンを取得します。

## 3. セキュリティとプライバシーの考慮事項

フォロー機能と楽曲再生機能を実装する際は、以下のセキュリティとプライバシーの考慮事項に注意する必要があります：

1. **認証と認可**: フォロー/アンフォロー操作や楽曲再生は認証済みユーザーのみが行えるようにします。
2. **CSRF対策**: フォロー/アンフォロー操作にはCSRFトークンを使用します。
3. **レート制限**: フォロー/アンフォロー操作に対してレート制限を実装し、悪用を防ぎます。
4. **プライバシー設定**: ユーザーがフォロワーリストを非公開にできるオプションを提供します。
5. **Spotify APIキーの保護**: Spotify APIキーは環境変数として設定し、ソースコードに直接記述しないようにします。

## まとめ

Part4では、DailyBeatのフォロー機能と楽曲再生機能の実装について詳しく解説しました。これらの機能により、ユーザー間のつながりが強化され、アプリケーション内での音楽体験が向上します。

フォロー機能の実装では、SQLAlchemyの多対多リレーションシップを活用し、効率的なデータベース設計を行いました。楽曲再生機能では、Spotify Web Playback SDKを使用することで、外部サービスとの連携を実現しました。

セキュリティとプライバシーの考慮事項にも触れ、実際のプロダクション環境でのデプロイを見据えた実装について説明しました。

次回のPart5では、DailyBeatのセキュリティ対策、パフォーマンス最適化、そして今後の展望について詳しく解説する予定です。お楽しみに！

## 参考リンク

- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part1](https://qiita.com/taipilo/items/62490dfa93e41d4be0b4)
- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part2](https://qiita.com/taipilo/items/b74b2ed8ba690956bcac)
- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part3](https://qiita.com/taipilo/items/bce5d59c0a4c5fbc8a8c)
- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 詳細解説 Part5（最終回）](https://qiita.com/taipilo/items/ee5566c4f7601dc932dd)
- [Flask + SQLAlchemyで作る音楽共有SNS「DailyBeat」- 包括的ガイド]()