Add real-time upload progress tracking, mobile-optimized manage_group page with player status cards
Features: - Real-time upload progress tracking with AJAX polling and session-based monitoring - API endpoint /api/upload_progress/<session_id> for progress updates - Video conversion progress tracking with background threads - Mobile-responsive design for manage_group page - Player status cards with feedback, playlist sync, and last activity - Bootstrap Icons integration throughout UI - Responsive layout (1/4 group info, 3/4 players on desktop) - Video thumbnails with play icon, image thumbnails in media lists - Bulk selection and delete for group media - Enhanced logging for video conversion debugging
This commit is contained in:
@@ -55,39 +55,162 @@ def convert_video(input_file, output_folder):
|
||||
print(f"Video conversion skipped for: {input_file}")
|
||||
return input_file
|
||||
|
||||
def convert_video_and_update_playlist(app, file_path, original_filename, target_type, target_id, duration):
|
||||
def convert_video_and_update_playlist(app, file_path, original_filename, target_type, target_id, duration, upload_progress=None, session_id=None, file_index=0, total_files=1):
|
||||
"""
|
||||
Convert video to Raspberry Pi optimized format, then add to playlist.
|
||||
This ensures players only download optimized videos.
|
||||
|
||||
Args:
|
||||
upload_progress (dict): Global progress tracking dictionary
|
||||
session_id (str): Unique session identifier for progress tracking
|
||||
file_index (int): Current file index being processed
|
||||
total_files (int): Total number of files being processed
|
||||
"""
|
||||
import shutil
|
||||
import tempfile
|
||||
print(f"Starting video normalization for: {file_path}")
|
||||
# Only process .mp4 files
|
||||
if not file_path.lower().endswith('.mp4'):
|
||||
print(f"Skipping non-mp4 file: {file_path}")
|
||||
print(f"Starting video optimization for Raspberry Pi: {file_path}")
|
||||
|
||||
# Update progress - conversion starting
|
||||
if upload_progress and session_id:
|
||||
print(f"[VIDEO CONVERSION] Setting initial progress for session {session_id}")
|
||||
upload_progress[session_id] = {
|
||||
'status': 'converting',
|
||||
'progress': 40,
|
||||
'message': f'Optimizing video for Raspberry Pi (30fps, H.264)...',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
print(f"[VIDEO CONVERSION] Progress set: {upload_progress[session_id]}")
|
||||
else:
|
||||
print(f"[VIDEO CONVERSION] WARNING: upload_progress or session_id is None!")
|
||||
|
||||
# Only process video files
|
||||
if not file_path.lower().endswith(('.mp4', '.avi', '.mkv', '.mov', '.webm')):
|
||||
print(f"Skipping non-video file: {file_path}")
|
||||
return None
|
||||
|
||||
# Prepare temp output file
|
||||
temp_dir = tempfile.gettempdir()
|
||||
temp_output = os.path.join(temp_dir, f"normalized_{os.path.basename(file_path)}")
|
||||
temp_output = os.path.join(temp_dir, f"optimized_{os.path.basename(file_path)}")
|
||||
|
||||
# Enhanced ffmpeg command for Raspberry Pi optimization
|
||||
ffmpeg_cmd = [
|
||||
'ffmpeg', '-y', '-i', file_path,
|
||||
'-c:v', 'libx264', '-profile:v', 'main',
|
||||
# Bitrate is not forced, so we allow lower bitrates
|
||||
'-vf', 'scale=1920:1080,fps=29.97',
|
||||
'-c:a', 'copy',
|
||||
'-c:v', 'libx264', # H.264 codec
|
||||
'-preset', 'medium', # Balanced encoding speed/quality
|
||||
'-profile:v', 'main', # Main profile for compatibility
|
||||
'-crf', '23', # Constant quality (23 is good balance)
|
||||
'-maxrate', '8M', # Max bitrate 8Mbps
|
||||
'-bufsize', '12M', # Buffer size
|
||||
'-vf', 'scale=\'min(1920,iw)\':\'min(1080,ih)\':force_original_aspect_ratio=decrease,fps=30', # Scale down if needed, 30fps
|
||||
'-r', '30', # Output framerate 30fps
|
||||
'-c:a', 'aac', # AAC audio codec
|
||||
'-b:a', '128k', # Audio bitrate 128kbps
|
||||
'-movflags', '+faststart', # Enable fast start for web streaming
|
||||
temp_output
|
||||
]
|
||||
print(f"Running ffmpeg: {' '.join(ffmpeg_cmd)}")
|
||||
|
||||
print(f"Running ffmpeg optimization: {' '.join(ffmpeg_cmd)}")
|
||||
print(f"Settings: 1920x1080 max, 30fps, H.264, 8Mbps max bitrate")
|
||||
|
||||
# Update progress - conversion in progress
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id]['progress'] = 50
|
||||
upload_progress[session_id]['message'] = 'Converting video (this may take a few minutes)...'
|
||||
|
||||
try:
|
||||
result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True, timeout=1800)
|
||||
if result.returncode != 0:
|
||||
print(f"ffmpeg error: {result.stderr}")
|
||||
print(f"Video conversion failed for: {original_filename}")
|
||||
|
||||
# Update progress - error
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'error',
|
||||
'progress': 0,
|
||||
'message': f'Video conversion failed: {original_filename}',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
|
||||
# Delete the unconverted file
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
print(f"Removed unconverted video file: {file_path}")
|
||||
return None
|
||||
# Replace original file with normalized one
|
||||
|
||||
# Update progress - replacing file
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id]['progress'] = 80
|
||||
upload_progress[session_id]['message'] = 'Saving optimized video and adding to playlist...'
|
||||
|
||||
# Replace original file with optimized one
|
||||
shutil.move(temp_output, file_path)
|
||||
print(f"Video normalized and replaced: {file_path}")
|
||||
print(f"Video optimized and replaced: {file_path}")
|
||||
print(f"Video is now optimized for Raspberry Pi playback (30fps, max 1080p)")
|
||||
|
||||
# NOW add to playlist after successful conversion
|
||||
with app.app_context():
|
||||
if target_type == 'group':
|
||||
group = Group.query.get_or_404(target_id)
|
||||
for player in group.players:
|
||||
new_content = Content(file_name=original_filename, duration=duration, player_id=player.id)
|
||||
db.session.add(new_content)
|
||||
player.playlist_version += 1
|
||||
group.playlist_version += 1
|
||||
print(f"Video added to group '{group.name}' playlist after optimization")
|
||||
elif target_type == 'player':
|
||||
player = Player.query.get_or_404(target_id)
|
||||
new_content = Content(file_name=original_filename, duration=duration, player_id=target_id)
|
||||
db.session.add(new_content)
|
||||
player.playlist_version += 1
|
||||
print(f"Video added to player '{player.username}' playlist after optimization")
|
||||
|
||||
db.session.commit()
|
||||
print(f"Playlist updated with optimized video: {original_filename}")
|
||||
|
||||
# Update progress - complete
|
||||
if upload_progress and session_id:
|
||||
print(f"[VIDEO CONVERSION] Video conversion complete! Updating progress for session {session_id}")
|
||||
upload_progress[session_id] = {
|
||||
'status': 'complete',
|
||||
'progress': 100,
|
||||
'message': f'Video conversion complete! Added to playlist.',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index + 1
|
||||
}
|
||||
print(f"[VIDEO CONVERSION] Final progress: {upload_progress[session_id]}")
|
||||
else:
|
||||
print(f"[VIDEO CONVERSION] WARNING: Cannot update completion status - upload_progress or session_id is None!")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during video normalization: {e}")
|
||||
print(f"[VIDEO CONVERSION] ERROR during video optimization: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Update progress - error
|
||||
if upload_progress and session_id:
|
||||
print(f"[VIDEO CONVERSION] Setting error status for session {session_id}")
|
||||
upload_progress[session_id] = {
|
||||
'status': 'error',
|
||||
'progress': 0,
|
||||
'message': f'Error during video conversion: {str(e)}',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
else:
|
||||
print(f"[VIDEO CONVERSION] WARNING: Cannot update error status - upload_progress or session_id is None!")
|
||||
|
||||
# Delete the unconverted file on error
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
print(f"Removed unconverted video file due to error: {file_path}")
|
||||
return None
|
||||
# No need to update playlist, as filename remains the same
|
||||
|
||||
# Filename remains the same
|
||||
return True
|
||||
|
||||
# PDF conversion functions
|
||||
@@ -188,7 +311,7 @@ def update_playlist_with_files(image_filenames, duration, target_type, target_id
|
||||
print(f"Error updating playlist: {e}")
|
||||
return False
|
||||
|
||||
def process_pdf(input_file, output_folder, duration, target_type, target_id):
|
||||
def process_pdf(input_file, output_folder, duration, target_type, target_id, upload_progress=None, session_id=None, file_index=0, total_files=1):
|
||||
"""
|
||||
Process a PDF file: convert to images and update playlist.
|
||||
|
||||
@@ -198,6 +321,10 @@ def process_pdf(input_file, output_folder, duration, target_type, target_id):
|
||||
duration (int): Duration in seconds for each image
|
||||
target_type (str): 'player' or 'group'
|
||||
target_id (int): ID of the player or group
|
||||
upload_progress (dict): Global progress tracking dictionary
|
||||
session_id (str): Unique session identifier for progress tracking
|
||||
file_index (int): Current file index being processed
|
||||
total_files (int): Total number of files being processed
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
@@ -205,6 +332,16 @@ def process_pdf(input_file, output_folder, duration, target_type, target_id):
|
||||
print(f"Processing PDF file: {input_file}")
|
||||
print(f"Output folder: {output_folder}")
|
||||
|
||||
# Update progress - starting PDF conversion
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'converting',
|
||||
'progress': 50,
|
||||
'message': f'Converting PDF to images (300 DPI)...',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
|
||||
# Ensure output folder exists
|
||||
if not os.path.exists(output_folder):
|
||||
os.makedirs(output_folder, exist_ok=True)
|
||||
@@ -213,17 +350,42 @@ def process_pdf(input_file, output_folder, duration, target_type, target_id):
|
||||
# Convert PDF to images using standard quality (delete PDF after successful conversion)
|
||||
image_filenames = convert_pdf_to_images(input_file, output_folder, delete_pdf=True, dpi=300)
|
||||
|
||||
# Update progress - adding to playlist
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id]['progress'] = 80
|
||||
upload_progress[session_id]['message'] = f'Adding {len(image_filenames)} images to playlist...'
|
||||
|
||||
# Update playlist with generated images
|
||||
if image_filenames:
|
||||
success = update_playlist_with_files(image_filenames, duration, target_type, target_id)
|
||||
if success:
|
||||
print(f"Successfully processed PDF: {len(image_filenames)} images added to playlist")
|
||||
|
||||
# Update progress - complete
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'complete',
|
||||
'progress': 100,
|
||||
'message': f'PDF converted to {len(image_filenames)} images and added to playlist!',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index + 1
|
||||
}
|
||||
return success
|
||||
else:
|
||||
print("Failed to convert PDF to images")
|
||||
|
||||
# Update progress - error
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'error',
|
||||
'progress': 0,
|
||||
'message': 'Failed to convert PDF to images',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
return False
|
||||
|
||||
def process_pptx(input_file, output_folder, duration, target_type, target_id):
|
||||
def process_pptx(input_file, output_folder, duration, target_type, target_id, upload_progress=None, session_id=None, file_index=0, total_files=1):
|
||||
"""
|
||||
Process a PPTX file: convert to PDF first, then to JPG images (same workflow as PDF).
|
||||
|
||||
@@ -233,6 +395,10 @@ def process_pptx(input_file, output_folder, duration, target_type, target_id):
|
||||
duration (int): Duration in seconds for each image
|
||||
target_type (str): 'player' or 'group'
|
||||
target_id (int): ID of the player or group
|
||||
upload_progress (dict): Global progress tracking dictionary
|
||||
session_id (str): Unique session identifier for progress tracking
|
||||
file_index (int): Current file index being processed
|
||||
total_files (int): Total number of files being processed
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
@@ -240,6 +406,16 @@ def process_pptx(input_file, output_folder, duration, target_type, target_id):
|
||||
print(f"Processing PPTX file using PDF workflow: {input_file}")
|
||||
print(f"Output folder: {output_folder}")
|
||||
|
||||
# Update progress - starting PPTX conversion (step 1)
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'converting',
|
||||
'progress': 40,
|
||||
'message': f'Converting PowerPoint to PDF (Step 1/3)...',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
|
||||
# Ensure output folder exists
|
||||
if not os.path.exists(output_folder):
|
||||
os.makedirs(output_folder, exist_ok=True)
|
||||
@@ -258,10 +434,25 @@ def process_pptx(input_file, output_folder, duration, target_type, target_id):
|
||||
print("- Corrupted PPTX file")
|
||||
print("- Insufficient memory")
|
||||
print("- File permission issues")
|
||||
|
||||
# Update progress - error
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'error',
|
||||
'progress': 0,
|
||||
'message': 'Failed to convert PowerPoint to PDF',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
return False
|
||||
|
||||
print(f"PPTX successfully converted to PDF: {pdf_file}")
|
||||
|
||||
# Update progress - step 2
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id]['progress'] = 60
|
||||
upload_progress[session_id]['message'] = 'Converting PDF to images (Step 2/3, 300 DPI)...'
|
||||
|
||||
# Step 2: Use the same PDF to images workflow as direct PDF uploads
|
||||
print("Step 2: Converting PDF to JPG images...")
|
||||
# Convert PDF to JPG images (300 DPI, same as PDF workflow)
|
||||
@@ -274,6 +465,16 @@ def process_pptx(input_file, output_folder, duration, target_type, target_id):
|
||||
print("- PDF corruption during conversion")
|
||||
print("- Insufficient disk space")
|
||||
print("- Memory issues during image processing")
|
||||
|
||||
# Update progress - error
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'error',
|
||||
'progress': 0,
|
||||
'message': 'Failed to convert PDF to images',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
return False
|
||||
|
||||
print(f"Generated {len(image_filenames)} JPG images from PPTX → PDF")
|
||||
@@ -283,11 +484,26 @@ def process_pptx(input_file, output_folder, duration, target_type, target_id):
|
||||
os.remove(input_file)
|
||||
print(f"Original PPTX file deleted: {input_file}")
|
||||
|
||||
# Update progress - step 3
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id]['progress'] = 85
|
||||
upload_progress[session_id]['message'] = f'Adding {len(image_filenames)} images to playlist (Step 3/3)...'
|
||||
|
||||
# Step 4: Update playlist with generated images in sequential order
|
||||
print("Step 3: Adding images to playlist...")
|
||||
success = update_playlist_with_files(image_filenames, duration, target_type, target_id)
|
||||
if success:
|
||||
print(f"Successfully processed PPTX: {len(image_filenames)} images added to playlist")
|
||||
|
||||
# Update progress - complete
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'complete',
|
||||
'progress': 100,
|
||||
'message': f'PowerPoint converted to {len(image_filenames)} images and added to playlist!',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index + 1
|
||||
}
|
||||
else:
|
||||
print("Error: Failed to add images to playlist database")
|
||||
return success
|
||||
@@ -298,10 +514,14 @@ def process_pptx(input_file, output_folder, duration, target_type, target_id):
|
||||
traceback.print_exc()
|
||||
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, upload_progress=None, session_id=None):
|
||||
"""
|
||||
Process uploaded files based on media type and add them to playlists.
|
||||
|
||||
Args:
|
||||
upload_progress (dict): Global progress tracking dictionary
|
||||
session_id (str): Unique session identifier for progress tracking
|
||||
|
||||
Returns:
|
||||
list: List of result dictionaries with success status and messages
|
||||
"""
|
||||
@@ -316,8 +536,21 @@ def process_uploaded_files(app, files, media_type, duration, target_type, target
|
||||
player = Player.query.get_or_404(target_id)
|
||||
target_name = player.username
|
||||
|
||||
for file in files:
|
||||
total_files = len(files)
|
||||
|
||||
for file_index, file in enumerate(files):
|
||||
try:
|
||||
# Update progress - uploading phase
|
||||
if upload_progress and session_id:
|
||||
file_progress = int((file_index / total_files) * 30) # 0-30% for file uploads
|
||||
upload_progress[session_id] = {
|
||||
'status': 'uploading',
|
||||
'progress': file_progress,
|
||||
'message': f'Uploading file {file_index + 1}/{total_files}: {file.filename}...',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
|
||||
# Generate a secure filename and save the file
|
||||
filename = secure_filename(file.filename)
|
||||
|
||||
@@ -338,37 +571,54 @@ def process_uploaded_files(app, files, media_type, duration, target_type, target
|
||||
result = {'filename': filename, 'success': True, 'message': ''}
|
||||
|
||||
if media_type == 'image':
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id]['message'] = f'Adding image {file_index + 1}/{total_files} to playlist...'
|
||||
upload_progress[session_id]['progress'] = int(30 + (file_index / total_files) * 70)
|
||||
|
||||
add_image_to_playlist(app, file, filename, duration, target_type, target_id)
|
||||
result['message'] = f"Image {filename} added to playlist"
|
||||
log_upload('image', filename, target_type, target_id)
|
||||
|
||||
elif media_type == 'video':
|
||||
# For videos, add to playlist then start conversion in background
|
||||
if target_type == 'group':
|
||||
group = Group.query.get_or_404(target_id)
|
||||
for player in group.players:
|
||||
new_content = Content(file_name=filename, duration=duration, player_id=player.id)
|
||||
db.session.add(new_content)
|
||||
player.playlist_version += 1
|
||||
group.playlist_version += 1
|
||||
elif target_type == 'player':
|
||||
player = Player.query.get_or_404(target_id)
|
||||
new_content = Content(file_name=filename, duration=duration, player_id=target_id)
|
||||
db.session.add(new_content)
|
||||
player.playlist_version += 1
|
||||
# For videos, save file then start conversion in background
|
||||
# Video will be added to playlist AFTER conversion completes
|
||||
print(f"Video uploaded: {filename}")
|
||||
print(f"Starting background optimization - video will be added to playlist when ready")
|
||||
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'converting',
|
||||
'progress': 40,
|
||||
'message': f'Converting video {file_index + 1}/{total_files} to 30fps (this may take a few minutes)...',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
|
||||
db.session.commit()
|
||||
# Start background conversion using absolute path
|
||||
import threading
|
||||
threading.Thread(target=convert_video_and_update_playlist,
|
||||
args=(app, file_path, filename, target_type, target_id, duration)).start()
|
||||
result['message'] = f"Video {filename} added to playlist and being processed"
|
||||
print(f"[VIDEO UPLOAD] Starting background thread for video conversion. Session ID: {session_id}")
|
||||
print(f"[VIDEO UPLOAD] Parameters: file_path={file_path}, filename={filename}, target={target_type}/{target_id}")
|
||||
thread = threading.Thread(target=convert_video_and_update_playlist,
|
||||
args=(app, file_path, filename, target_type, target_id, duration, upload_progress, session_id, file_index, total_files))
|
||||
thread.daemon = True # Make thread daemon so it doesn't block shutdown
|
||||
thread.start()
|
||||
print(f"[VIDEO UPLOAD] Background thread started: {thread.name}")
|
||||
result['message'] = f"Video {filename} is being optimized for Raspberry Pi (30fps, max 1080p). It will be added to playlist when ready."
|
||||
log_upload('video', filename, target_type, target_id)
|
||||
|
||||
elif media_type == 'pdf':
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'converting',
|
||||
'progress': 40,
|
||||
'message': f'Converting PDF {file_index + 1}/{total_files} to images...',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
|
||||
# For PDFs, convert to images and update playlist using absolute path
|
||||
success = process_pdf(file_path, upload_folder,
|
||||
duration, target_type, target_id)
|
||||
duration, target_type, target_id, upload_progress, session_id, file_index, total_files)
|
||||
if success:
|
||||
result['message'] = f"PDF {filename} processed successfully"
|
||||
log_process('pdf', filename, target_type, target_id)
|
||||
@@ -377,9 +627,18 @@ def process_uploaded_files(app, files, media_type, duration, target_type, target
|
||||
result['message'] = f"Error processing PDF file: {filename}"
|
||||
|
||||
elif media_type == 'ppt':
|
||||
if upload_progress and session_id:
|
||||
upload_progress[session_id] = {
|
||||
'status': 'converting',
|
||||
'progress': 30,
|
||||
'message': f'Converting PowerPoint {file_index + 1}/{total_files} to images (PPTX → PDF → Images, may take 2-5 minutes)...',
|
||||
'files_total': total_files,
|
||||
'files_processed': file_index
|
||||
}
|
||||
|
||||
# For PPT/PPTX, convert to PDF, then to images, and update playlist using absolute path
|
||||
success = process_pptx(file_path, upload_folder,
|
||||
duration, target_type, target_id)
|
||||
duration, target_type, target_id, upload_progress, session_id, file_index, total_files)
|
||||
if success:
|
||||
result['message'] = f"PowerPoint {filename} processed successfully"
|
||||
log_process('ppt', filename, target_type, target_id)
|
||||
|
||||
Reference in New Issue
Block a user