updated upload page

This commit is contained in:
DigiServer Developer
2025-11-24 23:01:16 +02:00
parent 69562fbf22
commit b1dbacc679
2 changed files with 335 additions and 2 deletions

View File

@@ -3,7 +3,9 @@ from flask import (Blueprint, render_template, request, redirect, url_for,
flash, jsonify, current_app) flash, jsonify, current_app)
from flask_login import login_required from flask_login import login_required
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from typing import Optional
import os import os
import threading
from app.extensions import db, cache from app.extensions import db, cache
from app.models import Content, Playlist, Player from app.models import Content, Playlist, Player
@@ -11,6 +13,9 @@ from app.models.playlist import playlist_content
from app.utils.logger import log_action from app.utils.logger import log_action
from app.utils.uploads import process_video_file, set_upload_progress from app.utils.uploads import process_video_file, set_upload_progress
# Store for background processing status
_background_tasks = {}
content_bp = Blueprint('content', __name__, url_prefix='/content') content_bp = Blueprint('content', __name__, url_prefix='/content')
@@ -267,6 +272,48 @@ def remove_content_from_playlist(playlist_id: int, content_id: int):
return redirect(url_for('content.manage_playlist_content', playlist_id=playlist_id)) return redirect(url_for('content.manage_playlist_content', playlist_id=playlist_id))
@content_bp.route('/playlist/<int:playlist_id>/bulk-remove', methods=['POST'])
@login_required
def bulk_remove_from_playlist(playlist_id: int):
"""Remove multiple content items from playlist."""
playlist = Playlist.query.get_or_404(playlist_id)
try:
data = request.get_json()
content_ids = data.get('content_ids', [])
if not content_ids:
return jsonify({'success': False, 'message': 'No content IDs provided'}), 400
from app.models.playlist import playlist_content
# Remove all selected items
stmt = playlist_content.delete().where(
(playlist_content.c.playlist_id == playlist_id) &
(playlist_content.c.content_id.in_(content_ids))
)
result = db.session.execute(stmt)
# Increment version
playlist.increment_version()
db.session.commit()
cache.clear()
removed_count = result.rowcount if hasattr(result, 'rowcount') else len(content_ids)
log_action('info', f'Bulk removed {removed_count} items from playlist "{playlist.name}"')
return jsonify({
'success': True,
'message': f'Removed {removed_count} item(s) from playlist',
'removed_count': removed_count
})
except Exception as e:
db.session.rollback()
log_action('error', f'Error bulk removing from playlist: {str(e)}')
return jsonify({'success': False, 'message': str(e)}), 500
@content_bp.route('/playlist/<int:playlist_id>/reorder', methods=['POST']) @content_bp.route('/playlist/<int:playlist_id>/reorder', methods=['POST'])
@login_required @login_required
def reorder_playlist_content(playlist_id: int): def reorder_playlist_content(playlist_id: int):
@@ -479,6 +526,176 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
return False, f"PDF processing error: {str(e)}" return False, f"PDF processing error: {str(e)}"
def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
duration: int, playlist_id: Optional[int], task_id: str):
"""Process large files (PDF, PPTX, Video) in background thread."""
with app.app_context():
try:
_background_tasks[task_id] = {'status': 'processing', 'message': f'Processing {filename}...'}
upload_folder = app.config['UPLOAD_FOLDER']
processing_success = True
processing_message = ""
detected_type = file_ext
# Process based on file type
if file_ext == 'pdf':
processing_success, processing_message = process_pdf_file(filepath, filename)
if processing_success and "converted to" in processing_message.lower():
base_name = os.path.splitext(filename)[0]
page_pattern = f"{base_name}_page*.png"
import glob
page_files = sorted(glob.glob(os.path.join(upload_folder, page_pattern)))
if page_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
for page_file in page_files:
page_filename = os.path.basename(page_file)
page_content = Content(
filename=page_filename,
content_type='image',
duration=duration,
file_size=os.path.getsize(page_file)
)
db.session.add(page_content)
db.session.flush()
if playlist_id:
max_position += 1
stmt = playlist_content.insert().values(
playlist_id=playlist_id,
content_id=page_content.id,
position=max_position,
duration=duration
)
db.session.execute(stmt)
if playlist_id and page_files:
playlist = Playlist.query.get(playlist_id)
if playlist:
playlist.version += 1
db.session.commit()
if os.path.exists(filepath):
os.remove(filepath)
_background_tasks[task_id] = {
'status': 'complete',
'message': f'PDF converted to {len(page_files)} images successfully!'
}
log_action('info', f'Background: PDF {filename} converted to {len(page_files)} pages')
return
elif file_ext in ['ppt', 'pptx']:
processing_success, processing_message = process_presentation_file(filepath, filename)
if processing_success and "converted to" in processing_message.lower():
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
for slide_file in slide_files:
slide_filename = os.path.basename(slide_file)
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()
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)
if playlist_id and slide_files:
playlist = Playlist.query.get(playlist_id)
if playlist:
playlist.version += 1
db.session.commit()
if os.path.exists(filepath):
os.remove(filepath)
_background_tasks[task_id] = {
'status': 'complete',
'message': f'Presentation converted to {len(slide_files)} slides successfully!'
}
log_action('info', f'Background: PPTX {filename} converted to {len(slide_files)} slides')
return
elif file_ext in ['mp4', 'avi', 'mov', 'mkv', 'webm', 'flv', 'wmv']:
processing_success, processing_message = process_video_file_extended(filepath, filename)
detected_type = 'video'
# If file still exists, add as regular content
if processing_success and 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()
if playlist_id:
playlist = Playlist.query.get(playlist_id)
if playlist:
max_position = db.session.query(db.func.max(playlist_content.c.position))\
.filter(playlist_content.c.playlist_id == playlist_id)\
.scalar() or 0
stmt = playlist_content.insert().values(
playlist_id=playlist_id,
content_id=content.id,
position=max_position + 1,
duration=duration
)
db.session.execute(stmt)
playlist.version += 1
db.session.commit()
_background_tasks[task_id] = {'status': 'complete', 'message': f'{filename} processed successfully!'}
log_action('info', f'Background: {filename} processed successfully')
else:
_background_tasks[task_id] = {
'status': 'error',
'message': f'Failed to process {filename}: {processing_message}'
}
log_action('error', f'Background: Failed to process {filename}')
except Exception as e:
db.session.rollback()
_background_tasks[task_id] = {'status': 'error', 'message': f'Error: {str(e)}'}
log_action('error', f'Background processing error for {filename}: {str(e)}')
def process_presentation_file(filepath: str, filename: str) -> tuple[bool, str]: def process_presentation_file(filepath: str, filename: str) -> tuple[bool, str]:
"""Process PowerPoint presentation files by converting slides to images.""" """Process PowerPoint presentation files by converting slides to images."""
try: try:
@@ -738,6 +955,7 @@ def upload_media():
os.makedirs(upload_folder, exist_ok=True) os.makedirs(upload_folder, exist_ok=True)
uploaded_count = 0 uploaded_count = 0
background_count = 0
processing_errors = [] processing_errors = []
for file in files: for file in files:
@@ -759,6 +977,26 @@ def upload_media():
# Determine content type from extension # Determine content type from extension
file_ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else '' file_ext = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
# Check if file needs background processing (large files)
needs_background = file_ext in ['pdf', 'ppt', 'pptx', 'mp4', 'avi', 'mov', 'mkv', 'webm', 'flv', 'wmv']
if needs_background:
# Process in background thread
import uuid
task_id = str(uuid.uuid4())
_background_tasks[task_id] = {'status': 'queued', 'message': f'Queued {filename} for processing...'}
thread = threading.Thread(
target=process_file_in_background,
args=(current_app._get_current_object(), filepath, filename, file_ext, duration, playlist_id, task_id)
)
thread.daemon = True
thread.start()
background_count += 1
log_action('info', f'Queued {filename} for background processing (task: {task_id})')
continue
# Process file based on type # Process file based on type
processing_success = True processing_success = True
processing_message = "" processing_message = ""
@@ -936,7 +1174,7 @@ def upload_media():
db.session.commit() db.session.commit()
cache.clear() cache.clear()
log_action('info', f'Uploaded {uploaded_count} media files') log_action('info', f'Uploaded {uploaded_count} media files, {background_count} files processing in background')
# Show appropriate flash message # Show appropriate flash message
if processing_errors: if processing_errors:
@@ -944,6 +1182,14 @@ def upload_media():
if len(processing_errors) > 3: if len(processing_errors) > 3:
error_summary += f' and {len(processing_errors) - 3} more...' error_summary += f' and {len(processing_errors) - 3} more...'
flash(f'Uploaded {uploaded_count} file(s). Errors: {error_summary}', 'warning') flash(f'Uploaded {uploaded_count} file(s). Errors: {error_summary}', 'warning')
elif background_count > 0:
bg_msg = f'{background_count} file(s) are being processed in the background (PDF, PPTX, or large videos). '
bg_msg += 'They will appear in the media library/playlist automatically when conversion completes. '
bg_msg += 'This may take a few minutes.'
if uploaded_count > 0:
flash(f'✅ Uploaded {uploaded_count} file(s) immediately. ⏳ {bg_msg}', 'info')
else:
flash(f'{bg_msg}', 'info')
elif playlist_id: elif playlist_id:
playlist = Playlist.query.get(playlist_id) playlist = Playlist.query.get(playlist_id)
flash(f'Successfully uploaded {uploaded_count} file(s) to playlist "{playlist.name}"!', 'success') flash(f'Successfully uploaded {uploaded_count} file(s) to playlist "{playlist.name}"!', 'success')

