"""Players blueprint for player management and display.""" from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify from flask_login import login_required from werkzeug.security import generate_password_hash import secrets from typing import Optional, List from app.extensions import db, cache from app.models import Player, Group, Content, PlayerFeedback from app.utils.logger import log_action from app.utils.group_player_management import get_player_status_info players_bp = Blueprint('players', __name__, url_prefix='/players') @players_bp.route('/') @login_required def players_list(): """Display list of all players.""" try: players = Player.query.order_by(Player.name).all() groups = Group.query.all() # Get player status for each player player_statuses = {} for player in players: status_info = get_player_status_info(player.id) player_statuses[player.id] = status_info return render_template('players_list.html', players=players, groups=groups, player_statuses=player_statuses) except Exception as e: log_action('error', f'Error loading players list: {str(e)}') flash('Error loading players list.', 'danger') return redirect(url_for('main.dashboard')) @players_bp.route('/add', methods=['GET', 'POST']) @login_required def add_player(): """Add a new player.""" if request.method == 'GET': groups = Group.query.order_by(Group.name).all() return render_template('add_player.html', groups=groups) try: name = request.form.get('name', '').strip() location = request.form.get('location', '').strip() group_id = request.form.get('group_id') # Validation if not name or len(name) < 3: flash('Player name must be at least 3 characters long.', 'warning') return redirect(url_for('players.add_player')) # Generate unique auth code auth_code = secrets.token_urlsafe(16) # Create player new_player = Player( name=name, location=location or None, auth_code=auth_code, group_id=int(group_id) if group_id else None ) db.session.add(new_player) db.session.commit() log_action('info', f'Player "{name}" created with auth code {auth_code}') flash(f'Player "{name}" created successfully. Auth code: {auth_code}', 'success') return redirect(url_for('players.players_list')) except Exception as e: db.session.rollback() log_action('error', f'Error creating player: {str(e)}') flash('Error creating player. Please try again.', 'danger') return redirect(url_for('players.add_player')) @players_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit_player(player_id: int): """Edit player details.""" player = Player.query.get_or_404(player_id) if request.method == 'GET': groups = Group.query.order_by(Group.name).all() return render_template('edit_player.html', player=player, groups=groups) try: name = request.form.get('name', '').strip() location = request.form.get('location', '').strip() group_id = request.form.get('group_id') # Validation if not name or len(name) < 3: flash('Player name must be at least 3 characters long.', 'warning') return redirect(url_for('players.edit_player', player_id=player_id)) # Update player player.name = name player.location = location or None player.group_id = int(group_id) if group_id else None db.session.commit() # Clear cache for this player cache.delete_memoized(get_player_playlist, player_id) log_action('info', f'Player "{name}" (ID: {player_id}) updated') flash(f'Player "{name}" updated successfully.', 'success') return redirect(url_for('players.players_list')) except Exception as e: db.session.rollback() log_action('error', f'Error updating player: {str(e)}') flash('Error updating player. Please try again.', 'danger') return redirect(url_for('players.edit_player', player_id=player_id)) @players_bp.route('//delete', methods=['POST']) @login_required def delete_player(player_id: int): """Delete a player.""" try: player = Player.query.get_or_404(player_id) player_name = player.name # Delete associated feedback PlayerFeedback.query.filter_by(player_id=player_id).delete() db.session.delete(player) db.session.commit() # Clear cache cache.delete_memoized(get_player_playlist, player_id) log_action('info', f'Player "{player_name}" (ID: {player_id}) deleted') flash(f'Player "{player_name}" deleted successfully.', 'success') except Exception as e: db.session.rollback() log_action('error', f'Error deleting player: {str(e)}') flash('Error deleting player. Please try again.', 'danger') return redirect(url_for('players.players_list')) @players_bp.route('//regenerate-auth', methods=['POST']) @login_required def regenerate_auth_code(player_id: int): """Regenerate authentication code for a player.""" try: player = Player.query.get_or_404(player_id) # Generate new auth code new_auth_code = secrets.token_urlsafe(16) player.auth_code = new_auth_code db.session.commit() log_action('info', f'Auth code regenerated for player "{player.name}" (ID: {player_id})') flash(f'New auth code for "{player.name}": {new_auth_code}', 'success') except Exception as e: db.session.rollback() log_action('error', f'Error regenerating auth code: {str(e)}') flash('Error regenerating auth code. Please try again.', 'danger') return redirect(url_for('players.players_list')) @players_bp.route('/') @login_required def player_page(player_id: int): """Display player page with content and controls.""" try: player = Player.query.get_or_404(player_id) # Get player's playlist playlist = get_player_playlist(player_id) # Get player status status_info = get_player_status_info(player_id) # Get recent feedback recent_feedback = PlayerFeedback.query.filter_by(player_id=player_id)\ .order_by(PlayerFeedback.timestamp.desc())\ .limit(10)\ .all() return render_template('player_page.html', player=player, playlist=playlist, status_info=status_info, recent_feedback=recent_feedback) except Exception as e: log_action('error', f'Error loading player page: {str(e)}') flash('Error loading player page.', 'danger') return redirect(url_for('players.players_list')) @players_bp.route('//fullscreen') def player_fullscreen(player_id: int): """Display player fullscreen view (no authentication required for players).""" try: player = Player.query.get_or_404(player_id) # Verify auth code if provided auth_code = request.args.get('auth') if auth_code and auth_code != player.auth_code: log_action('warning', f'Invalid auth code attempt for player {player_id}') return "Invalid authentication code", 403 # Get player's playlist playlist = get_player_playlist(player_id) return render_template('player_fullscreen.html', player=player, playlist=playlist) except Exception as e: log_action('error', f'Error loading player fullscreen: {str(e)}') return "Error loading player", 500 @cache.memoize(timeout=300) # Cache for 5 minutes def get_player_playlist(player_id: int) -> List[dict]: """Get playlist for a player based on their group assignment. Args: player_id: The player's database ID Returns: List of content dictionaries with url, type, duration, and position """ player = Player.query.get(player_id) if not player: return [] # Get content from player's group if player.group_id: group = Group.query.get(player.group_id) if group: contents = group.contents.order_by(Content.position).all() else: contents = [] else: # Player not in a group - show all content contents = Content.query.order_by(Content.position).all() # Build playlist playlist = [] for content in contents: playlist.append({ 'id': content.id, 'url': url_for('static', filename=f'uploads/{content.filename}'), 'type': content.content_type, 'duration': content.duration or 10, # Default 10 seconds if not set 'position': content.position, 'filename': content.filename }) return playlist @players_bp.route('//reorder', methods=['POST']) @login_required def reorder_content(player_id: int): """Reorder content for a player's group.""" try: player = Player.query.get_or_404(player_id) if not player.group_id: flash('Player is not assigned to a group.', 'warning') return redirect(url_for('players.player_page', player_id=player_id)) # Get new order from request content_order = request.json.get('order', []) # Update positions for idx, content_id in enumerate(content_order): content = Content.query.get(content_id) if content and content in player.group.contents: content.position = idx db.session.commit() # Clear cache cache.delete_memoized(get_player_playlist, player_id) log_action('info', f'Content reordered for player {player_id}') return jsonify({'success': True}) except Exception as e: db.session.rollback() log_action('error', f'Error reordering content: {str(e)}') return jsonify({'success': False, 'error': str(e)}), 500 @players_bp.route('/bulk/delete', methods=['POST']) @login_required def bulk_delete_players(): """Delete multiple players at once.""" try: player_ids = request.json.get('player_ids', []) if not player_ids: return jsonify({'success': False, 'error': 'No players selected'}), 400 # Delete players deleted_count = 0 for player_id in player_ids: player = Player.query.get(player_id) if player: # Delete associated feedback PlayerFeedback.query.filter_by(player_id=player_id).delete() db.session.delete(player) cache.delete_memoized(get_player_playlist, player_id) deleted_count += 1 db.session.commit() log_action('info', f'Bulk deleted {deleted_count} players') return jsonify({'success': True, 'deleted': deleted_count}) except Exception as e: db.session.rollback() log_action('error', f'Error bulk deleting players: {str(e)}') return jsonify({'success': False, 'error': str(e)}), 500 @players_bp.route('/bulk/assign-group', methods=['POST']) @login_required def bulk_assign_group(): """Assign multiple players to a group.""" try: player_ids = request.json.get('player_ids', []) group_id = request.json.get('group_id') if not player_ids: return jsonify({'success': False, 'error': 'No players selected'}), 400 # Validate group if group_id: group = Group.query.get(group_id) if not group: return jsonify({'success': False, 'error': 'Invalid group'}), 400 # Assign players updated_count = 0 for player_id in player_ids: player = Player.query.get(player_id) if player: player.group_id = group_id cache.delete_memoized(get_player_playlist, player_id) updated_count += 1 db.session.commit() log_action('info', f'Bulk assigned {updated_count} players to group {group_id}') return jsonify({'success': True, 'updated': updated_count}) except Exception as e: db.session.rollback() log_action('error', f'Error bulk assigning players: {str(e)}') return jsonify({'success': False, 'error': str(e)}), 500