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): """ Converts a video file to MP4 format with H.264 codec. """ # Use simple path resolution for containerized environment if not os.path.isabs(output_folder): # In container, relative paths work from /app directory print(f"Using relative path: {output_folder}") else: print(f"Using absolute path: {output_folder}") if not os.path.exists(output_folder): os.makedirs(output_folder, exist_ok=True) print(f"Created output folder: {output_folder}") # Generate the output file path base_name = os.path.splitext(os.path.basename(input_file))[0] output_file = os.path.join(output_folder, f"{base_name}.mp4") print(f"Converting video: {input_file} -> {output_file}") # FFmpeg command to convert the video command = [ "ffmpeg", "-i", input_file, # Input file "-c:v", "libx264", # Video codec: H.264 "-preset", "fast", # Encoding speed/quality tradeoff "-crf", "23", # Constant Rate Factor (quality, lower is better) "-vf", "scale=-1:1080", # Scale video to 1080p (preserve aspect ratio) "-r", "30", # Frame rate: 30 FPS "-c:a", "aac", # Audio codec: AAC "-b:a", "128k", # Audio bitrate output_file # Output file ] try: # Run the FFmpeg command subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) print(f"Video converted successfully: {output_file}") return output_file except subprocess.CalledProcessError as e: print(f"Error converting video: {e.stderr.decode()}") return None def convert_video_and_update_playlist(app, file_path, original_filename, target_type, target_id, duration): """ Converts a video and updates the playlist database. """ print(f"Starting video conversion for: {file_path}") # Use simple path resolution for containerized environment upload_folder = app.config['UPLOAD_FOLDER'] print(f"Upload folder: {upload_folder}") converted_file = convert_video(file_path, upload_folder) if converted_file: converted_filename = os.path.basename(converted_file) print(f"Video converted successfully: {converted_filename}") # Use the application context to interact with the database with app.app_context(): # Update the database with the converted filename if target_type == 'group': group = Group.query.get_or_404(target_id) for player in group.players: content = Content.query.filter_by(player_id=player.id, file_name=original_filename).first() if content: content.file_name = converted_filename elif target_type == 'player': content = Content.query.filter_by(player_id=target_id, file_name=original_filename).first() if content: content.file_name = converted_filename db.session.commit() print(f"Database updated with converted video: {converted_filename}") # Delete the original file only if it exists if os.path.exists(file_path): os.remove(file_path) print(f"Original file deleted: {file_path}") else: print(f"Video conversion failed for: {file_path}") # 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): """ 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 Returns: bool: True if successful, False otherwise """ print(f"Processing PDF file: {input_file}") print(f"Output folder: {output_folder}") # 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 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") return success else: print("Failed to convert PDF to images") return False def process_pptx(input_file, output_folder, duration, target_type, target_id): """ 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 Returns: bool: True if successful, False otherwise """ print(f"Processing PPTX file using PDF workflow: {input_file}") print(f"Output folder: {output_folder}") # 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") return False print(f"PPTX successfully converted to PDF: {pdf_file}") # 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") 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}") # 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") 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): """ Process uploaded files based on media type and add them to playlists. 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 for file in files: try: # 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': 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 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" log_upload('video', filename, target_type, target_id) elif media_type == 'pdf': # For PDFs, convert to images and update playlist using absolute path success = process_pdf(file_path, upload_folder, duration, target_type, target_id) 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': # 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) 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