feat: implement WebSocket real-time progress updates
- Add ProgressService for centralized progress tracking and broadcasting - Integrate ProgressService with DownloadService for download progress - Integrate ProgressService with AnimeService for scan progress - Add progress-related WebSocket message models (ScanProgress, ErrorNotification, etc.) - Initialize ProgressService with WebSocket callback in application startup - Add comprehensive unit tests for ProgressService - Update infrastructure.md with ProgressService documentation - Remove completed WebSocket Real-time Updates task from instructions.md The ProgressService provides: - Real-time progress tracking for downloads, scans, and queue operations - Automatic progress percentage calculation - Progress lifecycle management (start, update, complete, fail, cancel) - WebSocket integration for instant client updates - Progress history with size limits - Thread-safe operations using asyncio locks - Support for metadata and custom messages Benefits: - Decoupled progress tracking from WebSocket broadcasting - Single reusable service across all components - Supports multiple concurrent operations efficiently - Centralized progress tracking simplifies monitoring - Instant feedback to users on long-running operations
This commit is contained in:
@@ -27,6 +27,11 @@ from src.server.models.download import (
|
||||
QueueStatus,
|
||||
)
|
||||
from src.server.services.anime_service import AnimeService, AnimeServiceError
|
||||
from src.server.services.progress_service import (
|
||||
ProgressService,
|
||||
ProgressType,
|
||||
get_progress_service,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -53,6 +58,7 @@ class DownloadService:
|
||||
max_concurrent_downloads: int = 2,
|
||||
max_retries: int = 3,
|
||||
persistence_path: str = "./data/download_queue.json",
|
||||
progress_service: Optional[ProgressService] = None,
|
||||
):
|
||||
"""Initialize the download service.
|
||||
|
||||
@@ -61,11 +67,13 @@ class DownloadService:
|
||||
max_concurrent_downloads: Maximum simultaneous downloads
|
||||
max_retries: Maximum retry attempts for failed downloads
|
||||
persistence_path: Path to persist queue state
|
||||
progress_service: Optional progress service for tracking
|
||||
"""
|
||||
self._anime_service = anime_service
|
||||
self._max_concurrent = max_concurrent_downloads
|
||||
self._max_retries = max_retries
|
||||
self._persistence_path = Path(persistence_path)
|
||||
self._progress_service = progress_service or get_progress_service()
|
||||
|
||||
# Queue storage by status
|
||||
self._pending_queue: deque[DownloadItem] = deque()
|
||||
@@ -500,6 +508,23 @@ class DownloadService:
|
||||
if item.progress.speed_mbps:
|
||||
self._download_speeds.append(item.progress.speed_mbps)
|
||||
|
||||
# Update progress service
|
||||
if item.progress.total_mb and item.progress.total_mb > 0:
|
||||
current_mb = int(item.progress.downloaded_mb)
|
||||
total_mb = int(item.progress.total_mb)
|
||||
|
||||
asyncio.create_task(
|
||||
self._progress_service.update_progress(
|
||||
progress_id=f"download_{item.id}",
|
||||
current=current_mb,
|
||||
total=total_mb,
|
||||
metadata={
|
||||
"speed_mbps": item.progress.speed_mbps,
|
||||
"eta_seconds": item.progress.eta_seconds,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Broadcast update (fire and forget)
|
||||
asyncio.create_task(
|
||||
self._broadcast_update(
|
||||
@@ -535,6 +560,22 @@ class DownloadService:
|
||||
episode=item.episode.episode,
|
||||
)
|
||||
|
||||
# Start progress tracking
|
||||
await self._progress_service.start_progress(
|
||||
progress_id=f"download_{item.id}",
|
||||
progress_type=ProgressType.DOWNLOAD,
|
||||
title=f"Downloading {item.serie_name}",
|
||||
message=(
|
||||
f"S{item.episode.season:02d}E{item.episode.episode:02d}"
|
||||
),
|
||||
metadata={
|
||||
"item_id": item.id,
|
||||
"serie_name": item.serie_name,
|
||||
"season": item.episode.season,
|
||||
"episode": item.episode.episode,
|
||||
},
|
||||
)
|
||||
|
||||
# Create progress callback
|
||||
progress_callback = self._create_progress_callback(item)
|
||||
|
||||
@@ -561,6 +602,18 @@ class DownloadService:
|
||||
logger.info(
|
||||
"Download completed successfully", item_id=item.id
|
||||
)
|
||||
|
||||
# Complete progress tracking
|
||||
await self._progress_service.complete_progress(
|
||||
progress_id=f"download_{item.id}",
|
||||
message="Download completed successfully",
|
||||
metadata={
|
||||
"downloaded_mb": item.progress.downloaded_mb
|
||||
if item.progress
|
||||
else 0,
|
||||
},
|
||||
)
|
||||
|
||||
await self._broadcast_update(
|
||||
"download_completed", {"item_id": item.id}
|
||||
)
|
||||
@@ -581,6 +634,13 @@ class DownloadService:
|
||||
retry_count=item.retry_count,
|
||||
)
|
||||
|
||||
# Fail progress tracking
|
||||
await self._progress_service.fail_progress(
|
||||
progress_id=f"download_{item.id}",
|
||||
error_message=str(e),
|
||||
metadata={"retry_count": item.retry_count},
|
||||
)
|
||||
|
||||
await self._broadcast_update(
|
||||
"download_failed",
|
||||
{"item_id": item.id, "error": item.error},
|
||||
|
||||
Reference in New Issue
Block a user