diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py
index 56528bb5..2b7cd0ec 100644
--- a/apps/m3u/tasks.py
+++ b/apps/m3u/tasks.py
@@ -530,6 +530,7 @@ def refresh_single_m3u_account(account_id):
total_batches = len(batches)
completed_batches = 0
+ streams_processed = 0 # Track total streams processed
logger.debug(f"Dispatched {len(batches)} parallel tasks for account_id={account_id}.")
# result = task_group.apply_async()
@@ -542,18 +543,47 @@ def refresh_single_m3u_account(account_id):
if async_result.ready() and async_result.id not in completed_task_ids: # If the task has completed and we haven't counted it
task_result = async_result.result # The result of the task
logger.debug(f"Task completed with result: {task_result}")
+
+ # Extract stream counts from result string if available
+ if isinstance(task_result, str):
+ try:
+ created_match = re.search(r"(\d+) created", task_result)
+ updated_match = re.search(r"(\d+) updated", task_result)
+
+ if created_match and updated_match:
+ created_count = int(created_match.group(1))
+ updated_count = int(updated_match.group(1))
+ streams_processed += created_count + updated_count
+ except (AttributeError, ValueError):
+ pass
+
completed_batches += 1
completed_task_ids.add(async_result.id) # Mark this task as processed
# Calculate progress
progress = int((completed_batches / total_batches) * 100)
+ # Calculate elapsed time and estimated remaining time
+ current_elapsed = time.time() - start_time
+ if progress > 0:
+ estimated_total = (current_elapsed / progress) * 100
+ time_remaining = max(0, estimated_total - current_elapsed)
+ else:
+ time_remaining = 0
+
# Send progress update via Channels
# Don't send 100% because we want to clean up after
if progress == 100:
progress = 99
- send_m3u_update(account_id, "parsing", progress)
+ send_m3u_update(
+ account_id,
+ "parsing",
+ progress,
+ elapsed_time=current_elapsed,
+ time_remaining=time_remaining,
+ streams_processed=streams_processed
+ )
# Optionally remove completed task from the group to prevent processing it again
result.remove(async_result)
@@ -567,7 +597,16 @@ def refresh_single_m3u_account(account_id):
# Now run cleanup
cleanup_streams(account_id)
- send_m3u_update(account_id, "parsing", 100)
+ # Send final update with complete metrics
+ elapsed_time = time.time() - start_time
+ send_m3u_update(
+ account_id,
+ "parsing",
+ 100,
+ elapsed_time=elapsed_time,
+ time_remaining=0,
+ streams_processed=streams_processed
+ )
end_time = time.time()
diff --git a/frontend/src/components/tables/M3UsTable.jsx b/frontend/src/components/tables/M3UsTable.jsx
index b114b0fe..6e018b95 100644
--- a/frontend/src/components/tables/M3UsTable.jsx
+++ b/frontend/src/components/tables/M3UsTable.jsx
@@ -43,17 +43,18 @@ const M3UTable = () => {
return buildDownloadingStats(data);
case 'processing_groups':
- return 'Processing groups...';
+ return buildGroupProcessingStats(data);
+
+ case 'parsing':
+ return buildParsingStats(data);
default:
- return buildParsingStats(data);
+ return data.status === 'error' ? buildErrorStats(data) : `${data.action || 'Processing'}...`;
}
};
const buildDownloadingStats = (data) => {
if (data.progress == 100) {
- // fetchChannelGroups();
- // fetchPlaylists();
return 'Download complete!';
}
@@ -61,21 +62,89 @@ const M3UTable = () => {
return 'Downloading...';
}
+ // Format time remaining in minutes:seconds
+ const timeRemaining = data.time_remaining ?
+ `${Math.floor(data.time_remaining / 60)}:${String(Math.floor(data.time_remaining % 60)).padStart(2, '0')}` :
+ 'calculating...';
+
+ // Format speed with appropriate unit (KB/s or MB/s)
+ const speed = data.speed >= 1024 ?
+ `${(data.speed / 1024).toFixed(2)} MB/s` :
+ `${Math.round(data.speed)} KB/s`;
+
return (
- Downloading: {parseInt(data.progress)}%
- {/* Speed: {parseInt(data.speed)} KB/s
- Time Remaining: {parseInt(data.time_remaining)} */}
+
+
+ Downloading:
+ {parseInt(data.progress)}%
+
+
+ Speed:
+ {speed}
+
+
+ Time left:
+ {timeRemaining}
+
+
+
+ );
+ };
+
+ const buildGroupProcessingStats = (data) => {
+ if (data.progress == 100) {
+ return 'Groups processed!';
+ }
+
+ if (data.progress == 0) {
+ return 'Processing groups...';
+ }
+
+ // Format time displays if available
+ const elapsedTime = data.elapsed_time ?
+ `${Math.floor(data.elapsed_time / 60)}:${String(Math.floor(data.elapsed_time % 60)).padStart(2, '0')}` :
+ null;
+
+ return (
+
+
+
+ Processing groups:
+ {parseInt(data.progress)}%
+
+ {elapsedTime && (
+
+ Elapsed:
+ {elapsedTime}
+
+ )}
+ {data.groups_processed && (
+
+ Groups:
+ {data.groups_processed}
+
+ )}
+
+
+ );
+ };
+
+ const buildErrorStats = (data) => {
+ return (
+
+
+
+ Error:
+
+ {data.error || "Unknown error occurred"}
+
);
};
const buildParsingStats = (data) => {
if (data.progress == 100) {
- // fetchStreams();
- // fetchChannelGroups();
- // fetchEPGData();
- // fetchPlaylists();
return 'Parsing complete!';
}
@@ -83,7 +152,43 @@ const M3UTable = () => {
return 'Parsing...';
}
- return `Parsing: ${data.progress}%`;
+ // Format time displays
+ const timeRemaining = data.time_remaining ?
+ `${Math.floor(data.time_remaining / 60)}:${String(Math.floor(data.time_remaining % 60)).padStart(2, '0')}` :
+ 'calculating...';
+
+ const elapsedTime = data.elapsed_time ?
+ `${Math.floor(data.elapsed_time / 60)}:${String(Math.floor(data.elapsed_time % 60)).padStart(2, '0')}` :
+ '0:00';
+
+ return (
+
+
+
+ Parsing:
+ {parseInt(data.progress)}%
+
+ {data.elapsed_time && (
+
+ Elapsed:
+ {elapsedTime}
+
+ )}
+ {data.time_remaining && (
+
+ Remaining:
+ {timeRemaining}
+
+ )}
+ {data.streams_processed && (
+
+ Streams:
+ {data.streams_processed}
+
+ )}
+
+
+ );
};
const toggleActive = async (playlist) => {
@@ -143,8 +248,23 @@ const M3UTable = () => {
return generateStatusString(refreshProgress[row.id]);
},
- size: 150,
- minSize: 80,
+ size: 180,
+ minSize: 150,
+ Cell: ({ cell }) => {
+ const value = cell.getValue();
+
+ // Return a visual component for the status
+ if (typeof value === 'object') {
+ return value;
+ }
+
+ // For simple string statuses
+ return (
+
+ {value}
+
+ );
+ },
},
{
header: 'Active',