"""Content blueprint for media upload and management.""" from flask import (Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app, send_from_directory) from flask_login import login_required from werkzeug.utils import secure_filename import os from typing import Optional, Dict import json from app.extensions import db, cache from app.models import Content, Group from app.utils.logger import log_action from app.utils.uploads import ( save_uploaded_file, process_video_file, process_pdf_file, get_upload_progress, set_upload_progress ) content_bp = Blueprint('content', __name__, url_prefix='/content') # In-memory storage for upload progress (for simple demo; use Redis in production) upload_progress = {} @content_bp.route('/') @login_required def content_list(): """Display list of all content.""" try: # Get all unique content files (by filename) from sqlalchemy import func # Get content with player information contents = Content.query.order_by(Content.filename, Content.uploaded_at.desc()).all() # Group content by filename to show which players have each file content_map = {} for content in contents: if content.filename not in content_map: content_map[content.filename] = { 'content': content, 'players': [], 'groups': [] } # Add player info if assigned to a player if content.player_id: from app.models import Player player = Player.query.get(content.player_id) if player: content_map[content.filename]['players'].append({ 'id': player.id, 'name': player.name, 'group': player.group.name if player.group else None }) # Convert to list for template content_list = [] for filename, data in content_map.items(): content_list.append({ 'filename': filename, 'content_type': data['content'].content_type, 'duration': data['content'].duration, 'file_size': data['content'].file_size_mb, 'uploaded_at': data['content'].uploaded_at, 'players': data['players'], 'player_count': len(data['players']) }) # Sort by upload date content_list.sort(key=lambda x: x['uploaded_at'], reverse=True) return render_template('content/content_list.html', content_list=content_list) except Exception as e: log_action('error', f'Error loading content list: {str(e)}') flash('Error loading content list.', 'danger') return redirect(url_for('main.dashboard')) @content_bp.route('/upload', methods=['GET', 'POST']) @login_required def upload_content(): """Upload new content.""" if request.method == 'GET': # Get parameters for return URL and pre-selection player_id = request.args.get('player_id', type=int) return_url = request.args.get('return_url', url_for('content.content_list')) # Get all players for selection from app.models import Player players = Player.query.order_by(Player.name).all() return render_template('content/upload_content.html', players=players, selected_player_id=player_id, return_url=return_url) try: # Get form data player_id = request.form.get('player_id', type=int) media_type = request.form.get('media_type', 'image') duration = request.form.get('duration', type=int, default=10) session_id = request.form.get('session_id', os.urandom(8).hex()) return_url = request.form.get('return_url', url_for('content.content_list')) # Get files files = request.files.getlist('files') if not files or files[0].filename == '': flash('No files provided.', 'warning') return redirect(url_for('content.upload_content')) if not player_id: flash('Please select a player.', 'warning') return redirect(url_for('content.upload_content')) # Initialize progress tracking using shared utility set_upload_progress(session_id, 0, 'Starting upload...', 'uploading') # Process each file upload_folder = current_app.config['UPLOAD_FOLDER'] os.makedirs(upload_folder, exist_ok=True) processed_count = 0 total_files = len(files) for idx, file in enumerate(files): if file.filename == '': continue # Update progress progress_pct = int((idx / total_files) * 80) # 0-80% for file processing set_upload_progress(session_id, progress_pct, f'Processing file {idx + 1} of {total_files}...', 'processing') filename = secure_filename(file.filename) filepath = os.path.join(upload_folder, filename) # Save file file.save(filepath) # Determine content type file_ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else '' if file_ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp']: content_type = 'image' elif file_ext in ['mp4', 'avi', 'mov', 'mkv', 'webm']: content_type = 'video' # Process video (convert to Raspberry Pi optimized format) set_upload_progress(session_id, progress_pct + 5, f'Optimizing video {idx + 1} for Raspberry Pi (30fps, H.264)...', 'processing') success, message = process_video_file(filepath, session_id) if not success: log_action('error', f'Video optimization failed: {message}') continue # Skip this file and move to next elif file_ext == 'pdf': content_type = 'pdf' # Process PDF (convert to images) set_upload_progress(session_id, progress_pct + 5, f'Converting PDF {idx + 1}...', 'processing') # process_pdf_file(filepath, session_id) elif file_ext in ['ppt', 'pptx']: content_type = 'presentation' # Process presentation (convert to PDF then images) set_upload_progress(session_id, progress_pct + 5, f'Converting PowerPoint {idx + 1}...', 'processing') # This would call pptx_converter utility else: content_type = 'other' # Create content record linked to player from app.models import Player player = Player.query.get(player_id) if player: new_content = Content( filename=filename, content_type=content_type, duration=duration, file_size=os.path.getsize(filepath), player_id=player_id ) db.session.add(new_content) # Increment playlist version player.playlist_version += 1 log_action('info', f'Content "{filename}" added to player "{player.name}" (version {player.playlist_version})') processed_count += 1 # Commit all changes set_upload_progress(session_id, 90, 'Saving to database...', 'processing') db.session.commit() # Complete set_upload_progress(session_id, 100, f'Successfully uploaded {processed_count} file(s)!', 'complete') # Clear all playlist caches cache.clear() log_action('info', f'{processed_count} files uploaded successfully (Type: {media_type})') flash(f'{processed_count} file(s) uploaded successfully.', 'success') return redirect(return_url) except Exception as e: db.session.rollback() # Update progress to error state if 'session_id' in locals(): set_upload_progress(session_id, 0, f'Upload failed: {str(e)}', 'error') log_action('error', f'Error uploading content: {str(e)}') flash('Error uploading content. Please try again.', 'danger') return redirect(url_for('content.upload_content')) @content_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit_content(content_id: int): """Edit content metadata.""" content = Content.query.get_or_404(content_id) if request.method == 'GET': return render_template('content/edit_content.html', content=content) try: duration = request.form.get('duration', type=int) description = request.form.get('description', '').strip() # Update content if duration is not None: content.duration = duration content.description = description or None db.session.commit() # Clear caches cache.clear() log_action('info', f'Content "{content.filename}" (ID: {content_id}) updated') flash(f'Content "{content.filename}" updated successfully.', 'success') return redirect(url_for('content.content_list')) except Exception as e: db.session.rollback() log_action('error', f'Error updating content: {str(e)}') flash('Error updating content. Please try again.', 'danger') return redirect(url_for('content.edit_content', content_id=content_id)) @content_bp.route('//delete', methods=['POST']) @login_required def delete_content(content_id: int): """Delete content and associated file.""" try: content = Content.query.get_or_404(content_id) filename = content.filename # Delete file from disk filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], filename) if os.path.exists(filepath): os.remove(filepath) # Delete from database db.session.delete(content) db.session.commit() # Clear caches cache.clear() log_action('info', f'Content "{filename}" (ID: {content_id}) deleted') flash(f'Content "{filename}" deleted successfully.', 'success') except Exception as e: db.session.rollback() log_action('error', f'Error deleting content: {str(e)}') flash('Error deleting content. Please try again.', 'danger') return redirect(url_for('content.content_list')) @content_bp.route('/delete-by-filename', methods=['POST']) @login_required def delete_by_filename(): """Delete all content entries with a specific filename.""" try: data = request.get_json() filename = data.get('filename') if not filename: return jsonify({'success': False, 'message': 'No filename provided'}), 400 # Find all content entries with this filename contents = Content.query.filter_by(filename=filename).all() if not contents: return jsonify({'success': False, 'message': 'Content not found'}), 404 deleted_count = len(contents) # Delete file from disk (only once) filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], filename) if os.path.exists(filepath): os.remove(filepath) log_action('info', f'Deleted file from disk: {filename}') # Delete all database entries for content in contents: db.session.delete(content) db.session.commit() # Clear caches cache.clear() log_action('info', f'Content "{filename}" deleted from {deleted_count} playlist(s)') return jsonify({ 'success': True, 'message': f'Content deleted from {deleted_count} playlist(s)', 'deleted_count': deleted_count }) except Exception as e: db.session.rollback() log_action('error', f'Error deleting content by filename: {str(e)}') return jsonify({'success': False, 'message': str(e)}), 500 @content_bp.route('/bulk/delete', methods=['POST']) @login_required def bulk_delete_content(): """Delete multiple content items at once.""" try: content_ids = request.json.get('content_ids', []) if not content_ids: return jsonify({'success': False, 'error': 'No content selected'}), 400 # Delete content deleted_count = 0 for content_id in content_ids: content = Content.query.get(content_id) if content: # Delete file filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], content.filename) if os.path.exists(filepath): os.remove(filepath) db.session.delete(content) deleted_count += 1 db.session.commit() # Clear caches cache.clear() log_action('info', f'Bulk deleted {deleted_count} content items') return jsonify({'success': True, 'deleted': deleted_count}) except Exception as e: db.session.rollback() log_action('error', f'Error bulk deleting content: {str(e)}') return jsonify({'success': False, 'error': str(e)}), 500 @content_bp.route('/upload-progress/') @login_required def upload_progress_status(upload_id: str): """Get upload progress for a specific upload.""" progress = get_upload_progress(upload_id) return jsonify(progress) @content_bp.route('/preview/') @login_required def preview_content(content_id: int): """Preview content in browser.""" try: content = Content.query.get_or_404(content_id) # Serve file from uploads folder return send_from_directory( current_app.config['UPLOAD_FOLDER'], content.filename, as_attachment=False ) except Exception as e: log_action('error', f'Error previewing content: {str(e)}') return "Error loading content", 500 @content_bp.route('//download') @login_required def download_content(content_id: int): """Download content file.""" try: content = Content.query.get_or_404(content_id) log_action('info', f'Content "{content.filename}" downloaded') return send_from_directory( current_app.config['UPLOAD_FOLDER'], content.filename, as_attachment=True ) except Exception as e: log_action('error', f'Error downloading content: {str(e)}') return "Error downloading content", 500 @content_bp.route('/statistics') @login_required def content_statistics(): """Get content statistics.""" try: total_content = Content.query.count() # Count by type type_counts = {} for content_type in ['image', 'video', 'pdf', 'presentation', 'other']: count = Content.query.filter_by(content_type=content_type).count() type_counts[content_type] = count # Calculate total storage upload_folder = current_app.config['UPLOAD_FOLDER'] total_size = 0 if os.path.exists(upload_folder): for dirpath, dirnames, filenames in os.walk(upload_folder): for filename in filenames: filepath = os.path.join(dirpath, filename) if os.path.exists(filepath): total_size += os.path.getsize(filepath) return jsonify({ 'total': total_content, 'by_type': type_counts, 'total_size_mb': round(total_size / (1024 * 1024), 2) }) except Exception as e: log_action('error', f'Error getting content statistics: {str(e)}') return jsonify({'error': str(e)}), 500 @content_bp.route('/check-duplicates') @login_required def check_duplicates(): """Check for duplicate filenames.""" try: # Get all filenames all_content = Content.query.all() filename_counts = {} for content in all_content: filename_counts[content.filename] = filename_counts.get(content.filename, 0) + 1 # Find duplicates duplicates = {fname: count for fname, count in filename_counts.items() if count > 1} return jsonify({ 'has_duplicates': len(duplicates) > 0, 'duplicates': duplicates }) except Exception as e: log_action('error', f'Error checking duplicates: {str(e)}') return jsonify({'error': str(e)}), 500 @content_bp.route('//groups') @login_required def content_groups_info(content_id: int): """Get groups that contain this content.""" try: content = Content.query.get_or_404(content_id) groups_data = [] for group in content.groups: groups_data.append({ 'id': group.id, 'name': group.name, 'description': group.description, 'player_count': group.players.count() }) return jsonify({ 'content_id': content_id, 'filename': content.filename, 'groups': groups_data }) except Exception as e: log_action('error', f'Error getting content groups: {str(e)}') return jsonify({'error': str(e)}), 500