From 4ca6bf763e1bef304a8803c04f088d9a95eaa578 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sat, 6 Sep 2025 10:16:54 -0500 Subject: [PATCH] Add position calculation --- apps/proxy/vod_proxy/connection_manager.py | 5 +- apps/proxy/vod_proxy/views.py | 73 +++++++++++++++++++++- frontend/src/pages/Stats.jsx | 69 ++++++++++++++++++++ 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/apps/proxy/vod_proxy/connection_manager.py b/apps/proxy/vod_proxy/connection_manager.py index b12996cf..dea5759b 100644 --- a/apps/proxy/vod_proxy/connection_manager.py +++ b/apps/proxy/vod_proxy/connection_manager.py @@ -475,7 +475,8 @@ class VODConnectionManager: "connected_at": str(time.time()), "last_activity": str(time.time()), "bytes_sent": "0", - "position_seconds": "0" + "position_seconds": "0", + "last_position_update": str(time.time()) } # Use pipeline for atomic operations @@ -1079,7 +1080,7 @@ class VODConnectionManager: if range_header: logger.info(f"[{client_id}] *** CLIENT RANGE REQUEST: {range_header} ***") - # Parse range for logging + # Parse range for seeking detection try: if 'bytes=' in range_header: range_part = range_header.replace('bytes=', '') diff --git a/apps/proxy/vod_proxy/views.py b/apps/proxy/vod_proxy/views.py index 8f17120e..204dca7d 100644 --- a/apps/proxy/vod_proxy/views.py +++ b/apps/proxy/vod_proxy/views.py @@ -199,6 +199,55 @@ class VODStreamView(View): connection_manager = MultiWorkerVODConnectionManager.get_instance() + # Calculate and update position if this is a range request (seeking) + if range_header and session_id: + try: + if 'bytes=' in range_header: + range_part = range_header.replace('bytes=', '') + if '-' in range_part: + start_byte, end_byte = range_part.split('-', 1) + if start_byte and int(start_byte) > 0: + # Get file size and duration for position calculation + file_size_bytes = None + duration_secs = None + + # Try to get file size from Redis (persistent connection) + try: + import redis + redis_client = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True) + persistent_conn_key = f"vod_persistent_connection:{session_id}" + + if redis_client.exists(persistent_conn_key): + conn_data = redis_client.hgetall(persistent_conn_key) + if conn_data: + content_length = conn_data.get('content_length') + if content_length: + file_size_bytes = int(content_length) + except Exception as e: + logger.warning(f"[VOD-POSITION] Could not get file size from Redis: {e}") + + # Get duration from content object + if hasattr(content_obj, 'duration_secs') and content_obj.duration_secs: + duration_secs = content_obj.duration_secs + + # Calculate position if we have the required data + if file_size_bytes and file_size_bytes > 0 and duration_secs and duration_secs > 0: + position_percentage = (int(start_byte) / file_size_bytes) * 100 + position_seconds = int((position_percentage / 100) * duration_secs) + current_timestamp = time.time() + + # Update persistent connection position with timestamp + try: + redis_client.hset(persistent_conn_key, mapping={ + "position_seconds": str(position_seconds), + "last_position_update": str(current_timestamp) + }) + logger.info(f"[VOD-POSITION] Updated position: {position_seconds}s ({position_percentage:.1f}%)") + except Exception as redis_e: + logger.error(f"[VOD-POSITION] Failed to update position: {redis_e}") + except Exception as e: + logger.warning(f"[VOD-POSITION] Position calculation error: {e}") + # Stream the content with session-based connection reuse logger.info("[VOD-STREAM] Calling connection manager to stream content") response = connection_manager.stream_content_with_session( @@ -760,6 +809,24 @@ class VODStatsView(View): 'account_name': 'Unknown Account' # We don't store account name directly } + # Calculate estimated current position based on last known position + elapsed time + last_known_position = int(combined_data.get('position_seconds', 0)) + last_position_update = combined_data.get('last_position_update') + estimated_position = last_known_position + + if last_position_update and content_metadata.get('duration_secs'): + try: + update_timestamp = float(last_position_update) + elapsed_since_update = current_time - update_timestamp + # Add elapsed time to last known position, but don't exceed content duration + estimated_position = min( + last_known_position + int(elapsed_since_update), + int(content_metadata['duration_secs']) + ) + except (ValueError, TypeError): + # If timestamp parsing fails, fall back to last known position + estimated_position = last_known_position + connection_info = { 'content_type': content_type, 'content_uuid': content_uuid, @@ -771,7 +838,11 @@ class VODStatsView(View): 'user_agent': combined_data.get('client_user_agent', 'Unknown'), 'connected_at': combined_data.get('created_at'), 'last_activity': combined_data.get('last_activity'), - 'm3u_profile_id': m3u_profile_id + 'm3u_profile_id': m3u_profile_id, + 'position_seconds': estimated_position, # Use estimated position + 'last_known_position': last_known_position, # Include raw position for debugging + 'last_position_update': last_position_update, # Include timestamp for frontend use + 'bytes_sent': int(combined_data.get('bytes_sent', 0)) } # Calculate connection duration diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx index 160ca132..ea790761 100644 --- a/frontend/src/pages/Stats.jsx +++ b/frontend/src/pages/Stats.jsx @@ -8,6 +8,7 @@ import { Container, Flex, Group, + Progress, SimpleGrid, Stack, Text, @@ -152,6 +153,43 @@ const VODCard = ({ vodContent }) => { ); }; + // Calculate progress percentage and time + const calculateProgress = useCallback(() => { + if (!connection || !metadata.duration_secs) { + return { + percentage: 0, + currentTime: 0, + totalTime: metadata.duration_secs || 0, + }; + } + + const positionSeconds = connection.position_seconds || 0; + const totalSeconds = metadata.duration_secs; + const percentage = + totalSeconds > 0 ? (positionSeconds / totalSeconds) * 100 : 0; + + return { + percentage: Math.min(percentage, 100), // Cap at 100% + currentTime: positionSeconds, + totalTime: totalSeconds, + }; + }, [connection, metadata.duration_secs]); + + // Format time for display (e.g., "1:23:45" or "23:45") + const formatTime = (seconds) => { + if (!seconds || seconds === 0) return '0:00'; + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } else { + return `${minutes}:${secs.toString().padStart(2, '0')}`; + } + }; + // Calculate duration for connection const calculateConnectionDuration = useCallback((connection) => { // If duration is provided by API, use it @@ -326,6 +364,37 @@ const VODCard = ({ vodContent }) => { )} + {/* Progress bar - show current position in content */} + {connection && + metadata.duration_secs && + (() => { + const progress = calculateProgress(); + return progress.totalTime > 0 ? ( + + + + Progress + + + {formatTime(progress.currentTime)} /{' '} + {formatTime(progress.totalTime)} + + + + + {progress.percentage.toFixed(1)}% watched + + + ) : null; + })()} + {/* Client information section - collapsible like channel cards */} {connection && (