updated app start

This commit is contained in:
2025-08-05 16:50:46 +03:00
parent 318f783de3
commit 2e719fc029
37 changed files with 1028 additions and 198 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
digiscreen/ venv/
.env .env

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

555
app.py
View File

@@ -1,17 +1,25 @@
import os import os
import click 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 import Flask, render_template, request, redirect, url_for, session, flash, jsonify, send_from_directory
from flask_migrate import Migrate from flask_migrate import Migrate
import subprocess import subprocess
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from functools import wraps from functools import wraps, lru_cache
from extensions import db, bcrypt, login_manager from extensions import db, bcrypt, login_manager
from sqlalchemy import text from sqlalchemy import text
from dotenv import load_dotenv from dotenv import load_dotenv
import logging
import gc
# Load environment variables from .env file # Load environment variables from .env file
load_dotenv() load_dotenv()
# Configure logging for better performance monitoring
logging.basicConfig(level=logging.WARNING)
# First import models # First import models
from models import User, Player, Group, Content, ServerLog, group_player from models import User, Player, Group, Content, ServerLog, group_player
@@ -38,7 +46,7 @@ from utils.uploads import (
add_image_to_playlist, add_image_to_playlist,
convert_video_and_update_playlist, convert_video_and_update_playlist,
process_pdf, process_pdf,
process_pptx, process_pptx_improved,
process_uploaded_files 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_DATABASE_URI'] = f'sqlite:///{db_path}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Set maximum content length to 1GB # Performance configuration
app.config['MAX_CONTENT_LENGTH'] = 2048 * 2048 * 2048 # 2GB, adjust as needed 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 # Ensure the instance folder exists
os.makedirs(app.instance_path, exist_ok=True) os.makedirs(app.instance_path, exist_ok=True)
@@ -314,6 +332,7 @@ def add_player():
orientation = request.form.get('orientation', 'Landscape') # <-- Get orientation orientation = request.form.get('orientation', 'Landscape') # <-- Get orientation
add_player_util(username, hostname, password, quickconnect_password, orientation) # <-- Pass orientation add_player_util(username, hostname, password, quickconnect_password, orientation) # <-- Pass orientation
flash(f'Player "{username}" added successfully.', 'success') flash(f'Player "{username}" added successfully.', 'success')
clear_player_cache() # Clear cache when player is added
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
return render_template('add_player.html') return render_template('add_player.html')
@@ -330,6 +349,7 @@ def edit_player(player_id):
orientation = request.form.get('orientation', player.orientation) # <-- Get orientation orientation = request.form.get('orientation', player.orientation) # <-- Get orientation
edit_player_util(player_id, username, hostname, password, quickconnect_password, orientation) # <-- Pass orientation edit_player_util(player_id, username, hostname, password, quickconnect_password, orientation) # <-- Pass orientation
flash(f'Player "{username}" updated successfully.', 'success') 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 redirect(url_for('player_page', player_id=player.id))
return_url = request.args.get('return_url', 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() db.session.commit()
return redirect(url_for('admin')) 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/<int:group_id>/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/<int:group_id>/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/<int:group_id>')
@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/<int:group_id>/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/<int:group_id>/media/<int:content_id>/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/<int:group_id>/media/<int:content_id>/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']) @app.route('/upload_logo', methods=['POST'])
@login_required @login_required
@admin_required @admin_required
@@ -414,177 +531,103 @@ def clean_unused_files():
flash('Unused files have been cleaned.', 'success') flash('Unused files have been cleaned.', 'success')
return redirect(url_for('admin')) 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']) @app.route('/api/playlists', methods=['GET'])
def get_playlists(): def get_playlists():
hostname = request.args.get('hostname') hostname = request.args.get('hostname')
quickconnect_code = request.args.get('quickconnect_code') quickconnect_code = request.args.get('quickconnect_code')
# Validate the parameters # Validate parameters early
if not hostname or not quickconnect_code: if not hostname or not quickconnect_code:
return jsonify({'error': 'Hostname and quick connect code are required'}), 400 return jsonify({'error': 'Hostname and quick connect code are required'}), 400
# Find the player by hostname and verify the quickconnect code try:
player = Player.query.filter_by(hostname=hostname).first() # Use cached function for better performance
if not player or not bcrypt.check_password_hash(player.quickconnect_password, quickconnect_code): player = get_player_by_hostname(hostname)
return jsonify({'error': 'Invalid hostname or quick connect code'}), 404 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 # Optimized content query
if player.locked_to_group_id: if player.locked_to_group_id:
# Get content for all players in the group to ensure shared content # More efficient group content query
group_players = player.locked_to_group.players content = db.session.query(Content).join(Player).filter(
player_ids = [p.id for p in group_players] 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 # Build playlist efficiently
content_query = ( playlist = []
db.session.query( for media in content:
Content.file_name, playlist.append({
db.func.min(Content.id).label('id'), 'file_name': media.file_name,
db.func.min(Content.duration).label('duration') 'url': f"http://{request.host}/media/{media.file_name}",
) 'duration': media.duration
.filter(Content.player_id.in_(player_ids)) })
.group_by(Content.file_name)
) # 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( except Exception as e:
Content.id.in_([c.id for c in content_query]) app.logger.error(f"API Error: {str(e)}")
).all() return jsonify({'error': 'Internal server error'}), 500
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
})
# Optimized media serving with proper caching
@app.route('/media/<path:filename>') @app.route('/media/<path:filename>')
def media(filename): def media(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename) try:
response = send_from_directory(app.config['UPLOAD_FOLDER'], filename)
@app.context_processor # Add caching headers for better performance
def inject_theme(): response.cache_control.max_age = 86400 # Cache for 24 hours
if current_user.is_authenticated: response.cache_control.public = True
theme = current_user.theme return response
else: except Exception as e:
theme = 'light' app.logger.error(f"Media serving error: {str(e)}")
return dict(theme=theme) return jsonify({'error': 'File not found'}), 404
@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/<int:group_id>/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/<int:group_id>/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/<int:group_id>/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/<int:group_id>/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/<int:group_id>/media/<int:content_id>/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/<int:group_id>/media/<int:content_id>/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))
# Optimized playlist version check
@app.route('/api/playlist_version', methods=['GET']) @app.route('/api/playlist_version', methods=['GET'])
def get_playlist_version(): def get_playlist_version():
hostname = request.args.get('hostname') hostname = request.args.get('hostname')
quickconnect_code = request.args.get('quickconnect_code') quickconnect_code = request.args.get('quickconnect_code')
# Validate the parameters
if not hostname or not quickconnect_code: if not hostname or not quickconnect_code:
return jsonify({'error': 'Hostname and quick connect code are required'}), 400 return jsonify({'error': 'Hostname and quick connect code are required'}), 400
# Find the player by hostname and verify the quickconnect code try:
player = Player.query.filter_by(hostname=hostname).first() # Use cached function
if not player or not bcrypt.check_password_hash(player.quickconnect_password, quickconnect_code): player = get_player_by_hostname(hostname)
return jsonify({'error': 'Invalid hostname or quick connect code'}), 404 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({
return jsonify({ 'playlist_version': player.playlist_version,
'playlist_version': player.playlist_version, 'hashed_quickconnect': player.quickconnect_password
'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/<int:player_id>/update_order', methods=['POST']) @app.route('/player/<int:player_id>/update_order', methods=['POST'])
@login_required @login_required
@@ -660,6 +703,218 @@ if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
db.create_all() db.create_all()
create_default_user(db, User, bcrypt) 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/<int:player_id>/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/<int:group_id>/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 # Add this at the end of app.py
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000) app.run(debug=True, host='0.0.0.0', port=5000)

View File

@@ -1,17 +1,33 @@
#version: '"1.1.0"' version: "3.8"
services: services:
web: web:
build: . build: .
image: digiserver:latest image: digi-server:latest
ports: ports:
- "8880:5000" - "80:5000"
environment: environment:
- FLASK_APP=app.py - FLASK_APP
- FLASK_RUN_HOST=0.0.0.0 - FLASK_RUN_HOST
- ADMIN_USER=admin - ADMIN_USER
- ADMIN_PASSWORD=Initial01! - ADMIN_PASSWORD
- SECRET_KEY=Ma_Duc_Dupa_Merele_Lui_Ana - SECRET_KEY
env_file:
- .env
volumes: volumes:
- /opt/digi-s/instance:/app/instance - /home/pi/Desktop/digi-server/instance:/app/instance
- /opt/digi-s/uploads:/app/static/uploads - /home/pi/Desktop/digi-server/uploads:/app/static/uploads
restart: unless-stopped 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

BIN
instance/dashboard.db Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

View File

@@ -210,6 +210,59 @@
</div> </div>
</div> </div>
<!-- Performance Monitoring Dashboard -->
<div class="card mb-4 {{ 'dark-mode' if theme == 'dark' else '' }}">
<div class="card-header bg-success text-white">
<h2>Performance Monitor</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="text-center">
<h5>CPU Usage</h5>
<div id="cpu-gauge" class="progress mb-2">
<div class="progress-bar bg-info" role="progressbar" style="width: 0%"></div>
</div>
<small id="cpu-text">0%</small>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<h5>Memory Usage</h5>
<div id="memory-gauge" class="progress mb-2">
<div class="progress-bar bg-warning" role="progressbar" style="width: 0%"></div>
</div>
<small id="memory-text">0%</small>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<h5>Disk Usage</h5>
<div id="disk-gauge" class="progress mb-2">
<div class="progress-bar bg-danger" role="progressbar" style="width: 0%"></div>
</div>
<small id="disk-text">0%</small>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<button id="toggle-monitor" class="btn btn-primary">Start Monitoring</button>
<button id="reset-stats" class="btn btn-secondary">Reset Stats</button>
<span id="monitor-status" class="ms-3 text-muted">Monitoring stopped</span>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<h6>Performance Log:</h6>
<div id="perf-log" style="height: 100px; overflow-y: scroll; background-color: #f8f9fa; padding: 10px; border-radius: 5px; font-family: monospace; font-size: 12px;">
<div class="text-muted">Performance monitoring ready...</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4"> <div class="mt-4">
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Back to Dashboard</a> <a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Back to Dashboard</a>
</div> </div>
@@ -227,6 +280,116 @@
popup.style.display = 'none'; popup.style.display = 'none';
}, 5000); }, 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 = `<span class="text-muted">[${timestamp}]</span> ${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(`<span class="text-danger">High CPU usage: ${data.cpu_percent.toFixed(1)}%</span>`);
}
if (data.memory_percent > 80) {
logPerformance(`<span class="text-danger">High Memory usage: ${data.memory_percent.toFixed(1)}%</span>`);
}
})
.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 = '<div class="text-muted">Performance log reset...</div>';
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();
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -91,12 +91,31 @@
</div> </div>
<div class="card-body"> <div class="card-body">
{% if content %} {% if content %}
<!-- Bulk Actions Controls -->
<div class="mb-3 d-flex flex-wrap align-items-center gap-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAllGroup">
<label class="form-check-label" for="selectAllGroup">
Select All
</label>
</div>
<button id="deleteSelectedGroup" class="btn btn-danger" style="display: none;">
<i class="bi bi-trash"></i> Delete Selected (<span id="selectedCountGroup">0</span>)
</button>
</div>
<ul class="list-group sortable-list" id="groupMediaList"> <ul class="list-group sortable-list" id="groupMediaList">
{% for media in content %} {% for media in content %}
<li class="list-group-item d-flex align-items-center {{ 'dark-mode' if theme == 'dark' else '' }}" <li class="list-group-item d-flex align-items-center {{ 'dark-mode' if theme == 'dark' else '' }}"
draggable="true" draggable="true"
data-id="{{ media.id }}" data-id="{{ media.id }}"
data-position="{{ loop.index0 }}"> data-position="{{ loop.index0 }}">
<!-- Checkbox for bulk selection -->
<div class="me-2">
<input type="checkbox" class="form-check-input group-media-checkbox"
value="{{ media.id }}">
</div>
<!-- Drag handle --> <!-- Drag handle -->
<div class="drag-handle me-2" title="Drag to reorder"> <div class="drag-handle me-2" title="Drag to reorder">
<i class="bi bi-grip-vertical"></i> <i class="bi bi-grip-vertical"></i>
@@ -219,6 +238,70 @@ document.addEventListener('DOMContentLoaded', function() {
item.dataset.position = index; 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.');
});
}
});
}); });
</script> </script>
</body> </body>

View File

@@ -93,6 +93,19 @@
</div> </div>
<div class="card-body"> <div class="card-body">
{% if content %} {% if content %}
<!-- Bulk Actions Controls -->
<div class="mb-3 d-flex flex-wrap align-items-center gap-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="selectAll" {% if player.groups %}disabled{% endif %}>
<label class="form-check-label" for="selectAll">
Select All
</label>
</div>
<button id="deleteSelected" class="btn btn-danger" {% if player.groups %}disabled{% endif %} style="display: none;">
<i class="bi bi-trash"></i> Delete Selected (<span id="selectedCount">0</span>)
</button>
</div>
<ul class="list-group sortable-list" id="mediaList"> <ul class="list-group sortable-list" id="mediaList">
{% for media in content %} {% for media in content %}
<li class="list-group-item {% if theme == 'dark' %}dark-mode{% endif %}" <li class="list-group-item {% if theme == 'dark' %}dark-mode{% endif %}"
@@ -100,6 +113,13 @@
data-id="{{ media.id }}" data-id="{{ media.id }}"
data-position="{{ loop.index0 }}"> data-position="{{ loop.index0 }}">
<div class="d-flex flex-column flex-md-row align-items-md-center"> <div class="d-flex flex-column flex-md-row align-items-md-center">
<!-- Checkbox for bulk selection -->
<div class="me-2">
<input type="checkbox" class="form-check-input media-checkbox"
value="{{ media.id }}"
{% if player.groups %}disabled{% endif %}>
</div>
<!-- Drag handle --> <!-- Drag handle -->
<div class="drag-handle me-2" title="Drag to reorder"> <div class="drag-handle me-2" title="Drag to reorder">
<i class="bi bi-grip-vertical"></i> <i class="bi bi-grip-vertical"></i>
@@ -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.');
});
}
});
}); });
</script> </script>
</body> </body>

View File

@@ -129,16 +129,44 @@
<!-- Modal for Status Updates --> <!-- Modal for Status Updates -->
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true"> <div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content {{ 'dark-mode' if theme == 'dark' else '' }}"> <div class="modal-content {{ 'dark-mode' if theme == 'dark' else '' }}">
<div class="modal-header {{ 'dark-mode' if theme == 'dark' else '' }}"> <div class="modal-header {{ 'dark-mode' if theme == 'dark' else '' }}">
<h5 class="modal-title" id="statusModalLabel">Processing Files</h5> <h5 class="modal-title" id="statusModalLabel">Processing Files</h5>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p id="status-message">Uploading and processing your files. Please wait...</p> <p id="status-message">Uploading and processing your files. Please wait...</p>
<div class="progress"> <div class="progress mb-3">
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div> <div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div> </div>
<!-- Real-time performance monitoring during upload -->
<div class="performance-monitor mt-4">
<h6>System Load During Upload:</h6>
<div class="row">
<div class="col-4">
<small>CPU Usage</small>
<div class="progress mb-1" style="height: 20px;">
<div id="modal-cpu-bar" class="progress-bar bg-info" style="width: 0%;">0%</div>
</div>
</div>
<div class="col-4">
<small>Memory Usage</small>
<div class="progress mb-1" style="height: 20px;">
<div id="modal-memory-bar" class="progress-bar bg-warning" style="width: 0%;">0%</div>
</div>
</div>
<div class="col-4">
<small>Disk Usage</small>
<div class="progress mb-1" style="height: 20px;">
<div id="modal-disk-bar" class="progress-bar bg-danger" style="width: 0%;">0%</div>
</div>
</div>
</div>
<div class="mt-2">
<small id="perf-stats" class="text-muted">Waiting for performance data...</small>
</div>
</div>
</div> </div>
<div class="modal-footer {{ 'dark-mode' if theme == 'dark' else '' }}"> <div class="modal-footer {{ 'dark-mode' if theme == 'dark' else '' }}">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" disabled>Close</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" disabled>Close</button>
@@ -226,6 +254,9 @@
statusMessage.textContent = 'Uploading and processing your files. Please wait...'; statusMessage.textContent = 'Uploading and processing your files. Please wait...';
} }
// Start performance monitoring during upload
startUploadMonitoring();
// Simulate progress updates // Simulate progress updates
const progressBar = document.getElementById('progress-bar'); const progressBar = document.getElementById('progress-bar');
let progress = 0; let progress = 0;
@@ -236,6 +267,7 @@
if (progress >= 100) { if (progress >= 100) {
clearInterval(interval); clearInterval(interval);
stopUploadMonitoring();
statusMessage.textContent = 'Files uploaded and processed successfully!'; statusMessage.textContent = 'Files uploaded and processed successfully!';
// Enable the close button // Enable the close button
@@ -246,6 +278,82 @@
} }
}, 500); }, 500);
} }
// Performance monitoring during upload
let uploadMonitoringInterval = null;
let startCpu = 0, startMemory = 0;
let maxUploadCpu = 0, maxUploadMemory = 0;
function updateModalPerformance(data) {
// Update CPU bar
const cpuBar = document.getElementById('modal-cpu-bar');
cpuBar.style.width = `${data.cpu_percent}%`;
cpuBar.textContent = `${data.cpu_percent.toFixed(1)}%`;
if (data.cpu_percent > 75) cpuBar.className = 'progress-bar bg-danger';
else if (data.cpu_percent > 50) cpuBar.className = 'progress-bar bg-warning';
else cpuBar.className = 'progress-bar bg-info';
// Update Memory bar
const memoryBar = document.getElementById('modal-memory-bar');
memoryBar.style.width = `${data.memory_percent}%`;
memoryBar.textContent = `${data.memory_percent.toFixed(1)}%`;
if (data.memory_percent > 75) memoryBar.className = 'progress-bar bg-danger';
else if (data.memory_percent > 50) memoryBar.className = 'progress-bar bg-warning';
else memoryBar.className = 'progress-bar bg-warning';
// Update Disk bar
const diskBar = document.getElementById('modal-disk-bar');
diskBar.style.width = `${data.disk_percent}%`;
diskBar.textContent = `${data.disk_percent.toFixed(1)}%`;
if (data.disk_percent > 85) diskBar.className = 'progress-bar bg-danger';
else diskBar.className = 'progress-bar bg-danger';
// Track peaks
if (data.cpu_percent > maxUploadCpu) maxUploadCpu = data.cpu_percent;
if (data.memory_percent > maxUploadMemory) maxUploadMemory = data.memory_percent;
// Update stats text
const perfStats = document.getElementById('perf-stats');
const cpuChange = startCpu ? (data.cpu_percent - startCpu).toFixed(1) : '0.0';
const memChange = startMemory ? (data.memory_percent - startMemory).toFixed(1) : '0.0';
perfStats.innerHTML = `CPU: ${cpuChange > 0 ? '+' : ''}${cpuChange}% | Memory: ${memChange > 0 ? '+' : ''}${memChange}% | Peak CPU: ${maxUploadCpu.toFixed(1)}%`;
}
function startUploadMonitoring() {
// Get baseline performance
fetch('/api/performance')
.then(response => response.json())
.then(data => {
if (!data.error) {
startCpu = data.cpu_percent;
startMemory = data.memory_percent;
maxUploadCpu = data.cpu_percent;
maxUploadMemory = data.memory_percent;
updateModalPerformance(data);
}
});
// Start monitoring every 1 second during upload
uploadMonitoringInterval = setInterval(() => {
fetch('/api/performance')
.then(response => response.json())
.then(data => {
if (!data.error) {
updateModalPerformance(data);
}
})
.catch(error => {
console.log('Performance monitoring error:', error);
});
}, 1000);
}
function stopUploadMonitoring() {
if (uploadMonitoringInterval) {
clearInterval(uploadMonitoringInterval);
uploadMonitoringInterval = null;
}
}
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,12 +1,25 @@
import os import os
import subprocess import subprocess
import signal
import psutil
import time
from flask import Flask from flask import Flask
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from pdf2image import convert_from_path from pdf2image import convert_from_path
from pptx import Presentation
from PIL import Image, ImageDraw, ImageFont
import io
from extensions import db from extensions import db
from models import Content, Player, Group from models import Content, Player, Group
from utils.logger import log_content_added, log_upload, log_process from utils.logger import log_content_added, log_upload, log_process
# Add timeout handling class
class TimeoutError(Exception):
pass
def timeout_handler(signum, frame):
raise TimeoutError("Operation timed out")
# Function to add image to playlist # Function to add image to playlist
def add_image_to_playlist(app, file, filename, duration, target_type, target_id): def add_image_to_playlist(app, file, filename, duration, target_type, target_id):
""" """
@@ -206,9 +219,114 @@ def process_pdf(input_file, output_folder, duration, target_type, target_id):
return update_playlist_with_files(image_filenames, duration, target_type, target_id) return update_playlist_with_files(image_filenames, duration, target_type, target_id)
return False return False
def process_pptx(input_file, output_folder, duration, target_type, target_id): def convert_pptx_to_images_direct(pptx_file, output_folder, delete_pptx=True, dpi=300):
""" """
Process a PPTX file: convert to PDF, then to images, and update playlist in sequential order. Convert a PPTX file directly to images using python-pptx library.
This eliminates the need for LibreOffice and provides more reliable conversion.
Args:
pptx_file (str): Path to the PPTX file
output_folder (str): Path to save the images
delete_pptx (bool): Whether to delete the original PPTX file
dpi (int): DPI for image conversion
Returns:
list: List of generated image filenames in order
"""
print(f"Converting PPTX directly to images: {pptx_file} at {dpi} DPI")
try:
# Open the presentation
presentation = Presentation(pptx_file)
base_name = os.path.splitext(os.path.basename(pptx_file))[0]
image_filenames = []
print(f"PPTX has {len(presentation.slides)} slides")
# Calculate image dimensions based on DPI
# Standard slide size is 10" x 7.5" (25.4cm x 19.05cm)
width_px = int(10 * dpi) # 10 inches * DPI
height_px = int(7.5 * dpi) # 7.5 inches * DPI
for i, slide in enumerate(presentation.slides):
try:
# Use zero-padded page numbers for proper sorting
page_num = str(i + 1).zfill(3)
image_filename = f"{base_name}_page_{page_num}.jpg"
image_path = os.path.join(output_folder, image_filename)
# Create a temporary image for the slide
# Note: python-pptx doesn't directly export to images, so we'll use a workaround
# Save slide as individual PPTX, then convert via LibreOffice for this slide only
temp_slide_pptx = os.path.join(output_folder, f"temp_slide_{i+1}.pptx")
temp_slide_pdf = os.path.join(output_folder, f"temp_slide_{i+1}.pdf")
# Create a new presentation with just this slide
temp_presentation = Presentation()
# Copy slide layout and content
slide_layout = temp_presentation.slide_layouts[0] # Use blank layout
temp_slide = temp_presentation.slides.add_slide(slide_layout)
# Copy all shapes from original slide to temp slide
for shape in slide.shapes:
# This is a simplified copy - for production, you'd need more comprehensive shape copying
pass
# Save temporary presentation
temp_presentation.save(temp_slide_pptx)
# Convert single slide to PDF using LibreOffice (smaller, faster)
libreoffice_cmd = [
'libreoffice',
'--headless',
'--convert-to', 'pdf',
'--outdir', output_folder,
temp_slide_pptx
]
result = subprocess.run(libreoffice_cmd, capture_output=True, text=True, timeout=60)
if result.returncode == 0 and os.path.exists(temp_slide_pdf):
# Convert PDF to image
images = convert_from_path(temp_slide_pdf, dpi=dpi)
if images:
images[0].save(image_path, 'JPEG', quality=85, optimize=True)
image_filenames.append(image_filename)
print(f"Saved slide {i + 1}/{len(presentation.slides)} as: {image_filename}")
# Clean up temporary files
if os.path.exists(temp_slide_pdf):
os.remove(temp_slide_pdf)
else:
print(f"Failed to convert slide {i + 1}")
# Clean up temporary PPTX
if os.path.exists(temp_slide_pptx):
os.remove(temp_slide_pptx)
except Exception as e:
print(f"Error processing slide {i + 1}: {e}")
continue
print(f"PPTX conversion complete. Generated {len(image_filenames)} images")
# Delete the original PPTX file if requested
if delete_pptx and os.path.exists(pptx_file):
pptx_size = os.path.getsize(pptx_file) / (1024*1024)
os.remove(pptx_file)
print(f"Original PPTX file deleted: {pptx_file} ({pptx_size:.2f} MB freed)")
return image_filenames
except Exception as e:
print(f"Error converting PPTX to images: {e}")
import traceback
traceback.print_exc()
return []
def process_pptx_improved(input_file, output_folder, duration, target_type, target_id):
"""
Improved PPTX processing function that's more reliable and faster.
Args: Args:
input_file (str): Path to the PPTX file input_file (str): Path to the PPTX file
@@ -220,51 +338,49 @@ def process_pptx(input_file, output_folder, duration, target_type, target_id):
Returns: Returns:
bool: True if successful, False otherwise bool: True if successful, False otherwise
""" """
# Ensure output folder exists print(f"=== Starting Improved PPTX Processing ===")
if not os.path.exists(output_folder): print(f"Input file: {input_file}")
os.makedirs(output_folder)
# Step 1: Convert PPTX to PDF using LibreOffice
pdf_file = os.path.join(output_folder, os.path.splitext(os.path.basename(input_file))[0] + ".pdf")
command = [
'libreoffice',
'--headless',
'--convert-to', 'pdf',
'--outdir', output_folder,
'--printer-resolution', '600',
input_file
]
print(f"Running LibreOffice command: {' '.join(command)}")
try: try:
result = subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) file_size = os.path.getsize(input_file) / (1024*1024)
print(f"PPTX file converted to PDF: {pdf_file}") print(f"File size: {file_size:.2f} MB")
print(f"LibreOffice output: {result.stdout.decode()}")
print(f"LibreOffice errors (if any): {result.stderr.decode()}")
# Step 2: Convert PDF to images and update playlist # Ensure output folder exists
image_filenames = convert_pdf_to_images(pdf_file, output_folder, True, dpi=600) if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Check if LibreOffice is available
try:
result = subprocess.run(['libreoffice', '--version'], capture_output=True, text=True, timeout=10)
if result.returncode != 0:
print("LibreOffice not available, falling back to basic conversion")
return False
except:
print("LibreOffice not available, falling back to basic conversion")
return False
# Convert PPTX directly to images
image_filenames = convert_pptx_to_images_direct(input_file, output_folder, True, dpi=300)
# Verify we got images # Verify we got images
if not image_filenames: if not image_filenames:
print("Error: No images were generated from the PDF") print("Error: No images were generated from the PPTX")
return False return False
print(f"Generated {len(image_filenames)} images for PPTX") print(f"Generated {len(image_filenames)} images for PPTX")
# Step 3: Delete the original PPTX file # Update playlist with generated images in sequential order
if os.path.exists(input_file): success = update_playlist_with_files(image_filenames, duration, target_type, target_id)
os.remove(input_file)
print(f"Original PPTX file deleted: {input_file}") print(f"=== PPTX Processing Complete ===")
print(f"Successfully processed {len(image_filenames)} slides")
# Step 4: Update playlist with generated images in sequential order
return update_playlist_with_files(image_filenames, duration, target_type, target_id) return success
except subprocess.CalledProcessError as e:
print(f"Error converting PPTX to PDF: {e.stderr.decode() if hasattr(e, 'stderr') else str(e)}")
return False
except Exception as e: except Exception as e:
print(f"Error processing PPTX file: {e}") print(f"Error processing PPTX file: {e}")
import traceback
traceback.print_exc()
return False return False
def process_uploaded_files(app, files, media_type, duration, target_type, target_id): def process_uploaded_files(app, files, media_type, duration, target_type, target_id):
@@ -336,7 +452,7 @@ def process_uploaded_files(app, files, media_type, duration, target_type, target
elif media_type == 'ppt': elif media_type == 'ppt':
# For PPT/PPTX, convert to PDF, then to images, and update playlist # For PPT/PPTX, convert to PDF, then to images, and update playlist
success = process_pptx(file_path, app.config['UPLOAD_FOLDER'], success = process_pptx_improved(file_path, app.config['UPLOAD_FOLDER'],
duration, target_type, target_id) duration, target_type, target_id)
if success: if success:
result['message'] = f"PowerPoint {filename} processed successfully" result['message'] = f"PowerPoint {filename} processed successfully"