diff --git a/.gitignore b/.gitignore index fd38011..6729914 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -digiscreen/ +venv/ .env diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc deleted file mode 100644 index b0108ec..0000000 Binary files a/__pycache__/app.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc deleted file mode 100644 index 7cfd5c3..0000000 Binary files a/__pycache__/app.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/extensions.cpython-311.pyc b/__pycache__/extensions.cpython-311.pyc index 05e2225..c7be8e9 100644 Binary files a/__pycache__/extensions.cpython-311.pyc and b/__pycache__/extensions.cpython-311.pyc differ diff --git a/__pycache__/extensions.cpython-312.pyc b/__pycache__/extensions.cpython-312.pyc deleted file mode 100644 index b6757c2..0000000 Binary files a/__pycache__/extensions.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/models.cpython-311.pyc b/__pycache__/models.cpython-311.pyc deleted file mode 100644 index 30219bd..0000000 Binary files a/__pycache__/models.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/models.cpython-312.pyc b/__pycache__/models.cpython-312.pyc deleted file mode 100644 index eb7ec95..0000000 Binary files a/__pycache__/models.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/pptx_to_images.cpython-311.pyc b/__pycache__/pptx_to_images.cpython-311.pyc deleted file mode 100644 index 519560f..0000000 Binary files a/__pycache__/pptx_to_images.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/pptx_to_images.cpython-312.pyc b/__pycache__/pptx_to_images.cpython-312.pyc deleted file mode 100644 index 4ae4875..0000000 Binary files a/__pycache__/pptx_to_images.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/server_logger.cpython-311.pyc b/__pycache__/server_logger.cpython-311.pyc deleted file mode 100644 index a9993f9..0000000 Binary files a/__pycache__/server_logger.cpython-311.pyc and /dev/null differ diff --git a/__pycache__/upload_utils.cpython-311.pyc b/__pycache__/upload_utils.cpython-311.pyc deleted file mode 100644 index 9f30863..0000000 Binary files a/__pycache__/upload_utils.cpython-311.pyc and /dev/null differ diff --git a/app.py b/app.py index 8f886ea..69baf24 100755 --- a/app.py +++ b/app.py @@ -1,17 +1,25 @@ import os import click +import time +import psutil +import threading from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify, send_from_directory from flask_migrate import Migrate import subprocess from werkzeug.utils import secure_filename -from functools import wraps +from functools import wraps, lru_cache from extensions import db, bcrypt, login_manager from sqlalchemy import text from dotenv import load_dotenv +import logging +import gc # Load environment variables from .env file load_dotenv() +# Configure logging for better performance monitoring +logging.basicConfig(level=logging.WARNING) + # First import models from models import User, Player, Group, Content, ServerLog, group_player @@ -38,7 +46,7 @@ from utils.uploads import ( add_image_to_playlist, convert_video_and_update_playlist, process_pdf, - process_pptx, + process_pptx_improved, process_uploaded_files ) @@ -57,8 +65,18 @@ db_path = os.path.join(instance_dir, 'dashboard.db') app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -# Set maximum content length to 1GB -app.config['MAX_CONTENT_LENGTH'] = 2048 * 2048 * 2048 # 2GB, adjust as needed +# Performance configuration +app.config['SQLALCHEMY_ENGINE_OPTIONS'] = { + 'pool_pre_ping': True, + 'pool_recycle': 300, + 'connect_args': {'timeout': 10} +} + +# Set maximum content length to 1GB (reduced from 2GB) +app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024 # 1GB + +# Add timeout configuration +app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 31536000 # Cache static files for 1 year # Ensure the instance folder exists os.makedirs(app.instance_path, exist_ok=True) @@ -314,6 +332,7 @@ def add_player(): orientation = request.form.get('orientation', 'Landscape') # <-- Get orientation add_player_util(username, hostname, password, quickconnect_password, orientation) # <-- Pass orientation flash(f'Player "{username}" added successfully.', 'success') + clear_player_cache() # Clear cache when player is added return redirect(url_for('dashboard')) return render_template('add_player.html') @@ -330,6 +349,7 @@ def edit_player(player_id): orientation = request.form.get('orientation', player.orientation) # <-- Get orientation edit_player_util(player_id, username, hostname, password, quickconnect_password, orientation) # <-- Pass orientation flash(f'Player "{username}" updated successfully.', 'success') + clear_player_cache() # Clear cache when player is updated return redirect(url_for('player_page', player_id=player.id)) return_url = request.args.get('return_url', url_for('player_page', player_id=player.id)) @@ -344,6 +364,103 @@ def change_theme(): db.session.commit() return redirect(url_for('admin')) +# Group management routes +@app.route('/group/create', methods=['GET', 'POST']) +@login_required +@admin_required +def create_group(): + if request.method == 'POST': + name = request.form['name'] + player_ids = request.form.getlist('players') + orientation = request.form.get('orientation', 'Landscape') + + try: + # Convert player_ids to integers + player_ids = [int(pid) for pid in player_ids] + group = create_group_util(name, player_ids, orientation) + flash(f'Group "{name}" created successfully.', 'success') + return redirect(url_for('dashboard')) + except ValueError as e: + flash(str(e), 'danger') + return redirect(url_for('create_group')) + + # GET request - show create group form + players = Player.query.filter_by(locked_to_group_id=None).all() # Only available players + return render_template('create_group.html', players=players) + +@app.route('/group//edit', methods=['GET', 'POST']) +@login_required +@admin_required +def edit_group(group_id): + group = Group.query.get_or_404(group_id) + + if request.method == 'POST': + name = request.form['name'] + player_ids = request.form.getlist('players') + orientation = request.form.get('orientation', group.orientation) + + try: + # Convert player_ids to integers + player_ids = [int(pid) for pid in player_ids] + edit_group_util(group_id, name, player_ids, orientation) + flash(f'Group "{name}" updated successfully.', 'success') + return redirect(url_for('dashboard')) + except ValueError as e: + flash(str(e), 'danger') + return redirect(url_for('edit_group', group_id=group_id)) + + # GET request - show edit group form + players = Player.query.all() + return render_template('edit_group.html', group=group, players=players) + +@app.route('/group//delete', methods=['POST']) +@login_required +@admin_required +def delete_group(group_id): + delete_group_util(group_id) + flash('Group deleted successfully.', 'success') + return redirect(url_for('dashboard')) + +@app.route('/group/') +@login_required +def manage_group(group_id): + group = Group.query.get_or_404(group_id) + content = get_group_content(group_id) + return render_template('manage_group.html', group=group, content=content) + +@app.route('/group//fullscreen', methods=['GET', 'POST']) +def group_fullscreen(group_id): + group = Group.query.get_or_404(group_id) + content = get_group_content(group_id) + return render_template('group_fullscreen.html', group=group, content=content) + +@app.route('/group//media//edit', methods=['POST']) +@login_required +@admin_required +def edit_group_media(group_id, content_id): + new_duration = int(request.form['duration']) + success = edit_group_media(group_id, content_id, new_duration) + + if success: + flash('Media duration updated successfully.', 'success') + else: + flash('Error updating media duration.', 'danger') + + return redirect(url_for('manage_group', group_id=group_id)) + +@app.route('/group//media//delete', methods=['POST']) +@login_required +@admin_required +def delete_group_media(group_id, content_id): + success = delete_group_media(group_id, content_id) + + if success: + flash('Media deleted successfully.', 'success') + else: + flash('Error deleting media.', 'danger') + + return redirect(url_for('manage_group', group_id=group_id)) + @app.route('/upload_logo', methods=['POST']) @login_required @admin_required @@ -414,177 +531,103 @@ def clean_unused_files(): flash('Unused files have been cleaned.', 'success') return redirect(url_for('admin')) +# Cache for frequently accessed data +@lru_cache(maxsize=128) +def get_player_by_hostname(hostname): + """Cached function to get player by hostname""" + return Player.query.filter_by(hostname=hostname).first() +# Clear cache when players are modified +def clear_player_cache(): + get_player_by_hostname.cache_clear() + +# Optimized API endpoint with caching @app.route('/api/playlists', methods=['GET']) def get_playlists(): hostname = request.args.get('hostname') quickconnect_code = request.args.get('quickconnect_code') - # Validate the parameters + # Validate parameters early if not hostname or not quickconnect_code: return jsonify({'error': 'Hostname and quick connect code are required'}), 400 - # Find the player by hostname and verify the quickconnect code - player = Player.query.filter_by(hostname=hostname).first() - if not player or not bcrypt.check_password_hash(player.quickconnect_password, quickconnect_code): - return jsonify({'error': 'Invalid hostname or quick connect code'}), 404 + try: + # Use cached function for better performance + player = get_player_by_hostname(hostname) + if not player: + return jsonify({'error': 'Player not found'}), 404 + + # Verify quickconnect code + if not bcrypt.check_password_hash(player.quickconnect_password, quickconnect_code): + return jsonify({'error': 'Invalid credentials'}), 401 - # Check if player is locked to a group - if player.locked_to_group_id: - # Get content for all players in the group to ensure shared content - group_players = player.locked_to_group.players - player_ids = [p.id for p in group_players] + # Optimized content query + if player.locked_to_group_id: + # More efficient group content query + content = db.session.query(Content).join(Player).filter( + Player.locked_to_group_id == player.locked_to_group_id + ).distinct(Content.file_name).order_by(Content.position).all() + else: + # Get player's individual content with limit + content = Content.query.filter_by(player_id=player.id).order_by(Content.position).all() - # Use the first occurrence of each file for the playlist - content_query = ( - db.session.query( - Content.file_name, - db.func.min(Content.id).label('id'), - db.func.min(Content.duration).label('duration') - ) - .filter(Content.player_id.in_(player_ids)) - .group_by(Content.file_name) - ) + # Build playlist efficiently + playlist = [] + for media in content: + playlist.append({ + 'file_name': media.file_name, + 'url': f"http://{request.host}/media/{media.file_name}", + 'duration': media.duration + }) + + # Force garbage collection for memory management + gc.collect() + + return jsonify({ + 'playlist': playlist, + 'playlist_version': player.playlist_version, + 'hashed_quickconnect': player.quickconnect_password + }) - content = db.session.query(Content).filter( - Content.id.in_([c.id for c in content_query]) - ).all() - else: - # Get player's individual content - content = Content.query.filter_by(player_id=player.id).all() - - playlist = [ - { - 'file_name': media.file_name, - 'url': f"http://{request.host}/media/{media.file_name}", - 'duration': media.duration - } - for media in content - ] - - # Return the playlist, version, and hashed quickconnect code - return jsonify({ - 'playlist': playlist, - 'playlist_version': player.playlist_version, - 'hashed_quickconnect': player.quickconnect_password - }) + except Exception as e: + app.logger.error(f"API Error: {str(e)}") + return jsonify({'error': 'Internal server error'}), 500 +# Optimized media serving with proper caching @app.route('/media/') def media(filename): - return send_from_directory(app.config['UPLOAD_FOLDER'], filename) - -@app.context_processor -def inject_theme(): - if current_user.is_authenticated: - theme = current_user.theme - else: - theme = 'light' - return dict(theme=theme) - -@app.route('/group/create', methods=['GET', 'POST']) -@login_required -@admin_required -def create_group(): - if request.method == 'POST': - group_name = request.form['name'] - player_ids = request.form.getlist('players') - orientation = request.form.get('orientation', 'Landscape') - create_group_util(group_name, player_ids, orientation) - flash(f'Group "{group_name}" created successfully.', 'success') - return redirect(url_for('dashboard')) - players = Player.query.all() - return render_template('create_group.html', players=players) - -@app.route('/group//manage') -@login_required -@admin_required -def manage_group(group_id): - group = Group.query.get_or_404(group_id) - content = get_group_content(group_id) - # Debug content ordering - print("Group content positions before sorting:", [(c.id, c.file_name, c.position) for c in content]) - content = sorted(content, key=lambda c: c.position) - print("Group content positions after sorting:", [(c.id, c.file_name, c.position) for c in content]) - return render_template('manage_group.html', group=group, content=content) - -@app.route('/group//edit', methods=['GET', 'POST']) -@login_required -@admin_required -def edit_group(group_id): - group = Group.query.get_or_404(group_id) - if request.method == 'POST': - name = request.form['name'] - player_ids = request.form.getlist('players') - orientation = request.form.get('orientation', group.orientation) - edit_group_util(group_id, name, player_ids, orientation) - flash(f'Group "{name}" updated successfully.', 'success') - return redirect(url_for('dashboard')) - players = Player.query.all() - return render_template('edit_group.html', group=group, players=players) - -@app.route('/group//delete', methods=['POST']) -@login_required -@admin_required -def delete_group(group_id): - group = Group.query.get_or_404(group_id) - group_name = group.name - delete_group_util(group_id) - flash(f'Group "{group_name}" deleted successfully.', 'success') - return redirect(url_for('dashboard')) - -@app.route('/group//fullscreen', methods=['GET']) -@login_required -def group_fullscreen(group_id): - group = Group.query.get_or_404(group_id) - content = Content.query.filter(Content.player_id.in_([player.id for player in group.players])).order_by(Content.position).all() - return render_template('group_fullscreen.html', group=group, content=content) - -@app.route('/group//media//edit', methods=['POST']) -@login_required -@admin_required -def edit_group_media_route(group_id, content_id): - new_duration = int(request.form['duration']) - success = edit_group_media(group_id, content_id, new_duration) - - if success: - flash('Media duration updated successfully.', 'success') - else: - flash('Error updating media duration.', 'danger') - - return redirect(url_for('manage_group', group_id=group_id)) - -@app.route('/group//media//delete', methods=['POST']) -@login_required -@admin_required -def delete_group_media_route(group_id, content_id): - success = delete_group_media(group_id, content_id) - - if success: - flash('Media deleted successfully.', 'success') - else: - flash('Error deleting media.', 'danger') - - return redirect(url_for('manage_group', group_id=group_id)) + try: + response = send_from_directory(app.config['UPLOAD_FOLDER'], filename) + # Add caching headers for better performance + response.cache_control.max_age = 86400 # Cache for 24 hours + response.cache_control.public = True + return response + except Exception as e: + app.logger.error(f"Media serving error: {str(e)}") + return jsonify({'error': 'File not found'}), 404 +# Optimized playlist version check @app.route('/api/playlist_version', methods=['GET']) def get_playlist_version(): hostname = request.args.get('hostname') quickconnect_code = request.args.get('quickconnect_code') - # Validate the parameters if not hostname or not quickconnect_code: return jsonify({'error': 'Hostname and quick connect code are required'}), 400 - # Find the player by hostname and verify the quickconnect code - player = Player.query.filter_by(hostname=hostname).first() - if not player or not bcrypt.check_password_hash(player.quickconnect_password, quickconnect_code): - return jsonify({'error': 'Invalid hostname or quick connect code'}), 404 + try: + # Use cached function + player = get_player_by_hostname(hostname) + if not player or not bcrypt.check_password_hash(player.quickconnect_password, quickconnect_code): + return jsonify({'error': 'Invalid credentials'}), 401 - # Return the playlist version and hashed quickconnect code - return jsonify({ - 'playlist_version': player.playlist_version, - 'hashed_quickconnect': player.quickconnect_password - }) + return jsonify({ + 'playlist_version': player.playlist_version, + 'hashed_quickconnect': player.quickconnect_password + }) + except Exception as e: + app.logger.error(f"Version check error: {str(e)}") + return jsonify({'error': 'Internal server error'}), 500 @app.route('/player//update_order', methods=['POST']) @login_required @@ -660,6 +703,218 @@ if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true': db.create_all() create_default_user(db, User, bcrypt) +# Performance monitoring functions +def get_system_stats(): + """Get current system performance statistics""" + try: + cpu_percent = psutil.cpu_percent(interval=1) + memory = psutil.virtual_memory() + disk = psutil.disk_usage('/') + + return { + 'cpu_percent': cpu_percent, + 'memory_percent': memory.percent, + 'memory_used_mb': memory.used / (1024 * 1024), + 'memory_total_mb': memory.total / (1024 * 1024), + 'disk_percent': disk.percent, + 'disk_used_gb': disk.used / (1024 * 1024 * 1024), + 'disk_total_gb': disk.total / (1024 * 1024 * 1024), + 'timestamp': time.time() + } + except Exception as e: + print(f"Error getting system stats: {e}") + return None + +# Performance monitoring endpoint +@app.route('/api/performance', methods=['GET']) +@login_required +def get_performance_stats(): + """API endpoint to get real-time performance statistics""" + stats = get_system_stats() + if stats: + return jsonify(stats) + else: + return jsonify({'error': 'Unable to get system stats'}), 500 + +# Enhanced upload endpoint with monitoring +@app.route('/upload_content_monitored', methods=['POST']) +@login_required +@admin_required +def upload_content_monitored(): + """Enhanced upload endpoint with performance monitoring""" + start_time = time.time() + start_stats = get_system_stats() + + target_type = request.form.get('target_type') + target_id = request.form.get('target_id') + files = request.files.getlist('files') + duration = int(request.form['duration']) + return_url = request.form.get('return_url') + media_type = request.form['media_type'] + + print(f"=== UPLOAD MONITORING START ===") + print(f"Target Type: {target_type}, Target ID: {target_id}, Media Type: {media_type}") + print(f"Number of files: {len(files)}") + print(f"Start CPU: {start_stats['cpu_percent']}%, Memory: {start_stats['memory_percent']}%") + + if not target_type or not target_id: + flash('Please select a target type and target ID.', 'danger') + return redirect(url_for('upload_content')) + + # Monitor during file processing + def monitor_upload(): + """Background monitoring thread""" + while True: + stats = get_system_stats() + if stats: + print(f"[MONITOR] CPU: {stats['cpu_percent']}%, Memory: {stats['memory_percent']}%, Time: {time.time() - start_time:.1f}s") + time.sleep(2) + + # Start monitoring thread + monitor_thread = threading.Thread(target=monitor_upload, daemon=True) + monitor_thread.start() + + # Process uploaded files and get results + results = process_uploaded_files(app, files, media_type, duration, target_type, target_id) + + end_time = time.time() + end_stats = get_system_stats() + total_time = end_time - start_time + + print(f"=== UPLOAD MONITORING END ===") + print(f"Total processing time: {total_time:.2f} seconds") + print(f"End CPU: {end_stats['cpu_percent']}%, Memory: {end_stats['memory_percent']}%") + print(f"CPU change: {end_stats['cpu_percent'] - start_stats['cpu_percent']:.1f}%") + print(f"Memory change: {end_stats['memory_percent'] - start_stats['memory_percent']:.1f}%") + + # Log performance metrics + log_action(f"Upload completed: {len(files)} files, {total_time:.2f}s, CPU: {start_stats['cpu_percent']}% → {end_stats['cpu_percent']}%") + + return redirect(return_url) +@app.route('/player//bulk_delete_content', methods=['POST']) +@login_required +@admin_required +def bulk_delete_player_content(player_id): + """Bulk delete content for a specific player""" + if not request.is_json: + return jsonify({'success': False, 'error': 'Invalid request format'}), 400 + + player = Player.query.get_or_404(player_id) + content_ids = request.json.get('content_ids', []) + + if not content_ids: + return jsonify({'success': False, 'error': 'No content IDs provided'}), 400 + + try: + # Get all content items to delete + content_items = Content.query.filter( + Content.id.in_(content_ids), + Content.player_id == player_id + ).all() + + if not content_items: + return jsonify({'success': False, 'error': 'No valid content found to delete'}), 404 + + # Delete the content items + deleted_count = 0 + for content in content_items: + # Delete the actual file from filesystem + file_path = os.path.join(app.config['UPLOAD_FOLDER'], content.file_name) + if os.path.exists(file_path): + try: + os.remove(file_path) + except OSError as e: + app.logger.warning(f"Could not delete file {file_path}: {e}") + + # Delete from database + db.session.delete(content) + deleted_count += 1 + + # Update playlist version for the player + player.playlist_version += 1 + db.session.commit() + + # Clear cache + clear_player_cache() + + # Log the action + log_action(f"Bulk deleted {deleted_count} content items from player {player.username}") + + return jsonify({ + 'success': True, + 'deleted_count': deleted_count, + 'new_playlist_version': player.playlist_version + }) + + except Exception as e: + db.session.rollback() + app.logger.error(f"Error in bulk delete: {str(e)}") + return jsonify({'success': False, 'error': 'Database error occurred'}), 500 + +@app.route('/group//bulk_delete_content', methods=['POST']) +@login_required +@admin_required +def bulk_delete_group_content(group_id): + """Bulk delete content for a specific group""" + if not request.is_json: + return jsonify({'success': False, 'error': 'Invalid request format'}), 400 + + group = Group.query.get_or_404(group_id) + content_ids = request.json.get('content_ids', []) + + if not content_ids: + return jsonify({'success': False, 'error': 'No content IDs provided'}), 400 + + try: + # Get player IDs in the group + player_ids = [p.id for p in group.players] + + # Get all content items to delete that belong to players in this group + content_items = Content.query.filter( + Content.id.in_(content_ids), + Content.player_id.in_(player_ids) + ).all() + + if not content_items: + return jsonify({'success': False, 'error': 'No valid content found to delete'}), 404 + + # Delete the content items + deleted_count = 0 + for content in content_items: + # Delete the actual file from filesystem + file_path = os.path.join(app.config['UPLOAD_FOLDER'], content.file_name) + if os.path.exists(file_path): + try: + os.remove(file_path) + except OSError as e: + app.logger.warning(f"Could not delete file {file_path}: {e}") + + # Delete from database + db.session.delete(content) + deleted_count += 1 + + # Update playlist version for all players in the group + for player in group.players: + player.playlist_version += 1 + + db.session.commit() + + # Clear cache + clear_player_cache() + + # Log the action + log_action(f"Bulk deleted {deleted_count} content items from group {group.name}") + + return jsonify({ + 'success': True, + 'deleted_count': deleted_count + }) + + except Exception as e: + db.session.rollback() + app.logger.error(f"Error in group bulk delete: {str(e)}") + return jsonify({'success': False, 'error': 'Database error occurred'}), 500 + # Add this at the end of app.py if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/docker-compose.yml b/docker-compose.yml index f1ef723..beb67f4 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,33 @@ -#version: '"1.1.0"' +version: "3.8" services: web: build: . - image: digiserver:latest + image: digi-server:latest ports: - - "8880:5000" + - "80:5000" environment: - - FLASK_APP=app.py - - FLASK_RUN_HOST=0.0.0.0 - - ADMIN_USER=admin - - ADMIN_PASSWORD=Initial01! - - SECRET_KEY=Ma_Duc_Dupa_Merele_Lui_Ana + - FLASK_APP + - FLASK_RUN_HOST + - ADMIN_USER + - ADMIN_PASSWORD + - SECRET_KEY + env_file: + - .env volumes: - - /opt/digi-s/instance:/app/instance - - /opt/digi-s/uploads:/app/static/uploads + - /home/pi/Desktop/digi-server/instance:/app/instance + - /home/pi/Desktop/digi-server/uploads:/app/static/uploads restart: unless-stopped + deploy: + resources: + limits: + memory: 512M + cpus: '1.0' + reservations: + memory: 128M + cpus: '0.25' + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s diff --git a/instance/dashboard.db b/instance/dashboard.db new file mode 100644 index 0000000..c7f7e59 Binary files /dev/null and b/instance/dashboard.db differ diff --git a/models/__pycache__/__init__.cpython-311.pyc b/models/__pycache__/__init__.cpython-311.pyc index 1135501..9e7f5c5 100644 Binary files a/models/__pycache__/__init__.cpython-311.pyc and b/models/__pycache__/__init__.cpython-311.pyc differ diff --git a/models/__pycache__/content.cpython-311.pyc b/models/__pycache__/content.cpython-311.pyc index d1742f7..e994ec6 100644 Binary files a/models/__pycache__/content.cpython-311.pyc and b/models/__pycache__/content.cpython-311.pyc differ diff --git a/models/__pycache__/create_default_user.cpython-311.pyc b/models/__pycache__/create_default_user.cpython-311.pyc index 7d01eaa..d2d0def 100644 Binary files a/models/__pycache__/create_default_user.cpython-311.pyc and b/models/__pycache__/create_default_user.cpython-311.pyc differ diff --git a/models/__pycache__/group.cpython-311.pyc b/models/__pycache__/group.cpython-311.pyc index 353c366..2688675 100644 Binary files a/models/__pycache__/group.cpython-311.pyc and b/models/__pycache__/group.cpython-311.pyc differ diff --git a/models/__pycache__/player.cpython-311.pyc b/models/__pycache__/player.cpython-311.pyc index bff7c05..02ef07f 100644 Binary files a/models/__pycache__/player.cpython-311.pyc and b/models/__pycache__/player.cpython-311.pyc differ diff --git a/models/__pycache__/server_log.cpython-311.pyc b/models/__pycache__/server_log.cpython-311.pyc index daa62a6..ab0aa14 100644 Binary files a/models/__pycache__/server_log.cpython-311.pyc and b/models/__pycache__/server_log.cpython-311.pyc differ diff --git a/models/__pycache__/user.cpython-311.pyc b/models/__pycache__/user.cpython-311.pyc index 09c1534..3832b69 100644 Binary files a/models/__pycache__/user.cpython-311.pyc and b/models/__pycache__/user.cpython-311.pyc differ diff --git a/static/uploads/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg b/static/uploads/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg new file mode 100644 index 0000000..ad8cba1 Binary files /dev/null and b/static/uploads/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg differ diff --git a/static/uploads/SampleVideo_1280x720_1mb.mp4 b/static/uploads/SampleVideo_1280x720_1mb.mp4 new file mode 100644 index 0000000..ed139d6 Binary files /dev/null and b/static/uploads/SampleVideo_1280x720_1mb.mp4 differ diff --git a/static/uploads/wp2782770-1846651530.jpg b/static/uploads/wp2782770-1846651530.jpg new file mode 100644 index 0000000..d6613fb Binary files /dev/null and b/static/uploads/wp2782770-1846651530.jpg differ diff --git a/templates/admin.html b/templates/admin.html index 7d5e627..323e51f 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -210,6 +210,59 @@ + +
+
+

