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):