feat: Implement comprehensive progress callback system
- Created callback interfaces (ProgressCallback, ErrorCallback, CompletionCallback) - Defined rich context objects (ProgressContext, ErrorContext, CompletionContext) - Implemented CallbackManager for managing multiple callbacks - Integrated callbacks into SerieScanner for scan progress reporting - Enhanced SeriesApp with download progress tracking via callbacks - Added error and completion notifications throughout core operations - Maintained backward compatibility with legacy callback system - Created 22 comprehensive unit tests with 100% pass rate - Updated infrastructure.md with callback system documentation - Removed completed tasks from instructions.md The callback system provides: - Real-time progress updates with percentage and phase tracking - Comprehensive error reporting with recovery information - Operation completion notifications with statistics - Thread-safe callback execution with exception handling - Support for multiple simultaneous callbacks per type
This commit is contained in:
@@ -8,11 +8,20 @@ progress reporting, error handling, and operation cancellation.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from src.core.entities.SerieList import SerieList
|
||||
from src.core.interfaces.callbacks import (
|
||||
CallbackManager,
|
||||
CompletionContext,
|
||||
ErrorContext,
|
||||
OperationType,
|
||||
ProgressContext,
|
||||
ProgressPhase,
|
||||
)
|
||||
from src.core.providers.provider_factory import Loaders
|
||||
from src.core.SerieScanner import SerieScanner
|
||||
|
||||
@@ -66,15 +75,17 @@ class SeriesApp:
|
||||
self,
|
||||
directory_to_search: str,
|
||||
progress_callback: Optional[Callable[[ProgressInfo], None]] = None,
|
||||
error_callback: Optional[Callable[[Exception], None]] = None
|
||||
error_callback: Optional[Callable[[Exception], None]] = None,
|
||||
callback_manager: Optional[CallbackManager] = None
|
||||
):
|
||||
"""
|
||||
Initialize SeriesApp.
|
||||
|
||||
Args:
|
||||
directory_to_search: Base directory for anime series
|
||||
progress_callback: Optional callback for progress updates
|
||||
error_callback: Optional callback for error notifications
|
||||
progress_callback: Optional legacy callback for progress updates
|
||||
error_callback: Optional legacy callback for error notifications
|
||||
callback_manager: Optional callback manager for new callback system
|
||||
"""
|
||||
SeriesApp._initialization_count += 1
|
||||
|
||||
@@ -86,9 +97,13 @@ class SeriesApp:
|
||||
self.progress_callback = progress_callback
|
||||
self.error_callback = error_callback
|
||||
|
||||
# Initialize new callback system
|
||||
self._callback_manager = callback_manager or CallbackManager()
|
||||
|
||||
# Cancellation support
|
||||
self._cancel_flag = False
|
||||
self._current_operation: Optional[str] = None
|
||||
self._current_operation_id: Optional[str] = None
|
||||
self._operation_status = OperationStatus.IDLE
|
||||
|
||||
# Initialize components
|
||||
@@ -96,7 +111,9 @@ class SeriesApp:
|
||||
self.Loaders = Loaders()
|
||||
self.loader = self.Loaders.GetLoader(key="aniworld.to")
|
||||
self.SerieScanner = SerieScanner(
|
||||
directory_to_search, self.loader
|
||||
directory_to_search,
|
||||
self.loader,
|
||||
self._callback_manager
|
||||
)
|
||||
self.List = SerieList(self.directory_to_search)
|
||||
self.__InitList__()
|
||||
@@ -110,6 +127,11 @@ class SeriesApp:
|
||||
self._handle_error(e)
|
||||
raise
|
||||
|
||||
@property
|
||||
def callback_manager(self) -> CallbackManager:
|
||||
"""Get the callback manager instance."""
|
||||
return self._callback_manager
|
||||
|
||||
def __InitList__(self):
|
||||
"""Initialize the series list with missing episodes."""
|
||||
try:
|
||||
@@ -163,13 +185,14 @@ class SeriesApp:
|
||||
season: Season number
|
||||
episode: Episode number
|
||||
key: Serie key
|
||||
callback: Optional progress callback
|
||||
callback: Optional legacy progress callback
|
||||
language: Language preference
|
||||
|
||||
Returns:
|
||||
OperationResult with download status
|
||||
"""
|
||||
self._current_operation = f"download_S{season:02d}E{episode:02d}"
|
||||
self._current_operation_id = str(uuid.uuid4())
|
||||
self._operation_status = OperationStatus.RUNNING
|
||||
self._cancel_flag = False
|
||||
|
||||
@@ -179,19 +202,81 @@ class SeriesApp:
|
||||
serieFolder, season, episode
|
||||
)
|
||||
|
||||
# Notify download starting
|
||||
start_msg = (
|
||||
f"Starting download: {serieFolder} "
|
||||
f"S{season:02d}E{episode:02d}"
|
||||
)
|
||||
self._callback_manager.notify_progress(
|
||||
ProgressContext(
|
||||
operation_type=OperationType.DOWNLOAD,
|
||||
operation_id=self._current_operation_id,
|
||||
phase=ProgressPhase.STARTING,
|
||||
current=0,
|
||||
total=100,
|
||||
percentage=0.0,
|
||||
message=start_msg,
|
||||
metadata={
|
||||
"series": serieFolder,
|
||||
"season": season,
|
||||
"episode": episode,
|
||||
"key": key,
|
||||
"language": language
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Check for cancellation before starting
|
||||
if self._is_cancelled():
|
||||
self._callback_manager.notify_completion(
|
||||
CompletionContext(
|
||||
operation_type=OperationType.DOWNLOAD,
|
||||
operation_id=self._current_operation_id,
|
||||
success=False,
|
||||
message="Download cancelled before starting"
|
||||
)
|
||||
)
|
||||
return OperationResult(
|
||||
success=False,
|
||||
message="Download cancelled before starting"
|
||||
)
|
||||
|
||||
# Wrap callback to check for cancellation
|
||||
# Wrap callback to check for cancellation and report progress
|
||||
def wrapped_callback(progress: float):
|
||||
if self._is_cancelled():
|
||||
raise InterruptedError("Download cancelled by user")
|
||||
|
||||
# Notify progress via new callback system
|
||||
self._callback_manager.notify_progress(
|
||||
ProgressContext(
|
||||
operation_type=OperationType.DOWNLOAD,
|
||||
operation_id=self._current_operation_id,
|
||||
phase=ProgressPhase.IN_PROGRESS,
|
||||
current=int(progress),
|
||||
total=100,
|
||||
percentage=progress,
|
||||
message=f"Downloading: {progress:.1f}%",
|
||||
metadata={
|
||||
"series": serieFolder,
|
||||
"season": season,
|
||||
"episode": episode
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Call legacy callback if provided
|
||||
if callback:
|
||||
callback(progress)
|
||||
|
||||
# Call legacy progress_callback if provided
|
||||
if self.progress_callback:
|
||||
self.progress_callback(ProgressInfo(
|
||||
current=int(progress),
|
||||
total=100,
|
||||
message=f"Downloading S{season:02d}E{episode:02d}",
|
||||
percentage=progress,
|
||||
status=OperationStatus.RUNNING
|
||||
))
|
||||
|
||||
# Perform download
|
||||
self.loader.Download(
|
||||
@@ -210,7 +295,22 @@ class SeriesApp:
|
||||
serieFolder, season, episode
|
||||
)
|
||||
|
||||
# Notify completion
|
||||
msg = f"Successfully downloaded S{season:02d}E{episode:02d}"
|
||||
self._callback_manager.notify_completion(
|
||||
CompletionContext(
|
||||
operation_type=OperationType.DOWNLOAD,
|
||||
operation_id=self._current_operation_id,
|
||||
success=True,
|
||||
message=msg,
|
||||
statistics={
|
||||
"series": serieFolder,
|
||||
"season": season,
|
||||
"episode": episode
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return OperationResult(
|
||||
success=True,
|
||||
message=msg
|
||||
@@ -219,6 +319,17 @@ class SeriesApp:
|
||||
except InterruptedError as e:
|
||||
self._operation_status = OperationStatus.CANCELLED
|
||||
logger.warning("Download cancelled: %s", e)
|
||||
|
||||
# Notify cancellation
|
||||
self._callback_manager.notify_completion(
|
||||
CompletionContext(
|
||||
operation_type=OperationType.DOWNLOAD,
|
||||
operation_id=self._current_operation_id,
|
||||
success=False,
|
||||
message="Download cancelled"
|
||||
)
|
||||
)
|
||||
|
||||
return OperationResult(
|
||||
success=False,
|
||||
message="Download cancelled",
|
||||
@@ -227,14 +338,43 @@ class SeriesApp:
|
||||
except (IOError, OSError, RuntimeError) as e:
|
||||
self._operation_status = OperationStatus.FAILED
|
||||
logger.error("Download failed: %s", e)
|
||||
|
||||
# Notify error
|
||||
error_msg = f"Download failed: {str(e)}"
|
||||
self._callback_manager.notify_error(
|
||||
ErrorContext(
|
||||
operation_type=OperationType.DOWNLOAD,
|
||||
operation_id=self._current_operation_id,
|
||||
error=e,
|
||||
message=error_msg,
|
||||
recoverable=False,
|
||||
metadata={
|
||||
"series": serieFolder,
|
||||
"season": season,
|
||||
"episode": episode
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Notify completion with failure
|
||||
self._callback_manager.notify_completion(
|
||||
CompletionContext(
|
||||
operation_type=OperationType.DOWNLOAD,
|
||||
operation_id=self._current_operation_id,
|
||||
success=False,
|
||||
message=error_msg
|
||||
)
|
||||
)
|
||||
|
||||
self._handle_error(e)
|
||||
return OperationResult(
|
||||
success=False,
|
||||
message=f"Download failed: {str(e)}",
|
||||
message=error_msg,
|
||||
error=e
|
||||
)
|
||||
finally:
|
||||
self._current_operation = None
|
||||
self._current_operation_id = None
|
||||
|
||||
def ReScan(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user