From d0fbfe25b31c5d41c66ef4c60a8bd81d412b5955 Mon Sep 17 00:00:00 2001 From: DigiServer Developer Date: Mon, 3 Nov 2025 16:09:18 +0200 Subject: [PATCH] Add real-time upload progress tracking, mobile-optimized manage_group page with player status cards Features: - Real-time upload progress tracking with AJAX polling and session-based monitoring - API endpoint /api/upload_progress/ for progress updates - Video conversion progress tracking with background threads - Mobile-responsive design for manage_group page - Player status cards with feedback, playlist sync, and last activity - Bootstrap Icons integration throughout UI - Responsive layout (1/4 group info, 3/4 players on desktop) - Video thumbnails with play icon, image thumbnails in media lists - Bulk selection and delete for group media - Enhanced logging for video conversion debugging --- app/app.py | 94 ++++++++- app/templates/manage_group.html | 248 ++++++++++++++++++---- app/templates/player_page.html | 20 +- app/templates/upload_content.html | 183 ++++++++++------ app/utils/uploads.py | 335 ++++++++++++++++++++++++++---- 5 files changed, 720 insertions(+), 160 deletions(-) diff --git a/app/app.py b/app/app.py index 00f8189..949c095 100755 --- a/app/app.py +++ b/app/app.py @@ -106,6 +106,10 @@ login_manager.login_view = 'login' migrate = Migrate(app, db) +# Global dictionary to track upload progress +# Format: {session_id: {'status': 'uploading/converting/complete', 'progress': 0-100, 'message': 'details', 'files_total': N, 'files_processed': N}} +upload_progress = {} + @app.route('/api/player-feedback', methods=['POST']) def api_player_feedback(): from datetime import datetime @@ -259,35 +263,72 @@ def logout(): @admin_required def upload_content(): if request.method == 'POST': + import uuid + target_type = request.form.get('target_type') target_id = request.form.get('target_id') files = request.files.getlist('files') duration = int(request.form['duration']) return_url = request.form.get('return_url') media_type = request.form['media_type'] + session_id = request.form.get('session_id', str(uuid.uuid4())) - print(f"Target Type: {target_type}, Target ID: {target_id}, Media Type: {media_type}") + print(f"Target Type: {target_type}, Target ID: {target_id}, Media Type: {media_type}, Session ID: {session_id}") if not target_type or not target_id: flash('Please select a target type and target ID.', 'danger') return redirect(url_for('upload_content')) + # Initialize progress tracking + upload_progress[session_id] = { + 'status': 'uploading', + 'progress': 0, + 'message': 'Starting upload...', + 'files_total': len(files), + 'files_processed': 0 + } + try: # Process uploaded files and get results - results = process_uploaded_files(app, files, media_type, duration, target_type, target_id) + results = process_uploaded_files(app, files, media_type, duration, target_type, target_id, upload_progress, session_id) - # Check for any failed uploads - failed_files = [r for r in results if not r.get('success', True)] - if failed_files: - for failed in failed_files: - flash(f"Error uploading {failed.get('filename', 'unknown file')}: {failed.get('message', 'Unknown error')}", 'warning') + # Check if video conversion is happening in background + if media_type == 'video': + # For videos, don't mark as complete yet - background thread will do it + # Status remains as "converting" set by the background thread + flash('Video upload started. Conversion is in progress...', 'info') else: - flash('All files uploaded and processed successfully!', 'success') + # For non-videos (images, PDF, PPT), mark as complete + upload_progress[session_id] = { + 'status': 'complete', + 'progress': 100, + 'message': 'All files processed successfully!', + 'files_total': len(files), + 'files_processed': len(files) + } + + # Check for any failed uploads + failed_files = [r for r in results if not r.get('success', True)] + if failed_files: + for failed in failed_files: + flash(f"Error uploading {failed.get('filename', 'unknown file')}: {failed.get('message', 'Unknown error')}", 'warning') + else: + flash('All files uploaded and processed successfully!', 'success') except Exception as e: print(f"Error in upload_content: {e}") import traceback traceback.print_exc() + + # Mark as error + upload_progress[session_id] = { + 'status': 'error', + 'progress': 0, + 'message': f'Upload failed: {str(e)}', + 'files_total': len(files), + 'files_processed': 0 + } + flash(f'Upload failed: {str(e)}', 'danger') return redirect(return_url) @@ -714,6 +755,22 @@ def get_playlists(): def media(filename): return send_from_directory(app.config['UPLOAD_FOLDER'], filename) +@app.route('/api/upload_progress/', methods=['GET']) +@login_required +def get_upload_progress(session_id): + """ + API endpoint to get upload/conversion progress for a session. + Returns JSON with status, progress percentage, and current message. + """ + progress_data = upload_progress.get(session_id, { + 'status': 'unknown', + 'progress': 0, + 'message': 'No active upload found', + 'files_total': 0, + 'files_processed': 0 + }) + return jsonify(progress_data) + @app.context_processor def inject_theme(): if current_user.is_authenticated: @@ -740,13 +797,32 @@ def create_group(): @login_required @admin_required def manage_group(group_id): + from models.player_feedback import PlayerFeedback + group = Group.query.get_or_404(group_id) content = get_group_content(group_id) # Debug content ordering print("Group content positions before sorting:", [(c.id, c.file_name, c.position) for c in content]) content = sorted(content, key=lambda c: c.position) print("Group content positions after sorting:", [(c.id, c.file_name, c.position) for c in content]) - return render_template('manage_group.html', group=group, content=content) + + # Fetch player feedback for all players in the group + players_status = [] + for player in group.players: + player_feedback = PlayerFeedback.query.filter_by(player_name=player.username)\ + .order_by(PlayerFeedback.timestamp.desc())\ + .limit(5)\ + .all() + players_status.append({ + 'player': player, + 'feedback': player_feedback, + 'server_playlist_version': player.playlist_version + }) + + return render_template('manage_group.html', + group=group, + content=content, + players_status=players_status) @app.route('/group//edit', methods=['GET', 'POST']) @login_required diff --git a/app/templates/manage_group.html b/app/templates/manage_group.html index ef37338..4438e7f 100644 --- a/app/templates/manage_group.html +++ b/app/templates/manage_group.html @@ -3,8 +3,9 @@ - Manage Group + Manage Group - {{ group.name }} + -
-

