diff --git a/app/blueprints/admin.py b/app/blueprints/admin.py index 66bcea6..e4977f9 100644 --- a/app/blueprints/admin.py +++ b/app/blueprints/admin.py @@ -490,3 +490,29 @@ def delete_leftover_videos(): flash(f'Error deleting leftover videos: {str(e)}', 'danger') return redirect(url_for('admin.leftover_media')) + +@admin_bp.route('/delete-single-leftover/', methods=['POST']) +@login_required +@admin_required +def delete_single_leftover(content_id): + """Delete a single leftover content file""" + try: + content = Content.query.get_or_404(content_id) + + # Delete physical file + if content.file_path: + file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], content.file_path) + if os.path.exists(file_path): + os.remove(file_path) + + # Delete database record + db.session.delete(content) + db.session.commit() + + flash(f'Successfully deleted {content.file_path}', 'success') + + except Exception as e: + db.session.rollback() + flash(f'Error deleting file: {str(e)}', 'danger') + + return redirect(url_for('admin.leftover_media')) diff --git a/app/blueprints/content.py b/app/blueprints/content.py index d37b391..f9afd7f 100644 --- a/app/blueprints/content.py +++ b/app/blueprints/content.py @@ -295,17 +295,58 @@ def process_video_file_extended(filepath: str, filename: str) -> tuple[bool, str def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]: - """Process PDF files.""" + """Process PDF files by converting each page to PNG images.""" try: + from pdf2image import convert_from_path + from pathlib import Path + # 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" + log_action('info', f'Converting PDF to images: {filename}') + + # Convert PDF pages to images at Full HD resolution + images = convert_from_path( + filepath, + dpi=150, # Good quality for Full HD display + fmt='png', + size=(1920, 1080) # Direct Full HD output + ) + + if not images: + return False, "No pages found in PDF" + + # Generate base filename without extension + base_filename = Path(filename).stem + upload_folder = os.path.dirname(filepath) + + # Save each page directly as PNG + converted_files = [] + for idx, image in enumerate(images, start=1): + # Create filename for this page + page_filename = f"{base_filename}_page{idx:03d}.png" + page_filepath = os.path.join(upload_folder, page_filename) + + # Save the image directly without additional optimization + image.save(page_filepath, 'PNG', optimize=True) + + converted_files.append((page_filepath, page_filename)) + log_action('info', f'Converted PDF page {idx}/{len(images)}: {page_filename}') + + log_action('info', f'PDF converted successfully: {len(images)} pages from {filename}') + + # Return success with file info for later processing + return True, f"PDF converted to {len(images)} images" + + except ImportError: + return False, "pdf2image library not installed. Install with: pip install pdf2image" except Exception as e: + import traceback + error_details = traceback.format_exc() + log_action('error', f'PDF processing error: {str(e)}\n{error_details}') return False, f"PDF processing error: {str(e)}" @@ -421,32 +462,38 @@ def process_presentation_file(filepath: str, filename: str) -> tuple[bool, str]: return False, f"Presentation processing error: {str(e)}" +def create_fullhd_image(img): + """Create a Full HD (1920x1080) image from PIL Image object, centered on white background.""" + from PIL import Image as PILImage + + target_size = (1920, 1080) + + # Resize maintaining aspect ratio + img_copy = img.copy() + img_copy.thumbnail(target_size, PILImage.Resampling.LANCZOS) + + # Create canvas with white background + fullhd_img = PILImage.new('RGB', target_size, (255, 255, 255)) + + # Center the image + x = (target_size[0] - img_copy.width) // 2 + y = (target_size[1] - img_copy.height) // 2 + + if img_copy.mode == 'RGBA': + fullhd_img.paste(img_copy, (x, y), img_copy) + else: + fullhd_img.paste(img_copy, (x, y)) + + return fullhd_img + + def optimize_image_to_fullhd(filepath: str) -> bool: - """Optimize and resize image to Full HD (1920x1080) maintaining aspect ratio.""" + """Optimize and resize image file 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 = create_fullhd_image(img) fullhd_img.save(filepath, 'PNG', optimize=True) return True @@ -510,6 +557,61 @@ def upload_media(): detected_type = 'pdf' processing_success, processing_message = process_pdf_file(filepath, filename) + # For PDFs, pages are converted to individual images + # We need to add each page image as a separate content item + if processing_success and "converted to" in processing_message.lower(): + # Find all page images that were created + base_name = os.path.splitext(filename)[0] + page_pattern = f"{base_name}_page*.png" + import glob + page_files = sorted(glob.glob(os.path.join(upload_folder, page_pattern))) + + if page_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 page as separate content + for page_file in page_files: + page_filename = os.path.basename(page_file) + + # Create content record for page + page_content = Content( + filename=page_filename, + content_type='image', + duration=duration, + file_size=os.path.getsize(page_file) + ) + db.session.add(page_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=page_content.id, + position=max_position, + duration=duration + ) + db.session.execute(stmt) + + uploaded_count += 1 + + # Increment playlist version if pages were added + if playlist_id and page_files: + playlist.version += 1 + + # Delete original PDF file + if os.path.exists(filepath): + os.remove(filepath) + log_action('info', f'Removed original PDF after conversion: {filename}') + + continue # Skip normal content creation below + elif file_ext in ['ppt', 'pptx']: detected_type = 'pptx' processing_success, processing_message = process_presentation_file(filepath, filename) @@ -562,6 +664,11 @@ def upload_media(): if playlist_id and slide_files: playlist.version += 1 + # Delete original PPTX file + if os.path.exists(filepath): + os.remove(filepath) + log_action('info', f'Removed original PPTX after conversion: {filename}') + continue # Skip normal content creation below else: diff --git a/app/templates/admin/leftover_media.html b/app/templates/admin/leftover_media.html index a19a46d..6b0ce03 100644 --- a/app/templates/admin/leftover_media.html +++ b/app/templates/admin/leftover_media.html @@ -63,6 +63,7 @@ Size Duration Uploaded + Action @@ -72,6 +73,11 @@ {{ "%.2f"|format(img.file_size_mb) }} MB {{ img.duration }}s {{ img.uploaded_at.strftime('%Y-%m-%d %H:%M') if img.uploaded_at else 'N/A' }} + +
+ +
+ {% endfor %} @@ -107,6 +113,7 @@ Size Duration Uploaded + Action @@ -116,6 +123,11 @@ {{ "%.2f"|format(video.file_size_mb) }} MB {{ video.duration }}s {{ video.uploaded_at.strftime('%Y-%m-%d %H:%M') if video.uploaded_at else 'N/A' }} + +
+ +
+ {% endfor %} diff --git a/app/templates/playlist/manage_playlist.html b/app/templates/playlist/manage_playlist.html index e225b87..00debf4 100644 --- a/app/templates/playlist/manage_playlist.html +++ b/app/templates/playlist/manage_playlist.html @@ -217,19 +217,27 @@ } .duration-input { - width: 80px; + width: 70px !important; + padding: 5px 8px !important; text-align: center; transition: all 0.3s ease; + background: white !important; + border: 2px solid #ced4da !important; + border-radius: 6px; + font-size: 14px; + font-weight: 500; } .duration-input:hover { - border-color: #667eea; + border-color: #667eea !important; box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1); } .duration-input:focus { - border-color: #667eea; + border-color: #667eea !important; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); + background: white !important; + outline: none; } .save-duration-btn { @@ -248,6 +256,29 @@ } } + .content-type-badge { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + } + + .badge-image { + background: #e3f2fd; + color: #1976d2; + } + + .badge-video { + background: #f3e5f5; + color: #7b1fa2; + } + + .badge-pdf { + background: #ffebee; + color: #c62828; + } + /* Dark mode support */ body.dark-mode .playlist-section { background: #2d3748; @@ -306,6 +337,38 @@ body.dark-mode .drag-handle { color: #718096; } + + body.dark-mode .duration-input { + background: #1a202c !important; + border-color: #4a5568 !important; + color: #e2e8f0 !important; + } + + body.dark-mode .duration-input:hover { + border-color: #667eea !important; + box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); + } + + body.dark-mode .duration-input:focus { + background: #2d3748 !important; + border-color: #667eea !important; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.3); + } + + body.dark-mode .badge-image { + background: #1e3a5f; + color: #64b5f6; + } + + body.dark-mode .badge-video { + background: #4a1e5a; + color: #ce93d8; + } + + body.dark-mode .badge-pdf { + background: #5a1e1e; + color: #ef5350; + }
@@ -383,10 +446,14 @@ {{ loop.index }} {{ content.filename }} - {% if content.content_type == 'image' %}📷 Image - {% elif content.content_type == 'video' %}🎥 Video - {% elif content.content_type == 'pdf' %}📄 PDF - {% else %}📁 {{ content.content_type }} + {% if content.content_type == 'image' %} + 📷 Image + {% elif content.content_type == 'video' %} + 🎥 Video + {% elif content.content_type == 'pdf' %} + 📄 PDF + {% else %} + 📁 {{ content.content_type }} {% endif %} @@ -400,14 +467,13 @@ onclick="event.stopPropagation()" onmousedown="event.stopPropagation()" oninput="markDurationChanged({{ content.id }})" - onkeypress="if(event.key==='Enter') saveDuration({{ content.id }})" - style="width: 60px; padding: 5px 8px;"> + onkeypress="if(event.key==='Enter') saveDuration({{ content.id }})"> @@ -626,7 +692,10 @@ function saveDuration(contentId) { const formData = new FormData(); formData.append('duration', duration); - fetch(`{{ url_for("playlist.update_duration", player_id=player.id, content_id=0) }}`.replace('/0', `/${contentId}`), { + const playerId = {{ player.id }}; + const url = `/playlist/${playerId}/update-duration/${contentId}`; + + fetch(url, { method: 'POST', body: formData })