updated features to upload pptx files

This commit is contained in:
DigiServer Developer
2025-11-15 01:26:12 +02:00
parent 9d4f932a95
commit 930a5bf636
24 changed files with 1963 additions and 2218 deletions

View File

@@ -8,7 +8,7 @@ from datetime import datetime
from typing import Optional
from app.extensions import db, bcrypt
from app.models import User, Player, Group, Content, ServerLog
from app.models import User, Player, Group, Content, ServerLog, Playlist
from app.utils.logger import log_action
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@@ -38,7 +38,7 @@ def admin_panel():
# Get statistics
total_users = User.query.count()
total_players = Player.query.count()
total_groups = Group.query.count()
total_playlists = Playlist.query.count()
total_content = Content.query.count()
# Get recent logs
@@ -62,7 +62,7 @@ def admin_panel():
return render_template('admin/admin.html',
total_users=total_users,
total_players=total_players,
total_groups=total_groups,
total_playlists=total_playlists,
total_content=total_content,
storage_mb=storage_mb,
users=users,
@@ -347,3 +347,146 @@ def system_info():
except Exception as e:
log_action('error', f'Error getting system info: {str(e)}')
return jsonify({'error': str(e)}), 500
@admin_bp.route('/leftover-media')
@login_required
@admin_required
def leftover_media():
"""Display leftover media files not assigned to any playlist."""
from app.models.playlist import playlist_content
from sqlalchemy import select
try:
# Get all content IDs that are in playlists
stmt = select(playlist_content.c.content_id).distinct()
content_in_playlists = set(row[0] for row in db.session.execute(stmt))
# Get all content
all_content = Content.query.all()
# Filter content not in any playlist
leftover_content = [c for c in all_content if c.id not in content_in_playlists]
# Separate by type
leftover_images = [c for c in leftover_content if c.content_type == 'image']
leftover_videos = [c for c in leftover_content if c.content_type == 'video']
leftover_pdfs = [c for c in leftover_content if c.content_type == 'pdf']
leftover_pptx = [c for c in leftover_content if c.content_type == 'pptx']
# Calculate storage
total_leftover_size = sum(c.file_size for c in leftover_content)
images_size = sum(c.file_size for c in leftover_images)
videos_size = sum(c.file_size for c in leftover_videos)
pdfs_size = sum(c.file_size for c in leftover_pdfs)
pptx_size = sum(c.file_size for c in leftover_pptx)
return render_template('admin/leftover_media.html',
leftover_images=leftover_images,
leftover_videos=leftover_videos,
leftover_pdfs=leftover_pdfs,
leftover_pptx=leftover_pptx,
total_leftover=len(leftover_content),
total_leftover_size_mb=total_leftover_size / (1024 * 1024),
images_size_mb=images_size / (1024 * 1024),
videos_size_mb=videos_size / (1024 * 1024),
pdfs_size_mb=pdfs_size / (1024 * 1024),
pptx_size_mb=pptx_size / (1024 * 1024))
except Exception as e:
log_action('error', f'Error loading leftover media: {str(e)}')
flash('Error loading leftover media.', 'danger')
return redirect(url_for('admin.admin_panel'))
@admin_bp.route('/delete-leftover-images', methods=['POST'])
@login_required
@admin_required
def delete_leftover_images():
"""Delete all leftover images that are not part of any playlist"""
from app.models.playlist import playlist_content
try:
# Find all leftover image content
leftover_images = db.session.query(Content).filter(
Content.media_type == 'image',
~Content.id.in_(
db.session.query(playlist_content.c.content_id)
)
).all()
deleted_count = 0
errors = []
for content in leftover_images:
try:
# Delete physical file
if content.file_path:
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], content.file_path)
if os.path.exists(file_path):
os.remove(file_path)
# Delete database record
db.session.delete(content)
deleted_count += 1
except Exception as e:
errors.append(f"Error deleting {content.file_path}: {str(e)}")
db.session.commit()
if errors:
flash(f'Deleted {deleted_count} images with {len(errors)} errors', 'warning')
else:
flash(f'Successfully deleted {deleted_count} leftover images', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error deleting leftover images: {str(e)}', 'danger')
return redirect(url_for('admin.leftover_media'))
@admin_bp.route('/delete-leftover-videos', methods=['POST'])
@login_required
@admin_required
def delete_leftover_videos():
"""Delete all leftover videos that are not part of any playlist"""
from app.models.playlist import playlist_content
try:
# Find all leftover video content
leftover_videos = db.session.query(Content).filter(
Content.media_type == 'video',
~Content.id.in_(
db.session.query(playlist_content.c.content_id)
)
).all()
deleted_count = 0
errors = []
for content in leftover_videos:
try:
# Delete physical file
if content.file_path:
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], content.file_path)
if os.path.exists(file_path):
os.remove(file_path)
# Delete database record
db.session.delete(content)
deleted_count += 1
except Exception as e:
errors.append(f"Error deleting {content.file_path}: {str(e)}")
db.session.commit()
if errors:
flash(f'Deleted {deleted_count} videos with {len(errors)} errors', 'warning')
else:
flash(f'Successfully deleted {deleted_count} leftover videos', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error deleting leftover videos: {str(e)}', 'danger')
return redirect(url_for('admin.leftover_media'))

View File

@@ -243,10 +243,222 @@ def upload_media_page():
return render_template('content/upload_media.html', playlists=playlists)
def process_image_file(filepath: str, filename: str) -> tuple[bool, str]:
"""Process and optimize image files."""
try:
from PIL import Image
# Open and optimize image
img = Image.open(filepath)
# Convert RGBA to RGB for JPEGs
if img.mode == 'RGBA' and filename.lower().endswith(('.jpg', '.jpeg')):
rgb_img = Image.new('RGB', img.size, (255, 255, 255))
rgb_img.paste(img, mask=img.split()[3])
img = rgb_img
# Resize if too large (max 1920x1080 for display efficiency)
max_size = (1920, 1080)
if img.width > max_size[0] or img.height > max_size[1]:
img.thumbnail(max_size, Image.Resampling.LANCZOS)
img.save(filepath, optimize=True, quality=85)
log_action('info', f'Optimized image: {filename}')
return True, "Image processed successfully"
except Exception as e:
return False, f"Image processing error: {str(e)}"
def process_video_file_extended(filepath: str, filename: str) -> tuple[bool, str]:
"""Process and optimize video files for Raspberry Pi playback."""
try:
# Basic video validation
import subprocess
# Check if video is playable
result = subprocess.run(
['ffprobe', '-v', 'error', '-select_streams', 'v:0',
'-show_entries', 'stream=codec_name,width,height',
'-of', 'default=noprint_wrappers=1', filepath],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
log_action('info', f'Video validated: {filename}')
return True, "Video validated successfully"
else:
return False, "Video validation failed"
except Exception as e:
# If ffprobe not available, just accept the video
log_action('warning', f'Video validation skipped (ffprobe unavailable): {filename}')
return True, "Video accepted without validation"
def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
"""Process PDF files."""
try:
# Basic PDF validation - check if it's a valid PDF
with open(filepath, 'rb') as f:
header = f.read(5)
if header != b'%PDF-':
return False, "Invalid PDF file"
log_action('info', f'PDF validated: {filename}')
return True, "PDF processed successfully"
except Exception as e:
return False, f"PDF processing error: {str(e)}"
def process_presentation_file(filepath: str, filename: str) -> tuple[bool, str]:
"""Process PowerPoint presentation files by converting slides to images."""
try:
import subprocess
import tempfile
import shutil
from pathlib import Path
# Basic validation - check file exists and has content
file_size = os.path.getsize(filepath)
if file_size < 1024: # Less than 1KB is suspicious
return False, "File too small to be a valid presentation"
# Check if LibreOffice is available
libreoffice_paths = [
'/usr/bin/libreoffice',
'/usr/bin/soffice',
'/snap/bin/libreoffice',
'libreoffice', # Try in PATH
'soffice'
]
libreoffice_cmd = None
for cmd in libreoffice_paths:
try:
result = subprocess.run([cmd, '--version'],
capture_output=True,
timeout=5)
if result.returncode == 0:
libreoffice_cmd = cmd
log_action('info', f'Found LibreOffice at: {cmd}')
break
except (FileNotFoundError, subprocess.TimeoutExpired):
continue
if not libreoffice_cmd:
log_action('warning', f'LibreOffice not found, skipping slide conversion for: {filename}')
return True, "Presentation accepted without conversion (LibreOffice unavailable)"
# Create temporary directory for conversion
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Copy presentation to temp directory
temp_ppt = temp_path / filename
shutil.copy2(filepath, temp_ppt)
# Convert presentation to images (PNG format)
# Using LibreOffice headless mode with custom resolution
convert_cmd = [
libreoffice_cmd,
'--headless',
'--convert-to', 'png',
'--outdir', str(temp_path),
str(temp_ppt)
]
log_action('info', f'Converting presentation to images: {filename}')
try:
result = subprocess.run(
convert_cmd,
capture_output=True,
text=True,
timeout=120 # 2 minutes timeout
)
if result.returncode != 0:
log_action('error', f'LibreOffice conversion failed: {result.stderr}')
return True, "Presentation accepted without conversion (conversion failed)"
# Find generated PNG files
png_files = sorted(temp_path.glob('*.png'))
if not png_files:
log_action('warning', f'No images generated from presentation: {filename}')
return True, "Presentation accepted without images"
# Get upload folder from app config
upload_folder = current_app.config['UPLOAD_FOLDER']
base_name = os.path.splitext(filename)[0]
# Move converted images to upload folder
slide_count = 0
for idx, png_file in enumerate(png_files, start=1):
# Create descriptive filename
slide_filename = f"{base_name}_slide_{idx:03d}.png"
destination = os.path.join(upload_folder, slide_filename)
shutil.move(str(png_file), destination)
# Optimize the image to Full HD (1920x1080)
optimize_image_to_fullhd(destination)
slide_count += 1
log_action('info', f'Converted {slide_count} slides from {filename} to images')
# Remove original PPTX file as we now have the images
os.remove(filepath)
return True, f"Presentation converted to {slide_count} Full HD images"
except subprocess.TimeoutExpired:
log_action('error', f'LibreOffice conversion timeout for: {filename}')
return True, "Presentation accepted without conversion (timeout)"
except Exception as e:
log_action('error', f'Presentation processing error: {str(e)}')
return False, f"Presentation processing error: {str(e)}"
def optimize_image_to_fullhd(filepath: str) -> bool:
"""Optimize and resize image to Full HD (1920x1080) maintaining aspect ratio."""
try:
from PIL import Image
img = Image.open(filepath)
# Target Full HD resolution
target_size = (1920, 1080)
# Calculate resize maintaining aspect ratio
img.thumbnail(target_size, Image.Resampling.LANCZOS)
# Create Full HD canvas with white background
fullhd_img = Image.new('RGB', target_size, (255, 255, 255))
# Center the image on the canvas
x = (target_size[0] - img.width) // 2
y = (target_size[1] - img.height) // 2
if img.mode == 'RGBA':
fullhd_img.paste(img, (x, y), img)
else:
fullhd_img.paste(img, (x, y))
# Save optimized image
fullhd_img.save(filepath, 'PNG', optimize=True)
return True
except Exception as e:
log_action('error', f'Image optimization error: {str(e)}')
return False
@content_bp.route('/upload-media', methods=['POST'])
@login_required
def upload_media():
"""Upload media files to library."""
"""Upload media files to library with type-specific processing."""
try:
files = request.files.getlist('files')
content_type = request.form.get('content_type', 'image')
@@ -261,6 +473,7 @@ def upload_media():
os.makedirs(upload_folder, exist_ok=True)
uploaded_count = 0
processing_errors = []
for file in files:
if file.filename == '':
@@ -275,64 +488,138 @@ def upload_media():
log_action('warning', f'File {filename} already exists, skipping')
continue
# Save file
# Save file first
file.save(filepath)
# Determine content type from extension
file_ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
if file_ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp']:
# Process file based on type
processing_success = True
processing_message = ""
if file_ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']:
detected_type = 'image'
elif file_ext in ['mp4', 'avi', 'mov', 'mkv', 'webm']:
processing_success, processing_message = process_image_file(filepath, filename)
elif file_ext in ['mp4', 'avi', 'mov', 'mkv', 'webm', 'flv', 'wmv']:
detected_type = 'video'
# Process video for Raspberry Pi
success, message = process_video_file(filepath, os.urandom(8).hex())
if not success:
log_action('error', f'Video processing failed: {message}')
processing_success, processing_message = process_video_file_extended(filepath, filename)
elif file_ext == 'pdf':
detected_type = 'pdf'
processing_success, processing_message = process_pdf_file(filepath, filename)
elif file_ext in ['ppt', 'pptx']:
detected_type = 'pptx'
processing_success, processing_message = process_presentation_file(filepath, filename)
# For presentations, slides are converted to individual images
# We need to add each slide image as a separate content item
if processing_success and "converted to" in processing_message.lower():
# Find all slide images that were created
base_name = os.path.splitext(filename)[0]
slide_pattern = f"{base_name}_slide_*.png"
import glob
slide_files = sorted(glob.glob(os.path.join(upload_folder, slide_pattern)))
if slide_files:
max_position = 0
if playlist_id:
playlist = Playlist.query.get(playlist_id)
max_position = db.session.query(db.func.max(playlist_content.c.position))\
.filter(playlist_content.c.playlist_id == playlist_id)\
.scalar() or 0
# Add each slide as separate content
for slide_file in slide_files:
slide_filename = os.path.basename(slide_file)
# Create content record for slide
slide_content = Content(
filename=slide_filename,
content_type='image',
duration=duration,
file_size=os.path.getsize(slide_file)
)
db.session.add(slide_content)
db.session.flush()
# Add to playlist if specified
if playlist_id:
max_position += 1
stmt = playlist_content.insert().values(
playlist_id=playlist_id,
content_id=slide_content.id,
position=max_position,
duration=duration
)
db.session.execute(stmt)
uploaded_count += 1
# Increment playlist version if slides were added
if playlist_id and slide_files:
playlist.version += 1
continue # Skip normal content creation below
else:
detected_type = 'other'
# Create content record
content = Content(
filename=filename,
content_type=detected_type,
duration=duration,
file_size=os.path.getsize(filepath)
)
db.session.add(content)
db.session.flush() # Get content ID
if not processing_success:
processing_errors.append(f"{filename}: {processing_message}")
if os.path.exists(filepath):
os.remove(filepath) # Remove failed file
log_action('error', f'Processing failed for {filename}: {processing_message}')
continue
# Add to playlist if specified
if playlist_id:
playlist = Playlist.query.get(playlist_id)
if playlist:
# Get max position
max_position = db.session.query(db.func.max(playlist_content.c.position))\
.filter(playlist_content.c.playlist_id == playlist_id)\
.scalar() or 0
# Add to playlist
stmt = playlist_content.insert().values(
playlist_id=playlist_id,
content_id=content.id,
position=max_position + 1,
duration=duration
)
db.session.execute(stmt)
# Increment playlist version
playlist.version += 1
uploaded_count += 1
# Create content record (for non-presentation files or failed conversions)
if os.path.exists(filepath):
content = Content(
filename=filename,
content_type=detected_type,
duration=duration,
file_size=os.path.getsize(filepath)
)
db.session.add(content)
db.session.flush() # Get content ID
# Add to playlist if specified
if playlist_id:
playlist = Playlist.query.get(playlist_id)
if playlist:
# Get max position
max_position = db.session.query(db.func.max(playlist_content.c.position))\
.filter(playlist_content.c.playlist_id == playlist_id)\
.scalar() or 0
# Add to playlist
stmt = playlist_content.insert().values(
playlist_id=playlist_id,
content_id=content.id,
position=max_position + 1,
duration=duration
)
db.session.execute(stmt)
# Increment playlist version
playlist.version += 1
uploaded_count += 1
db.session.commit()
cache.clear()
log_action('info', f'Uploaded {uploaded_count} media files')
if playlist_id:
# Show appropriate flash message
if processing_errors:
error_summary = '; '.join(processing_errors[:3])
if len(processing_errors) > 3:
error_summary += f' and {len(processing_errors) - 3} more...'
flash(f'Uploaded {uploaded_count} file(s). Errors: {error_summary}', 'warning')
elif playlist_id:
playlist = Playlist.query.get(playlist_id)
flash(f'Successfully uploaded {uploaded_count} file(s) to playlist "{playlist.name}"!', 'success')
else:

View File

@@ -213,31 +213,8 @@ def regenerate_auth_code(player_id: int):
@players_bp.route('/<int:player_id>')
@login_required
def player_page(player_id: int):
"""Display player page with content and controls."""
try:
player = Player.query.get_or_404(player_id)
# Get player's playlist
playlist = get_player_playlist(player_id)
# Get player status
status_info = get_player_status_info(player_id)
# Get recent feedback
recent_feedback = PlayerFeedback.query.filter_by(player_id=player_id)\
.order_by(PlayerFeedback.timestamp.desc())\
.limit(10)\
.all()
return render_template('players/player_page.html',
player=player,
playlist=playlist,
status_info=status_info,
recent_feedback=recent_feedback)
except Exception as e:
log_action('error', f'Error loading player page: {str(e)}')
flash('Error loading player page.', 'danger')
return redirect(url_for('players.list'))
"""Redirect to manage player page (combined view)."""
return redirect(url_for('players.manage_player', player_id=player_id))
@players_bp.route('/<int:player_id>/manage', methods=['GET', 'POST'])
@@ -347,7 +324,7 @@ def player_fullscreen(player_id: int):
@cache.memoize(timeout=300) # Cache for 5 minutes
def get_player_playlist(player_id: int) -> List[dict]:
"""Get playlist for a player based on their direct content assignment.
"""Get playlist for a player based on their assigned playlist.
Args:
player_id: The player's database ID
@@ -356,23 +333,26 @@ def get_player_playlist(player_id: int) -> List[dict]:
List of content dictionaries with url, type, duration, and position
"""
player = Player.query.get(player_id)
if not player:
if not player or not player.playlist_id:
return []
# Get content directly assigned to this player
contents = Content.query.filter_by(player_id=player_id)\
.order_by(Content.position, Content.uploaded_at)\
.all()
# Get the player's assigned playlist
playlist_obj = Playlist.query.get(player.playlist_id)
if not playlist_obj:
return []
# Get ordered content from the playlist
ordered_content = playlist_obj.get_content_ordered()
# Build playlist
playlist = []
for content in contents:
for content in ordered_content:
playlist.append({
'id': content.id,
'url': url_for('static', filename=f'uploads/{content.filename}'),
'type': content.content_type,
'duration': content.duration or 10, # Default 10 seconds if not set
'position': content.position,
'duration': getattr(content, '_playlist_duration', content.duration or 10),
'position': getattr(content, '_playlist_position', 0),
'filename': content.filename
})

View File

@@ -2,11 +2,12 @@
from flask import (Blueprint, render_template, request, redirect, url_for,
flash, jsonify, current_app)
from flask_login import login_required
from sqlalchemy import desc
from sqlalchemy import desc, update
import os
from app.extensions import db, cache
from app.models import Player, Content
from app.models import Player, Content, Playlist
from app.models.playlist import playlist_content
from app.utils.logger import log_action
playlist_bp = Blueprint('playlist', __name__, url_prefix='/playlist')
@@ -18,20 +19,22 @@ def manage_playlist(player_id: int):
"""Manage playlist for a specific player."""
player = Player.query.get_or_404(player_id)
# Get all content for this player, ordered by position
playlist_content = Content.query.filter_by(
player_id=player_id
).order_by(Content.position).all()
# Get content from player's assigned playlist
playlist_items = []
if player.playlist_id:
playlist = Playlist.query.get(player.playlist_id)
if playlist:
playlist_items = playlist.get_content_ordered()
# Get available content (files not already in this player's playlist)
all_files = db.session.query(Content.filename).distinct().all()
playlist_filenames = {c.filename for c in playlist_content}
available_files = [f[0] for f in all_files if f[0] not in playlist_filenames]
# Get available content (all content not in current playlist)
all_content = Content.query.all()
playlist_content_ids = {item.id for item in playlist_items}
available_content = [c for c in all_content if c.id not in playlist_content_ids]
return render_template('playlist/manage_playlist.html',
player=player,
playlist_content=playlist_content,
available_files=available_files)
playlist_content=playlist_items,
available_content=available_content)
@playlist_bp.route('/<int:player_id>/add', methods=['POST'])
@@ -40,44 +43,46 @@ def add_to_playlist(player_id: int):
"""Add content to player's playlist."""
player = Player.query.get_or_404(player_id)
if not player.playlist_id:
flash('Player has no playlist assigned.', 'warning')
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
try:
filename = request.form.get('filename')
content_id = request.form.get('content_id', type=int)
duration = request.form.get('duration', type=int, default=10)
if not filename:
flash('Please provide a filename.', 'warning')
if not content_id:
flash('Please select content.', 'warning')
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
content = Content.query.get_or_404(content_id)
playlist = Playlist.query.get(player.playlist_id)
# Get max position
max_position = db.session.query(db.func.max(Content.position)).filter_by(
player_id=player_id
from sqlalchemy import select, func
max_pos = db.session.execute(
select(func.max(playlist_content.c.position)).where(
playlist_content.c.playlist_id == playlist.id
)
).scalar() or 0
# Get file info from existing content
existing_content = Content.query.filter_by(filename=filename).first()
if not existing_content:
flash('File not found.', 'danger')
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
# Create new content entry for this player
new_content = Content(
filename=filename,
content_type=existing_content.content_type,
duration=duration,
file_size=existing_content.file_size,
player_id=player_id,
position=max_position + 1
# Add to playlist_content association table
stmt = playlist_content.insert().values(
playlist_id=playlist.id,
content_id=content.id,
position=max_pos + 1,
duration=duration
)
db.session.add(new_content)
db.session.execute(stmt)
# Increment playlist version
player.playlist_version += 1
playlist.increment_version()
db.session.commit()
cache.clear()
log_action('info', f'Added "{filename}" to playlist for player "{player.name}"')
flash(f'Added "{filename}" to playlist.', 'success')
log_action('info', f'Added "{content.filename}" to playlist for player "{player.name}"')
flash(f'Added "{content.filename}" to playlist.', 'success')
except Exception as e:
db.session.rollback()
@@ -92,28 +97,41 @@ def add_to_playlist(player_id: int):
def remove_from_playlist(player_id: int, content_id: int):
"""Remove content from player's playlist."""
player = Player.query.get_or_404(player_id)
content = Content.query.get_or_404(content_id)
if content.player_id != player_id:
flash('Content does not belong to this player.', 'danger')
if not player.playlist_id:
flash('Player has no playlist assigned.', 'danger')
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
try:
content = Content.query.get_or_404(content_id)
playlist = Playlist.query.get(player.playlist_id)
filename = content.filename
# Delete content
db.session.delete(content)
# Remove from playlist_content association table
from sqlalchemy import delete
stmt = delete(playlist_content).where(
(playlist_content.c.playlist_id == playlist.id) &
(playlist_content.c.content_id == content_id)
)
db.session.execute(stmt)
# Reorder remaining content
remaining_content = Content.query.filter_by(
player_id=player_id
).order_by(Content.position).all()
from sqlalchemy import select
remaining = db.session.execute(
select(playlist_content.c.content_id, playlist_content.c.position).where(
playlist_content.c.playlist_id == playlist.id
).order_by(playlist_content.c.position)
).fetchall()
for idx, item in enumerate(remaining_content, start=1):
item.position = idx
for idx, row in enumerate(remaining, start=1):
stmt = update(playlist_content).where(
(playlist_content.c.playlist_id == playlist.id) &
(playlist_content.c.content_id == row.content_id)
).values(position=idx)
db.session.execute(stmt)
# Increment playlist version
player.playlist_version += 1
playlist.increment_version()
db.session.commit()
cache.clear()
@@ -135,7 +153,12 @@ def reorder_playlist(player_id: int):
"""Reorder playlist items."""
player = Player.query.get_or_404(player_id)
if not player.playlist_id:
return jsonify({'success': False, 'message': 'Player has no playlist'}), 400
try:
playlist = Playlist.query.get(player.playlist_id)
# Get new order from JSON
data = request.get_json()
content_ids = data.get('content_ids', [])
@@ -143,24 +166,26 @@ def reorder_playlist(player_id: int):
if not content_ids:
return jsonify({'success': False, 'message': 'No content IDs provided'}), 400
# Update positions
# Update positions in association table
for idx, content_id in enumerate(content_ids, start=1):
content = Content.query.get(content_id)
if content and content.player_id == player_id:
content.position = idx
stmt = update(playlist_content).where(
(playlist_content.c.playlist_id == playlist.id) &
(playlist_content.c.content_id == content_id)
).values(position=idx)
db.session.execute(stmt)
# Increment playlist version
player.playlist_version += 1
playlist.increment_version()
db.session.commit()
cache.clear()
log_action('info', f'Reordered playlist for player "{player.name}" (version {player.playlist_version})')
log_action('info', f'Reordered playlist for player "{player.name}" (version {playlist.version})')
return jsonify({
'success': True,
'message': 'Playlist reordered successfully',
'version': player.playlist_version
'version': playlist.version
})
except Exception as e:
@@ -174,19 +199,28 @@ def reorder_playlist(player_id: int):
def update_duration(player_id: int, content_id: int):
"""Update content duration in playlist."""
player = Player.query.get_or_404(player_id)
content = Content.query.get_or_404(content_id)
if content.player_id != player_id:
return jsonify({'success': False, 'message': 'Content does not belong to this player'}), 403
if not player.playlist_id:
return jsonify({'success': False, 'message': 'Player has no playlist'}), 400
try:
playlist = Playlist.query.get(player.playlist_id)
content = Content.query.get_or_404(content_id)
duration = request.form.get('duration', type=int)
if not duration or duration < 1:
return jsonify({'success': False, 'message': 'Invalid duration'}), 400
content.duration = duration
player.playlist_version += 1
# Update duration in association table
stmt = update(playlist_content).where(
(playlist_content.c.playlist_id == playlist.id) &
(playlist_content.c.content_id == content_id)
).values(duration=duration)
db.session.execute(stmt)
# Increment playlist version
playlist.increment_version()
db.session.commit()
cache.clear()
@@ -196,7 +230,7 @@ def update_duration(player_id: int, content_id: int):
return jsonify({
'success': True,
'message': 'Duration updated',
'version': player.playlist_version
'version': playlist.version
})
except Exception as e:
@@ -211,12 +245,22 @@ def clear_playlist(player_id: int):
"""Clear all content from player's playlist."""
player = Player.query.get_or_404(player_id)
if not player.playlist_id:
flash('Player has no playlist assigned.', 'warning')
return redirect(url_for('playlist.manage_playlist', player_id=player_id))
try:
# Delete all content for this player
Content.query.filter_by(player_id=player_id).delete()
playlist = Playlist.query.get(player.playlist_id)
# Delete all content from playlist
from sqlalchemy import delete
stmt = delete(playlist_content).where(
playlist_content.c.playlist_id == playlist.id
)
db.session.execute(stmt)
# Increment playlist version
player.playlist_version += 1
playlist.increment_version()
db.session.commit()
cache.clear()