Implementação de Comentários, Favoritos e Danmaku em Aplicações de Streaming com Flask

Sistema de Comentários e Estatísticas

Para permitir que os usuários interajam com o conteúdo e para rastrear métricas de engajamento, implementamos um formulário de revisão e atualizamos a lógica da rota de reprodução. O formulário é construído utilizando o WTForms para validação integrada.

class ReviewForm(FlaskForm):
    review_text = TextAreaField(
        'Seu Comentário',
        validators=[DataRequired(message="O comentário não pode estar vazio.")],
        render_kw={"id": "review-input", "placeholder": "Deixe sua opinião..."}
    )
    submit_btn = SubmitField(
        'Publicar',
        render_kw={"class": "btn btn-primary", "id": "submit-review"}
    )

A rota de reprodução precisa gerenciar a exibição do vídeo, a paginação dos comentários e o incremento das estatísticas de visualização e quantidade de comentários. Para evitar que a contagem de visualizações aumente duplicadamente ao enviar um comentário, o incremento de visualização é condicionado ao método da requisição.

@main_blueprint.route("/assistir/<movie_id>/<page_num>/", methods=["GET", "POST"])
def stream_movie(movie_id, page_num=1):
    film = Movie.query.join(Tag).filter(
        Tag.id == Movie.tag_id, 
        Movie.id == movie_id
    ).first_or_404()

    reviews = Review.query.join(User).filter(
        Review.movie_id == film.id
    ).order_by(Review.created_at.desc()).paginate(
        page=page_num, per_page=15, error_out=False
    )
    
    form = ReviewForm()
    if current_user.is_authenticated and form.validate_on_submit():
        new_review = Review(
            text=form.review_text.data,
            movie_id=film.id,
            user_id=current_user.id
        )
        db.session.add(new_review)
        film.review_count = (film.review_count or 0) + 1
        db.session.commit()
        flash('Comentário publicado com sucesso!', 'success')
        return redirect(url_for('main.stream_movie', movie_id=film.id, page_num=1))
        
    if request.method == "GET":
        film.view_count = (film.view_count or 0) + 1
        db.session.commit()
        
    return render_template("stream.html", film=film, form=form, reviews=reviews)
</page_num></movie_id>

Gerenciamento de Favoritos via AJAX

A funcionalidade de adicionar filmes aos favoritos é processada de forma assíncrona para evitar recarregamentos de página. A rota abaixo verifica se o item já existe na base de dados antes de inserir um novo registro.

@main_blueprint.route("/favoritos/adicionar/", methods=["POST"])
@login_required
def add_to_favorites():
    payload = request.get_json()
    user_id = current_user.id
    film_id = payload.get("movie_id")
    
    existing_fav = Favorite.query.filter_by(
        user_id=user_id, 
        movie_id=film_id
    ).first()
    
    if existing_fav:
        return jsonify({"status": "already_favorited", "message": "Filme já está nos favoritos."})
        
    new_fav = Favorite(user_id=user_id, movie_id=film_id)
    db.session.add(new_fav)
    db.session.commit()
    
    return jsonify({"status": "success", "message": "Adicionado aos favoritos!"})

No frontend, utilizaremos a API Fetch nativa do JavaScript para realizar a requisição POST, substituindo abordagens antigas com jQuery. Isso torna o código mais limpo e moderno.

document.addEventListener("DOMContentLoaded", function() {
    const favBtn = document.getElementById("btn-favorite");
    if(favBtn) {
        favBtn.addEventListener("click", function() {
            const movieId = this.dataset.movieId;
            fetch("/favoritos/adicionar/", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "X-CSRFToken": getCsrfToken() // Função auxiliar para obter o token CSRF
                },
                body: JSON.stringify({ movie_id: movieId })
            })
            .then(response => response.json())
            .then(data => {
                const msgContainer = document.getElementById("fav-message");
                msgContainer.textContent = data.message;
            });
        });
    }
});

Integração de Danmaku com Redis e DPlayer

O recurso de danmaku (comentários sobrepostos no vídeo) exige alta performance de leitura e escrita. Utilizamos o Redis como fila de mensagens para armazenar e recuperar os comentários em tempo real, integrando-o ao player DPlayer.

@main_blueprint.route("/api/danmaku/<int:film_id>/", methods=["GET", "POST"])
def manage_danmaku(film_id):
    redis_key = f"danmaku:film:{film_id}"
    
    if request.method == "GET":
        raw_messages = redis_client.lrange(redis_key, 0, -1)
        formatted_data = [json.loads(msg) for msg in raw_messages]
        return jsonify({"code": 0, "data": formatted_data})
        
    elif request.method == "POST":
        payload = request.get_json()
        danmaku_obj = {
            "author": payload.get("author", "Anônimo"),
            "time": payload.get("time", 0),
            "text": payload.get("text", ""),
            "color": payload.get("color", 16777215),
            "type": payload.get("type", 0)
        }
        
        redis_client.lpush(redis_key, json.dumps(danmaku_obj))
        
        response_obj = {
            "code": 0,
            "data": {
                "_id": uuid.uuid4().hex,
                "player": payload.get("id"),
                "author": danmaku_obj["author"],
                "time": danmaku_obj["time"],
                "text": danmaku_obj["text"],
                "color": danmaku_obj["color"],
                "type": danmaku_obj["type"],
                "ip": request.remote_addr,
                "date": int(time.time() * 1000)
            }
        }
        return jsonify(response_obj)

Para o front end, o DPlayer é configurado apontando para a API de danmaku criada. Certifique-se de incluir os arquivos CSS e JS necessários do DPlayer, bem como as bibliotecas de suporte para streaming (como flv.js ou hls.js).

<link rel="stylesheet" href="{{ url_for('static', filename='dplayer/DPlayer.min.css') }}">
<script src="https://cdn.jsdelivr.net/npm/flv.js/dist/flv.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hls.js/dist/hls.min.js"></script>
<script src="{{ url_for('static', filename='dplayer/DPlayer.min.js') }}"></script>

<div id="video-player"></div>

<script>
    const dp = new DPlayer({
        container: document.getElementById('video-player'),
        video: {
            url: "{{ url_for('static', filename='media/' + film.video_file) }}",
        },
        danmaku: {
            id: '{{ film.id }}',
            api: "/api/danmaku/",
        }
    });
</script>

A configuração do cliente Redis no aplicativo Flask é feita através da extensão flask-redis, que deve ser instalada via pip (pip install flask-redis).

from flask_redis import FlaskRedis

app.config['REDIS_URL'] = 'redis://localhost:6379/2'
redis_client = FlaskRedis(app)

Considerações para Ambiente de Produção

Ao migrar a aplicação para um ambiente de produção, é fundamental utilizar servidores WSGI robustos como Gunicorn ou uWSGI, acompanhados de um proxy reverso como Nginx para gerenciar requisições estáticas e balanceamento de carga. Além disso, o serviço do Redis deve ser configurado com persistência e políticas de segurança adequadas, e as variáveis de ambiente devem ser gerenciadas fora do código-fonte.

Tags: Flask Redis DPlayer SQLAlchemy ajax

Publicado em 6-30 19:44