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)
from flask_login import login_required
from werkzeug.utils import secure_filename
from typing import Optional
import os
import threading
from app.extensions import db, cache
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.uploads import process_video_file, set_upload_progress
# Store for background processing status
_background_tasks = {}
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))
@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'])
@login_required
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)}"
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]:
"""Process PowerPoint presentation files by converting slides to images."""
try:
@@ -738,6 +955,7 @@ def upload_media():
os.makedirs(upload_folder, exist_ok=True)
uploaded_count = 0
background_count = 0
processing_errors = []
for file in files:
@@ -759,6 +977,26 @@ def upload_media():
# Determine content type from extension
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
processing_success = True
processing_message = ""
@@ -936,7 +1174,7 @@ def upload_media():
db.session.commit()
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
if processing_errors:
@@ -944,6 +1182,14 @@ def upload_media():
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 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:
playlist = Playlist.query.get(playlist_id)
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="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 %}
<table class="playlist-table" id="playlist-table">
<thead>
<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: 50px;">#</th>
<th>Filename</th>
@@ -209,6 +217,9 @@
<tbody id="playlist-tbody">
{% for content in playlist_content %}
<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>{{ loop.index }}</td>
<td>{{ content.filename }}</td>
@@ -415,6 +426,82 @@ function toggleAudio(contentId, 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>
{% endblock %}