diff --git a/app/routes/api.py b/app/routes/api.py index 6b56724..abf2abd 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -142,6 +142,71 @@ def health_check(): 'version': '2.0.0' }) +@bp.route('/content//remove-from-player', methods=['POST']) +def remove_content_from_player(content_id): + """Remove content from a specific player""" + from flask_login import login_required, current_user + + # Require authentication for this operation + if not current_user.is_authenticated: + return jsonify({'error': 'Authentication required'}), 401 + + data = request.get_json() + if not data or 'player_id' not in data: + return jsonify({'error': 'Player ID required'}), 400 + + player_id = data.get('player_id') + + # Find the content item + content = Content.query.filter_by(id=content_id, player_id=player_id).first() + if not content: + return jsonify({'error': 'Content not found for this player'}), 404 + + # Remove the content + try: + db.session.delete(content) + db.session.commit() + + return jsonify({ + 'success': True, + 'message': f'Content {content.file_name} removed from player' + }) + except Exception as e: + db.session.rollback() + return jsonify({'error': f'Failed to remove content: {str(e)}'}), 500 + +@bp.route('/player//heartbeat', methods=['POST']) +def player_heartbeat(player_id): + """Update player heartbeat/last seen timestamp""" + try: + player = Player.query.get_or_404(player_id) + player.last_seen = db.func.current_timestamp() + db.session.commit() + + return jsonify({ + 'success': True, + 'timestamp': player.last_seen.isoformat() if player.last_seen else None + }) + except Exception as e: + db.session.rollback() + return jsonify({'error': f'Failed to update heartbeat: {str(e)}'}), 500 + +@bp.route('/player//content', methods=['GET']) +def get_player_content_status(player_id): + """Get player content status for checking updates""" + try: + player = Player.query.get_or_404(player_id) + content_count = Content.query.filter_by(player_id=player_id).count() + + return jsonify({ + 'player_id': player_id, + 'playlist_version': player.playlist_version, + 'content_count': content_count, + 'updated': False # Could implement version checking logic here + }) + except Exception as e: + return jsonify({'error': f'Failed to get content status: {str(e)}'}), 500 + @bp.errorhandler(404) def api_not_found(error): """API 404 handler""" diff --git a/app/routes/content.py b/app/routes/content.py index 2af29f0..f376c16 100644 --- a/app/routes/content.py +++ b/app/routes/content.py @@ -72,12 +72,16 @@ def upload(): players = Player.query.order_by(Player.username).all() groups = Group.query.order_by(Group.name).all() + # Convert players and groups to dictionaries for JSON serialization + players_data = [{'id': p.id, 'username': p.username, 'hostname': p.hostname} for p in players] + groups_data = [{'id': g.id, 'name': g.name, 'description': g.description, 'player_count': len(g.players)} for g in groups] + return render_template( 'content/upload.html', target_type=target_type, target_id=target_id, - players=players, - groups=groups, + players=players_data, + groups=groups_data, return_url=return_url ) diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index 99f36e0..33e62d6 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -18,6 +18,10 @@ def index(): players = Player.query.order_by(Player.username).all() groups = Group.query.order_by(Group.name).all() + # Calculate statistics + total_content = sum(len(player.content) for player in players) + active_players = sum(1 for player in players if player.is_active) + # Check if logo exists from flask import current_app logo_path = os.path.join(current_app.static_folder, 'assets', 'logo.png') @@ -31,5 +35,7 @@ def index(): players=players, groups=groups, logo_exists=logo_exists, - server_logs=server_logs + server_logs=server_logs, + total_content=total_content, + active_players=active_players ) diff --git a/app/static/uploads/123.jpeg b/app/static/uploads/123.jpeg new file mode 100644 index 0000000..29d8372 Binary files /dev/null and b/app/static/uploads/123.jpeg differ diff --git a/app/templates/content/upload.html b/app/templates/content/upload.html new file mode 100644 index 0000000..6df5b3b --- /dev/null +++ b/app/templates/content/upload.html @@ -0,0 +1,173 @@ +{% extends "base.html" %} + +{% block title %}Upload Content - SKE Digital Signage{% endblock %} + +{% block content %} +
+ +
+
+

Upload Content

+

Upload images, videos, or documents to your players and groups

