import os import subprocess from flask import Flask from werkzeug.utils import secure_filename from pdf2image import convert_from_path from extensions import db from models import Content, Player, Group from utils.logger import log_content_added, log_upload, log_process # Function to add image to playlist def add_image_to_playlist(app, file, filename, duration, target_type, target_id): """ Save the image file and add it to the playlist database. """ # Use simple path resolution for containerized environment upload_folder = app.config['UPLOAD_FOLDER'] # In container, working directory is /app, so static/uploads resolves correctly print(f"Upload folder config: {upload_folder}") # Ensure upload folder exists if not os.path.exists(upload_folder): os.makedirs(upload_folder, exist_ok=True) print(f"Created upload folder: {upload_folder}") file_path = os.path.join(upload_folder, filename) print(f"Saving image to: {file_path}") # Only save if file does not already exist if not os.path.exists(file_path): file.save(file_path) print(f"Image saved successfully: {file_path}") else: print(f"File already exists: {file_path}") print(f"Adding image to playlist: {filename}, Target Type: {target_type}, Target ID: {target_id}") 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) log_content_added(filename, target_type, group.name) 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) log_content_added(filename, target_type, player.username) db.session.commit() log_upload('image', filename, target_type, target_id) return True # Video conversion functions 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, 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 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"optimized_{os.path.basename(file_path)}") # Enhanced ffmpeg command for Raspberry Pi optimization ffmpeg_cmd = [ 'ffmpeg', '-y', '-i', file_path, '-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 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 # 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 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"[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 # Filename remains the same return True # PDF conversion functions def convert_pdf_to_images(pdf_file, output_folder, delete_pdf=True, dpi=300): """ Convert a PDF file to high-quality JPG images in sequential order. Uses standard 300 DPI for reliable conversion. """ print(f"Converting PDF to JPG images: {pdf_file} at {dpi} DPI") print(f"Output folder: {output_folder}") try: # Ensure output folder exists if not os.path.exists(output_folder): os.makedirs(output_folder, exist_ok=True) print(f"Created output folder: {output_folder}") # Convert PDF to images using pdf2image print("Starting PDF conversion...") images = convert_from_path(pdf_file, dpi=dpi) print(f"PDF converted to {len(images)} page(s)") if not images: print("ERROR: No images generated from PDF") return [] base_name = os.path.splitext(os.path.basename(pdf_file))[0] image_filenames = [] # Save each page as JPG image for i, image in enumerate(images): # Convert to RGB if necessary if image.mode != 'RGB': image = image.convert('RGB') # Simple naming with page numbers page_num = str(i + 1).zfill(3) # e.g., 001, 002, etc. image_filename = f"{base_name}_page_{page_num}.jpg" image_path = os.path.join(output_folder, image_filename) # Save as JPG image.save(image_path, 'JPEG', quality=85, optimize=True) image_filenames.append(image_filename) print(f"Saved page {i + 1} to: {image_path}") print(f"PDF conversion complete. {len(image_filenames)} JPG images saved to {output_folder}") # Delete the PDF file if requested and conversion was successful if delete_pdf and os.path.exists(pdf_file) and image_filenames: os.remove(pdf_file) print(f"PDF file deleted: {pdf_file}") return image_filenames except Exception as e: print(f"Error converting PDF to JPG images: {e}") import traceback traceback.print_exc() return [] def update_playlist_with_files(image_filenames, duration, target_type, target_id): """ Add files to a player or group playlist and update version numbers. Args: image_filenames (list): List of filenames to add to playlist duration (int): Duration in seconds for each file target_type (str): 'player' or 'group' target_id (int): ID of the player or group Returns: bool: True if successful, False otherwise """ try: if target_type == 'group': group = Group.query.get_or_404(target_id) for player in group.players: for image_filename in image_filenames: new_content = Content(file_name=image_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) for image_filename in image_filenames: new_content = Content(file_name=image_filename, duration=duration, player_id=target_id) db.session.add(new_content) player.playlist_version += 1 else: print(f"Invalid target type: {target_type}") return False db.session.commit() print(f"Added {len(image_filenames)} files to playlist") return True except Exception as e: db.session.rollback() print(f"Error updating playlist: {e}") return False 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. Args: input_file (str): Path to the PDF file output_folder (str): Path to save the images 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 """ 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) print(f"Created output folder: {output_folder}") # 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, 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). Args: input_file (str): Path to the PPTX file output_folder (str): Path to save the images 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 """ 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) print(f"Created output folder: {output_folder}") try: # Step 1: Convert PPTX to PDF using LibreOffice for vector quality print("Step 1: Converting PPTX to PDF...") from utils.pptx_converter import pptx_to_pdf_libreoffice pdf_file = pptx_to_pdf_libreoffice(input_file, output_folder) if not pdf_file: print("Error: Failed to convert PPTX to PDF") print("This could be due to:") print("- LibreOffice not properly installed") 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) image_filenames = convert_pdf_to_images(pdf_file, output_folder, delete_pdf=True, dpi=300) if not image_filenames: print("Error: Failed to convert PDF to images") print("This could be due to:") print("- poppler-utils not properly installed") 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") # Step 3: Delete the original PPTX file after successful conversion if os.path.exists(input_file): 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 except Exception as e: print(f"Error processing PPTX file: {e}") import traceback traceback.print_exc() return False 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 """ results = [] # Get target name for logging target_name = "" if target_type == 'group': group = Group.query.get_or_404(target_id) target_name = group.name elif target_type == 'player': player = Player.query.get_or_404(target_id) target_name = player.username 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) # Use simple path resolution for containerized environment upload_folder = app.config['UPLOAD_FOLDER'] print(f"Upload folder: {upload_folder}") # Ensure upload folder exists if not os.path.exists(upload_folder): os.makedirs(upload_folder, exist_ok=True) print(f"Created upload folder: {upload_folder}") file_path = os.path.join(upload_folder, filename) file.save(file_path) print(f"File saved to: {file_path}") print(f"Processing file: {filename}, Media Type: {media_type}") 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, 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 } # Start background conversion using absolute path import threading 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, 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) else: result['success'] = False 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, 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) else: result['success'] = False result['message'] = f"Error processing PowerPoint file: {filename}" results.append(result) except Exception as e: print(f"Error processing file {file.filename}: {e}") results.append({ 'filename': file.filename, 'success': False, 'message': f"Error processing file {file.filename}: {str(e)}" }) return results