updated first commit

This commit is contained in:
2025-07-16 08:03:57 +03:00
parent 78641b633a
commit c36ba9dc64
34 changed files with 3938 additions and 0 deletions

14
app/routes/__init__.py Normal file
View File

@@ -0,0 +1,14 @@
"""
Routes package
"""
# Import all blueprints to make them available
from .auth import bp as auth_bp
from .dashboard import bp as dashboard_bp
from .admin import bp as admin_bp
from .player import bp as player_bp
from .group import bp as group_bp
from .content import bp as content_bp
from .api import bp as api_bp
__all__ = ['auth_bp', 'dashboard_bp', 'admin_bp', 'player_bp', 'group_bp', 'content_bp', 'api_bp']

245
app/routes/admin.py Normal file
View File

@@ -0,0 +1,245 @@
"""
Admin routes
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from functools import wraps
from app.models.user import User
from app.extensions import db
from app.utils.logger import log_user_created, log_user_deleted, log_action
import os
bp = Blueprint('admin', __name__)
def admin_required(f):
"""Decorator to require admin role"""
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.is_admin:
flash('Admin access required.', 'danger')
return redirect(url_for('dashboard.index'))
return f(*args, **kwargs)
return decorated_function
@bp.route('/')
@login_required
@admin_required
def index():
"""Admin dashboard"""
from flask import current_app
# Check if assets exist
logo_path = os.path.join(current_app.static_folder, 'assets', 'logo.png')
login_picture_path = os.path.join(current_app.static_folder, 'assets', 'login_picture.png')
logo_exists = os.path.exists(logo_path)
login_picture_exists = os.path.exists(login_picture_path)
# Get all users
users = User.query.order_by(User.username).all()
return render_template(
'admin/index.html',
users=users,
logo_exists=logo_exists,
login_picture_exists=login_picture_exists
)
@bp.route('/create_user', methods=['POST'])
@login_required
@admin_required
def create_user():
"""Create a new user"""
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
role = request.form.get('role', 'user')
# Validation
if not username or not password:
flash('Username and password are required.', 'danger')
return redirect(url_for('admin.index'))
if len(password) < 6:
flash('Password must be at least 6 characters long.', 'danger')
return redirect(url_for('admin.index'))
if role not in ['user', 'admin']:
flash('Invalid role specified.', 'danger')
return redirect(url_for('admin.index'))
# Check if user already exists
if User.query.filter_by(username=username).first():
flash(f'User "{username}" already exists.', 'danger')
return redirect(url_for('admin.index'))
try:
# Create new user
user = User(username=username, role=role)
user.set_password(password)
db.session.add(user)
db.session.commit()
log_user_created(username, role)
flash(f'User "{username}" created successfully.', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error creating user: {str(e)}', 'danger')
return redirect(url_for('admin.index'))
@bp.route('/delete_user/<int:user_id>', methods=['POST'])
@login_required
@admin_required
def delete_user(user_id):
"""Delete a user"""
# Prevent self-deletion
if user_id == current_user.id:
flash('You cannot delete your own account.', 'danger')
return redirect(url_for('admin.index'))
user = User.query.get_or_404(user_id)
username = user.username
try:
db.session.delete(user)
db.session.commit()
log_user_deleted(username)
flash(f'User "{username}" deleted successfully.', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error deleting user: {str(e)}', 'danger')
return redirect(url_for('admin.index'))
@bp.route('/change_role/<int:user_id>', methods=['POST'])
@login_required
@admin_required
def change_role(user_id):
"""Change user role"""
# Prevent changing own role
if user_id == current_user.id:
flash('You cannot change your own role.', 'danger')
return redirect(url_for('admin.index'))
user = User.query.get_or_404(user_id)
new_role = request.form.get('role')
if new_role not in ['user', 'admin']:
flash('Invalid role specified.', 'danger')
return redirect(url_for('admin.index'))
try:
old_role = user.role
user.role = new_role
db.session.commit()
log_action(f"User '{user.username}' role changed from '{old_role}' to '{new_role}'")
flash(f'User "{user.username}" role changed to "{new_role}".', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error changing user role: {str(e)}', 'danger')
return redirect(url_for('admin.index'))
@bp.route('/change_theme', methods=['POST'])
@login_required
def change_theme():
"""Change user theme"""
theme = request.form.get('theme', 'light')
if theme not in ['light', 'dark']:
flash('Invalid theme specified.', 'danger')
return redirect(request.referrer or url_for('admin.index'))
try:
current_user.theme = theme
db.session.commit()
flash(f'Theme changed to "{theme}".', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error changing theme: {str(e)}', 'danger')
return redirect(request.referrer or url_for('admin.index'))
@bp.route('/upload_assets', methods=['POST'])
@login_required
@admin_required
def upload_assets():
"""Upload logo and login picture"""
from flask import current_app
from werkzeug.utils import secure_filename
assets_folder = os.path.join(current_app.static_folder, 'assets')
os.makedirs(assets_folder, exist_ok=True)
# Handle logo upload
logo_file = request.files.get('logo')
if logo_file and logo_file.filename:
try:
logo_path = os.path.join(assets_folder, 'logo.png')
logo_file.save(logo_path)
flash('Logo uploaded successfully.', 'success')
log_action('Logo uploaded')
except Exception as e:
flash(f'Error uploading logo: {str(e)}', 'danger')
# Handle login picture upload
login_picture_file = request.files.get('login_picture')
if login_picture_file and login_picture_file.filename:
try:
login_picture_path = os.path.join(assets_folder, 'login_picture.png')
login_picture_file.save(login_picture_path)
flash('Login picture uploaded successfully.', 'success')
log_action('Login picture uploaded')
except Exception as e:
flash(f'Error uploading login picture: {str(e)}', 'danger')
return redirect(url_for('admin.index'))
@bp.route('/clean_unused_files', methods=['POST'])
@login_required
@admin_required
def clean_unused_files():
"""Clean unused files from uploads folder"""
from flask import current_app
from app.models.content import Content
try:
upload_folder = os.path.join(current_app.static_folder, 'uploads')
# Get all file names from database
content_files = {content.file_name for content in Content.query.all()}
# Get all files in upload folder
if os.path.exists(upload_folder):
all_files = set(os.listdir(upload_folder))
# Find unused files
unused_files = all_files - content_files
# Delete unused files
deleted_count = 0
for file_name in unused_files:
file_path = os.path.join(upload_folder, file_name)
if os.path.isfile(file_path):
try:
os.remove(file_path)
deleted_count += 1
except Exception as e:
print(f"Error deleting {file_path}: {e}")
flash(f'Cleaned {deleted_count} unused files.', 'success')
log_action(f'Cleaned {deleted_count} unused files')
else:
flash('Upload folder does not exist.', 'info')
except Exception as e:
flash(f'Error cleaning files: {str(e)}', 'danger')
return redirect(url_for('admin.index'))

153
app/routes/api.py Normal file
View File

@@ -0,0 +1,153 @@
"""
API routes for player clients
"""
from flask import Blueprint, request, jsonify, url_for
from app.models.player import Player
from app.models.content import Content
from app.extensions import bcrypt, db
bp = Blueprint('api', __name__)
@bp.route('/playlists', methods=['GET'])
def get_playlists():
"""Get playlist for a player"""
hostname = request.args.get('hostname')
quickconnect_code = request.args.get('quickconnect_code')
# Validate parameters
if not hostname or not quickconnect_code:
return jsonify({'error': 'Hostname and quick connect code are required'}), 400
# Find player and verify credentials
player = Player.query.filter_by(hostname=hostname).first()
if not player or not player.verify_quickconnect_code(quickconnect_code):
return jsonify({'error': 'Invalid hostname or quick connect code'}), 404
# Update last seen
player.last_seen = db.func.current_timestamp()
db.session.commit()
# Get content based on player's group status
if player.is_locked_to_group:
# Player is locked to a group - get shared content
group = player.locked_to_group
player_ids = [p.id for p in group.players]
# Get unique content by filename (first occurrence)
content_query = (
db.session.query(
Content.file_name,
db.func.min(Content.id).label('id'),
db.func.min(Content.duration).label('duration'),
db.func.min(Content.position).label('position'),
db.func.min(Content.content_type).label('content_type')
)
.filter(Content.player_id.in_(player_ids))
.group_by(Content.file_name)
)
content = db.session.query(Content).filter(
Content.id.in_([c.id for c in content_query])
).order_by(Content.position).all()
else:
# Individual player content
content = Content.query.filter_by(player_id=player.id).order_by(Content.position).all()
# Build playlist
playlist = []
for media in content:
playlist.append({
'file_name': media.file_name,
'url': url_for('content.media', filename=media.file_name, _external=True),
'duration': media.duration,
'content_type': media.content_type,
'position': media.position
})
return jsonify({
'playlist': playlist,
'playlist_version': player.playlist_version,
'hashed_quickconnect': player.quickconnect_password,
'player_id': player.id,
'player_name': player.username
})
@bp.route('/playlist_version', methods=['GET'])
def get_playlist_version():
"""Get playlist version for a player (for checking updates)"""
hostname = request.args.get('hostname')
quickconnect_code = request.args.get('quickconnect_code')
# Validate parameters
if not hostname or not quickconnect_code:
return jsonify({'error': 'Hostname and quick connect code are required'}), 400
# Find player and verify credentials
player = Player.query.filter_by(hostname=hostname).first()
if not player or not player.verify_quickconnect_code(quickconnect_code):
return jsonify({'error': 'Invalid hostname or quick connect code'}), 404
# Update last seen
player.last_seen = db.func.current_timestamp()
db.session.commit()
return jsonify({
'playlist_version': player.playlist_version,
'hashed_quickconnect': player.quickconnect_password
})
@bp.route('/player_status', methods=['POST'])
def update_player_status():
"""Update player status (heartbeat)"""
data = request.get_json()
if not data:
return jsonify({'error': 'JSON data required'}), 400
hostname = data.get('hostname')
quickconnect_code = data.get('quickconnect_code')
if not hostname or not quickconnect_code:
return jsonify({'error': 'Hostname and quick connect code are required'}), 400
# Find player and verify credentials
player = Player.query.filter_by(hostname=hostname).first()
if not player or not player.verify_quickconnect_code(quickconnect_code):
return jsonify({'error': 'Invalid hostname or quick connect code'}), 404
# Update player status
player.last_seen = db.func.current_timestamp()
player.is_active = True
# Optional: Update additional status info if provided
if 'status' in data:
# Could store additional status information in the future
pass
db.session.commit()
return jsonify({
'success': True,
'playlist_version': player.playlist_version,
'message': 'Status updated successfully'
})
@bp.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
'status': 'healthy',
'service': 'SKE Digital Signage Server',
'version': '2.0.0'
})
@bp.errorhandler(404)
def api_not_found(error):
"""API 404 handler"""
return jsonify({'error': 'API endpoint not found'}), 404
@bp.errorhandler(500)
def api_internal_error(error):
"""API 500 handler"""
return jsonify({'error': 'Internal server error'}), 500

111
app/routes/auth.py Normal file
View File

@@ -0,0 +1,111 @@
"""
Authentication routes
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, session
from flask_login import login_user, logout_user, login_required, current_user
from app.models.user import User
from app.extensions import db
from app.utils.logger import log_action
import os
bp = Blueprint('auth', __name__)
@bp.route('/login', methods=['GET', 'POST'])
def login():
"""User login"""
if current_user.is_authenticated:
return redirect(url_for('dashboard.index'))
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
if not username or not password:
flash('Please enter both username and password.', 'danger')
return render_template('auth/login.html')
user = User.query.filter_by(username=username).first()
if user and user.check_password(password) and user.is_active:
login_user(user)
user.last_login = db.func.current_timestamp()
db.session.commit()
log_action(f"User '{username}' logged in")
# Redirect to next page or dashboard
next_page = request.args.get('next')
if next_page:
return redirect(next_page)
return redirect(url_for('dashboard.index'))
else:
flash('Invalid username or password.', 'danger')
log_action(f"Failed login attempt for username '{username}'", level='WARNING')
# Check if login picture exists
from flask import current_app
login_picture_path = os.path.join(current_app.static_folder, 'assets', 'login_picture.png')
login_picture_exists = os.path.exists(login_picture_path)
return render_template('auth/login.html', login_picture_exists=login_picture_exists)
@bp.route('/logout')
@login_required
def logout():
"""User logout"""
username = current_user.username
logout_user()
log_action(f"User '{username}' logged out")
flash('You have been logged out successfully.', 'info')
return redirect(url_for('auth.login'))
@bp.route('/register', methods=['GET', 'POST'])
def register():
"""User registration (for development only)"""
from flask import current_app
# Only allow registration in development mode
if not current_app.config.get('DEBUG', False):
flash('Registration is disabled.', 'danger')
return redirect(url_for('auth.login'))
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
confirm_password = request.form.get('confirm_password', '')
# Validation
if not username or not password:
flash('Please enter both username and password.', 'danger')
return render_template('auth/register.html')
if password != confirm_password:
flash('Passwords do not match.', 'danger')
return render_template('auth/register.html')
if len(password) < 6:
flash('Password must be at least 6 characters long.', 'danger')
return render_template('auth/register.html')
# Check if user already exists
if User.query.filter_by(username=username).first():
flash('Username already exists.', 'danger')
return render_template('auth/register.html')
# Create new user
try:
user = User(username=username, role='user')
user.set_password(password)
db.session.add(user)
db.session.commit()
log_action(f"New user '{username}' registered")
flash('Registration successful! Please log in.', 'success')
return redirect(url_for('auth.login'))
except Exception as e:
db.session.rollback()
flash(f'Registration failed: {str(e)}', 'danger')
return render_template('auth/register.html')