+
+ +
+ + +
+
+
+
+
Upload Files
+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ Supported formats: Images (PNG, JPG, GIF), Videos (MP4, AVI, MOV), Documents (PDF, PowerPoint) +
+
+ + +
+ + +
+ How long each item should be displayed (for images and documents) +
+
+ + + + + +
+ +
+
+
+
+ + +
+
+
Upload Guidelines
+
+
+
+
+
Images
+
    +
  • PNG, JPG, GIF, BMP, WebP
  • +
  • Will be optimized automatically
  • +
  • Best: 1920x1080 resolution
  • +
+
+
+
Videos
+
    +
  • MP4, AVI, MOV, WMV, WebM
  • +
  • Converted to web-compatible MP4
  • +
  • Duration setting is ignored
  • +
+
+
+
Documents
+
    +
  • PDF, PowerPoint (PPT/PPTX)
  • +
  • Converted to images per page/slide
  • +
  • Each page uses duration setting
  • +
+
+
+
+
+
+
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/app/templates/dashboard/index.html b/app/templates/dashboard/index.html index c525b09..a0fa576 100644 --- a/app/templates/dashboard/index.html +++ b/app/templates/dashboard/index.html @@ -59,7 +59,7 @@
Active Players
-

{{ players|selectattr('is_active')|list|length }}

+

{{ active_players }}

@@ -75,7 +75,7 @@
Total Content
-

{{ players|sum(attribute='content')|length }}

+

{{ total_content }}

diff --git a/app/templates/player/add.html b/app/templates/player/add.html new file mode 100644 index 0000000..1c813e0 --- /dev/null +++ b/app/templates/player/add.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} + +{% block title %}Add Player - SKE Digital Signage{% endblock %} + +{% block content %} +
+ +
+
+

Add Player

+

Create a new digital signage player

+
+ +
+ + +
+
+
+
+
Player Information
+
+
+
+
+ + +
Display name for this player
+
+ +
+ + +
Unique identifier for API access (e.g., display-001)
+
+ +
+ + +
Password for manual authentication
+
+ +
+ + +
Optional code for quick API access
+
+ +
+ +
+
+
+
+ + +
+
+
Player Setup Guide
+
+
+
    +
  1. Username: Choose a descriptive name (e.g., "Lobby Display", "Conference Room")
  2. +
  3. Hostname: Must be unique across all players (e.g., "lobby-01", "conf-room-a")
  4. +
  5. Password: Used for manual authentication in fullscreen mode
  6. +
  7. Quick Connect: Optional code for automated player client connections
  8. +
+
+
+
+
+
+{% endblock %} diff --git a/app/templates/player/auth.html b/app/templates/player/auth.html new file mode 100644 index 0000000..3dd9fd1 --- /dev/null +++ b/app/templates/player/auth.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} + +{% block title %}Player Authentication - SKE Digital Signage{% endblock %} + +{% block content %} +
+
+
+
+
+

Player Authentication

+

{{ player.username }}

+
+
+ {% if error %} +
{{ error }}
+ {% endif %} + +
+
+ + +
+ +
+ +
+
+ +
+ +
+ + + Enter the password to access this player's display + +
+
+
+
+
+
+ + +{% endblock %} diff --git a/app/templates/player/edit.html b/app/templates/player/edit.html new file mode 100644 index 0000000..11c252e --- /dev/null +++ b/app/templates/player/edit.html @@ -0,0 +1,168 @@ +{% extends "base.html" %} + +{% block title %}Edit Player - SKE Digital Signage{% endblock %} + +{% block content %} +
+ +
+
+

Edit Player: {{ player.username }}

+

Modify player settings