Performance Monitor

+
+
+
+
+
+
CPU Usage
+
+
+
+ 0% +
+
+
+
+
Memory Usage
+
+
+
+ 0% +
+
+
+
+
Disk Usage
+
+
+
+ 0% +
+
+
+
+
+ + + Monitoring stopped +
+
+
+
+
Performance Log:
+
+
Performance monitoring ready...
+
+
+
+
+
+ @@ -227,6 +280,116 @@ popup.style.display = 'none'; }, 5000); } + + // Performance monitoring functionality + let monitoringInterval = null; + let isMonitoring = false; + let maxCpu = 0, maxMemory = 0; + + function updateGauge(elementId, textId, value, color) { + const gauge = document.querySelector(`#${elementId} .progress-bar`); + const text = document.getElementById(textId); + gauge.style.width = `${value}%`; + gauge.className = `progress-bar ${color}`; + text.textContent = `${value.toFixed(1)}%`; + } + + function logPerformance(message) { + const log = document.getElementById('perf-log'); + const timestamp = new Date().toLocaleTimeString(); + const logEntry = document.createElement('div'); + logEntry.innerHTML = `[${timestamp}] ${message}`; + log.appendChild(logEntry); + log.scrollTop = log.scrollHeight; + + // Keep only last 50 entries + if (log.children.length > 50) { + log.removeChild(log.firstChild); + } + } + + function updatePerformanceStats() { + fetch('/api/performance') + .then(response => response.json()) + .then(data => { + if (data.error) { + logPerformance(`Error: ${data.error}`); + return; + } + + // Update gauges + updateGauge('cpu-gauge', 'cpu-text', data.cpu_percent, 'bg-info'); + updateGauge('memory-gauge', 'memory-text', data.memory_percent, 'bg-warning'); + updateGauge('disk-gauge', 'disk-text', data.disk_percent, 'bg-danger'); + + // Track maximum values + if (data.cpu_percent > maxCpu) { + maxCpu = data.cpu_percent; + logPerformance(`New CPU peak: ${maxCpu.toFixed(1)}%`); + } + if (data.memory_percent > maxMemory) { + maxMemory = data.memory_percent; + logPerformance(`New Memory peak: ${maxMemory.toFixed(1)}%`); + } + + // Log significant changes + if (data.cpu_percent > 80) { + logPerformance(`High CPU usage: ${data.cpu_percent.toFixed(1)}%`); + } + if (data.memory_percent > 80) { + logPerformance(`High Memory usage: ${data.memory_percent.toFixed(1)}%`); + } + }) + .catch(error => { + logPerformance(`Fetch error: ${error.message}`); + }); + } + + function toggleMonitoring() { + const toggleButton = document.getElementById('toggle-monitor'); + const statusSpan = document.getElementById('monitor-status'); + + if (isMonitoring) { + // Stop monitoring + clearInterval(monitoringInterval); + isMonitoring = false; + toggleButton.textContent = 'Start Monitoring'; + toggleButton.className = 'btn btn-primary'; + statusSpan.textContent = 'Monitoring stopped'; + statusSpan.className = 'ms-3 text-muted'; + logPerformance('Monitoring stopped'); + } else { + // Start monitoring + isMonitoring = true; + toggleButton.textContent = 'Stop Monitoring'; + toggleButton.className = 'btn btn-danger'; + statusSpan.textContent = 'Monitoring active'; + statusSpan.className = 'ms-3 text-success'; + logPerformance('Monitoring started'); + + // Update immediately and then every 2 seconds + updatePerformanceStats(); + monitoringInterval = setInterval(updatePerformanceStats, 2000); + } + } + + function resetStats() { + maxCpu = 0; + maxMemory = 0; + const log = document.getElementById('perf-log'); + log.innerHTML = '
Performance log reset...
'; + logPerformance('Stats reset'); + } + + // Event listeners + document.getElementById('toggle-monitor').addEventListener('click', toggleMonitoring); + document.getElementById('reset-stats').addEventListener('click', resetStats); + + // Auto-start monitoring when page loads + document.addEventListener('DOMContentLoaded', function() { + // Initial stats load + updatePerformanceStats(); + }); diff --git a/templates/manage_group.html b/templates/manage_group.html index aaaf8a8..ce1dfa0 100644 --- a/templates/manage_group.html +++ b/templates/manage_group.html @@ -91,12 +91,31 @@
{% if content %} + +
+
+ + +
+ +
+
    {% for media in content %}
  • + +
    + +
    +
    @@ -219,6 +238,70 @@ document.addEventListener('DOMContentLoaded', function() { item.dataset.position = index; }); } + + // Bulk delete functionality + const selectAllGroup = document.getElementById('selectAllGroup'); + const deleteSelectedGroup = document.getElementById('deleteSelectedGroup'); + const selectedCountGroup = document.getElementById('selectedCountGroup'); + const groupMediaCheckboxes = document.querySelectorAll('.group-media-checkbox'); + + // Update selected count and toggle delete button visibility + function updateSelectedCount() { + const selectedCount = document.querySelectorAll('.group-media-checkbox:checked').length; + selectedCountGroup.textContent = selectedCount; + deleteSelectedGroup.style.display = selectedCount > 0 ? 'inline-block' : 'none'; + } + + // Select/Deselect all checkboxes + selectAllGroup.addEventListener('change', function() { + const isChecked = selectAllGroup.checked; + groupMediaCheckboxes.forEach(checkbox => { + checkbox.checked = isChecked; + }); + updateSelectedCount(); + }); + + // Individual checkbox change + groupMediaCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', updateSelectedCount); + }); + + // Delete selected button click + deleteSelectedGroup.addEventListener('click', function() { + const selectedIds = Array.from(groupMediaCheckboxes) + .filter(checkbox => checkbox.checked) + .map(checkbox => checkbox.value); + + if (selectedIds.length === 0) { + alert('No media selected for deletion.'); + return; + } + + if (confirm(`Are you sure you want to delete ${selectedIds.length} selected media items?`)) { + // Send bulk delete request + fetch('{{ url_for("bulk_delete_group_content", group_id=group.id) }}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}' + }, + body: JSON.stringify({content_ids: selectedIds}) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert(`Successfully deleted ${data.deleted_count} media items.`); + location.reload(); // Reload the page to update the media list + } else { + alert('Error deleting media: ' + (data.error || 'Unknown error')); + } + }) + .catch(error => { + console.error('Error:', error); + alert('An error occurred while deleting the media.'); + }); + } + }); }); diff --git a/templates/player_page.html b/templates/player_page.html index 3ba5814..4d339b8 100644 --- a/templates/player_page.html +++ b/templates/player_page.html @@ -93,6 +93,19 @@
    {% if content %} + +
    +
    + + +
    + +
    +
      {% for media in content %}
    • + +
      + +
      +
      @@ -235,6 +255,75 @@ document.addEventListener('DOMContentLoaded', function() { }); } } + + // Bulk delete functionality + const selectAllCheckbox = document.getElementById('selectAll'); + const mediaCheckboxes = document.querySelectorAll('.media-checkbox'); + const deleteSelectedButton = document.getElementById('deleteSelected'); + const selectedCountSpan = document.getElementById('selectedCount'); + + // Update selected count and toggle delete button visibility + function updateSelectedCount() { + const selectedCount = document.querySelectorAll('.media-checkbox:checked').length; + selectedCountSpan.textContent = selectedCount; + deleteSelectedButton.style.display = selectedCount > 0 ? 'inline-block' : 'none'; + } + + // Select/Deselect all checkboxes + selectAllCheckbox.addEventListener('change', function() { + mediaCheckboxes.forEach(checkbox => { + checkbox.checked = selectAllCheckbox.checked; + }); + updateSelectedCount(); + }); + + // Individual checkbox change + mediaCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', function() { + // Uncheck "Select All" if any checkbox is unchecked + if (!this.checked) { + selectAllCheckbox.checked = false; + } + updateSelectedCount(); + }); + }); + + // Delete selected media + deleteSelectedButton.addEventListener('click', function() { + const selectedIds = Array.from(mediaCheckboxes) + .filter(checkbox => checkbox.checked) + .map(checkbox => checkbox.value); + + if (selectedIds.length === 0) { + alert('No media selected for deletion.'); + return; + } + + if (confirm(`Are you sure you want to delete ${selectedIds.length} selected media items?`)) { + // Send bulk delete request + fetch('{{ url_for("bulk_delete_player_content", player_id=player.id) }}', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}' + }, + body: JSON.stringify({content_ids: selectedIds}) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert('Selected media deleted successfully.'); + location.reload(); // Reload the page to update the media list + } else { + alert('Error deleting media: ' + (data.error || 'Unknown error')); + } + }) + .catch(error => { + console.error('Error:', error); + alert('An error occurred while deleting the media.'); + }); + } + }); }); diff --git a/templates/upload_content.html b/templates/upload_content.html index 9508c25..5b4fd27 100644 --- a/templates/upload_content.html +++ b/templates/upload_content.html @@ -129,16 +129,44 @@