232
app/routes/content.py Normal file
View File

@@ -0,0 +1,232 @@
"""
Content management routes
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, send_from_directory, current_app
from flask_login import login_required
from app.models.content import Content
from app.models.player import Player
from app.models.group import Group
from app.extensions import db
from app.utils.uploads import process_uploaded_files
from app.routes.admin import admin_required
import os
bp = Blueprint('content', __name__)
@bp.route('/upload', methods=['GET', 'POST'])
@login_required
@admin_required
def upload():
"""Upload content to players or groups"""
if request.method == 'POST':
target_type = request.form.get('target_type')
target_id = request.form.get('target_id')
files = request.files.getlist('files')
duration = request.form.get('duration', 10, type=int)
return_url = request.form.get('return_url', url_for('dashboard.index'))
# Validation
if not target_type or not target_id:
flash('Please select a target type and target.', 'danger')
return redirect(url_for('content.upload'))
if not files or all(not file.filename for file in files):
flash('Please select at least one file to upload.', 'danger')
return redirect(url_for('content.upload'))
if duration < 1:
flash('Duration must be at least 1 second.', 'danger')
return redirect(url_for('content.upload'))
try:
target_id = int(target_id)
except ValueError:
flash('Invalid target ID.', 'danger')
return redirect(url_for('content.upload'))
# Process files
results = process_uploaded_files(
app=current_app,
files=files,
duration=duration,
target_type=target_type,
target_id=target_id
)
# Show results
if results['success']:
flash(f'Successfully uploaded {len(results["success"])} files.', 'success')
if results['errors']:
for error in results['errors']:
flash(f'Error: {error}', 'danger')
return redirect(return_url)
# GET request - show upload form
target_type = request.args.get('target_type')
target_id = request.args.get('target_id')
return_url = request.args.get('return_url', url_for('dashboard.index'))
players = Player.query.order_by(Player.username).all()
groups = Group.query.order_by(Group.name).all()
return render_template(
'content/upload.html',
target_type=target_type,
target_id=target_id,
players=players,
groups=groups,
return_url=return_url
)
@bp.route('/<int:content_id>/edit', methods=['POST'])
@login_required
def edit(content_id):
"""Edit content duration"""
content = Content.query.get_or_404(content_id)
new_duration = request.form.get('duration', type=int)
if not new_duration or new_duration < 1:
flash('Duration must be at least 1 second.', 'danger')
return redirect(request.referrer or url_for('dashboard.index'))
try:
content.duration = new_duration
# Update playlist version for the player
content.player.increment_playlist_version()
db.session.commit()
flash(f'Content duration updated to {new_duration} seconds.', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error updating content: {str(e)}', 'danger')
return redirect(request.referrer or url_for('dashboard.index'))
@bp.route('/<int:content_id>/delete', methods=['POST'])
@login_required
@admin_required
def delete(content_id):
"""Delete content"""
content = Content.query.get_or_404(content_id)
player_id = content.player_id
file_name = content.file_name
try:
# Delete file from disk
file_path = os.path.join(current_app.static_folder, 'uploads', content.file_name)
if os.path.exists(file_path):
# Check if file is used by other content
other_content = Content.query.filter(
Content.file_name == content.file_name,
Content.id != content_id
).first()
if not other_content:
os.remove(file_path)
# Delete from database
db.session.delete(content)
# Update playlist version for the player
player = Player.query.get(player_id)
if player:
player.increment_playlist_version()
db.session.commit()
flash(f'Content "{file_name}" deleted successfully.', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error deleting content: {str(e)}', 'danger')
return redirect(request.referrer or url_for('dashboard.index'))
@bp.route('/media/<path:filename>')
def media(filename):
"""Serve media files"""
upload_folder = os.path.join(current_app.static_folder, 'uploads')
return send_from_directory(upload_folder, filename)
@bp.route('/group/<int:group_id>/media/<int:content_id>/edit', methods=['POST'])
@login_required
@admin_required
def edit_group_media(group_id, content_id):
"""Edit media duration for group content"""
group = Group.query.get_or_404(group_id)
content = Content.query.get_or_404(content_id)
new_duration = request.form.get('duration', type=int)
if not new_duration or new_duration < 1:
flash('Duration must be at least 1 second.', 'danger')
return redirect(url_for('group.manage', group_id=group_id))
try:
# Update duration for all content with the same filename in the group
player_ids = [player.id for player in group.players]
Content.query.filter(
Content.player_id.in_(player_ids),
Content.file_name == content.file_name
).update({Content.duration: new_duration})
# Update playlist version for group
group.increment_playlist_version()
db.session.commit()
flash('Media duration updated successfully.', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error updating media duration: {str(e)}', 'danger')
return redirect(url_for('group.manage', group_id=group_id))
@bp.route('/group/<int:group_id>/media/<int:content_id>/delete', methods=['POST'])
@login_required
@admin_required
def delete_group_media(group_id, content_id):
"""Delete media from group"""
group = Group.query.get_or_404(group_id)
content = Content.query.get_or_404(content_id)
file_name = content.file_name
try:
player_ids = [player.id for player in group.players]
# Get all content with the same filename in the group
group_content = Content.query.filter(
Content.player_id.in_(player_ids),
Content.file_name == file_name
).all()
# Delete all instances
for content_item in group_content:
db.session.delete(content_item)
# Check if file is used elsewhere
other_content = Content.query.filter(
~Content.player_id.in_(player_ids),
Content.file_name == file_name
).first()
# Delete file if not used elsewhere
if not other_content:
file_path = os.path.join(current_app.static_folder, 'uploads', file_name)
if os.path.exists(file_path):
os.remove(file_path)
# Update playlist version for group
group.increment_playlist_version()
db.session.commit()
flash('Media deleted successfully.', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error deleting media: {str(e)}', 'danger')
return redirect(url_for('group.manage', group_id=group_id))

35
app/routes/dashboard.py Normal file
View File

@@ -0,0 +1,35 @@
"""
Dashboard routes
"""
from flask import Blueprint, render_template
from flask_login import login_required
from app.models.player import Player
from app.models.group import Group
from app.utils.logger import get_recent_logs
import os
bp = Blueprint('dashboard', __name__)
@bp.route('/')
@login_required
def index():
"""Main dashboard"""
players = Player.query.order_by(Player.username).all()
groups = Group.query.order_by(Group.name).all()
# Check if logo exists
from flask import current_app
logo_path = os.path.join(current_app.static_folder, 'assets', 'logo.png')
logo_exists = os.path.exists(logo_path)
# Get recent server logs
server_logs = get_recent_logs(20)
return render_template(
'dashboard/index.html',
players=players,
groups=groups,
logo_exists=logo_exists,
server_logs=server_logs
)

152
app/routes/group.py Normal file
View File

@@ -0,0 +1,152 @@
"""
Group management routes
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required
from app.models.group import Group
from app.models.player import Player
from app.utils.group_management import (
create_group, edit_group, delete_group,
get_group_content, update_group_content_order
)
from app.routes.admin import admin_required
bp = Blueprint('group', __name__)
@bp.route('/')
@login_required
@admin_required
def index():
"""List all groups"""
groups = Group.query.order_by(Group.name).all()
return render_template('group/index.html', groups=groups)
@bp.route('/create', methods=['GET', 'POST'])
@login_required
@admin_required
def create():
"""Create new group"""
if request.method == 'POST':
name = request.form.get('name', '').strip()
description = request.form.get('description', '').strip()
player_ids = request.form.getlist('players')
# Validation
if not name:
flash('Group name is required.', 'danger')
players = Player.query.order_by(Player.username).all()
return render_template('group/create.html', players=players)
# Convert player IDs to integers
try:
player_ids = [int(pid) for pid in player_ids if pid]
except ValueError:
flash('Invalid player selection.', 'danger')
players = Player.query.order_by(Player.username).all()
return render_template('group/create.html', players=players)
# Create group
success, result = create_group(
name=name,
player_ids=player_ids,
description=description if description else None
)
if success:
flash(f'Group "{name}" created successfully.', 'success')
return redirect(url_for('group.manage', group_id=result.id))
else:
flash(f'Error creating group: {result}', 'danger')
players = Player.query.order_by(Player.username).all()
return render_template('group/create.html', players=players)
@bp.route('/<int:group_id>')
@login_required
@admin_required
def manage(group_id):
"""Manage group content"""
group = Group.query.get_or_404(group_id)
content = get_group_content(group_id)
return render_template('group/manage.html', group=group, content=content)
@bp.route('/<int:group_id>/edit', methods=['GET', 'POST'])
@login_required
@admin_required
def edit(group_id):
"""Edit group"""
group = Group.query.get_or_404(group_id)
if request.method == 'POST':
name = request.form.get('name', '').strip()
description = request.form.get('description', '').strip()
player_ids = request.form.getlist('players')
# Convert player IDs to integers
try:
player_ids = [int(pid) for pid in player_ids if pid]
except ValueError:
flash('Invalid player selection.', 'danger')
players = Player.query.order_by(Player.username).all()
return render_template('group/edit.html', group=group, players=players)
# Update group
success, result = edit_group(
group_id=group_id,
name=name if name else None,
player_ids=player_ids,
description=description
)
if success:
flash(f'Group "{result.name}" updated successfully.', 'success')
return redirect(url_for('group.manage', group_id=group_id))
else:
flash(f'Error updating group: {result}', 'danger')
players = Player.query.order_by(Player.username).all()
return render_template('group/edit.html', group=group, players=players)
@bp.route('/<int:group_id>/delete', methods=['POST'])
@login_required
@admin_required
def delete(group_id):
"""Delete group"""
group = Group.query.get_or_404(group_id)
group_name = group.name
success, error = delete_group(group_id)
if success:
flash(f'Group "{group_name}" deleted successfully.', 'success')
else:
flash(f'Error deleting group: {error}', 'danger')
return redirect(url_for('dashboard.index'))
@bp.route('/<int:group_id>/fullscreen')
@login_required
def fullscreen(group_id):
"""Group fullscreen view"""
group = Group.query.get_or_404(group_id)
content = get_group_content(group_id)
return render_template('group/fullscreen.html', group=group, content=content)
@bp.route('/<int:group_id>/update_order', methods=['POST'])
@login_required
@admin_required
def update_order(group_id):
"""Update content order for group"""
if not request.is_json:
return jsonify({'success': False, 'error': 'Invalid request format'}), 400
items = request.json.get('items', [])
success, error = update_group_content_order(group_id, items)
if success:
return jsonify({'success': True})
else:
return jsonify({'success': False, 'error': error}), 500