+
+ +
+ +
+ +
+
+
+
Player Settings
+
+
+
+
+ + +
+ +
+ + +
Must be unique across all players
+
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
Inactive players cannot receive content updates
+
+ +
+ +
+
+
+
+
+ + +
+
+
+
Player Details
+
+
+
+
ID:
+
{{ player.id }}
+
+
+
Created:
+
{{ player.created_at.strftime('%Y-%m-%d %H:%M') if player.created_at else 'N/A' }}
+
+
+
Last Seen:
+
+ {% if player.last_seen %} + {{ player.last_seen.strftime('%Y-%m-%d %H:%M') }} + {% else %} + Never + {% endif %} +
+
+
+
Status:
+
+ {% if player.is_active %} + Active + {% else %} + Inactive + {% endif %} +
+
+
+
Groups:
+
+ {% if player.groups %} + {% for group in player.groups %} + {{ group.name }} + {% endfor %} + {% else %} + No groups assigned + {% endif %} +
+
+
+
+ + +
+
+
Actions
+
+
+
+ + Open Fullscreen Display + + +
+
+
+
+
+
+ + + +{% endblock %} diff --git a/app/templates/player/fullscreen.html b/app/templates/player/fullscreen.html new file mode 100644 index 0000000..5e18db5 --- /dev/null +++ b/app/templates/player/fullscreen.html @@ -0,0 +1,273 @@ + + + + + + {{ player.username }} - Digital Signage Display + + + +
+
+ Loading content... +
+ + {% if content %} + {% for item in content %} +
+ {% if item.content_type.startswith('image/') %} + {{ item.original_name or item.file_name }} + {% elif item.content_type.startswith('video/') %} + + {% endif %} +
+ {% endfor %} + {% else %} +
+

📺

+

{{ player.username }}

+

No content available

+

+ Waiting for content assignment... +

+
+ {% endif %} + +
+
{{ player.username }}
+
{{ player.hostname }}
+
Last updated: --:--:--
+
+
+ + + + diff --git a/app/templates/player/view.html b/app/templates/player/view.html new file mode 100644 index 0000000..5737355 --- /dev/null +++ b/app/templates/player/view.html @@ -0,0 +1,271 @@ +{% extends "base.html" %} + +{% block title %}{{ player.username }} - Player View{% endblock %} + +{% block content %} +
+ +
+
+

{{ player.username }}

+

Player details and content management

+
+ +
+ +
+ +
+
+
+
Player Information
+
+
+
+
Username:
+
{{ player.username }}
+
+
+
Hostname:
+
{{ player.hostname }}
+
+
+
Status:
+
+ {% if player.is_active %} + Active + {% else %} + Inactive + {% endif %} +
+
+
+
Created:
+
{{ player.created_at.strftime('%Y-%m-%d %H:%M') if player.created_at else 'N/A' }}
+
+
+
Last Seen:
+
+ {% if player.last_seen %} + {{ player.last_seen.strftime('%Y-%m-%d %H:%M') }} + {% else %} + Never + {% endif %} +
+
+
+
Groups:
+
+ {% if player.groups %} + {% for group in player.groups %} + {{ group.name }} + {% endfor %} + {% else %} + No groups + {% endif %} +
+
+
+
+ + +
+
+
Quick Actions
+
+
+
+ + Open Display + + +
+
+
+
+ + +
+
+
+
Player Content
+ + Add Content + +
+
+ {% if content %} +
+ + + + + + + + + + + + + {% for item in content %} + + + + + + + + + {% endfor %} + +
PositionFilenameTypeDurationUploadedActions
+ {{ item.position }} + + {{ item.file_name }} + {% if item.original_name %} +
{{ item.original_name }} + {% endif %} +
+ {% if item.content_type.startswith('image/') %} + Image + {% elif item.content_type.startswith('video/') %} + Video + {% else %} + {{ item.content_type }} + {% endif %} + {{ item.duration }}s{{ item.uploaded_at.strftime('%m/%d %H:%M') if item.uploaded_at else 'N/A' }} +
+ + +
+
+
+ {% else %} +
+ +
No Content Available
+

This player doesn't have any content assigned yet.