Manage Group: {{ group.name }}

- - -
-
-

Group Info

-
-
-

Group Name: {{ group.name }}

-

Number of Players: {{ group.players|length }}

+
+ +
+ {% if logo_exists %} + + {% endif %} +
+

Manage Group

+

{{ group.name }}

- -
-
-

Players in Group

+ +
+
{{ group.name }}
+
+ + +
+ +
+
+
+
Group Info
+
+
+
+ Group Name +

{{ group.name }}

+
+
+ Players +

{{ group.players|length }}

+
+
+ Playlist Version +

v{{ group.playlist_version }}

+
+
+
-
-
    - {% for player in group.players %} -
  • -
    - {{ player.username }} ({{ player.hostname }}) + + +
    +
    +
    +
    Players ({{ group.players|length }})
    +
    +
    + {% if players_status %} +
    + {% for player_status in players_status %} +
    +
    +
    +
    {{ player_status.player.username }}
    + + + +
    +
    +
    + Hostname: + {{ player_status.player.hostname }} +
    + + {% if player_status.feedback %} +
    + Status: + + {{ player_status.feedback[0].status|title }} + +
    +
    + Last Activity: + {{ player_status.feedback[0].timestamp.strftime('%Y-%m-%d %H:%M:%S') }} +
    +
    + Message: + {{ player_status.feedback[0].message[:50] }}{% if player_status.feedback[0].message|length > 50 %}...{% endif %} +
    +
    + Playlist: + {% if player_status.feedback[0].playlist_version %} + {% if player_status.feedback[0].playlist_version|int == player_status.server_playlist_version %} + v{{ player_status.feedback[0].playlist_version }} ✓ + In sync + {% else %} + v{{ player_status.feedback[0].playlist_version }} + ⚠ Out of sync (server: v{{ player_status.server_playlist_version }}) + {% endif %} + {% else %} + Unknown + {% endif %} +
    + {% else %} +
    +

    No status data

    + Player hasn't reported yet +
    + {% endif %} +
    +
    -
  • - {% endfor %} -
+ {% endfor %} +
+ {% else %} +
+ +

No players in this group

+
+ {% endif %} +
+
@@ -130,16 +284,23 @@
- thumbnail + {% set file_ext = media.file_name.lower().split('.')[-1] %} + {% if file_ext in ['mp4', 'avi', 'mkv', 'mov', 'webm'] %} + +
+ + + +
+ {% else %} + + thumbnail + {% endif %}

Media Name: {{ media.file_name }}

-<<<<<<< HEAD -======= - ->>>>>>> 2255cc2 (Show media thumbnails in manage group page, matching player page style)
seconds @@ -162,12 +323,19 @@
- diff --git a/app/templates/player_page.html b/app/templates/player_page.html index 4fd259e..c0f7ac2 100644 --- a/app/templates/player_page.html +++ b/app/templates/player_page.html @@ -214,10 +214,22 @@
- thumbnail + {% set file_ext = media.file_name.lower().split('.')[-1] %} + {% if file_ext in ['mp4', 'avi', 'mkv', 'mov', 'webm'] %} + +
+ + + + +
+ {% else %} + + thumbnail + {% endif %}

Media Name: {{ media.file_name }}

diff --git a/app/templates/upload_content.html b/app/templates/upload_content.html index 519d583..90664b5 100644 --- a/app/templates/upload_content.html +++ b/app/templates/upload_content.html @@ -57,7 +57,7 @@ {% endif %}

Upload Content

- +
@@ -223,82 +223,127 @@