updated features to upload pptx files
This commit is contained in:
@@ -243,10 +243,222 @@ def upload_media_page():
|
||||
return render_template('content/upload_media.html', playlists=playlists)
|
||||
|
||||
|
||||
def process_image_file(filepath: str, filename: str) -> tuple[bool, str]:
|
||||
"""Process and optimize image files."""
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
# Open and optimize image
|
||||
img = Image.open(filepath)
|
||||
|
||||
# Convert RGBA to RGB for JPEGs
|
||||
if img.mode == 'RGBA' and filename.lower().endswith(('.jpg', '.jpeg')):
|
||||
rgb_img = Image.new('RGB', img.size, (255, 255, 255))
|
||||
rgb_img.paste(img, mask=img.split()[3])
|
||||
img = rgb_img
|
||||
|
||||
# Resize if too large (max 1920x1080 for display efficiency)
|
||||
max_size = (1920, 1080)
|
||||
if img.width > max_size[0] or img.height > max_size[1]:
|
||||
img.thumbnail(max_size, Image.Resampling.LANCZOS)
|
||||
img.save(filepath, optimize=True, quality=85)
|
||||
log_action('info', f'Optimized image: {filename}')
|
||||
|
||||
return True, "Image processed successfully"
|
||||
except Exception as e:
|
||||
return False, f"Image processing error: {str(e)}"
|
||||
|
||||
|
||||
def process_video_file_extended(filepath: str, filename: str) -> tuple[bool, str]:
|
||||
"""Process and optimize video files for Raspberry Pi playback."""
|
||||
try:
|
||||
# Basic video validation
|
||||
import subprocess
|
||||
|
||||
# Check if video is playable
|
||||
result = subprocess.run(
|
||||
['ffprobe', '-v', 'error', '-select_streams', 'v:0',
|
||||
'-show_entries', 'stream=codec_name,width,height',
|
||||
'-of', 'default=noprint_wrappers=1', filepath],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
log_action('info', f'Video validated: {filename}')
|
||||
return True, "Video validated successfully"
|
||||
else:
|
||||
return False, "Video validation failed"
|
||||
except Exception as e:
|
||||
# If ffprobe not available, just accept the video
|
||||
log_action('warning', f'Video validation skipped (ffprobe unavailable): {filename}')
|
||||
return True, "Video accepted without validation"
|
||||
|
||||
|
||||
def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
|
||||
"""Process PDF files."""
|
||||
try:
|
||||
# Basic PDF validation - check if it's a valid PDF
|
||||
with open(filepath, 'rb') as f:
|
||||
header = f.read(5)
|
||||
if header != b'%PDF-':
|
||||
return False, "Invalid PDF file"
|
||||
|
||||
log_action('info', f'PDF validated: {filename}')
|
||||
return True, "PDF processed successfully"
|
||||
except Exception as e:
|
||||
return False, f"PDF processing error: {str(e)}"
|
||||
|
||||
|
||||
def process_presentation_file(filepath: str, filename: str) -> tuple[bool, str]:
|
||||
"""Process PowerPoint presentation files by converting slides to images."""
|
||||
try:
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
# Basic validation - check file exists and has content
|
||||
file_size = os.path.getsize(filepath)
|
||||
if file_size < 1024: # Less than 1KB is suspicious
|
||||
return False, "File too small to be a valid presentation"
|
||||
|
||||
# Check if LibreOffice is available
|
||||
libreoffice_paths = [
|
||||
'/usr/bin/libreoffice',
|
||||
'/usr/bin/soffice',
|
||||
'/snap/bin/libreoffice',
|
||||
'libreoffice', # Try in PATH
|
||||
'soffice'
|
||||
]
|
||||
|
||||
libreoffice_cmd = None
|
||||
for cmd in libreoffice_paths:
|
||||
try:
|
||||
result = subprocess.run([cmd, '--version'],
|
||||
capture_output=True,
|
||||
timeout=5)
|
||||
if result.returncode == 0:
|
||||
libreoffice_cmd = cmd
|
||||
log_action('info', f'Found LibreOffice at: {cmd}')
|
||||
break
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
continue
|
||||
|
||||
if not libreoffice_cmd:
|
||||
log_action('warning', f'LibreOffice not found, skipping slide conversion for: {filename}')
|
||||
return True, "Presentation accepted without conversion (LibreOffice unavailable)"
|
||||
|
||||
# Create temporary directory for conversion
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Copy presentation to temp directory
|
||||
temp_ppt = temp_path / filename
|
||||
shutil.copy2(filepath, temp_ppt)
|
||||
|
||||
# Convert presentation to images (PNG format)
|
||||
# Using LibreOffice headless mode with custom resolution
|
||||
convert_cmd = [
|
||||
libreoffice_cmd,
|
||||
'--headless',
|
||||
'--convert-to', 'png',
|
||||
'--outdir', str(temp_path),
|
||||
str(temp_ppt)
|
||||
]
|
||||
|
||||
log_action('info', f'Converting presentation to images: {filename}')
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
convert_cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120 # 2 minutes timeout
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
log_action('error', f'LibreOffice conversion failed: {result.stderr}')
|
||||
return True, "Presentation accepted without conversion (conversion failed)"
|
||||
|
||||
# Find generated PNG files
|
||||
png_files = sorted(temp_path.glob('*.png'))
|
||||
|
||||
if not png_files:
|
||||
log_action('warning', f'No images generated from presentation: {filename}')
|
||||
return True, "Presentation accepted without images"
|
||||
|
||||
# Get upload folder from app config
|
||||
upload_folder = current_app.config['UPLOAD_FOLDER']
|
||||
base_name = os.path.splitext(filename)[0]
|
||||
|
||||
# Move converted images to upload folder
|
||||
slide_count = 0
|
||||
for idx, png_file in enumerate(png_files, start=1):
|
||||
# Create descriptive filename
|
||||
slide_filename = f"{base_name}_slide_{idx:03d}.png"
|
||||
destination = os.path.join(upload_folder, slide_filename)
|
||||
|
||||
shutil.move(str(png_file), destination)
|
||||
|
||||
# Optimize the image to Full HD (1920x1080)
|
||||
optimize_image_to_fullhd(destination)
|
||||
|
||||
slide_count += 1
|
||||
|
||||
log_action('info', f'Converted {slide_count} slides from {filename} to images')
|
||||
|
||||
# Remove original PPTX file as we now have the images
|
||||
os.remove(filepath)
|
||||
|
||||
return True, f"Presentation converted to {slide_count} Full HD images"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
log_action('error', f'LibreOffice conversion timeout for: {filename}')
|
||||
return True, "Presentation accepted without conversion (timeout)"
|
||||
|
||||
except Exception as e:
|
||||
log_action('error', f'Presentation processing error: {str(e)}')
|
||||
return False, f"Presentation processing error: {str(e)}"
|
||||
|
||||
|
||||
def optimize_image_to_fullhd(filepath: str) -> bool:
|
||||
"""Optimize and resize image to Full HD (1920x1080) maintaining aspect ratio."""
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
img = Image.open(filepath)
|
||||
|
||||
# Target Full HD resolution
|
||||
target_size = (1920, 1080)
|
||||
|
||||
# Calculate resize maintaining aspect ratio
|
||||
img.thumbnail(target_size, Image.Resampling.LANCZOS)
|
||||
|
||||
# Create Full HD canvas with white background
|
||||
fullhd_img = Image.new('RGB', target_size, (255, 255, 255))
|
||||
|
||||
# Center the image on the canvas
|
||||
x = (target_size[0] - img.width) // 2
|
||||
y = (target_size[1] - img.height) // 2
|
||||
|
||||
if img.mode == 'RGBA':
|
||||
fullhd_img.paste(img, (x, y), img)
|
||||
else:
|
||||
fullhd_img.paste(img, (x, y))
|
||||
|
||||
# Save optimized image
|
||||
fullhd_img.save(filepath, 'PNG', optimize=True)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
log_action('error', f'Image optimization error: {str(e)}')
|
||||
return False
|
||||
|
||||
|
||||
@content_bp.route('/upload-media', methods=['POST'])
|
||||
@login_required
|
||||
def upload_media():
|
||||
"""Upload media files to library."""
|
||||
"""Upload media files to library with type-specific processing."""
|
||||
try:
|
||||
files = request.files.getlist('files')
|
||||
content_type = request.form.get('content_type', 'image')
|
||||
@@ -261,6 +473,7 @@ def upload_media():
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
|
||||
uploaded_count = 0
|
||||
processing_errors = []
|
||||
|
||||
for file in files:
|
||||
if file.filename == '':
|
||||
@@ -275,64 +488,138 @@ def upload_media():
|
||||
log_action('warning', f'File {filename} already exists, skipping')
|
||||
continue
|
||||
|
||||
# Save file
|
||||
# Save file first
|
||||
file.save(filepath)
|
||||
|
||||
# Determine content type from extension
|
||||
file_ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
|
||||
|
||||
if file_ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp']:
|
||||
# Process file based on type
|
||||
processing_success = True
|
||||
processing_message = ""
|
||||
|
||||
if file_ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']:
|
||||
detected_type = 'image'
|
||||
elif file_ext in ['mp4', 'avi', 'mov', 'mkv', 'webm']:
|
||||
processing_success, processing_message = process_image_file(filepath, filename)
|
||||
|
||||
elif file_ext in ['mp4', 'avi', 'mov', 'mkv', 'webm', 'flv', 'wmv']:
|
||||
detected_type = 'video'
|
||||
# Process video for Raspberry Pi
|
||||
success, message = process_video_file(filepath, os.urandom(8).hex())
|
||||
if not success:
|
||||
log_action('error', f'Video processing failed: {message}')
|
||||
processing_success, processing_message = process_video_file_extended(filepath, filename)
|
||||
|
||||
elif file_ext == 'pdf':
|
||||
detected_type = 'pdf'
|
||||
processing_success, processing_message = process_pdf_file(filepath, filename)
|
||||
|
||||
elif file_ext in ['ppt', 'pptx']:
|
||||
detected_type = 'pptx'
|
||||
processing_success, processing_message = process_presentation_file(filepath, filename)
|
||||
|
||||
# For presentations, slides are converted to individual images
|
||||
# We need to add each slide image as a separate content item
|
||||
if processing_success and "converted to" in processing_message.lower():
|
||||
# Find all slide images that were created
|
||||
base_name = os.path.splitext(filename)[0]
|
||||
slide_pattern = f"{base_name}_slide_*.png"
|
||||
import glob
|
||||
slide_files = sorted(glob.glob(os.path.join(upload_folder, slide_pattern)))
|
||||
|
||||
if slide_files:
|
||||
max_position = 0
|
||||
if playlist_id:
|
||||
playlist = Playlist.query.get(playlist_id)
|
||||
max_position = db.session.query(db.func.max(playlist_content.c.position))\
|
||||
.filter(playlist_content.c.playlist_id == playlist_id)\
|
||||
.scalar() or 0
|
||||
|
||||
# Add each slide as separate content
|
||||
for slide_file in slide_files:
|
||||
slide_filename = os.path.basename(slide_file)
|
||||
|
||||
# Create content record for slide
|
||||
slide_content = Content(
|
||||
filename=slide_filename,
|
||||
content_type='image',
|
||||
duration=duration,
|
||||
file_size=os.path.getsize(slide_file)
|
||||
)
|
||||
db.session.add(slide_content)
|
||||
db.session.flush()
|
||||
|
||||
# Add to playlist if specified
|
||||
if playlist_id:
|
||||
max_position += 1
|
||||
stmt = playlist_content.insert().values(
|
||||
playlist_id=playlist_id,
|
||||
content_id=slide_content.id,
|
||||
position=max_position,
|
||||
duration=duration
|
||||
)
|
||||
db.session.execute(stmt)
|
||||
|
||||
uploaded_count += 1
|
||||
|
||||
# Increment playlist version if slides were added
|
||||
if playlist_id and slide_files:
|
||||
playlist.version += 1
|
||||
|
||||
continue # Skip normal content creation below
|
||||
|
||||
else:
|
||||
detected_type = 'other'
|
||||
|
||||
# Create content record
|
||||
content = Content(
|
||||
filename=filename,
|
||||
content_type=detected_type,
|
||||
duration=duration,
|
||||
file_size=os.path.getsize(filepath)
|
||||
)
|
||||
db.session.add(content)
|
||||
db.session.flush() # Get content ID
|
||||
if not processing_success:
|
||||
processing_errors.append(f"{filename}: {processing_message}")
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath) # Remove failed file
|
||||
log_action('error', f'Processing failed for {filename}: {processing_message}')
|
||||
continue
|
||||
|
||||
# Add to playlist if specified
|
||||
if playlist_id:
|
||||
playlist = Playlist.query.get(playlist_id)
|
||||
if playlist:
|
||||
# Get max position
|
||||
max_position = db.session.query(db.func.max(playlist_content.c.position))\
|
||||
.filter(playlist_content.c.playlist_id == playlist_id)\
|
||||
.scalar() or 0
|
||||
|
||||
# Add to playlist
|
||||
stmt = playlist_content.insert().values(
|
||||
playlist_id=playlist_id,
|
||||
content_id=content.id,
|
||||
position=max_position + 1,
|
||||
duration=duration
|
||||
)
|
||||
db.session.execute(stmt)
|
||||
|
||||
# Increment playlist version
|
||||
playlist.version += 1
|
||||
|
||||
uploaded_count += 1
|
||||
# Create content record (for non-presentation files or failed conversions)
|
||||
if os.path.exists(filepath):
|
||||
content = Content(
|
||||
filename=filename,
|
||||
content_type=detected_type,
|
||||
duration=duration,
|
||||
file_size=os.path.getsize(filepath)
|
||||
)
|
||||
db.session.add(content)
|
||||
db.session.flush() # Get content ID
|
||||
|
||||
# Add to playlist if specified
|
||||
if playlist_id:
|
||||
playlist = Playlist.query.get(playlist_id)
|
||||
if playlist:
|
||||
# Get max position
|
||||
max_position = db.session.query(db.func.max(playlist_content.c.position))\
|
||||
.filter(playlist_content.c.playlist_id == playlist_id)\
|
||||
.scalar() or 0
|
||||
|
||||
# Add to playlist
|
||||
stmt = playlist_content.insert().values(
|
||||
playlist_id=playlist_id,
|
||||
content_id=content.id,
|
||||
position=max_position + 1,
|
||||
duration=duration
|
||||
)
|
||||
db.session.execute(stmt)
|
||||
|
||||
# Increment playlist version
|
||||
playlist.version += 1
|
||||
|
||||
uploaded_count += 1
|
||||
|
||||
db.session.commit()
|
||||
cache.clear()
|
||||
|
||||
log_action('info', f'Uploaded {uploaded_count} media files')
|
||||
|
||||
if playlist_id:
|
||||
# Show appropriate flash message
|
||||
if processing_errors:
|
||||
error_summary = '; '.join(processing_errors[:3])
|
||||
if len(processing_errors) > 3:
|
||||
error_summary += f' and {len(processing_errors) - 3} more...'
|
||||
flash(f'Uploaded {uploaded_count} file(s). Errors: {error_summary}', 'warning')
|
||||
elif playlist_id:
|
||||
playlist = Playlist.query.get(playlist_id)
|
||||
flash(f'Successfully uploaded {uploaded_count} file(s) to playlist "{playlist.name}"!', 'success')
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user