+ + Upload First Content + +
+ {% endif %} +
+
+
+
+
+ + + + + + + + +{% endblock %} diff --git a/app/utils/uploads.py b/app/utils/uploads.py index fdbb71d..4937420 100644 --- a/app/utils/uploads.py +++ b/app/utils/uploads.py @@ -1,382 +1,229 @@ """ -File upload processing utilities +File upload and processing utilities """ import os import subprocess -import shutil +from flask import current_app from werkzeug.utils import secure_filename -from pdf2image import convert_from_path -from PIL import Image from app.extensions import db from app.models.content import Content +from app.models.player import Player +from app.models.group import Group from app.utils.logger import log_upload, log_process, log_content_added -def allowed_file(filename, file_type='all'): - """ - Check if file extension is allowed - - Args: - filename (str): Name of the file - file_type (str): Type of file to check ('images', 'videos', 'documents', 'all') - - Returns: - bool: True if file is allowed - """ - from flask import current_app - - if '.' not in filename: - return False - - ext = filename.rsplit('.', 1)[1].lower() - allowed_extensions = current_app.config['ALLOWED_EXTENSIONS'] - - if file_type == 'all': - all_extensions = set() - for extensions in allowed_extensions.values(): - all_extensions.update(extensions) - return ext in all_extensions - - return ext in allowed_extensions.get(file_type, set()) - -def get_file_type(filename): - """ - Determine file type based on extension - - Args: - filename (str): Name of the file - - Returns: - str: File type ('image', 'video', 'document') - """ - from flask import current_app - - if '.' not in filename: - return 'unknown' - - ext = filename.rsplit('.', 1)[1].lower() - allowed_extensions = current_app.config['ALLOWED_EXTENSIONS'] - - for file_type, extensions in allowed_extensions.items(): - if ext in extensions: - return file_type.rstrip('s') # Remove 's' from 'images', 'videos', etc. - - return 'unknown' - -def save_uploaded_file(file, upload_folder): - """ - Save uploaded file to disk - - Args: - file: FileStorage object from request - upload_folder (str): Path to upload folder - - Returns: - tuple: (success, filename, error_message) - """ - try: - if not file or file.filename == '': - return False, None, "No file selected" - - if not allowed_file(file.filename): - return False, None, f"File type not allowed: {file.filename}" - - # Generate secure filename - original_filename = file.filename - filename = secure_filename(original_filename) - - # Handle duplicate filenames - base_name, ext = os.path.splitext(filename) - counter = 1 - while os.path.exists(os.path.join(upload_folder, filename)): - filename = f"{base_name}_{counter}{ext}" - counter += 1 - - # Save file - file_path = os.path.join(upload_folder, filename) - file.save(file_path) - - return True, filename, None - - except Exception as e: - return False, None, str(e) - -def process_image(file_path, max_width=1920, max_height=1080): - """ - Process and optimize image file - - Args: - file_path (str): Path to image file - max_width (int): Maximum width for resizing - max_height (int): Maximum height for resizing - - Returns: - tuple: (width, height) of processed image - """ - try: - with Image.open(file_path) as img: - # Get original dimensions - original_width, original_height = img.size - - # Calculate new dimensions while maintaining aspect ratio - if original_width > max_width or original_height > max_height: - img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) - img.save(file_path, optimize=True, quality=85) - - return img.size - - except Exception as e: - print(f"Error processing image {file_path}: {e}") - return None, None - -def process_video(file_path, output_path=None): - """ - Process video file (convert to web-compatible format) - - Args: - file_path (str): Path to input video file - output_path (str): Path for output file (optional) - - Returns: - tuple: (success, output_filename, error_message) - """ - try: - if output_path is None: - base_name = os.path.splitext(file_path)[0] - output_path = f"{base_name}_converted.mp4" - - # Use FFmpeg to convert video - cmd = [ - 'ffmpeg', '-i', file_path, - '-c:v', 'libx264', - '-preset', 'medium', - '-crf', '23', - '-c:a', 'aac', - '-b:a', '128k', - '-movflags', '+faststart', - '-y', # Overwrite output file - output_path - ] - - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode == 0: - # Remove original file if conversion successful - if os.path.exists(output_path) and file_path != output_path: - os.remove(file_path) - return True, os.path.basename(output_path), None - else: - return False, None, result.stderr - - except Exception as e: - return False, None, str(e) - -def process_pdf(file_path, output_folder): - """ - Convert PDF to images - - Args: - file_path (str): Path to PDF file - output_folder (str): Folder to save converted images - - Returns: - list: List of generated image filenames - """ - try: - images = convert_from_path(file_path, dpi=150) - base_name = os.path.splitext(os.path.basename(file_path))[0] - - image_files = [] - for i, image in enumerate(images): - image_filename = f"{base_name}_page_{i+1}.png" - image_path = os.path.join(output_folder, image_filename) - image.save(image_path, 'PNG') - image_files.append(image_filename) - - # Remove original PDF - os.remove(file_path) - - return image_files - - except Exception as e: - print(f"Error processing PDF {file_path}: {e}") - return [] - -def process_pptx(file_path, output_folder): - """ - Convert PowerPoint to images using LibreOffice - - Args: - file_path (str): Path to PPTX file - output_folder (str): Folder to save converted images - - Returns: - list: List of generated image filenames - """ - try: - # Use LibreOffice to convert PPTX to PDF first - temp_dir = os.path.join(output_folder, 'temp') - os.makedirs(temp_dir, exist_ok=True) - - cmd = [ - 'libreoffice', '--headless', - '--convert-to', 'pdf', - '--outdir', temp_dir, - file_path - ] - - result = subprocess.run(cmd, capture_output=True, text=True) - - if result.returncode == 0: - # Find the generated PDF - base_name = os.path.splitext(os.path.basename(file_path))[0] - pdf_path = os.path.join(temp_dir, f"{base_name}.pdf") - - if os.path.exists(pdf_path): - # Convert PDF to images - image_files = process_pdf(pdf_path, output_folder) - - # Clean up - shutil.rmtree(temp_dir) - os.remove(file_path) - - return image_files - - return [] - - except Exception as e: - print(f"Error processing PPTX {file_path}: {e}") - return [] - def process_uploaded_files(app, files, duration, target_type, target_id): """ - Process uploaded files and add them to the database + Process uploaded files and add them to playlists Args: app: Flask application instance files: List of uploaded files - duration (int): Duration for each file in seconds - target_type (str): 'player' or 'group' - target_id (int): ID of the target player or group + duration: Display duration in seconds + target_type: 'player' or 'group' + target_id: Target ID Returns: - dict: Results of processing + dict: Results with success and error lists """ + results = {'success': [], 'errors': []} + upload_folder = os.path.join(app.static_folder, 'uploads') - results = { - 'success': [], - 'errors': [], - 'processed': 0 - } + os.makedirs(upload_folder, exist_ok=True) for file in files: - if not file or file.filename == '': - continue - - try: - # Save the file - success, filename, error = save_uploaded_file(file, upload_folder) - if not success: - results['errors'].append(f"{file.filename}: {error}") - continue - - file_path = os.path.join(upload_folder, filename) - file_type = get_file_type(filename) - - # Process based on file type - processed_files = [] - - if file_type == 'image': - width, height = process_image(file_path) - processed_files = [filename] - - elif file_type == 'video': - success, converted_filename, error = process_video(file_path) - if success: - processed_files = [converted_filename] - else: - results['errors'].append(f"{filename}: Video conversion failed - {error}") - continue - - elif file_type == 'document': - if filename.lower().endswith('.pdf'): - processed_files = process_pdf(file_path, upload_folder) - elif filename.lower().endswith(('.pptx', '.ppt')): - processed_files = process_pptx(file_path, upload_folder) - - if not processed_files: - results['errors'].append(f"{filename}: Document conversion failed") - continue - - # Add processed files to database - from app.models.player import Player - - if target_type == 'player': - player = Player.query.get(target_id) - if not player: - results['errors'].append(f"Player {target_id} not found") + if file and file.filename: + try: + # Secure the filename + filename = secure_filename(file.filename) + if not filename: + results['errors'].append(f"Invalid filename: {file.filename}") continue - # Get max position for ordering - max_position = db.session.query(db.func.max(Content.position)).filter_by(player_id=target_id).scalar() or 0 + # Get file extension and determine content type + file_ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else '' + content_type = get_content_type(file_ext) - for processed_file in processed_files: - content = Content( - file_name=processed_file, + if not content_type: + results['errors'].append(f"Unsupported file type: {file_ext}") + continue + + # Save file + file_path = os.path.join(upload_folder, filename) + file.save(file_path) + + # Get file size + file_size = os.path.getsize(file_path) + + # Process based on target type + if target_type == 'player': + success = add_content_to_player( + player_id=target_id, + filename=filename, original_name=file.filename, duration=duration, - player_id=target_id, - content_type=file_type, - position=max_position + 1 + content_type=content_type, + file_size=file_size ) - db.session.add(content) - max_position += 1 - - # Update playlist version - player.increment_playlist_version() - log_content_added(file.filename, 'player', player.username) - - elif target_type == 'group': - from app.models.group import Group - group = Group.query.get(target_id) - if not group: - results['errors'].append(f"Group {target_id} not found") + elif target_type == 'group': + success = add_content_to_group( + group_id=target_id, + filename=filename, + original_name=file.filename, + duration=duration, + content_type=content_type, + file_size=file_size + ) + else: + results['errors'].append(f"Invalid target type: {target_type}") continue - # Add content to all players in the group - for player in group.players: - max_position = db.session.query(db.func.max(Content.position)).filter_by(player_id=player.id).scalar() or 0 + if success: + results['success'].append(filename) + log_upload(content_type, filename, target_type, str(target_id)) + else: + results['errors'].append(f"Failed to add {filename} to {target_type}") - for processed_file in processed_files: - content = Content( - file_name=processed_file, - original_name=file.filename, - duration=duration, - player_id=player.id, - content_type=file_type, - position=max_position + 1 - ) - db.session.add(content) - max_position += 1 - - player.increment_playlist_version() - - log_content_added(file.filename, 'group', group.name) - - results['success'].append(file.filename) - results['processed'] += len(processed_files) - - # Log the upload - log_upload(file_type, file.filename, target_type, target_id) - - except Exception as e: - results['errors'].append(f"{file.filename}: {str(e)}") - - # Commit all changes - try: - db.session.commit() - except Exception as e: - db.session.rollback() - results['errors'].append(f"Database error: {str(e)}") + except Exception as e: + results['errors'].append(f"Error processing {file.filename}: {str(e)}") return results + +def get_content_type(file_ext): + """Determine content type from file extension""" + image_extensions = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'} + video_extensions = {'mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'} + document_extensions = {'pdf', 'pptx', 'ppt'} + + if file_ext in image_extensions: + return 'image' + elif file_ext in video_extensions: + return 'video' + elif file_ext in document_extensions: + return 'document' + else: + return None + +def add_content_to_player(player_id, filename, original_name, duration, content_type, file_size): + """Add content to a specific player""" + try: + player = Player.query.get(player_id) + if not player: + return False + + # Get next position + max_position = db.session.query(db.func.max(Content.position)).filter_by(player_id=player_id).scalar() or 0 + + # Create content entry + content = Content( + file_name=filename, + original_name=original_name, + duration=duration, + position=max_position + 1, + player_id=player_id, + content_type=content_type, + file_size=file_size + ) + + db.session.add(content) + player.increment_playlist_version() + db.session.commit() + + log_content_added(filename, 'player', player.username) + return True + + except Exception as e: + db.session.rollback() + print(f"Error adding content to player: {e}") + return False + +def add_content_to_group(group_id, filename, original_name, duration, content_type, file_size): + """Add content to all players in a group""" + try: + group = Group.query.get(group_id) + if not group: + return False + + # Add content to all players in the group + for player in group.players: + # Get next position for this player + max_position = db.session.query(db.func.max(Content.position)).filter_by(player_id=player.id).scalar() or 0 + + # Create content entry + content = Content( + file_name=filename, + original_name=original_name, + duration=duration, + position=max_position + 1, + player_id=player.id, + content_type=content_type, + file_size=file_size + ) + + db.session.add(content) + + # Update playlist version for group + group.increment_playlist_version() + db.session.commit() + + log_content_added(filename, 'group', group.name) + return True + + except Exception as e: + db.session.rollback() + print(f"Error adding content to group: {e}") + return False + +def allowed_file(filename, allowed_extensions=None): + """Check if file has an allowed extension""" + if allowed_extensions is None: + allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', + 'mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', + 'pdf', 'pptx', 'ppt'} + + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in allowed_extensions + +def get_file_info(file_path): + """Get basic file information""" + try: + stat = os.stat(file_path) + return { + 'size': stat.st_size, + 'modified': stat.st_mtime, + 'exists': True + } + except OSError: + return {'exists': False} + +def cleanup_orphaned_files(upload_folder): + """Remove files that are not referenced in the database""" + try: + # Get all filenames from database + db_files = {content.file_name for content in Content.query.all()} + + # Get all files in upload folder + if os.path.exists(upload_folder): + disk_files = set(os.listdir(upload_folder)) + + # Find orphaned files + orphaned = disk_files - db_files + + # Remove orphaned files + removed_count = 0 + for filename in orphaned: + file_path = os.path.join(upload_folder, filename) + if os.path.isfile(file_path): + try: + os.remove(file_path) + removed_count += 1 + except OSError as e: + print(f"Error removing {file_path}: {e}") + + return removed_count + + return 0 + + except Exception as e: + print(f"Error during cleanup: {e}") + return 0