updated first commit
This commit is contained in:
23
app/utils/__init__.py
Normal file
23
app/utils/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Utility functions package
|
||||
"""
|
||||
|
||||
from .logger import log_action, get_recent_logs
|
||||
from .uploads import process_uploaded_files
|
||||
from .player_management import (
|
||||
create_player, edit_player, delete_player,
|
||||
get_player_content, update_player_content_order
|
||||
)
|
||||
from .group_management import (
|
||||
create_group, edit_group, delete_group,
|
||||
get_group_content, update_group_content_order
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'log_action', 'get_recent_logs',
|
||||
'process_uploaded_files',
|
||||
'create_player', 'edit_player', 'delete_player',
|
||||
'get_player_content', 'update_player_content_order',
|
||||
'create_group', 'edit_group', 'delete_group',
|
||||
'get_group_content', 'update_group_content_order'
|
||||
]
|
||||
324
app/utils/group_management.py
Normal file
324
app/utils/group_management.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
Group management utilities
|
||||
"""
|
||||
|
||||
from app.extensions import db
|
||||
from app.models.group import Group
|
||||
from app.models.player import Player
|
||||
from app.models.content import Content
|
||||
from app.utils.logger import log_group_created, log_group_edited, log_group_deleted, log_content_reordered
|
||||
|
||||
def create_group(name, player_ids=None, description=None):
|
||||
"""
|
||||
Create a new group
|
||||
|
||||
Args:
|
||||
name (str): Group name
|
||||
player_ids (list): List of player IDs to add to group (optional)
|
||||
description (str): Group description (optional)
|
||||
|
||||
Returns:
|
||||
tuple: (success, group_or_error_message)
|
||||
"""
|
||||
try:
|
||||
# Check if group name already exists
|
||||
if Group.query.filter_by(name=name).first():
|
||||
return False, f"Group with name '{name}' already exists"
|
||||
|
||||
# Create new group
|
||||
group = Group(
|
||||
name=name,
|
||||
description=description or ""
|
||||
)
|
||||
|
||||
# Add players to group
|
||||
if player_ids:
|
||||
players = Player.query.filter(Player.id.in_(player_ids)).all()
|
||||
for player in players:
|
||||
group.players.append(player)
|
||||
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
|
||||
log_group_created(name)
|
||||
return True, group
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def edit_group(group_id, name=None, player_ids=None, description=None):
|
||||
"""
|
||||
Edit an existing group
|
||||
|
||||
Args:
|
||||
group_id (int): Group ID
|
||||
name (str): New group name (optional)
|
||||
player_ids (list): New list of player IDs (optional)
|
||||
description (str): New description (optional)
|
||||
|
||||
Returns:
|
||||
tuple: (success, group_or_error_message)
|
||||
"""
|
||||
try:
|
||||
group = Group.query.get(group_id)
|
||||
if not group:
|
||||
return False, "Group not found"
|
||||
|
||||
# Check for name conflicts
|
||||
if name and name != group.name:
|
||||
existing = Group.query.filter_by(name=name).first()
|
||||
if existing and existing.id != group_id:
|
||||
return False, f"Group with name '{name}' already exists"
|
||||
group.name = name
|
||||
|
||||
# Update description
|
||||
if description is not None:
|
||||
group.description = description
|
||||
|
||||
# Update players
|
||||
if player_ids is not None:
|
||||
# Clear current players
|
||||
group.players.clear()
|
||||
|
||||
# Add new players
|
||||
if player_ids:
|
||||
players = Player.query.filter(Player.id.in_(player_ids)).all()
|
||||
for player in players:
|
||||
group.players.append(player)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
log_group_edited(group.name)
|
||||
return True, group
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def delete_group(group_id):
|
||||
"""
|
||||
Delete a group (players remain, just removed from group)
|
||||
|
||||
Args:
|
||||
group_id (int): Group ID
|
||||
|
||||
Returns:
|
||||
tuple: (success, error_message)
|
||||
"""
|
||||
try:
|
||||
group = Group.query.get(group_id)
|
||||
if not group:
|
||||
return False, "Group not found"
|
||||
|
||||
group_name = group.name
|
||||
|
||||
# Remove all players from group
|
||||
group.players.clear()
|
||||
|
||||
# Delete group
|
||||
db.session.delete(group)
|
||||
db.session.commit()
|
||||
|
||||
log_group_deleted(group_name)
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def get_group_content(group_id):
|
||||
"""
|
||||
Get all unique content for players in a group
|
||||
|
||||
Args:
|
||||
group_id (int): Group ID
|
||||
|
||||
Returns:
|
||||
list: List of Content objects
|
||||
"""
|
||||
group = Group.query.get(group_id)
|
||||
if not group:
|
||||
return []
|
||||
|
||||
player_ids = [player.id for player in group.players]
|
||||
if not player_ids:
|
||||
return []
|
||||
|
||||
# Get unique content by filename, taking the first occurrence
|
||||
content_query = (
|
||||
db.session.query(
|
||||
Content.file_name,
|
||||
db.func.min(Content.id).label('id'),
|
||||
db.func.min(Content.duration).label('duration'),
|
||||
db.func.min(Content.position).label('position')
|
||||
)
|
||||
.filter(Content.player_id.in_(player_ids))
|
||||
.group_by(Content.file_name)
|
||||
)
|
||||
|
||||
# Get the actual content objects
|
||||
content_ids = [c.id for c in content_query.all()]
|
||||
content = Content.query.filter(Content.id.in_(content_ids)).order_by(Content.position).all()
|
||||
|
||||
return content
|
||||
|
||||
def update_group_content_order(group_id, content_items):
|
||||
"""
|
||||
Update the order of content items for all players in a group
|
||||
|
||||
Args:
|
||||
group_id (int): Group ID
|
||||
content_items (list): List of content items with new positions
|
||||
|
||||
Returns:
|
||||
tuple: (success, error_message)
|
||||
"""
|
||||
try:
|
||||
group = Group.query.get(group_id)
|
||||
if not group:
|
||||
return False, "Group not found"
|
||||
|
||||
player_ids = [player.id for player in group.players]
|
||||
if not player_ids:
|
||||
return True, None # No players in group
|
||||
|
||||
# Update positions for all matching content across all players
|
||||
for i, item in enumerate(content_items):
|
||||
file_name = item.get('file_name')
|
||||
if file_name:
|
||||
Content.query.filter(
|
||||
Content.player_id.in_(player_ids),
|
||||
Content.file_name == file_name
|
||||
).update({Content.position: i})
|
||||
|
||||
# Increment playlist version for all players and group
|
||||
group.increment_playlist_version()
|
||||
db.session.commit()
|
||||
|
||||
log_content_reordered('group', group.name)
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def add_player_to_group(group_id, player_id):
|
||||
"""
|
||||
Add a player to a group
|
||||
|
||||
Args:
|
||||
group_id (int): Group ID
|
||||
player_id (int): Player ID
|
||||
|
||||
Returns:
|
||||
tuple: (success, error_message)
|
||||
"""
|
||||
try:
|
||||
group = Group.query.get(group_id)
|
||||
player = Player.query.get(player_id)
|
||||
|
||||
if not group:
|
||||
return False, "Group not found"
|
||||
if not player:
|
||||
return False, "Player not found"
|
||||
|
||||
if player not in group.players:
|
||||
group.players.append(player)
|
||||
db.session.commit()
|
||||
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def remove_player_from_group(group_id, player_id):
|
||||
"""
|
||||
Remove a player from a group
|
||||
|
||||
Args:
|
||||
group_id (int): Group ID
|
||||
player_id (int): Player ID
|
||||
|
||||
Returns:
|
||||
tuple: (success, error_message)
|
||||
"""
|
||||
try:
|
||||
group = Group.query.get(group_id)
|
||||
player = Player.query.get(player_id)
|
||||
|
||||
if not group:
|
||||
return False, "Group not found"
|
||||
if not player:
|
||||
return False, "Player not found"
|
||||
|
||||
if player in group.players:
|
||||
group.players.remove(player)
|
||||
db.session.commit()
|
||||
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def get_available_players():
|
||||
"""
|
||||
Get all players that are not locked to any group
|
||||
|
||||
Returns:
|
||||
list: List of Player objects
|
||||
"""
|
||||
return Player.query.filter_by(locked_to_group_id=None).all()
|
||||
|
||||
def lock_players_to_group(group_id, player_ids):
|
||||
"""
|
||||
Lock specified players to a group (exclusive membership)
|
||||
|
||||
Args:
|
||||
group_id (int): Group ID
|
||||
player_ids (list): List of player IDs to lock
|
||||
|
||||
Returns:
|
||||
tuple: (success, error_message)
|
||||
"""
|
||||
try:
|
||||
group = Group.query.get(group_id)
|
||||
if not group:
|
||||
return False, "Group not found"
|
||||
|
||||
players = Player.query.filter(Player.id.in_(player_ids)).all()
|
||||
|
||||
for player in players:
|
||||
player.locked_to_group_id = group_id
|
||||
|
||||
db.session.commit()
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def unlock_players_from_group(group_id):
|
||||
"""
|
||||
Unlock all players from a group
|
||||
|
||||
Args:
|
||||
group_id (int): Group ID
|
||||
|
||||
Returns:
|
||||
tuple: (success, error_message)
|
||||
"""
|
||||
try:
|
||||
players = Player.query.filter_by(locked_to_group_id=group_id).all()
|
||||
|
||||
for player in players:
|
||||
player.locked_to_group_id = None
|
||||
|
||||
db.session.commit()
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
116
app/utils/logger.py
Normal file
116
app/utils/logger.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Logging utilities for server actions
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
from app.extensions import db
|
||||
from app.models.server_log import ServerLog
|
||||
|
||||
def log_action(action, level='INFO', user_id=None, ip_address=None, user_agent=None):
|
||||
"""
|
||||
Log an action to the server log database
|
||||
|
||||
Args:
|
||||
action (str): Description of the action
|
||||
level (str): Log level (INFO, WARNING, ERROR, DEBUG)
|
||||
user_id (int): User ID if action is user-specific
|
||||
ip_address (str): IP address of the request
|
||||
user_agent (str): User agent string
|
||||
"""
|
||||
try:
|
||||
# Auto-detect user and request info if not provided
|
||||
if user_id is None and hasattr(current_user, 'id') and current_user.is_authenticated:
|
||||
user_id = current_user.id
|
||||
|
||||
if ip_address is None and request:
|
||||
ip_address = request.remote_addr
|
||||
|
||||
if user_agent is None and request:
|
||||
user_agent = request.user_agent.string
|
||||
|
||||
ServerLog.log(
|
||||
action=action,
|
||||
user_id=user_id,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
level=level
|
||||
)
|
||||
print(f"[{level}] {action}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error logging action: {e}")
|
||||
# Try to rollback and log without context
|
||||
try:
|
||||
db.session.rollback()
|
||||
log_entry = ServerLog(action=f"ERROR: {action} (logging failed: {str(e)})", level='ERROR')
|
||||
db.session.add(log_entry)
|
||||
db.session.commit()
|
||||
except:
|
||||
pass # If this fails too, we can't do much
|
||||
|
||||
def get_recent_logs(limit=50):
|
||||
"""
|
||||
Get the most recent log entries
|
||||
|
||||
Args:
|
||||
limit (int): Maximum number of log entries to return
|
||||
|
||||
Returns:
|
||||
list: List of ServerLog objects
|
||||
"""
|
||||
return ServerLog.get_recent_logs(limit)
|
||||
|
||||
# Helper functions for common log actions
|
||||
def log_upload(file_type, file_name, target_type, target_name):
|
||||
"""Log file upload action"""
|
||||
log_action(f"{file_type.upper()} file '{file_name}' uploaded for {target_type} '{target_name}'")
|
||||
|
||||
def log_process(file_type, file_name, target_type, target_name):
|
||||
"""Log file processing action"""
|
||||
log_action(f"{file_type.upper()} file '{file_name}' processed for {target_type} '{target_name}'")
|
||||
|
||||
def log_player_created(username, hostname):
|
||||
"""Log player creation"""
|
||||
log_action(f"Player '{username}' with hostname '{hostname}' was created")
|
||||
|
||||
def log_player_edited(username):
|
||||
"""Log player edit"""
|
||||
log_action(f"Player '{username}' was edited")
|
||||
|
||||
def log_player_deleted(username):
|
||||
"""Log player deletion"""
|
||||
log_action(f"Player '{username}' was deleted")
|
||||
|
||||
def log_group_created(name):
|
||||
"""Log group creation"""
|
||||
log_action(f"Group '{name}' was created")
|
||||
|
||||
def log_group_edited(name):
|
||||
"""Log group edit"""
|
||||
log_action(f"Group '{name}' was edited")
|
||||
|
||||
def log_group_deleted(name):
|
||||
"""Log group deletion"""
|
||||
log_action(f"Group '{name}' was deleted")
|
||||
|
||||
def log_user_created(username, role):
|
||||
"""Log user creation"""
|
||||
log_action(f"User '{username}' with role '{role}' was created")
|
||||
|
||||
def log_user_deleted(username):
|
||||
"""Log user deletion"""
|
||||
log_action(f"User '{username}' was deleted")
|
||||
|
||||
def log_content_added(file_name, target_type, target_name):
|
||||
"""Log content addition"""
|
||||
log_action(f"Content '{file_name}' added to {target_type} '{target_name}'")
|
||||
|
||||
def log_content_reordered(target_type, target_name):
|
||||
"""Log content reordering"""
|
||||
log_action(f"Content reordered for {target_type} '{target_name}'")
|
||||
|
||||
def log_content_duration_changed(file_name, new_duration):
|
||||
"""Log content duration change"""
|
||||
log_action(f"Content '{file_name}' duration changed to {new_duration} seconds")
|
||||
222
app/utils/player_management.py
Normal file
222
app/utils/player_management.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
Player management utilities
|
||||
"""
|
||||
|
||||
from app.extensions import db, bcrypt
|
||||
from app.models.player import Player
|
||||
from app.models.content import Content
|
||||
from app.utils.logger import log_player_created, log_player_edited, log_player_deleted, log_content_reordered
|
||||
|
||||
def create_player(username, hostname, password, quickconnect_password=None):
|
||||
"""
|
||||
Create a new player
|
||||
|
||||
Args:
|
||||
username (str): Player username
|
||||
hostname (str): Player hostname
|
||||
password (str): Player password
|
||||
quickconnect_password (str): Quick connect password (optional)
|
||||
|
||||
Returns:
|
||||
tuple: (success, player_or_error_message)
|
||||
"""
|
||||
try:
|
||||
# Check if hostname already exists
|
||||
if Player.query.filter_by(hostname=hostname).first():
|
||||
return False, f"Player with hostname '{hostname}' already exists"
|
||||
|
||||
# Create new player
|
||||
player = Player(
|
||||
username=username,
|
||||
hostname=hostname
|
||||
)
|
||||
|
||||
# Set passwords
|
||||
player.set_password(password)
|
||||
if quickconnect_password:
|
||||
player.set_quickconnect_password(quickconnect_password)
|
||||
|
||||
db.session.add(player)
|
||||
db.session.commit()
|
||||
|
||||
log_player_created(username, hostname)
|
||||
return True, player
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def edit_player(player_id, username=None, hostname=None, password=None, quickconnect_password=None):
|
||||
"""
|
||||
Edit an existing player
|
||||
|
||||
Args:
|
||||
player_id (int): Player ID
|
||||
username (str): New username (optional)
|
||||
hostname (str): New hostname (optional)
|
||||
password (str): New password (optional)
|
||||
quickconnect_password (str): New quick connect password (optional)
|
||||
|
||||
Returns:
|
||||
tuple: (success, player_or_error_message)
|
||||
"""
|
||||
try:
|
||||
player = Player.query.get(player_id)
|
||||
if not player:
|
||||
return False, "Player not found"
|
||||
|
||||
# Check for hostname conflicts
|
||||
if hostname and hostname != player.hostname:
|
||||
existing = Player.query.filter_by(hostname=hostname).first()
|
||||
if existing and existing.id != player_id:
|
||||
return False, f"Player with hostname '{hostname}' already exists"
|
||||
player.hostname = hostname
|
||||
|
||||
# Update fields
|
||||
if username:
|
||||
player.username = username
|
||||
|
||||
if password:
|
||||
player.set_password(password)
|
||||
|
||||
if quickconnect_password:
|
||||
player.set_quickconnect_password(quickconnect_password)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
log_player_edited(player.username)
|
||||
return True, player
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def delete_player(player_id):
|
||||
"""
|
||||
Delete a player and all its content
|
||||
|
||||
Args:
|
||||
player_id (int): Player ID
|
||||
|
||||
Returns:
|
||||
tuple: (success, error_message)
|
||||
"""
|
||||
try:
|
||||
player = Player.query.get(player_id)
|
||||
if not player:
|
||||
return False, "Player not found"
|
||||
|
||||
username = player.username
|
||||
|
||||
# Delete all content files
|
||||
import os
|
||||
from flask import current_app
|
||||
|
||||
upload_folder = os.path.join(current_app.static_folder, 'uploads')
|
||||
for content in player.content:
|
||||
file_path = os.path.join(upload_folder, content.file_name)
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except:
|
||||
pass # File might be used by other content
|
||||
|
||||
# Remove from groups
|
||||
for group in player.groups:
|
||||
group.players.remove(player)
|
||||
|
||||
# Delete player (cascades to content)
|
||||
db.session.delete(player)
|
||||
db.session.commit()
|
||||
|
||||
log_player_deleted(username)
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e)
|
||||
|
||||
def get_player_content(player_id):
|
||||
"""
|
||||
Get all content for a player ordered by position
|
||||
|
||||
Args:
|
||||
player_id (int): Player ID
|
||||
|
||||
Returns:
|
||||
list: List of Content objects
|
||||
"""
|
||||
return Content.query.filter_by(player_id=player_id).order_by(Content.position).all()
|
||||
|
||||
def update_player_content_order(player_id, content_items):
|
||||
"""
|
||||
Update the order of content items for a player
|
||||
|
||||
Args:
|
||||
player_id (int): Player ID
|
||||
content_items (list): List of content items with new positions
|
||||
|
||||
Returns:
|
||||
tuple: (success, error_message, new_playlist_version)
|
||||
"""
|
||||
try:
|
||||
player = Player.query.get(player_id)
|
||||
if not player:
|
||||
return False, "Player not found", None
|
||||
|
||||
# Update positions
|
||||
for i, item in enumerate(content_items):
|
||||
content_id = item.get('id')
|
||||
content = Content.query.filter_by(id=content_id, player_id=player_id).first()
|
||||
if content:
|
||||
content.position = i
|
||||
|
||||
# Increment playlist version
|
||||
player.increment_playlist_version()
|
||||
db.session.commit()
|
||||
|
||||
log_content_reordered('player', player.username)
|
||||
return True, None, player.playlist_version
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return False, str(e), None
|
||||
|
||||
def get_player_by_hostname(hostname):
|
||||
"""
|
||||
Get player by hostname
|
||||
|
||||
Args:
|
||||
hostname (str): Player hostname
|
||||
|
||||
Returns:
|
||||
Player: Player object or None
|
||||
"""
|
||||
return Player.query.filter_by(hostname=hostname).first()
|
||||
|
||||
def verify_player_credentials(hostname, password, quickconnect_code=None):
|
||||
"""
|
||||
Verify player credentials
|
||||
|
||||
Args:
|
||||
hostname (str): Player hostname
|
||||
password (str): Player password
|
||||
quickconnect_code (str): Quick connect code (optional)
|
||||
|
||||
Returns:
|
||||
tuple: (success, player_or_error_message)
|
||||
"""
|
||||
player = get_player_by_hostname(hostname)
|
||||
if not player:
|
||||
return False, "Player not found"
|
||||
|
||||
if quickconnect_code:
|
||||
if player.verify_quickconnect_code(quickconnect_code):
|
||||
return True, player
|
||||
else:
|
||||
return False, "Invalid quick connect code"
|
||||
else:
|
||||
if player.check_password(password):
|
||||
return True, player
|
||||
else:
|
||||
return False, "Invalid password"
|
||||
382
app/utils/uploads.py
Normal file
382
app/utils/uploads.py
Normal file
@@ -0,0 +1,382 @@
|
||||
"""
|
||||
File upload processing utilities
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
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.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
|
||||
|
||||
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
|
||||
|
||||
Returns:
|
||||
dict: Results of processing
|
||||
"""
|
||||
upload_folder = os.path.join(app.static_folder, 'uploads')
|
||||
results = {
|
||||
'success': [],
|
||||
'errors': [],
|
||||
'processed': 0
|
||||
}
|
||||
|
||||
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")
|
||||
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
|
||||
|
||||
for processed_file in processed_files:
|
||||
content = Content(
|
||||
file_name=processed_file,
|
||||
original_name=file.filename,
|
||||
duration=duration,
|
||||
player_id=target_id,
|
||||
content_type=file_type,
|
||||
position=max_position + 1
|
||||
)
|
||||
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")
|
||||
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
|
||||
|
||||
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)}")
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user