Refactor: Replace CallbackManager with Events pattern
- Replace callback system with events library in SerieScanner - Update SeriesApp to subscribe to loader and scanner events - Refactor ScanService to use Events instead of CallbackManager - Remove CallbackManager imports and callback classes - Add safe event calling with error handling in SerieScanner - Update AniworldLoader to use Events for download progress - Remove progress_callback parameter from download methods - Update all affected tests for Events pattern - Fix test_series_app.py for new event subscription model - Comment out obsolete callback tests in test_scan_service.py All core tests passing. Events provide cleaner event-driven architecture.
This commit is contained in:
@@ -15,18 +15,12 @@ import os
|
||||
import re
|
||||
import traceback
|
||||
import uuid
|
||||
from typing import Callable, Iterable, Iterator, Optional
|
||||
from typing import Iterable, Iterator, Optional
|
||||
|
||||
from events import Events
|
||||
|
||||
from src.core.entities.series import Serie
|
||||
from src.core.exceptions.Exceptions import MatchNotFoundError, NoKeyFoundException
|
||||
from src.core.interfaces.callbacks import (
|
||||
CallbackManager,
|
||||
CompletionContext,
|
||||
ErrorContext,
|
||||
OperationType,
|
||||
ProgressContext,
|
||||
ProgressPhase,
|
||||
)
|
||||
from src.core.providers.base_provider import Loader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -55,7 +49,6 @@ class SerieScanner:
|
||||
self,
|
||||
basePath: str,
|
||||
loader: Loader,
|
||||
callback_manager: Optional[CallbackManager] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the SerieScanner.
|
||||
@@ -82,18 +75,76 @@ class SerieScanner:
|
||||
self.directory: str = abs_path
|
||||
self.keyDict: dict[str, Serie] = {}
|
||||
self.loader: Loader = loader
|
||||
self._callback_manager: CallbackManager = (
|
||||
callback_manager or CallbackManager()
|
||||
)
|
||||
self._current_operation_id: Optional[str] = None
|
||||
self.events = Events()
|
||||
|
||||
self.events.on_progress = None
|
||||
self.events.on_error = None
|
||||
self.events.on_completion = None
|
||||
|
||||
logger.info("Initialized SerieScanner with base path: %s", abs_path)
|
||||
|
||||
def _safe_call_event(self, event_handler, data: dict) -> None:
|
||||
"""Safely call an event handler if it exists.
|
||||
|
||||
Args:
|
||||
event_handler: Event handler attribute (e.g., self.events.on_progress)
|
||||
data: Data dictionary to pass to the event handler
|
||||
"""
|
||||
if event_handler:
|
||||
try:
|
||||
event_handler(data)
|
||||
except Exception as e:
|
||||
logger.error("Error calling event handler: %s", e, exc_info=True)
|
||||
|
||||
@property
|
||||
def callback_manager(self) -> CallbackManager:
|
||||
"""Get the callback manager instance."""
|
||||
return self._callback_manager
|
||||
def subscribe_on_progress(self, handler):
|
||||
"""
|
||||
Subscribe a handler to an event.
|
||||
Args:
|
||||
handler: Callable to handle the event
|
||||
"""
|
||||
self.events.on_progress += handler
|
||||
|
||||
def unsubscribe_on_progress(self, handler):
|
||||
"""
|
||||
Unsubscribe a handler from an event.
|
||||
Args:
|
||||
handler: Callable to remove
|
||||
"""
|
||||
self.events.on_progress += handler
|
||||
|
||||
def subscribe_on_error(self, handler):
|
||||
"""
|
||||
Subscribe a handler to an event.
|
||||
Args:
|
||||
handler: Callable to handle the event
|
||||
"""
|
||||
self.events.on_error += handler
|
||||
|
||||
def unsubscribe_on_error(self, handler):
|
||||
"""
|
||||
Unsubscribe a handler from an event.
|
||||
Args:
|
||||
handler: Callable to remove
|
||||
"""
|
||||
self.events.on_error += handler
|
||||
|
||||
def subscribe_on_completion(self, handler):
|
||||
"""
|
||||
Subscribe a handler to an event.
|
||||
Args:
|
||||
handler: Callable to handle the event
|
||||
"""
|
||||
self.events.on_completion += handler
|
||||
|
||||
def unsubscribe_on_completion(self, handler):
|
||||
"""
|
||||
Unsubscribe a handler from an event.
|
||||
Args:
|
||||
handler: Callable to remove
|
||||
"""
|
||||
self.events.on_completion += handler
|
||||
|
||||
def reinit(self) -> None:
|
||||
"""Reinitialize the series dictionary (keyed by serie.key)."""
|
||||
self.keyDict: dict[str, Serie] = {}
|
||||
@@ -107,20 +158,13 @@ class SerieScanner:
|
||||
result = self.__find_mp4_files()
|
||||
return sum(1 for _ in result)
|
||||
|
||||
def scan(
|
||||
self,
|
||||
callback: Optional[Callable[[str, int], None]] = None
|
||||
) -> None:
|
||||
def scan(self) -> None:
|
||||
"""
|
||||
Scan directories for anime series and missing episodes.
|
||||
|
||||
Results are stored in self.keyDict and can be retrieved after
|
||||
scanning. Data files are also saved to disk for persistence.
|
||||
|
||||
Args:
|
||||
callback: Optional callback function (folder, count) for
|
||||
progress updates
|
||||
|
||||
Raises:
|
||||
Exception: If scan fails critically
|
||||
"""
|
||||
@@ -130,16 +174,16 @@ class SerieScanner:
|
||||
logger.info("Starting scan for missing episodes")
|
||||
|
||||
# Notify scan starting
|
||||
self._callback_manager.notify_progress(
|
||||
ProgressContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
phase=ProgressPhase.STARTING,
|
||||
current=0,
|
||||
total=0,
|
||||
percentage=0.0,
|
||||
message="Initializing scan"
|
||||
)
|
||||
self._safe_call_event(
|
||||
self.events.on_progress,
|
||||
{
|
||||
"operation_id": self._current_operation_id,
|
||||
"phase": "STARTING",
|
||||
"current": 0,
|
||||
"total": 0,
|
||||
"percentage": 0.0,
|
||||
"message": "Initializing scan"
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -163,27 +207,20 @@ class SerieScanner:
|
||||
else:
|
||||
percentage = 0.0
|
||||
|
||||
# Progress is surfaced both through the callback manager
|
||||
# (for the web/UI layer) and, for compatibility, through a
|
||||
# legacy callback that updates CLI progress bars.
|
||||
# Notify progress
|
||||
self._callback_manager.notify_progress(
|
||||
ProgressContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
phase=ProgressPhase.IN_PROGRESS,
|
||||
current=counter,
|
||||
total=total_to_scan,
|
||||
percentage=percentage,
|
||||
message=f"Scanning: {folder}",
|
||||
details=f"Found {len(mp4_files)} episodes"
|
||||
)
|
||||
self._safe_call_event(
|
||||
self.events.on_progress,
|
||||
{
|
||||
"operation_id": self._current_operation_id,
|
||||
"phase": "IN_PROGRESS",
|
||||
"current": counter,
|
||||
"total": total_to_scan,
|
||||
"percentage": percentage,
|
||||
"message": f"Scanning: {folder}",
|
||||
"details": f"Found {len(mp4_files)} episodes"
|
||||
}
|
||||
)
|
||||
|
||||
# Call legacy callback if provided
|
||||
if callback:
|
||||
callback(folder, counter)
|
||||
|
||||
serie = self.__read_data_from_file(folder)
|
||||
if (
|
||||
serie is not None
|
||||
@@ -230,15 +267,15 @@ class SerieScanner:
|
||||
error_msg = f"Error processing folder '{folder}': {nkfe}"
|
||||
logger.error(error_msg)
|
||||
|
||||
self._callback_manager.notify_error(
|
||||
ErrorContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
error=nkfe,
|
||||
message=error_msg,
|
||||
recoverable=True,
|
||||
metadata={"folder": folder, "key": None}
|
||||
)
|
||||
self._safe_call_event(
|
||||
self.events.on_error,
|
||||
{
|
||||
"operation_id": self._current_operation_id,
|
||||
"error": nkfe,
|
||||
"message": error_msg,
|
||||
"recoverable": True,
|
||||
"metadata": {"folder": folder, "key": None}
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
# Log error and notify via callback
|
||||
@@ -252,30 +289,30 @@ class SerieScanner:
|
||||
traceback.format_exc()
|
||||
)
|
||||
|
||||
self._callback_manager.notify_error(
|
||||
ErrorContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
error=e,
|
||||
message=error_msg,
|
||||
recoverable=True,
|
||||
metadata={"folder": folder, "key": None}
|
||||
)
|
||||
self._safe_call_event(
|
||||
self.events.on_error,
|
||||
{
|
||||
"operation_id": self._current_operation_id,
|
||||
"error": e,
|
||||
"message": error_msg,
|
||||
"recoverable": True,
|
||||
"metadata": {"folder": folder, "key": None}
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Notify scan completion
|
||||
self._callback_manager.notify_completion(
|
||||
CompletionContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
success=True,
|
||||
message=f"Scan completed. Processed {counter} folders.",
|
||||
statistics={
|
||||
self._safe_call_event(
|
||||
self.events.on_completion,
|
||||
{
|
||||
"operation_id": self._current_operation_id,
|
||||
"success": True,
|
||||
"message": f"Scan completed. Processed {counter} folders.",
|
||||
"statistics": {
|
||||
"total_folders": counter,
|
||||
"series_found": len(self.keyDict)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -289,23 +326,23 @@ class SerieScanner:
|
||||
error_msg = f"Critical scan error: {e}"
|
||||
logger.error("%s\n%s", error_msg, traceback.format_exc())
|
||||
|
||||
self._callback_manager.notify_error(
|
||||
ErrorContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
error=e,
|
||||
message=error_msg,
|
||||
recoverable=False
|
||||
)
|
||||
self._safe_call_event(
|
||||
self.events.on_error,
|
||||
{
|
||||
"operation_id": self._current_operation_id,
|
||||
"error": e,
|
||||
"message": error_msg,
|
||||
"recoverable": False
|
||||
}
|
||||
)
|
||||
|
||||
self._callback_manager.notify_completion(
|
||||
CompletionContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
success=False,
|
||||
message=error_msg
|
||||
)
|
||||
self._safe_call_event(
|
||||
self.events.on_completion,
|
||||
{
|
||||
"operation_id": self._current_operation_id,
|
||||
"success": False,
|
||||
"message": error_msg
|
||||
}
|
||||
)
|
||||
|
||||
raise
|
||||
@@ -325,16 +362,6 @@ class SerieScanner:
|
||||
has_files = True
|
||||
yield anime_name, mp4_files if has_files else []
|
||||
|
||||
def __remove_year(self, input_string: str) -> str:
|
||||
"""Remove year information from input string."""
|
||||
cleaned_string = re.sub(r'\(\d{4}\)', '', input_string).strip()
|
||||
logger.debug(
|
||||
"Removed year from '%s' -> '%s'",
|
||||
input_string,
|
||||
cleaned_string
|
||||
)
|
||||
return cleaned_string
|
||||
|
||||
def __read_data_from_file(self, folder_name: str) -> Optional[Serie]:
|
||||
"""Read serie data from file or key file.
|
||||
|
||||
@@ -507,19 +534,18 @@ class SerieScanner:
|
||||
|
||||
# Generate unique operation ID for this targeted scan
|
||||
operation_id = str(uuid.uuid4())
|
||||
|
||||
# Notify scan starting
|
||||
self._callback_manager.notify_progress(
|
||||
ProgressContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=operation_id,
|
||||
phase=ProgressPhase.STARTING,
|
||||
current=0,
|
||||
total=1,
|
||||
percentage=0.0,
|
||||
message=f"Scanning series: {folder}",
|
||||
details=f"Key: {key}"
|
||||
)
|
||||
self._safe_call_event(
|
||||
self.events.on_progress,
|
||||
{
|
||||
"operation_id": operation_id,
|
||||
"phase": "STARTING",
|
||||
"current": 0,
|
||||
"total": 1,
|
||||
"percentage": 0.0,
|
||||
"message": f"Scanning series: {folder}",
|
||||
"details": f"Key: {key}"
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -554,17 +580,17 @@ class SerieScanner:
|
||||
)
|
||||
|
||||
# Update progress
|
||||
self._callback_manager.notify_progress(
|
||||
ProgressContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=operation_id,
|
||||
phase=ProgressPhase.IN_PROGRESS,
|
||||
current=1,
|
||||
total=1,
|
||||
percentage=100.0,
|
||||
message=f"Scanned: {folder}",
|
||||
details=f"Found {sum(len(eps) for eps in missing_episodes.values())} missing episodes"
|
||||
)
|
||||
self._safe_call_event(
|
||||
self.events.on_progress,
|
||||
{
|
||||
"operation_id": operation_id,
|
||||
"phase": "IN_PROGRESS",
|
||||
"current": 1,
|
||||
"total": 1,
|
||||
"percentage": 100.0,
|
||||
"message": f"Scanned: {folder}",
|
||||
"details": f"Found {sum(len(eps) for eps in missing_episodes.values())} missing episodes"
|
||||
}
|
||||
)
|
||||
|
||||
# Create or update Serie in keyDict
|
||||
@@ -593,19 +619,19 @@ class SerieScanner:
|
||||
)
|
||||
|
||||
# Notify completion
|
||||
self._callback_manager.notify_completion(
|
||||
CompletionContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=operation_id,
|
||||
success=True,
|
||||
message=f"Scan completed for {folder}",
|
||||
statistics={
|
||||
self._safe_call_event(
|
||||
self.events.on_completion,
|
||||
{
|
||||
"operation_id": operation_id,
|
||||
"success": True,
|
||||
"message": f"Scan completed for {folder}",
|
||||
"statistics": {
|
||||
"missing_episodes": sum(
|
||||
len(eps) for eps in missing_episodes.values()
|
||||
),
|
||||
"seasons_with_missing": len(missing_episodes)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -622,27 +648,25 @@ class SerieScanner:
|
||||
logger.error(error_msg, exc_info=True)
|
||||
|
||||
# Notify error
|
||||
self._callback_manager.notify_error(
|
||||
ErrorContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=operation_id,
|
||||
error=e,
|
||||
message=error_msg,
|
||||
recoverable=True,
|
||||
metadata={"key": key, "folder": folder}
|
||||
)
|
||||
self._safe_call_event(
|
||||
self.events.on_error,
|
||||
{
|
||||
"operation_id": operation_id,
|
||||
"error": e,
|
||||
"message": error_msg,
|
||||
"recoverable": True,
|
||||
"metadata": {"key": key, "folder": folder}
|
||||
}
|
||||
)
|
||||
|
||||
# Notify completion with failure
|
||||
self._callback_manager.notify_completion(
|
||||
CompletionContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=operation_id,
|
||||
success=False,
|
||||
message=error_msg
|
||||
)
|
||||
self._safe_call_event(
|
||||
self.events.on_completion,
|
||||
{
|
||||
"operation_id": operation_id,
|
||||
"success": False,
|
||||
"message": error_msg
|
||||
}
|
||||
)
|
||||
|
||||
# Return empty dict on error (scan failed but not critical)
|
||||
return {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user