🐳 Docker Configuration Improvements: - Simplified docker-compose.yml to use single app folder bind mount - Removed complex data folder mapping that caused path confusion - Updated environment variables to match entrypoint script expectations - Streamlined deployment for better reliability 🔧 Upload System Fixes: - Fixed path resolution issues in uploads.py for containerized deployment - Simplified upload folder path handling to work correctly in containers - Removed complex absolute path conversion logic that caused file placement issues - Ensured all file operations use consistent /app/static/uploads path 📁 File Processing Improvements: - Fixed PPTX to JPG conversion workflow path handling - Corrected PDF processing to save files in correct container location - Improved video conversion path resolution - Enhanced error handling and logging for upload operations 🚀 Production Benefits: - Eliminates 404 errors for uploaded media files - Ensures files are saved in correct locations within container - Simplifies development and debugging with direct app folder mounting - Maintains data consistency across container restarts ✅ This resolves the upload workflow issues where PPTX files were not being correctly processed and saved to the expected locations.
428 lines
18 KiB
Python
428 lines
18 KiB
Python
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
|
|
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")
|
|
return False
|
|
|
|
print(f"PPTX successfully converted to PDF: {pdf_file}")
|
|
|
|
# Step 2: Use the same PDF to images workflow as direct PDF uploads
|
|
# 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")
|
|
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
|
|
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")
|
|
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 |