167
app/routes/player.py Normal file
View File

@@ -0,0 +1,167 @@
"""
Player management routes
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from app.models.player import Player
from app.models.content import Content
from app.utils.player_management import (
create_player, edit_player, delete_player,
get_player_content, update_player_content_order,
verify_player_credentials
)
from app.routes.admin import admin_required
bp = Blueprint('player', __name__)
@bp.route('/')
@login_required
@admin_required
def index():
"""List all players"""
players = Player.query.order_by(Player.username).all()
return render_template('player/index.html', players=players)
@bp.route('/add', methods=['GET', 'POST'])
@login_required
@admin_required
def add():
"""Add new player"""
if request.method == 'POST':
username = request.form.get('username', '').strip()
hostname = request.form.get('hostname', '').strip()
password = request.form.get('password', '')
quickconnect_password = request.form.get('quickconnect_password', '')
# Validation
if not username or not hostname or not password:
flash('Username, hostname, and password are required.', 'danger')
return render_template('player/add.html')
# Create player
success, result = create_player(
username=username,
hostname=hostname,
password=password,
quickconnect_password=quickconnect_password if quickconnect_password else None
)
if success:
flash(f'Player "{username}" created successfully.', 'success')
return redirect(url_for('player.view', player_id=result.id))
else:
flash(f'Error creating player: {result}', 'danger')
return render_template('player/add.html')
@bp.route('/<int:player_id>')
@login_required
def view(player_id):
"""View player details and content"""
player = Player.query.get_or_404(player_id)
content = get_player_content(player_id)
return render_template('player/view.html', player=player, content=content)
@bp.route('/<int:player_id>/edit', methods=['GET', 'POST'])
@login_required
@admin_required
def edit(player_id):
"""Edit player"""
player = Player.query.get_or_404(player_id)
if request.method == 'POST':
username = request.form.get('username', '').strip()
hostname = request.form.get('hostname', '').strip()
password = request.form.get('password', '')
quickconnect_password = request.form.get('quickconnect_password', '')
# Update player
success, result = edit_player(
player_id=player_id,
username=username if username else None,
hostname=hostname if hostname else None,
password=password if password else None,
quickconnect_password=quickconnect_password if quickconnect_password else None
)
if success:
flash(f'Player "{result.username}" updated successfully.', 'success')
return redirect(url_for('player.view', player_id=player_id))
else:
flash(f'Error updating player: {result}', 'danger')
return_url = request.args.get('return_url', url_for('player.view', player_id=player_id))
return render_template('player/edit.html', player=player, return_url=return_url)
@bp.route('/<int:player_id>/delete', methods=['POST'])
@login_required
@admin_required
def delete(player_id):
"""Delete player"""
player = Player.query.get_or_404(player_id)
username = player.username
success, error = delete_player(player_id)
if success:
flash(f'Player "{username}" deleted successfully.', 'success')
else:
flash(f'Error deleting player: {error}', 'danger')
return redirect(url_for('dashboard.index'))
@bp.route('/<int:player_id>/fullscreen', methods=['GET', 'POST'])
def fullscreen(player_id):
"""Player fullscreen view with authentication"""
player = Player.query.get_or_404(player_id)
if request.method == 'POST':
hostname = request.form.get('hostname', '')
password = request.form.get('password', '')
quickconnect_code = request.form.get('quickconnect_password', '')
# Verify credentials
if quickconnect_code:
success, result = verify_player_credentials(hostname, None, quickconnect_code)
else:
success, result = verify_player_credentials(hostname, password)
if success and result.id == player_id:
authenticated = True
else:
authenticated = False
flash('Invalid credentials.', 'danger')
else:
authenticated = current_user.is_authenticated
if authenticated:
content = get_player_content(player_id)
return render_template('player/fullscreen.html', player=player, content=content)
else:
return render_template('player/auth.html', player=player)
@bp.route('/<int:player_id>/update_order', methods=['POST'])
@login_required
def update_order(player_id):
"""Update content order for player"""
if not request.is_json:
return jsonify({'success': False, 'error': 'Invalid request format'}), 400
player = Player.query.get_or_404(player_id)
# Check if player is locked to a group (only admin can reorder)
if player.is_locked_to_group and not current_user.is_admin:
return jsonify({
'success': False,
'error': 'Cannot reorder playlist for players locked to groups'
}), 403
items = request.json.get('items', [])
success, error, new_version = update_player_content_order(player_id, items)
if success:
return jsonify({'success': True, 'new_version': new_version})
else:
return jsonify({'success': False, 'error': error}), 500