updated first commit
This commit is contained in:
14
app/routes/__init__.py
Normal file
14
app/routes/__init__.py
Normal 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
245
app/routes/admin.py
Normal 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
153
app/routes/api.py
Normal 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
111
app/routes/auth.py
Normal 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
232
app/routes/content.py
Normal 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
35
app/routes/dashboard.py
Normal 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
152
app/routes/group.py
Normal 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
167
app/routes/player.py
Normal 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
|
||||
Reference in New Issue
Block a user