refactored callback
This commit is contained in:
parent
8a49db2a10
commit
e414a1a358
@ -17,7 +17,7 @@
|
|||||||
"keep_days": 30
|
"keep_days": 30
|
||||||
},
|
},
|
||||||
"other": {
|
"other": {
|
||||||
"master_password_hash": "$pbkdf2-sha256$29000$MkbonbMWolTKOUfIOcc4Jw$8Aza9RknTXDSwQ1/mc.EwerqRrZ4Yo6tQlust.Nm/kQ",
|
"master_password_hash": "$pbkdf2-sha256$29000$GiPkvJeS8j4HwBjDmNOaMw$8k4ShYlk51ZsxoiQBZGjXCsvl0xbbiXIFYI/EWlqVrI",
|
||||||
"anime_directory": "/home/lukas/Volume/serien/"
|
"anime_directory": "/home/lukas/Volume/serien/"
|
||||||
},
|
},
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
|
|||||||
@ -129,3 +129,6 @@ For each task completed:
|
|||||||
- WebSocket infrastructure remains unchanged
|
- WebSocket infrastructure remains unchanged
|
||||||
|
|
||||||
# Tasks
|
# Tasks
|
||||||
|
|
||||||
|
[] check method from SeriesApp are used in a correct way. SeriesApp method changed. make sure that classes that use SeriesApp take the latest interface.
|
||||||
|
[] SeriesApp no have events make sure services and api use them
|
||||||
|
|||||||
@ -197,7 +197,7 @@ class SeriesApp:
|
|||||||
results = await asyncio.to_thread(self.loader.search, words)
|
results = await asyncio.to_thread(self.loader.search, words)
|
||||||
logger.info("Found %d results", len(results))
|
logger.info("Found %d results", len(results))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
async def download(
|
async def download(
|
||||||
self,
|
self,
|
||||||
serie_folder: str,
|
serie_folder: str,
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Callable, List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
@ -22,9 +21,10 @@ class AnimeServiceError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class AnimeService:
|
class AnimeService:
|
||||||
"""Wraps the blocking SeriesApp for use in the FastAPI web layer.
|
"""Wraps SeriesApp for use in the FastAPI web layer.
|
||||||
|
|
||||||
- Runs blocking operations in a threadpool
|
- SeriesApp methods are now async, no need for threadpool
|
||||||
|
- Subscribes to SeriesApp events for progress tracking
|
||||||
- Exposes async methods
|
- Exposes async methods
|
||||||
- Adds simple in-memory caching for read operations
|
- Adds simple in-memory caching for read operations
|
||||||
"""
|
"""
|
||||||
@ -32,152 +32,208 @@ class AnimeService:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
directory: str,
|
directory: str,
|
||||||
max_workers: int = 4,
|
|
||||||
progress_service: Optional[ProgressService] = None,
|
progress_service: Optional[ProgressService] = None,
|
||||||
):
|
):
|
||||||
self._directory = directory
|
self._directory = directory
|
||||||
self._executor = ThreadPoolExecutor(max_workers=max_workers)
|
|
||||||
self._progress_service = progress_service or get_progress_service()
|
self._progress_service = progress_service or get_progress_service()
|
||||||
# SeriesApp is blocking; instantiate per-service
|
# Initialize SeriesApp with async methods
|
||||||
try:
|
try:
|
||||||
self._app = SeriesApp(directory)
|
self._app = SeriesApp(directory)
|
||||||
|
# Subscribe to SeriesApp events
|
||||||
|
self._app.download_status += self._on_download_status
|
||||||
|
self._app.scan_status += self._on_scan_status
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Failed to initialize SeriesApp")
|
logger.exception("Failed to initialize SeriesApp")
|
||||||
raise AnimeServiceError("Initialization failed") from e
|
raise AnimeServiceError("Initialization failed") from e
|
||||||
|
|
||||||
async def _run_in_executor(self, func, *args, **kwargs):
|
def _on_download_status(self, args) -> None:
|
||||||
loop = asyncio.get_event_loop()
|
"""Handle download status events from SeriesApp.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: DownloadStatusEventArgs from SeriesApp
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return await loop.run_in_executor(self._executor, lambda: func(*args, **kwargs))
|
# Map SeriesApp download events to progress service
|
||||||
except Exception as e:
|
if args.status == "started":
|
||||||
logger.exception("Executor task failed")
|
asyncio.create_task(
|
||||||
raise AnimeServiceError(str(e)) from e
|
self._progress_service.start_progress(
|
||||||
|
progress_id=f"download_{args.serie_folder}_{args.season}_{args.episode}", # noqa: E501
|
||||||
|
progress_type=ProgressType.DOWNLOAD,
|
||||||
|
title=f"Downloading {args.serie_folder}",
|
||||||
|
message=f"S{args.season:02d}E{args.episode:02d}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif args.status == "progress":
|
||||||
|
asyncio.create_task(
|
||||||
|
self._progress_service.update_progress(
|
||||||
|
progress_id=f"download_{args.serie_folder}_{args.season}_{args.episode}", # noqa: E501
|
||||||
|
current=int(args.progress),
|
||||||
|
total=100,
|
||||||
|
message=args.message or "Downloading...",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif args.status == "completed":
|
||||||
|
asyncio.create_task(
|
||||||
|
self._progress_service.complete_progress(
|
||||||
|
progress_id=f"download_{args.serie_folder}_{args.season}_{args.episode}", # noqa: E501
|
||||||
|
message="Download completed",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif args.status == "failed":
|
||||||
|
asyncio.create_task(
|
||||||
|
self._progress_service.fail_progress(
|
||||||
|
progress_id=f"download_{args.serie_folder}_{args.season}_{args.episode}", # noqa: E501
|
||||||
|
error_message=args.message or str(args.error),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(
|
||||||
|
"Error handling download status event",
|
||||||
|
error=str(exc)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_scan_status(self, args) -> None:
|
||||||
|
"""Handle scan status events from SeriesApp.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: ScanStatusEventArgs from SeriesApp
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
scan_id = "library_scan"
|
||||||
|
|
||||||
|
# Map SeriesApp scan events to progress service
|
||||||
|
if args.status == "started":
|
||||||
|
asyncio.create_task(
|
||||||
|
self._progress_service.start_progress(
|
||||||
|
progress_id=scan_id,
|
||||||
|
progress_type=ProgressType.SCAN,
|
||||||
|
title="Scanning anime library",
|
||||||
|
message=args.message or "Initializing scan...",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif args.status == "progress":
|
||||||
|
asyncio.create_task(
|
||||||
|
self._progress_service.update_progress(
|
||||||
|
progress_id=scan_id,
|
||||||
|
current=args.current,
|
||||||
|
total=args.total,
|
||||||
|
message=args.message or f"Scanning: {args.folder}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif args.status == "completed":
|
||||||
|
asyncio.create_task(
|
||||||
|
self._progress_service.complete_progress(
|
||||||
|
progress_id=scan_id,
|
||||||
|
message=args.message or "Scan completed",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif args.status == "failed":
|
||||||
|
asyncio.create_task(
|
||||||
|
self._progress_service.fail_progress(
|
||||||
|
progress_id=scan_id,
|
||||||
|
error_message=args.message or str(args.error),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif args.status == "cancelled":
|
||||||
|
asyncio.create_task(
|
||||||
|
self._progress_service.fail_progress(
|
||||||
|
progress_id=scan_id,
|
||||||
|
error_message=args.message or "Scan cancelled",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Error handling scan status event", error=str(exc))
|
||||||
|
|
||||||
@lru_cache(maxsize=128)
|
@lru_cache(maxsize=128)
|
||||||
def _cached_list_missing(self) -> List[dict]:
|
def _cached_list_missing(self) -> List[dict]:
|
||||||
# Synchronous cached call used by async wrapper
|
# Synchronous cached call - SeriesApp.series_list is populated
|
||||||
|
# during initialization
|
||||||
try:
|
try:
|
||||||
series = self._app.series_list
|
series = self._app.series_list
|
||||||
# normalize to simple dicts
|
# normalize to simple dicts
|
||||||
return [s.to_dict() if hasattr(s, "to_dict") else s for s in series]
|
return [
|
||||||
except Exception as e:
|
s.to_dict() if hasattr(s, "to_dict") else s
|
||||||
|
for s in series
|
||||||
|
]
|
||||||
|
except Exception:
|
||||||
logger.exception("Failed to get missing episodes list")
|
logger.exception("Failed to get missing episodes list")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def list_missing(self) -> List[dict]:
|
async def list_missing(self) -> List[dict]:
|
||||||
"""Return list of series with missing episodes."""
|
"""Return list of series with missing episodes."""
|
||||||
try:
|
try:
|
||||||
return await self._run_in_executor(self._cached_list_missing)
|
# series_list is already populated, just access it
|
||||||
|
return self._cached_list_missing()
|
||||||
except AnimeServiceError:
|
except AnimeServiceError:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
logger.exception("list_missing failed")
|
logger.exception("list_missing failed")
|
||||||
raise AnimeServiceError("Failed to list missing series") from e
|
raise AnimeServiceError("Failed to list missing series") from exc
|
||||||
|
|
||||||
async def search(self, query: str) -> List[dict]:
|
async def search(self, query: str) -> List[dict]:
|
||||||
"""Search for series using underlying loader.Search."""
|
"""Search for series using underlying loader.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Search query string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of search results as dictionaries
|
||||||
|
"""
|
||||||
if not query:
|
if not query:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
result = await self._run_in_executor(self._app.search, query)
|
# SeriesApp.search is now async
|
||||||
# result may already be list of dicts or objects
|
result = await self._app.search(query)
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
logger.exception("search failed")
|
logger.exception("search failed")
|
||||||
raise AnimeServiceError("Search failed") from e
|
raise AnimeServiceError("Search failed") from exc
|
||||||
|
|
||||||
async def rescan(self, callback: Optional[Callable] = None) -> None:
|
async def rescan(self) -> None:
|
||||||
"""Trigger a re-scan. Accepts an optional callback function.
|
"""Trigger a re-scan.
|
||||||
|
|
||||||
The callback is executed in the threadpool by SeriesApp.
|
The SeriesApp now handles progress tracking via events which are
|
||||||
Progress updates are tracked and broadcasted via ProgressService.
|
forwarded to the ProgressService through event handlers.
|
||||||
"""
|
"""
|
||||||
scan_id = "library_scan"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Start progress tracking
|
# SeriesApp.re_scan is now async and handles events internally
|
||||||
await self._progress_service.start_progress(
|
await self._app.re_scan()
|
||||||
progress_id=scan_id,
|
|
||||||
progress_type=ProgressType.SCAN,
|
|
||||||
title="Scanning anime library",
|
|
||||||
message="Initializing scan...",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create wrapped callback for progress updates
|
|
||||||
def progress_callback(progress_data: dict) -> None:
|
|
||||||
"""Update progress during scan."""
|
|
||||||
try:
|
|
||||||
if callback:
|
|
||||||
callback(progress_data)
|
|
||||||
|
|
||||||
# Update progress service
|
|
||||||
current = progress_data.get("current", 0)
|
|
||||||
total = progress_data.get("total", 0)
|
|
||||||
message = progress_data.get("message", "Scanning...")
|
|
||||||
|
|
||||||
# Schedule the coroutine without waiting for it
|
|
||||||
# This is safe because we don't need the result
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
if loop.is_running():
|
|
||||||
asyncio.ensure_future(
|
|
||||||
self._progress_service.update_progress(
|
|
||||||
progress_id=scan_id,
|
|
||||||
current=current,
|
|
||||||
total=total,
|
|
||||||
message=message,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Scan progress callback error", error=str(e))
|
|
||||||
|
|
||||||
# Run scan
|
|
||||||
await self._run_in_executor(self._app.ReScan, progress_callback)
|
|
||||||
|
|
||||||
# invalidate cache
|
# invalidate cache
|
||||||
try:
|
try:
|
||||||
self._cached_list_missing.cache_clear()
|
self._cached_list_missing.cache_clear()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Complete progress tracking
|
|
||||||
await self._progress_service.complete_progress(
|
|
||||||
progress_id=scan_id,
|
|
||||||
message="Scan completed successfully",
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("rescan failed")
|
|
||||||
|
|
||||||
# Fail progress tracking
|
|
||||||
await self._progress_service.fail_progress(
|
|
||||||
progress_id=scan_id,
|
|
||||||
error_message=str(e),
|
|
||||||
)
|
|
||||||
|
|
||||||
raise AnimeServiceError("Rescan failed") from e
|
|
||||||
|
|
||||||
async def download(self, serie_folder: str, season: int, episode: int, key: str, callback=None) -> bool:
|
except Exception as exc:
|
||||||
"""Start a download via the underlying loader.
|
logger.exception("rescan failed")
|
||||||
|
raise AnimeServiceError("Rescan failed") from exc
|
||||||
|
|
||||||
|
async def download(
|
||||||
|
self,
|
||||||
|
serie_folder: str,
|
||||||
|
season: int,
|
||||||
|
episode: int,
|
||||||
|
key: str,
|
||||||
|
) -> bool:
|
||||||
|
"""Start a download.
|
||||||
|
|
||||||
|
The SeriesApp now handles progress tracking via events which are
|
||||||
|
forwarded to the ProgressService through event handlers.
|
||||||
|
|
||||||
Returns True on success or raises AnimeServiceError on failure.
|
Returns True on success or raises AnimeServiceError on failure.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = await self._run_in_executor(
|
# SeriesApp.download is now async and handles events internally
|
||||||
self._app.download, serie_folder, season, episode,
|
return await self._app.download(
|
||||||
key, callback
|
serie_folder=serie_folder,
|
||||||
|
season=season,
|
||||||
|
episode=episode,
|
||||||
|
key=key,
|
||||||
)
|
)
|
||||||
# OperationResult has a success attribute
|
except Exception as exc:
|
||||||
if hasattr(result, 'success'):
|
|
||||||
logger.debug(
|
|
||||||
"Download result",
|
|
||||||
success=result.success,
|
|
||||||
message=result.message
|
|
||||||
)
|
|
||||||
return result.success
|
|
||||||
return bool(result)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("download failed")
|
logger.exception("download failed")
|
||||||
raise AnimeServiceError("Download failed") from e
|
raise AnimeServiceError("Download failed") from exc
|
||||||
|
|
||||||
|
|
||||||
def get_anime_service(directory: str = "./") -> AnimeService:
|
def get_anime_service(directory: str = "./") -> AnimeService:
|
||||||
|
|||||||
@ -27,11 +27,7 @@ from src.server.models.download import (
|
|||||||
QueueStatus,
|
QueueStatus,
|
||||||
)
|
)
|
||||||
from src.server.services.anime_service import AnimeService, AnimeServiceError
|
from src.server.services.anime_service import AnimeService, AnimeServiceError
|
||||||
from src.server.services.progress_service import (
|
from src.server.services.progress_service import ProgressService, get_progress_service
|
||||||
ProgressService,
|
|
||||||
ProgressType,
|
|
||||||
get_progress_service,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
@ -92,10 +88,18 @@ class DownloadService:
|
|||||||
# Statistics tracking
|
# Statistics tracking
|
||||||
self._total_downloaded_mb: float = 0.0
|
self._total_downloaded_mb: float = 0.0
|
||||||
self._download_speeds: deque[float] = deque(maxlen=10)
|
self._download_speeds: deque[float] = deque(maxlen=10)
|
||||||
|
|
||||||
|
# Subscribe to SeriesApp download events for progress tracking
|
||||||
|
if hasattr(anime_service, '_app') and hasattr(
|
||||||
|
anime_service._app, 'download_status'
|
||||||
|
):
|
||||||
|
anime_service._app.download_status += (
|
||||||
|
self._on_seriesapp_download_status
|
||||||
|
)
|
||||||
|
|
||||||
# Load persisted queue
|
# Load persisted queue
|
||||||
self._load_queue()
|
self._load_queue()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"DownloadService initialized",
|
"DownloadService initialized",
|
||||||
max_retries=max_retries,
|
max_retries=max_retries,
|
||||||
@ -146,6 +150,69 @@ class DownloadService:
|
|||||||
self._broadcast_callback = callback
|
self._broadcast_callback = callback
|
||||||
logger.debug("Broadcast callback registered")
|
logger.debug("Broadcast callback registered")
|
||||||
|
|
||||||
|
def _on_seriesapp_download_status(self, args) -> None:
|
||||||
|
"""Handle download status events from SeriesApp.
|
||||||
|
|
||||||
|
Updates the active download item with progress information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: DownloadStatusEventArgs from SeriesApp
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Only process if we have an active download
|
||||||
|
if not self._active_download:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Match the event to the active download item
|
||||||
|
# SeriesApp events include serie_folder, season, episode
|
||||||
|
if (
|
||||||
|
self._active_download.serie_folder == args.serie_folder
|
||||||
|
and self._active_download.episode.season == args.season
|
||||||
|
and self._active_download.episode.episode == args.episode
|
||||||
|
):
|
||||||
|
if args.status == "progress":
|
||||||
|
# Update item progress
|
||||||
|
self._active_download.progress = DownloadProgress(
|
||||||
|
percent=args.progress,
|
||||||
|
downloaded_mb=(
|
||||||
|
args.progress * args.mbper_sec / 100
|
||||||
|
if args.mbper_sec
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
total_mb=None, # Not provided by SeriesApp
|
||||||
|
speed_mbps=args.mbper_sec,
|
||||||
|
eta_seconds=args.eta,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track speed
|
||||||
|
if args.mbper_sec:
|
||||||
|
self._download_speeds.append(args.mbper_sec)
|
||||||
|
|
||||||
|
# Broadcast update
|
||||||
|
asyncio.create_task(
|
||||||
|
self._broadcast_update(
|
||||||
|
"download_progress",
|
||||||
|
{
|
||||||
|
"download_id": self._active_download.id,
|
||||||
|
"item_id": self._active_download.id,
|
||||||
|
"serie_name": self._active_download.serie_name,
|
||||||
|
"season": args.season,
|
||||||
|
"episode": args.episode,
|
||||||
|
"progress": (
|
||||||
|
self._active_download.progress.model_dump(
|
||||||
|
mode="json"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(
|
||||||
|
"Error handling SeriesApp download status",
|
||||||
|
error=str(exc)
|
||||||
|
)
|
||||||
|
|
||||||
async def _broadcast_update(self, update_type: str, data: dict) -> None:
|
async def _broadcast_update(self, update_type: str, data: dict) -> None:
|
||||||
"""Broadcast update to connected WebSocket clients.
|
"""Broadcast update to connected WebSocket clients.
|
||||||
|
|
||||||
@ -689,107 +756,6 @@ class DownloadService:
|
|||||||
f"Failed to retry: {str(e)}"
|
f"Failed to retry: {str(e)}"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
def _create_progress_callback(self, item: DownloadItem) -> Callable:
|
|
||||||
"""Create a progress callback for a download item.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item: Download item to track progress for
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Callback function for progress updates
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
f"Creating progress callback for item {item.id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def progress_callback(progress_data: dict) -> None:
|
|
||||||
"""Update progress and broadcast to clients."""
|
|
||||||
try:
|
|
||||||
logger.debug(
|
|
||||||
f"Progress callback received: {progress_data}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update item progress
|
|
||||||
item.progress = DownloadProgress(
|
|
||||||
percent=progress_data.get("percent", 0.0),
|
|
||||||
downloaded_mb=progress_data.get("downloaded_mb", 0.0),
|
|
||||||
total_mb=progress_data.get("total_mb"),
|
|
||||||
speed_mbps=progress_data.get("speed_mbps"),
|
|
||||||
eta_seconds=progress_data.get("eta_seconds"),
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
f"Updated item progress: percent={item.progress.percent:.1f}%, "
|
|
||||||
f"downloaded={item.progress.downloaded_mb:.1f}MB, "
|
|
||||||
f"total={item.progress.total_mb}MB, "
|
|
||||||
f"speed={item.progress.speed_mbps}MB/s"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Track speed for statistics
|
|
||||||
if item.progress.speed_mbps:
|
|
||||||
self._download_speeds.append(item.progress.speed_mbps)
|
|
||||||
|
|
||||||
# Update progress service
|
|
||||||
# Schedule coroutines in a thread-safe manner
|
|
||||||
# (callback may be called from executor thread)
|
|
||||||
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)
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
f"Updating progress service: current={current_mb}MB, "
|
|
||||||
f"total={total_mb}MB"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
loop
|
|
||||||
)
|
|
||||||
except RuntimeError as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Could not schedule progress update: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Broadcast update (fire and forget)
|
|
||||||
logger.debug(
|
|
||||||
f"Broadcasting download_progress event for item {item.id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
asyncio.run_coroutine_threadsafe(
|
|
||||||
self._broadcast_update(
|
|
||||||
"download_progress",
|
|
||||||
{
|
|
||||||
"download_id": item.id,
|
|
||||||
"item_id": item.id,
|
|
||||||
"serie_name": item.serie_name,
|
|
||||||
"season": item.episode.season,
|
|
||||||
"episode": item.episode.episode,
|
|
||||||
"progress": item.progress.model_dump(mode="json"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
loop
|
|
||||||
)
|
|
||||||
except RuntimeError as e:
|
|
||||||
logger.warning(
|
|
||||||
f"Could not schedule broadcast: {e}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Progress callback error", error=str(e))
|
|
||||||
|
|
||||||
return progress_callback
|
|
||||||
|
|
||||||
async def _process_download(self, item: DownloadItem) -> None:
|
async def _process_download(self, item: DownloadItem) -> None:
|
||||||
"""Process a single download item.
|
"""Process a single download item.
|
||||||
|
|
||||||
@ -809,31 +775,10 @@ class DownloadService:
|
|||||||
season=item.episode.season,
|
season=item.episode.season,
|
||||||
episode=item.episode.episode,
|
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)
|
|
||||||
logger.info(
|
|
||||||
f"Passing callback {progress_callback} to anime_service for "
|
|
||||||
f"item {item.id}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Execute download via anime service
|
# Execute download via anime service
|
||||||
|
# Note: AnimeService handles progress via SeriesApp events
|
||||||
|
# Progress updates received via _on_seriesapp_download_status
|
||||||
# Use serie_folder if available, otherwise fall back to serie_id
|
# Use serie_folder if available, otherwise fall back to serie_id
|
||||||
# for backwards compatibility with old queue items
|
# for backwards compatibility with old queue items
|
||||||
folder = item.serie_folder if item.serie_folder else item.serie_id
|
folder = item.serie_folder if item.serie_folder else item.serie_id
|
||||||
@ -842,7 +787,6 @@ class DownloadService:
|
|||||||
season=item.episode.season,
|
season=item.episode.season,
|
||||||
episode=item.episode.episode,
|
episode=item.episode.episode,
|
||||||
key=item.serie_id,
|
key=item.serie_id,
|
||||||
callback=progress_callback,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle result
|
# Handle result
|
||||||
@ -860,17 +804,7 @@ class DownloadService:
|
|||||||
"Download completed successfully", item_id=item.id
|
"Download completed successfully", item_id=item.id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Complete progress tracking
|
# Broadcast completion (progress already handled by events)
|
||||||
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(
|
await self._broadcast_update(
|
||||||
"download_complete",
|
"download_complete",
|
||||||
{
|
{
|
||||||
@ -901,13 +835,7 @@ class DownloadService:
|
|||||||
retry_count=item.retry_count,
|
retry_count=item.retry_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fail progress tracking
|
# Broadcast failure (progress already handled by events)
|
||||||
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(
|
await self._broadcast_update(
|
||||||
"download_failed",
|
"download_failed",
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user