View File

@@ -191,12 +191,20 @@
<div class="content-grid"> <div class="content-grid">
<div class="card"> <div class="card">
<h2 style="margin-bottom: 20px;">📋 Playlist Content (Drag to Reorder)</h2> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2 style="margin: 0;">📋 Playlist Content (Drag to Reorder)</h2>
<button id="bulk-delete-btn" class="btn btn-danger" style="display: none;" onclick="bulkDeleteSelected()">
🗑️ Delete Selected (<span id="selected-count">0</span>)
</button>
</div>
{% if playlist_content %} {% if playlist_content %}
<table class="playlist-table" id="playlist-table"> <table class="playlist-table" id="playlist-table">
<thead> <thead>
<tr> <tr>
<th style="width: 40px;">
<input type="checkbox" id="select-all" onchange="toggleSelectAll(this)" title="Select all">
</th>
<th style="width: 40px;"></th> <th style="width: 40px;"></th>
<th style="width: 50px;">#</th> <th style="width: 50px;">#</th>
<th>Filename</th> <th>Filename</th>
@@ -209,6 +217,9 @@
<tbody id="playlist-tbody"> <tbody id="playlist-tbody">
{% for content in playlist_content %} {% for content in playlist_content %}
<tr class="draggable-row" draggable="true" data-content-id="{{ content.id }}"> <tr class="draggable-row" draggable="true" data-content-id="{{ content.id }}">
<td>
<input type="checkbox" class="content-checkbox" data-content-id="{{ content.id }}" onchange="updateBulkDeleteButton()">
</td>
<td><span class="drag-handle">⋮⋮</span></td> <td><span class="drag-handle">⋮⋮</span></td>
<td>{{ loop.index }}</td> <td>{{ loop.index }}</td>
<td>{{ content.filename }}</td> <td>{{ content.filename }}</td>
@@ -415,6 +426,82 @@ function toggleAudio(contentId, enabled) {
if (checkbox) checkbox.checked = !enabled; if (checkbox) checkbox.checked = !enabled;
}); });
} }
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.content-checkbox');
checkboxes.forEach(cb => {
cb.checked = checkbox.checked;
});
updateBulkDeleteButton();
}
function updateBulkDeleteButton() {
const checkboxes = document.querySelectorAll('.content-checkbox:checked');
const count = checkboxes.length;
const bulkDeleteBtn = document.getElementById('bulk-delete-btn');
const selectedCount = document.getElementById('selected-count');
if (count > 0) {
bulkDeleteBtn.style.display = 'block';
selectedCount.textContent = count;
} else {
bulkDeleteBtn.style.display = 'none';
}
// Update select-all checkbox state
const allCheckboxes = document.querySelectorAll('.content-checkbox');
const selectAllCheckbox = document.getElementById('select-all');
if (selectAllCheckbox) {
selectAllCheckbox.checked = allCheckboxes.length > 0 && count === allCheckboxes.length;
selectAllCheckbox.indeterminate = count > 0 && count < allCheckboxes.length;
}
}
function bulkDeleteSelected() {
const checkboxes = document.querySelectorAll('.content-checkbox:checked');
const contentIds = Array.from(checkboxes).map(cb => parseInt(cb.dataset.contentId));
if (contentIds.length === 0) {
alert('No items selected');
return;
}
const confirmMsg = `Are you sure you want to remove ${contentIds.length} item(s) from this playlist?`;
if (!confirm(confirmMsg)) {
return;
}
// Show loading state
const bulkDeleteBtn = document.getElementById('bulk-delete-btn');
const originalText = bulkDeleteBtn.innerHTML;
bulkDeleteBtn.disabled = true;
bulkDeleteBtn.innerHTML = '⏳ Removing...';
fetch('{{ url_for("content.bulk_remove_from_playlist", playlist_id=playlist.id) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content_ids: contentIds })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload page to show updated playlist
window.location.reload();
} else {
alert('Error removing items: ' + data.message);
bulkDeleteBtn.disabled = false;
bulkDeleteBtn.innerHTML = originalText;
}
})
.catch(error => {
console.error('Error:', error);
alert('Error removing items from playlist');
bulkDeleteBtn.disabled = false;
bulkDeleteBtn.innerHTML = originalText;
});
}
</script> </script>
{% endblock %} {% endblock %}