rework of SeriesApp.py

This commit is contained in:
Lukas 2025-11-02 10:20:10 +01:00
parent 2de3317aee
commit 8a49db2a10

View File

@ -8,7 +8,9 @@ progress reporting, and error handling.
import asyncio import asyncio
import logging import logging
from typing import Any, Callable, Dict, List, Optional from typing import Any, Dict, List, Optional
from events import Events
from src.core.entities.SerieList import SerieList from src.core.entities.SerieList import SerieList
from src.core.providers.provider_factory import Loaders from src.core.providers.provider_factory import Loaders
@ -17,6 +19,80 @@ from src.core.SerieScanner import SerieScanner
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DownloadStatusEventArgs:
"""Event arguments for download status events."""
def __init__(
self,
serie_folder: str,
season: int,
episode: int,
status: str,
progress: float = 0.0,
message: Optional[str] = None,
error: Optional[Exception] = None,
eta: Optional[int] = None,
mbper_sec: Optional[float] = None,
):
"""
Initialize download status event arguments.
Args:
serie_folder: Serie folder name
season: Season number
episode: Episode number
status: Status message (e.g., "started", "progress", "completed", "failed")
progress: Download progress (0.0 to 1.0)
message: Optional status message
error: Optional error if status is "failed"
eta: Estimated time remaining in seconds
mbper_sec: Download speed in MB/s
"""
self.serie_folder = serie_folder
self.season = season
self.episode = episode
self.status = status
self.progress = progress
self.message = message
self.error = error
self.eta = eta
self.mbper_sec = mbper_sec
class ScanStatusEventArgs:
"""Event arguments for scan status events."""
def __init__(
self,
current: int,
total: int,
folder: str,
status: str,
progress: float = 0.0,
message: Optional[str] = None,
error: Optional[Exception] = None,
):
"""
Initialize scan status event arguments.
Args:
current: Current item being scanned
total: Total items to scan
folder: Current folder being scanned
status: Status message (e.g., "started", "progress", "completed", "failed", "cancelled")
progress: Scan progress (0.0 to 1.0)
message: Optional status message
error: Optional error if status is "failed"
"""
self.current = current
self.total = total
self.folder = folder
self.status = status
self.progress = progress
self.message = message
self.error = error
class SeriesApp: class SeriesApp:
""" """
Main application class for anime series management. Main application class for anime series management.
@ -28,9 +104,13 @@ class SeriesApp:
- Managing series lists - Managing series lists
Supports async callbacks for progress reporting. Supports async callbacks for progress reporting.
"""
_initialization_count = 0 Events:
download_status: Raised when download status changes.
Handler signature: def handler(args: DownloadStatusEventArgs)
scan_status: Raised when scan status changes.
Handler signature: def handler(args: ScanStatusEventArgs)
"""
def __init__( def __init__(
self, self,
@ -42,44 +122,63 @@ class SeriesApp:
Args: Args:
directory_to_search: Base directory for anime series directory_to_search: Base directory for anime series
""" """
SeriesApp._initialization_count += 1
# Only show initialization message for the first instance
if SeriesApp._initialization_count <= 1:
logger.info("Initializing SeriesApp...")
self.directory_to_search = directory_to_search self.directory_to_search = directory_to_search
# Initialize events
self._events = Events()
self._events.download_status = None
self._events.scan_status = None
self.loaders = Loaders() self.loaders = Loaders()
self.loader = self.loaders.GetLoader(key="aniworld.to") self.loader = self.loaders.GetLoader(key="aniworld.to")
self.serie_scanner = SerieScanner( self.serie_scanner = SerieScanner(directory_to_search, self.loader)
directory_to_search,
self.loader
)
self.list = SerieList(self.directory_to_search) self.list = SerieList(self.directory_to_search)
# Synchronous init used during constructor to avoid awaiting in __init__ # Synchronous init used during constructor to avoid awaiting in __init__
self._init_list_sync() self._init_list_sync()
logger.info( logger.info("SeriesApp initialized for directory: %s", directory_to_search)
"SeriesApp initialized for directory: %s",
directory_to_search @property
) def download_status(self):
"""
Event raised when download status changes.
Subscribe using:
app.download_status += handler
"""
return self._events.download_status
@download_status.setter
def download_status(self, value):
"""Set download_status event handler."""
self._events.download_status = value
@property
def scan_status(self):
"""
Event raised when scan status changes.
Subscribe using:
app.scan_status += handler
"""
return self._events.scan_status
@scan_status.setter
def scan_status(self, value):
"""Set scan_status event handler."""
self._events.scan_status = value
def _init_list_sync(self) -> None: def _init_list_sync(self) -> None:
"""Synchronous initialization helper for constructor.""" """Synchronous initialization helper for constructor."""
self.series_list = self.list.GetMissingEpisode() self.series_list = self.list.GetMissingEpisode()
logger.debug( logger.debug("Loaded %d series with missing episodes", len(self.series_list))
"Loaded %d series with missing episodes",
len(self.series_list)
)
async def _init_list(self) -> None: async def _init_list(self) -> None:
"""Initialize the series list with missing episodes (async).""" """Initialize the series list with missing episodes (async)."""
self.series_list = await asyncio.to_thread(self.list.GetMissingEpisode) self.series_list = await asyncio.to_thread(self.list.GetMissingEpisode)
logger.debug( logger.debug("Loaded %d series with missing episodes", len(self.series_list))
"Loaded %d series with missing episodes",
len(self.series_list)
)
async def search(self, words: str) -> List[Dict[str, Any]]: async def search(self, words: str) -> List[Dict[str, Any]]:
""" """
@ -105,7 +204,7 @@ class SeriesApp:
season: int, season: int,
episode: int, episode: int,
key: str, key: str,
language: str = "German Dub" language: str = "German Dub",
) -> bool: ) -> bool:
""" """
Download an episode (async). Download an episode (async).
@ -120,11 +219,44 @@ class SeriesApp:
Returns: Returns:
True if download succeeded, False otherwise True if download succeeded, False otherwise
""" """
logger.info( logger.info("Starting download: %s S%02dE%02d", serie_folder, season, episode)
"Starting download: %s S%02dE%02d",
serie_folder, # Fire download started event
season, self._events.download_status(
episode DownloadStatusEventArgs(
serie_folder=serie_folder,
season=season,
episode=episode,
status="started",
message="Download started",
)
)
try:
def download_callback(progress_info):
logger.debug(f"wrapped_callback called with: {progress_info}")
downloaded = progress_info.get('downloaded_bytes', 0)
total_bytes = (
progress_info.get('total_bytes')
or progress_info.get('total_bytes_estimate', 0)
)
speed = progress_info.get('speed', 0) # bytes/sec
eta = progress_info.get('eta') # seconds
mbper_sec = speed / (1024 * 1024) if speed else None
self._events.download_status(
DownloadStatusEventArgs(
serie_folder=serie_folder,
season=season,
episode=episode,
status="progress",
message="Download progress",
progress=(downloaded / total_bytes) * 100 if total_bytes else 0,
eta=eta,
mbper_sec=mbper_sec,
)
) )
# Perform download in thread to avoid blocking event loop # Perform download in thread to avoid blocking event loop
download_success = await asyncio.to_thread( download_success = await asyncio.to_thread(
@ -134,22 +266,69 @@ class SeriesApp:
season, season,
episode, episode,
key, key,
language language,
download_callback
) )
if download_success:
logger.info( logger.info(
"Download completed: %s S%02dE%02d", "Download completed: %s S%02dE%02d", serie_folder, season, episode
serie_folder, )
season,
episode # Fire download completed event
self._events.download_status(
DownloadStatusEventArgs(
serie_folder=serie_folder,
season=season,
episode=episode,
status="completed",
progress=1.0,
message="Download completed successfully",
)
)
else:
logger.warning(
"Download failed: %s S%02dE%02d", serie_folder, season, episode
)
# Fire download failed event
self._events.download_status(
DownloadStatusEventArgs(
serie_folder=serie_folder,
season=season,
episode=episode,
status="failed",
message="Download failed",
)
) )
return download_success return download_success
except Exception as e:
logger.error(
"Download error: %s S%02dE%02d - %s",
serie_folder,
season,
episode,
str(e),
exc_info=True,
)
async def re_scan( # Fire download error event
self self._events.download_status(
) -> int: DownloadStatusEventArgs(
serie_folder=serie_folder,
season=season,
episode=episode,
status="failed",
error=e,
message=f"Download error: {str(e)}",
)
)
return False
async def re_scan(self) -> int:
""" """
Rescan directory for missing episodes (async). Rescan directory for missing episodes (async).
@ -158,15 +337,47 @@ class SeriesApp:
""" """
logger.info("Starting directory rescan") logger.info("Starting directory rescan")
try:
# Get total items to scan # Get total items to scan
total_to_scan = await asyncio.to_thread(self.serie_scanner.get_total_to_scan) total_to_scan = await asyncio.to_thread(self.serie_scanner.get_total_to_scan)
logger.info("Total folders to scan: %d", total_to_scan) logger.info("Total folders to scan: %d", total_to_scan)
# Fire scan started event
self._events.scan_status(
ScanStatusEventArgs(
current=0,
total=total_to_scan,
folder="",
status="started",
progress=0.0,
message="Scan started",
)
)
# Reinitialize scanner # Reinitialize scanner
await asyncio.to_thread(self.serie_scanner.reinit) await asyncio.to_thread(self.serie_scanner.reinit)
def scan_callback(folder: str, current: int):
# Calculate progress
if total_to_scan > 0:
progress = current / total_to_scan
else:
progress = 0.0
# Fire scan progress event
self._events.scan_status(
ScanStatusEventArgs(
current=current,
total=total_to_scan,
folder=folder,
status="progress",
progress=progress,
message=f"Scanning: {folder}",
)
)
# Perform scan # Perform scan
await asyncio.to_thread(self.serie_scanner.scan) await asyncio.to_thread(self.serie_scanner.scan, scan_callback)
# Reinitialize list # Reinitialize list
self.list = SerieList(self.directory_to_search) self.list = SerieList(self.directory_to_search)
@ -174,8 +385,51 @@ class SeriesApp:
logger.info("Directory rescan completed successfully") logger.info("Directory rescan completed successfully")
# Fire scan completed event
self._events.scan_status(
ScanStatusEventArgs(
current=total_to_scan,
total=total_to_scan,
folder="",
status="completed",
progress=1.0,
message=f"Scan completed. Found {len(self.series_list)} series with missing episodes.",
)
)
return len(self.series_list) return len(self.series_list)
except InterruptedError:
logger.warning("Scan cancelled by user")
# Fire scan cancelled event
self._events.scan_status(
ScanStatusEventArgs(
current=0,
total=total_to_scan if 'total_to_scan' in locals() else 0,
folder="",
status="cancelled",
message="Scan cancelled by user",
)
)
raise
except Exception as e:
logger.error("Scan error: %s", str(e), exc_info=True)
# Fire scan failed event
self._events.scan_status(
ScanStatusEventArgs(
current=0,
total=total_to_scan if 'total_to_scan' in locals() else 0,
folder="",
status="failed",
error=e,
message=f"Scan error: {str(e)}",
)
)
raise
async def get_series_list(self) -> List[Any]: async def get_series_list(self) -> List[Any]:
""" """
Get the current series list (async). Get the current series list (async).