Compare commits

...

2 Commits

24 changed files with 760 additions and 261 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

298
app.py
View File

@@ -1,15 +1,36 @@
import os
from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify, send_from_directory
from flask_migrate import Migrate
from pdf2image import convert_from_path
import subprocess
from werkzeug.utils import secure_filename
from functools import wraps
from extensions import db, bcrypt, login_manager
from models import User, Player, Content, Group # Add Group to the imports
# First import models
from models import User, Player, Content, Group, ServerLog
# Then import utilities that use the models
from flask_login import login_user, logout_user, login_required, current_user
from pptx_to_images import convert_pptx_to_images # Assuming you have a module for PPTX conversion
import os
from utils.logger import get_recent_logs, log_action, log_upload, log_process
from utils.group_player_management import (
create_group as create_group_util,
edit_group as edit_group_util,
delete_group as delete_group_util,
add_player as add_player_util,
edit_player as edit_player_util,
delete_player as delete_player_util,
get_group_content
)
# Finally, import modules that depend on both models and logger
from utils.uploads import (
add_image_to_playlist,
convert_video_and_update_playlist,
process_pdf,
process_pptx,
process_uploaded_files
)
# Define global variables for server version and build date
SERVER_VERSION = "1.0.0"
BUILD_DATE = "2025-06-25"
@@ -59,153 +80,14 @@ def admin_required(f):
return f(*args, **kwargs)
return decorated_function
def add_image_to_playlist(file, filename, duration, target_type, target_id):
"""
Save the image file and add it to the playlist database.
Increment the playlist version for the player or group.
"""
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
# Only save if file does not already exist (prevents double-saving)
if not os.path.exists(file_path):
file.save(file_path)
if target_type == 'group':
group = Group.query.get_or_404(target_id)
for player in group.players:
new_content = Content(file_name=filename, duration=duration, player_id=player.id)
db.session.add(new_content)
# Increment playlist version for each player in the group
player.playlist_version += 1
# Increment playlist version for the group
group.playlist_version += 1
elif target_type == 'player':
player = Player.query.get_or_404(target_id)
new_content = Content(file_name=filename, duration=duration, player_id=target_id)
db.session.add(new_content)
# Increment playlist version for the player
player.playlist_version += 1
db.session.commit()
def convert_pdf_to_images(input_file, output_folder):
"""
Converts a PDF file to images using pdf2image.
Each page is saved as a separate image in the output folder.
"""
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Convert the PDF file to images
images = convert_from_path(input_file, dpi=300)
base_name = os.path.splitext(os.path.basename(input_file))[0]
for i, image in enumerate(images):
image_filename = f"{base_name}_{i + 1}.jpg"
image_path = os.path.join(output_folder, image_filename)
image.save(image_path, 'JPEG')
# Delete the original PDF file after conversion
if os.path.exists(input_file):
os.remove(input_file)
print(f"Original PDF file deleted: {input_file}")
def convert_video(input_file, output_folder):
""" Converts a video file to MP4 format with H.264 codec, 720p resolution, and 30 FPS.
Args:
input_file (str): Path to the input video file.
output_folder (str): Path to the folder where the converted video will be saved.
Returns:
str: Path to the converted video file, or None if conversion fails.
"""
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Generate the output file path
base_name = os.path.splitext(os.path.basename(input_file))[0]
output_file = os.path.join(output_folder, f"{base_name}.mp4")
# FFmpeg command to convert the video
command = [
"ffmpeg",
"-i", input_file, # Input file
"-c:v", "libx264", # Video codec: H.264
"-preset", "fast", # Encoding speed/quality tradeoff
"-crf", "23", # Constant Rate Factor (quality, lower is better)
"-vf", "scale=-1:1080", # Scale video to 720p (preserve aspect ratio)
"-r", "30", # Frame rate: 30 FPS
"-c:a", "aac", # Audio codec: AAC
"-b:a", "128k", # Audio bitrate
output_file # Output file
]
try:
# Run the FFmpeg command
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"Video converted successfully: {output_file}")
return output_file
except subprocess.CalledProcessError as e:
print(f"Error converting video: {e.stderr.decode()}")
return None
def convert_video_and_update_playlist(file_path, original_filename, target_type, target_id, duration):
print(f"Starting video conversion for: {file_path}")
converted_file = convert_video(file_path, app.config['UPLOAD_FOLDER'])
if converted_file:
converted_filename = os.path.basename(converted_file)
print(f"Video converted successfully: {converted_filename}")
# Use the application context to interact with the database
with app.app_context():
# Update the database with the converted filename
if target_type == 'group':
group = Group.query.get_or_404(target_id)
for player in group.players:
content = Content.query.filter_by(player_id=player.id, file_name=original_filename).first()
if content:
content.file_name = converted_filename
elif target_type == 'player':
content = Content.query.filter_by(player_id=target_id, file_name=original_filename).first()
if content:
content.file_name = converted_filename
db.session.commit()
print(f"Database updated with converted video: {converted_filename}")
# Delete the original file only if it exists
if os.path.exists(file_path):
os.remove(file_path)
print(f"Original file deleted: {file_path}")
else:
print(f"Video conversion failed for: {file_path}")
def convert_pptx_to_images(input_file, output_folder):
"""
Calls the external pptx_to_images.py script to convert PPTX to images.
"""
command = [
"python", "pptx_to_images.py", input_file, output_folder
]
try:
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return True
except subprocess.CalledProcessError as e:
print(f"Error converting PPTX: {e.stderr.decode()}")
return False
# Convert EMU to pixels
def emu_to_pixels(emu):
return int(emu / 914400 * 96)
@app.route('/')
@login_required
def dashboard():
players = Player.query.all()
groups = Group.query.all()
logo_exists = os.path.exists(os.path.join(app.config['UPLOAD_FOLDERLOGO'], 'logo.png'))
return render_template('dashboard.html', players=players, groups=groups, logo_exists=logo_exists)
server_logs = get_recent_logs(20) # Get the 20 most recent logs
return render_template('dashboard.html', players=players, groups=groups, logo_exists=logo_exists, server_logs=server_logs)
@app.route('/register', methods=['GET', 'POST'])
def register():
@@ -252,46 +134,14 @@ def upload_content():
return_url = request.form.get('return_url')
media_type = request.form['media_type']
print(f"Target Type: {target_type}, Target ID: {target_id}, Media Type: {media_type}")
if not target_type or not target_id:
flash('Please select a target type and target ID.', 'danger')
return redirect(url_for('upload_content'))
for file in files:
try:
# Generate a secure filename and save the file
filename = secure_filename(file.filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
file_ext = os.path.splitext(filename)[1].lower()
if media_type == 'ppt':
print(f"Processing PPT file: {file_path}")
success = convert_pptx_to_images(file_path, app.config['UPLOAD_FOLDER'])
if success:
base_name = os.path.splitext(filename)[0]
# Find all PNGs generated for this PPTX
slide_images = sorted([
f for f in os.listdir(app.config['UPLOAD_FOLDER'])
if (f.startswith(base_name) and f.endswith('.png'))
])
print("Slide images found:", slide_images)
if target_type == 'group':
group = Group.query.get_or_404(target_id)
for player in group.players:
for slide_image in slide_images:
new_content = Content(file_name=slide_image, duration=duration, player_id=player.id)
db.session.add(new_content)
elif target_type == 'player':
for slide_image in slide_images:
new_content = Content(file_name=slide_image, duration=duration, player_id=target_id)
db.session.add(new_content)
except Exception as e:
print(f"Error processing file {file.filename}: {e}")
flash(f"Error processing file {file.filename}: {e}", 'danger')
db.session.commit()
# Process uploaded files and get results
results = process_uploaded_files(app, files, media_type, duration, target_type, target_id)
return redirect(return_url)
@@ -303,7 +153,8 @@ def upload_content():
players = [{'id': player.id, 'username': player.username} for player in Player.query.all()]
groups = [{'id': group.id, 'name': group.name} for group in Group.query.all()]
return render_template('upload_content.html', target_type=target_type, target_id=target_id, players=players, groups=groups, return_url=return_url)
return render_template('upload_content.html', target_type=target_type, target_id=target_id,
players=players, groups=groups, return_url=return_url)
@@ -339,8 +190,11 @@ def change_role(user_id):
@admin_required
def delete_user(user_id):
user = User.query.get_or_404(user_id)
username = user.username # Store username before deletion for logging
db.session.delete(user)
db.session.commit()
# Add log entry for user deletion
log_user_deleted(username)
return redirect(url_for('admin'))
@app.route('/admin/create_user', methods=['POST'])
@@ -354,6 +208,8 @@ def create_user():
new_user = User(username=username, password=hashed_password, role=role)
db.session.add(new_user)
db.session.commit()
# Add log entry for user creation
log_user_created(username, role)
return redirect(url_for('admin'))
@app.route('/player/<int:player_id>')
@@ -443,6 +299,7 @@ def delete_player(player_id):
return redirect(url_for('dashboard'))
# Update the add_player function
@app.route('/player/add', methods=['GET', 'POST'])
@login_required
@admin_required
@@ -452,9 +309,8 @@ def add_player():
hostname = request.form['hostname']
password = bcrypt.generate_password_hash(request.form['password']).decode('utf-8')
quickconnect_password = bcrypt.generate_password_hash(request.form['quickconnect_password']).decode('utf-8')
new_player = Player(username=username, hostname=hostname, password=password, quickconnect_password=quickconnect_password)
db.session.add(new_player)
db.session.commit()
add_player_util(username, hostname, password, quickconnect_password)
flash(f'Player "{username}" added successfully.', 'success')
return redirect(url_for('dashboard'))
return render_template('add_player.html')
@@ -464,13 +320,12 @@ def add_player():
def edit_player(player_id):
player = Player.query.get_or_404(player_id)
if request.method == 'POST':
player.username = request.form['username']
player.hostname = request.form['hostname']
if request.form['password']:
player.password = bcrypt.generate_password_hash(request.form['password']).decode('utf-8')
if request.form['quickconnect_password']:
player.quickconnect_password = bcrypt.generate_password_hash(request.form['quickconnect_password']).decode('utf-8')
db.session.commit()
username = request.form['username']
hostname = request.form['hostname']
password = request.form['password'] if request.form['password'] else None
quickconnect_password = request.form['quickconnect_password'] if request.form['quickconnect_password'] else None
edit_player_util(player_id, username, hostname, password, quickconnect_password)
flash(f'Player "{username}" updated successfully.', 'success')
return redirect(url_for('player_page', player_id=player.id))
return_url = request.args.get('return_url', url_for('player_page', player_id=player.id))
@@ -570,8 +425,30 @@ def get_playlists():
if not player or not bcrypt.check_password_hash(player.quickconnect_password, quickconnect_code):
return jsonify({'error': 'Invalid hostname or quick connect code'}), 404
# Query the Content table for media files associated with the player
content = Content.query.filter_by(player_id=player.id).all()
# Check if player is locked to a group
if player.locked_to_group_id:
# Get content for all players in the group to ensure shared content
group_players = player.locked_to_group.players
player_ids = [p.id for p in group_players]
# Use the first occurrence of each file for the playlist
content_query = (
db.session.query(
Content.file_name,
db.func.min(Content.id).label('id'),
db.func.min(Content.duration).label('duration')
)
.filter(Content.player_id.in_(player_ids))
.group_by(Content.file_name)
)
content = db.session.query(Content).filter(
Content.id.in_([c.id for c in content_query])
).all()
else:
# Get player's individual content
content = Content.query.filter_by(player_id=player.id).all()
playlist = [
{
'file_name': media.file_name,
@@ -607,13 +484,8 @@ def create_group():
if request.method == 'POST':
group_name = request.form['name']
player_ids = request.form.getlist('players')
new_group = Group(name=group_name)
for player_id in player_ids:
player = Player.query.get(player_id)
if player:
new_group.players.append(player)
db.session.add(new_group)
db.session.commit()
create_group_util(group_name, player_ids)
flash(f'Group "{group_name}" created successfully.', 'success')
return redirect(url_for('dashboard'))
players = Player.query.all()
return render_template('create_group.html', players=players)
@@ -623,15 +495,7 @@ def create_group():
@admin_required
def manage_group(group_id):
group = Group.query.get_or_404(group_id)
# Get unique media files for the group
content = (
db.session.query(Content.id, Content.file_name, db.func.min(Content.duration).label('duration'))
.filter(Content.player_id.in_([player.id for player in group.players]))
.group_by(Content.file_name)
.all()
)
content = get_group_content(group_id)
return render_template('manage_group.html', group=group, content=content)
@app.route('/group/<int:group_id>/edit', methods=['GET', 'POST'])
@@ -640,10 +504,10 @@ def manage_group(group_id):
def edit_group(group_id):
group = Group.query.get_or_404(group_id)
if request.method == 'POST':
group.name = request.form['name']
name = request.form['name']
player_ids = request.form.getlist('players')
group.players = [Player.query.get(player_id) for player_id in player_ids if Player.query.get(player_id)]
db.session.commit()
edit_group_util(group_id, name, player_ids)
flash(f'Group "{name}" updated successfully.', 'success')
return redirect(url_for('dashboard'))
players = Player.query.all()
return render_template('edit_group.html', group=group, players=players)
@@ -653,8 +517,9 @@ def edit_group(group_id):
@admin_required
def delete_group(group_id):
group = Group.query.get_or_404(group_id)
db.session.delete(group)
db.session.commit()
group_name = group.name
delete_group_util(group_id)
flash(f'Group "{group_name}" deleted successfully.', 'success')
return redirect(url_for('dashboard'))
@app.route('/group/<int:group_id>/fullscreen', methods=['GET'])
@@ -716,6 +581,5 @@ def get_playlist_version():
'hashed_quickconnect': player.quickconnect_password
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
app.run(debug=True, host='0.0.0.0')

View File

@@ -1,7 +1,14 @@
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
# drop_user_table.py
from app import app, db
# Create a minimal Flask app just for clearing the database
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///instance/dashboard.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
with app.app_context():
db.reflect() # This loads all tables from the database
db.drop_all()
print("Dropped all tables.")
print("Dropped all tables successfully.")

View File

@@ -1,5 +1,7 @@
import os
from app import app, db, User, bcrypt
from app import app
from extensions import db, bcrypt
from models import User, ServerLog # Import from models.py instead of app.py
def create_admin_user():
admin_username = os.getenv('ADMIN_USER', 'admin')
@@ -18,4 +20,5 @@ if __name__ == '__main__':
with app.app_context():
db.create_all()
create_admin_user()
print("Database initialized with all models including ServerLog")

Binary file not shown.

View File

@@ -1,9 +1,19 @@
from extensions import db
from flask_bcrypt import Bcrypt
from flask_login import UserMixin
import datetime # Add this import
bcrypt = Bcrypt()
# Add this new model
class ServerLog(db.Model):
id = db.Column(db.Integer, primary_key=True)
action = db.Column(db.String(255), nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.datetime.utcnow)
def __repr__(self):
return f"<ServerLog {self.action}>"
class Content(db.Model):
id = db.Column(db.Integer, primary_key=True)
file_name = db.Column(db.String(120), nullable=False)
@@ -17,6 +27,8 @@ class Player(db.Model):
password = db.Column(db.String(200), nullable=False)
quickconnect_password = db.Column(db.String(200), nullable=True) # Add this field
playlist_version = db.Column(db.Integer, default=0) # Playlist version counter
locked_to_group_id = db.Column(db.Integer, db.ForeignKey('group.id'), nullable=True)
locked_to_group = db.relationship('Group', foreign_keys=[locked_to_group_id], backref='locked_players')
def verify_quickconnect_code(self, code):
return bcrypt.check_password_hash(self.quickconnect_password, code)

View File

@@ -1,35 +0,0 @@
import os
import sys
import subprocess
def convert_pptx_to_images(input_file, output_folder):
"""
Converts a PowerPoint file (.ppt or .pptx) to images using LibreOffice.
Each slide is saved as a separate image in the output folder.
"""
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Convert the PowerPoint file to images using LibreOffice
command = [
'libreoffice',
'--headless',
'--convert-to', 'png',
'--outdir', output_folder,
input_file
]
try:
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"PPTX file converted to images: {input_file}")
except subprocess.CalledProcessError as e:
print(f"Error converting PPTX to images: {e.stderr.decode()}")
return False
# Rename the generated images to follow the naming convention
base_name = os.path.splitext(os.path.basename(input_file))[0]
png_files = sorted([f for f in os.listdir(output_folder) if f.endswith('.png') and base_name in f])
for i, file_name in enumerate(png_files):
new_name = f"{base_name}_{i + 1}.png"
os.rename(os.path.join(output_folder, file_name), os.path.join(output_folder, new_name))
print("Renamed slide images:", [f"{base_name}_{i + 1}.png" for i in range(len(png_files))])
return True

BIN
static/uploads/123.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

View File

@@ -50,6 +50,10 @@
</div>
</div>
</div>
<div class="alert alert-warning" role="alert">
<strong>Warning:</strong> Adding players to a group will delete their individual playlists.
All players in a group will share the same content.
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">Create Group</button>
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary mt-3">Back to Dashboard</a>

View File

@@ -137,6 +137,35 @@
</div>
{% endif %}
</div>
<!-- Server Activity Log Section -->
{% if current_user.role == 'admin' %}
<div class="card mb-4 {{ 'dark-mode' if theme == 'dark' else '' }}">
<div class="card-header bg-secondary text-white">
<h2>Server Activity Log</h2>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped {{ 'table-dark' if theme == 'dark' else '' }}">
<thead>
<tr>
<th>Time</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for log in server_logs %}
<tr>
<td>{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>{{ log.action }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"></script>

View File

@@ -50,6 +50,11 @@
</div>
</div>
</div>
<!-- Add this above the player selection -->
<div class="alert alert-warning" role="alert">
<strong>Warning:</strong> Adding new players to this group will delete their individual playlists.
Removing players from the group will allow them to have their own playlists again.
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary">Save Changes</button>
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary mt-3">Back to Dashboard</a>

View File

@@ -201,19 +201,48 @@
}
function showStatusModal() {
console.log("Processing popup triggered");
const statusModal = new bootstrap.Modal(document.getElementById('statusModal'));
statusModal.show();
// Update status message based on media type
const mediaType = document.getElementById('media_type').value;
const statusMessage = document.getElementById('status-message');
switch(mediaType) {
case 'image':
statusMessage.textContent = 'Uploading images...';
break;
case 'video':
statusMessage.textContent = 'Uploading and processing video. This may take a while...';
break;
case 'pdf':
statusMessage.textContent = 'Converting PDF to images. This may take a while...';
break;
case 'ppt':
statusMessage.textContent = 'Converting PowerPoint to images. This may take a while...';
break;
default:
statusMessage.textContent = 'Uploading and processing your files. Please wait...';
}
// Simulate progress updates
const progressBar = document.getElementById('progress-bar');
let progress = 0;
const interval = setInterval(() => {
progress += 10;
progressBar.style.width = `${progress}%`;
progressBar.setAttribute('aria-valuenow', progress);
// For slow processes, increment more slowly
const increment = (mediaType === 'image') ? 20 : 5;
progress += increment;
if (progress >= 100) {
clearInterval(interval);
document.getElementById('status-message').textContent = 'Files uploaded and processed successfully!';
statusMessage.textContent = 'Files uploaded and processed successfully!';
// Enable the close button
document.querySelector('[data-bs-dismiss="modal"]').disabled = false;
} else {
progressBar.style.width = `${progress}%`;
progressBar.setAttribute('aria-valuenow', progress);
}
}, 500);
}

0
utils/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,161 @@
from models import Player, Group, Content
from extensions import db
from utils.logger import log_group_created, log_group_edited, log_group_deleted
from utils.logger import log_player_created, log_player_edited, log_player_deleted
def create_group(name, player_ids):
"""
Create a new group with the given name and add selected players to it.
Clears individual playlists of players and locks them to the group.
"""
new_group = Group(name=name)
db.session.add(new_group)
db.session.flush() # Get the group ID
# Add players to the group and lock them
for player_id in player_ids:
player = Player.query.get(player_id)
if player:
# Add player to group
new_group.players.append(player)
# Delete player's individual playlist
Content.query.filter_by(player_id=player.id).delete()
# Lock player to this group
player.locked_to_group_id = new_group.id
db.session.commit()
log_group_created(name)
return new_group
def edit_group(group_id, name, player_ids):
"""
Edit an existing group, updating its name and players.
Handles locking/unlocking players appropriately.
"""
group = Group.query.get_or_404(group_id)
group.name = name
# Get current players in the group
current_player_ids = [player.id for player in group.players]
# Determine players to add and remove
players_to_add = [pid for pid in player_ids if pid not in current_player_ids]
players_to_remove = [pid for pid in current_player_ids if pid not in player_ids]
# Handle players to add
for player_id in players_to_add:
player = Player.query.get(player_id)
if player:
# Add to group
group.players.append(player)
# Delete individual playlist
Content.query.filter_by(player_id=player.id).delete()
# Lock to group
player.locked_to_group_id = group.id
# Handle players to remove
for player_id in players_to_remove:
player = Player.query.get(player_id)
if player:
# Remove from group
group.players.remove(player)
# Unlock from group
player.locked_to_group_id = None
db.session.commit()
log_group_edited(group.name)
return group
def delete_group(group_id):
"""
Delete a group and unlock all associated players.
"""
group = Group.query.get_or_404(group_id)
group_name = group.name
# Unlock all players in the group
for player in group.players:
player.locked_to_group_id = None
db.session.delete(group)
db.session.commit()
log_group_deleted(group_name)
def add_player(username, hostname, password, quickconnect_password):
"""
Add a new player with the given details.
"""
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt()
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
hashed_quickconnect = bcrypt.generate_password_hash(quickconnect_password).decode('utf-8')
new_player = Player(
username=username,
hostname=hostname,
password=hashed_password,
quickconnect_password=hashed_quickconnect
)
db.session.add(new_player)
db.session.commit()
log_player_created(username, hostname)
return new_player
def edit_player(player_id, username, hostname, password=None, quickconnect_password=None):
"""
Edit an existing player's details.
"""
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt()
player = Player.query.get_or_404(player_id)
player.username = username
player.hostname = hostname
if password:
player.password = bcrypt.generate_password_hash(password).decode('utf-8')
if quickconnect_password:
player.quickconnect_password = bcrypt.generate_password_hash(quickconnect_password).decode('utf-8')
db.session.commit()
log_player_edited(username)
return player
def delete_player(player_id):
"""
Delete a player and all its content.
"""
player = Player.query.get_or_404(player_id)
username = player.username
# Delete all media related to the player
Content.query.filter_by(player_id=player_id).delete()
# Delete the player
db.session.delete(player)
db.session.commit()
log_player_deleted(username)
def get_group_content(group_id):
"""
Get unique content for a group.
"""
group = Group.query.get_or_404(group_id)
# Get unique media files for the group
content = (
db.session.query(Content.id, Content.file_name, db.func.min(Content.duration).label('duration'))
.filter(Content.player_id.in_([player.id for player in group.players]))
.group_by(Content.file_name)
.all()
)
return content

65
utils/logger.py Normal file
View File

@@ -0,0 +1,65 @@
import datetime
from extensions import db
from models import ServerLog
def log_action(action):
"""
Log an action to the server log database
"""
try:
new_log = ServerLog(action=action)
db.session.add(new_log)
db.session.commit()
print(f"Logged action: {action}")
except Exception as e:
print(f"Error logging action: {e}")
db.session.rollback()
def get_recent_logs(limit=20):
"""
Get the most recent log entries
"""
return ServerLog.query.order_by(ServerLog.timestamp.desc()).limit(limit).all()
# Helper functions for common log actions
def log_upload(file_type, file_name, target_type, target_name):
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_action(f"{file_type.upper()} file '{file_name}' processed for {target_type} '{target_name}'")
def log_player_created(username, hostname):
log_action(f"Player '{username}' with hostname '{hostname}' was created")
def log_player_edited(username):
log_action(f"Player '{username}' was edited")
def log_player_deleted(username):
log_action(f"Player '{username}' was deleted")
def log_group_created(name):
log_action(f"Group '{name}' was created")
def log_group_edited(name):
log_action(f"Group '{name}' was edited")
def log_group_deleted(name):
log_action(f"Group '{name}' was deleted")
def log_user_created(username, role):
log_action(f"User '{username}' with role '{role}' was created")
def log_user_role_changed(username, new_role):
log_action(f"User '{username}' role changed to '{new_role}'")
def log_user_deleted(username):
log_action(f"User '{username}' was deleted")
def log_content_deleted(content_name, target_type, target_name):
log_action(f"Content '{content_name}' removed from {target_type} '{target_name}'")
def log_settings_changed(setting_name):
log_action(f"Setting '{setting_name}' was changed")
def log_files_cleaned(count):
log_action(f"{count} unused files were cleaned from storage")

355
utils/uploads.py Normal file
View File

@@ -0,0 +1,355 @@
import os
import subprocess
from flask import Flask
from werkzeug.utils import secure_filename
from pdf2image import convert_from_path
from extensions import db
from models import Content, Player, Group
from utils.logger import log_upload, log_process
# Function to add image to playlist
def add_image_to_playlist(app, file, filename, duration, target_type, target_id):
"""
Save the image file and add it to the playlist database.
"""
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
# Only save if file does not already exist
if not os.path.exists(file_path):
file.save(file_path)
print(f"Adding image to playlist: {filename}, Target Type: {target_type}, Target ID: {target_id}")
if target_type == 'group':
group = Group.query.get_or_404(target_id)
for player in group.players:
new_content = Content(file_name=filename, duration=duration, player_id=player.id)
db.session.add(new_content)
player.playlist_version += 1
group.playlist_version += 1
# Log the action
log_upload('image', filename, 'group', group.name)
elif target_type == 'player':
player = Player.query.get_or_404(target_id)
new_content = Content(file_name=filename, duration=duration, player_id=target_id)
db.session.add(new_content)
player.playlist_version += 1
# Log the action
log_upload('image', filename, 'player', player.username)
db.session.commit()
# Video conversion functions
def convert_video(input_file, output_folder):
""" Converts a video file to MP4 format with H.264 codec, 720p resolution, and 30 FPS.
Args:
input_file (str): Path to the input video file.
output_folder (str): Path to the folder where the converted video will be saved.
Returns:
str: Path to the converted video file, or None if conversion fails.
"""
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Generate the output file path
base_name = os.path.splitext(os.path.basename(input_file))[0]
output_file = os.path.join(output_folder, f"{base_name}.mp4")
# FFmpeg command to convert the video
command = [
"ffmpeg",
"-i", input_file, # Input file
"-c:v", "libx264", # Video codec: H.264
"-preset", "fast", # Encoding speed/quality tradeoff
"-crf", "23", # Constant Rate Factor (quality, lower is better)
"-vf", "scale=-1:1080", # Scale video to 1080p (preserve aspect ratio)
"-r", "30", # Frame rate: 30 FPS
"-c:a", "aac", # Audio codec: AAC
"-b:a", "128k", # Audio bitrate
output_file # Output file
]
try:
# Run the FFmpeg command
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"Video converted successfully: {output_file}")
return output_file
except subprocess.CalledProcessError as e:
print(f"Error converting video: {e.stderr.decode()}")
return None
def convert_video_and_update_playlist(app, file_path, original_filename, target_type, target_id, duration):
print(f"Starting video conversion for: {file_path}")
converted_file = convert_video(file_path, app.config['UPLOAD_FOLDER'])
if converted_file:
converted_filename = os.path.basename(converted_file)
print(f"Video converted successfully: {converted_filename}")
# Use the application context to interact with the database
with app.app_context():
# Update the database with the converted filename
if target_type == 'group':
group = Group.query.get_or_404(target_id)
for player in group.players:
content = Content.query.filter_by(player_id=player.id, file_name=original_filename).first()
if content:
content.file_name = converted_filename
elif target_type == 'player':
content = Content.query.filter_by(player_id=target_id, file_name=original_filename).first()
if content:
content.file_name = converted_filename
db.session.commit()
print(f"Database updated with converted video: {converted_filename}")
# Delete the original file only if it exists
if os.path.exists(file_path):
os.remove(file_path)
print(f"Original file deleted: {file_path}")
else:
print(f"Video conversion failed for: {file_path}")
# PDF conversion functions
def convert_pdf_to_images(pdf_file, output_folder, delete_pdf=True):
"""
Convert a PDF file to images.
Args:
pdf_file (str): Path to the PDF file
output_folder (str): Path to save the images
delete_pdf (bool): Whether to delete the PDF file after processing
Returns:
list: List of generated image filenames, or empty list if conversion failed
"""
print(f"Converting PDF to images: {pdf_file}")
try:
images = convert_from_path(pdf_file, dpi=300)
print(f"Number of pages in PDF: {len(images)}")
base_name = os.path.splitext(os.path.basename(pdf_file))[0]
image_filenames = []
for i, image in enumerate(images):
image_filename = f"{base_name}_page_{i + 1}.jpg"
image_path = os.path.join(output_folder, image_filename)
image.save(image_path, 'JPEG')
image_filenames.append(image_filename)
print(f"Saved page {i + 1} as image: {image_path}")
# Delete the PDF file if requested
if delete_pdf and os.path.exists(pdf_file):
os.remove(pdf_file)
print(f"PDF file deleted: {pdf_file}")
return image_filenames
except Exception as e:
print(f"Error converting PDF to images: {e}")
return []
def update_playlist_with_files(image_filenames, duration, target_type, target_id):
"""
Add files to a player or group playlist and update version numbers.
Args:
image_filenames (list): List of filenames to add to playlist
duration (int): Duration in seconds for each file
target_type (str): 'player' or 'group'
target_id (int): ID of the player or group
Returns:
bool: True if successful, False otherwise
"""
try:
if target_type == 'group':
group = Group.query.get_or_404(target_id)
for player in group.players:
for image_filename in image_filenames:
new_content = Content(file_name=image_filename, duration=duration, player_id=player.id)
db.session.add(new_content)
player.playlist_version += 1
group.playlist_version += 1
elif target_type == 'player':
player = Player.query.get_or_404(target_id)
for image_filename in image_filenames:
new_content = Content(file_name=image_filename, duration=duration, player_id=target_id)
db.session.add(new_content)
player.playlist_version += 1
else:
print(f"Invalid target type: {target_type}")
return False
db.session.commit()
print(f"Added {len(image_filenames)} files to playlist")
return True
except Exception as e:
db.session.rollback()
print(f"Error updating playlist: {e}")
return False
def process_pdf(input_file, output_folder, duration, target_type, target_id):
"""
Process a PDF file: convert to images and update playlist.
Args:
input_file (str): Path to the PDF file
output_folder (str): Path to save the images
duration (int): Duration in seconds for each image
target_type (str): 'player' or 'group'
target_id (int): ID of the player or group
Returns:
bool: True if successful, False otherwise
"""
# Ensure output folder exists
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Convert PDF to images
image_filenames = convert_pdf_to_images(input_file, output_folder)
# Update playlist with generated images
if image_filenames:
return update_playlist_with_files(image_filenames, duration, target_type, target_id)
return False
def process_pptx(input_file, output_folder, duration, target_type, target_id):
"""
Process a PPTX file: convert to PDF, then to images, and update playlist.
Args:
input_file (str): Path to the PPTX file
output_folder (str): Path to save the images
duration (int): Duration in seconds for each image
target_type (str): 'player' or 'group'
target_id (int): ID of the player or group
Returns:
bool: True if successful, False otherwise
"""
# Ensure output folder exists
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Step 1: Convert PPTX to PDF using LibreOffice
pdf_file = os.path.join(output_folder, os.path.splitext(os.path.basename(input_file))[0] + ".pdf")
command = [
'libreoffice',
'--headless',
'--convert-to', 'pdf',
'--outdir', output_folder,
input_file
]
print(f"Running LibreOffice command: {' '.join(command)}")
try:
result = subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"PPTX file converted to PDF: {pdf_file}")
# Step 2: Convert PDF to images and update playlist
image_filenames = convert_pdf_to_images(pdf_file, output_folder, True)
# Step 3: Delete the original PPTX file
if image_filenames and os.path.exists(input_file):
os.remove(input_file)
print(f"Original PPTX file deleted: {input_file}")
# Step 4: Update playlist with generated images
if image_filenames:
return update_playlist_with_files(image_filenames, duration, target_type, target_id)
return False
except subprocess.CalledProcessError as e:
print(f"Error converting PPTX to PDF: {e.stderr.decode() if hasattr(e, 'stderr') else str(e)}")
return False
except Exception as e:
print(f"Error processing PPTX file: {e}")
return False
def process_uploaded_files(app, files, media_type, duration, target_type, target_id):
"""
Process uploaded files based on media type and add them to playlists.
Returns:
list: List of result dictionaries with success status and messages
"""
results = []
# Get target name for logging
target_name = ""
if target_type == 'group':
group = Group.query.get_or_404(target_id)
target_name = group.name
elif target_type == 'player':
player = Player.query.get_or_404(target_id)
target_name = player.username
for file in files:
try:
# Generate a secure filename and save the file
filename = secure_filename(file.filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(file_path)
print(f"Processing file: {filename}, Media Type: {media_type}")
result = {'filename': filename, 'success': True, 'message': ''}
if media_type == 'image':
add_image_to_playlist(app, file, filename, duration, target_type, target_id)
result['message'] = f"Image {filename} added to playlist"
log_upload('image', filename, target_type, target_name)
elif media_type == 'video':
# For videos, add to playlist then start conversion in background
if target_type == 'group':
group = Group.query.get_or_404(target_id)
for player in group.players:
new_content = Content(file_name=filename, duration=duration, player_id=player.id)
db.session.add(new_content)
player.playlist_version += 1
group.playlist_version += 1
elif target_type == 'player':
player = Player.query.get_or_404(target_id)
new_content = Content(file_name=filename, duration=duration, player_id=target_id)
db.session.add(new_content)
player.playlist_version += 1
db.session.commit()
# Start background conversion
import threading
threading.Thread(target=convert_video_and_update_playlist,
args=(app, file_path, filename, target_type, target_id, duration)).start()
result['message'] = f"Video {filename} added to playlist and being processed"
log_upload('video', filename, target_type, target_name)
elif media_type == 'pdf':
# For PDFs, convert to images and update playlist
success = process_pdf(file_path, app.config['UPLOAD_FOLDER'],
duration, target_type, target_id)
if success:
result['message'] = f"PDF {filename} processed successfully"
log_process('pdf', filename, target_type, target_name)
else:
result['success'] = False
result['message'] = f"Error processing PDF file: {filename}"
elif media_type == 'ppt':
# For PPT/PPTX, convert to PDF, then to images, and update playlist
success = process_pptx(file_path, app.config['UPLOAD_FOLDER'],
duration, target_type, target_id)
if success:
result['message'] = f"PowerPoint {filename} processed successfully"
log_process('ppt', filename, target_type, target_name)
else:
result['success'] = False
result['message'] = f"Error processing PowerPoint file: {filename}"
results.append(result)
except Exception as e:
print(f"Error processing file {file.filename}: {e}")
results.append({
'filename': file.filename,
'success': False,
'message': f"Error processing file {file.filename}: {str(e)}"
})
return results