diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx index b868c7e5..47d6b337 100644 --- a/frontend/src/pages/Stats.jsx +++ b/frontend/src/pages/Stats.jsx @@ -93,23 +93,14 @@ const VODCard = ({ vodContent }) => { const isMovie = contentType === 'movie'; const isEpisode = contentType === 'episode'; + // Get the individual connection (since we now separate cards per connection) + const connection = + vodContent.individual_connection || + (vodContent.connections && vodContent.connections[0]); + // Get poster/logo URL const posterUrl = metadata.logo_url || logo; - // Transform VOD connections to match table data structure - const connectionData = useMemo(() => { - return (vodContent.connections || []).map((connection, index) => ({ - id: `${connection.client_id}-${index}`, - ip_address: connection.client_ip, - client_id: connection.client_id, - user_agent: connection.user_agent || 'Unknown', - connected_since: connection.duration || 0, - connected_at: connection.connected_at, - m3u_profile: connection.m3u_profile, - ...connection, - })); - }, [vodContent.connections]); - // Format duration const formatDuration = (seconds) => { if (!seconds) return 'Unknown'; @@ -166,7 +157,7 @@ const VODCard = ({ vodContent }) => { const duration = currentTime - clientStartTime; return dayjs.duration(duration, 'seconds').humanize(); } - } catch (e) { + } catch { // Ignore parsing errors } } @@ -191,7 +182,7 @@ const VODCard = ({ vodContent }) => { const clientStartTime = parseInt(parts[1]); return dayjs(clientStartTime).format(`${dateFormat} HH:mm:ss`); } - } catch (e) { + } catch { // Ignore parsing errors } } @@ -201,156 +192,6 @@ const VODCard = ({ vodContent }) => { [dateFormat] ); - // Define table columns similar to ChannelCard - const vodConnectionsColumns = useMemo( - () => [ - { - id: 'expand', - size: 20, - }, - { - header: 'IP Address', - accessorKey: 'ip_address', - cell: ({ cell }) => {cell.getValue()}, - }, - { - id: 'connected', - header: 'Connected', - accessorFn: (row) => { - return getConnectionStartTime(row); - }, - cell: ({ cell }) => ( - - {cell.getValue()} - - ), - }, - { - id: 'duration', - header: 'Duration', - accessorFn: (row) => { - return calculateConnectionDuration(row); - }, - cell: ({ cell, row }) => { - const exactDuration = row.original.duration; - return ( - - {cell.getValue()} - - ); - }, - }, - ], - [getConnectionStartTime, calculateConnectionDuration] - ); - - // Table configuration similar to ChannelCard - const vodConnectionsTable = useTable({ - ...TableHelper.defaultProperties, - columns: vodConnectionsColumns, - data: connectionData, - allRowIds: connectionData.map((connection) => connection.id), - tableCellProps: () => ({ - padding: 4, - borderColor: '#444', - color: '#E0E0E0', - fontSize: '0.85rem', - }), - headerCellRenderFns: { - ip_address: ({ header }) => ( - - - {header?.column?.columnDef?.header || 'IP Address'} - - - ), - connected: ({ header }) => ( - - - {header?.column?.columnDef?.header || 'Connected'} - - - ), - duration: ({ header }) => ( - - - {header?.column?.columnDef?.header || 'Duration'} - - - ), - }, - expandedRowRenderer: ({ row }) => { - return ( - - - - - Client ID: - - - {row.original.client_id} - - - - {row.original.user_agent && - row.original.user_agent !== 'Unknown' && ( - - - User Agent: - - - {row.original.user_agent.length > 60 - ? `${row.original.user_agent.substring(0, 60)}...` - : row.original.user_agent} - - - )} - - {row.original.m3u_profile && - (row.original.m3u_profile.profile_name || - row.original.m3u_profile.account_name) && ( - - - M3U Profile: - - - {row.original.m3u_profile.account_name || 'Unknown Account'}{' '} - →{' '} - {row.original.m3u_profile.profile_name || 'Default Profile'} - - - )} - - - ); - }, - mantineExpandButtonProps: ({ row }) => ({ - size: 'xs', - style: { - transform: row.getIsExpanded() ? 'rotate(180deg)' : 'rotate(-90deg)', - transition: 'transform 0.2s', - }, - }), - displayColumnDefOptions: { - 'mrt-row-expand': { - size: 15, - header: '', - }, - }, - }); - return ( { + {/* Display M3U profile information - matching channel card style */} + {connection && + connection.m3u_profile && + (connection.m3u_profile.profile_name || + connection.m3u_profile.account_name) && ( + + + + + + + {connection.m3u_profile.account_name || 'Unknown Account'} + + + + + {connection.m3u_profile.profile_name || 'Default Profile'} + + + + + + )} + {/* Subtitle/episode info */} {getSubtitle() && ( @@ -461,28 +326,77 @@ const VODCard = ({ vodContent }) => { )} - {/* Connection statistics */} - - - - - - {vodContent.connection_count} - - + {/* Individual client information */} + {connection && ( + + + + Client IP: + + + {connection.client_ip || 'Unknown'} + + + + + + Duration: + + {calculateConnectionDuration(connection)} + + )} - - - On Demand - - - + {/* Additional client details */} + {connection && ( + + {connection.user_agent && connection.user_agent !== 'Unknown' && ( + + + User Agent: + + + {connection.user_agent.length > 80 + ? `${connection.user_agent.substring(0, 80)}...` + : connection.user_agent} + + + )} - {/* Connection details table - similar to ChannelCard */} - + + + Client ID: + + + {connection.client_id || 'Unknown'} + + + + {connection.connected_at && ( + + + Connected: + + {getConnectionStartTime(connection)} + + )} + + )} ); @@ -1317,12 +1231,22 @@ const ChannelsPage = () => { sortKey: channel.uptime || 0, // Use uptime for sorting streams })); - const vodItems = vodConnections.map((vodContent) => ({ - type: 'vod', - data: vodContent, - id: `${vodContent.content_type}-${vodContent.content_uuid}`, - sortKey: Date.now() / 1000, // Use current time as fallback for VOD - })); + // Flatten VOD connections so each individual client gets its own card + const vodItems = vodConnections.flatMap((vodContent) => { + return (vodContent.connections || []).map((connection, index) => ({ + type: 'vod', + data: { + ...vodContent, + // Override the connections array to contain only this specific connection + connections: [connection], + connection_count: 1, // Each card now represents a single connection + // Add individual connection details at the top level for easier access + individual_connection: connection, + }, + id: `${vodContent.content_type}-${vodContent.content_uuid}-${connection.client_id}-${index}`, + sortKey: connection.connected_at || Date.now() / 1000, // Use connection time for sorting + })); + }); // Combine and sort by newest connections first (higher sortKey = more recent) return [...activeStreams, ...vodItems].sort( @@ -1339,8 +1263,19 @@ const ChannelsPage = () => { {Object.keys(channelHistory).length} stream {Object.keys(channelHistory).length !== 1 ? 's' : ''} •{' '} - {vodConnections.length} VOD connection - {vodConnections.length !== 1 ? 's' : ''} + {vodConnections.reduce( + (total, vodContent) => + total + (vodContent.connections?.length || 0), + 0 + )}{' '} + VOD connection + {vodConnections.reduce( + (total, vodContent) => + total + (vodContent.connections?.length || 0), + 0 + ) !== 1 + ? 's' + : ''} Refresh Interval (seconds):