"""Service for scheduled page visits.""" import asyncio import random import logging from typing import Optional, Callable, Awaitable from config.settings import settings from services.browser_service import BrowserService logger = logging.getLogger(__name__) class VisitScheduler: """ Scheduler for periodic page visits. Features: - Schedule periodic visits - Configurable delays and reading times - Progress tracking """ def __init__( self, browser_service: BrowserService, url: str, min_delay: int = None, max_delay: int = None, min_reading: int = None, max_reading: int = None, max_visits: Optional[int] = None, on_visit_complete: Optional[Callable] = None, on_progress: Optional[Callable] = None ): self.browser_service = browser_service self.url = url self.min_delay = min_delay or settings.DEFAULT_MIN_DELAY self.max_delay = max_delay or settings.DEFAULT_MAX_DELAY self.min_reading = min_reading or settings.DEFAULT_MIN_READING self.max_reading = max_reading or settings.DEFAULT_MAX_READING self.max_visits = max_visits self.on_visit_complete = on_visit_complete self.on_progress = on_progress self.visit_count = 0 self.successful = 0 self.failed = 0 async def run(self) -> dict: """ Run scheduled visits. Returns: Statistics dict """ total_reading_time = 0 loop = asyncio.get_event_loop() try: while True: if self.max_visits and self.visit_count >= self.max_visits: logger.info(f"Visit limit reached: {self.max_visits}") break # Randomize parameters once per cycle current_reading = random.randint(self.min_reading, self.max_reading) current_delay = random.randint(self.min_delay, self.max_delay) self.visit_count += 1 # Record start time so the next visit fires strictly on the timer tick_start = loop.time() logger.info( f"Visit {self.visit_count}/{'∞' if not self.max_visits else self.max_visits}: " f"reading={current_reading}s delay={current_delay}s" ) # Visit page result = await self.browser_service.visit_page( self.url, current_reading ) if result.success: self.successful += 1 total_reading_time += current_reading else: self.failed += 1 # Notify about visit completion if self.on_visit_complete: await self.on_visit_complete( self.visit_count, result, current_delay, current_reading ) if self.max_visits and self.visit_count >= self.max_visits: break # Wait only the remaining time so that the period between visit # starts is exactly current_delay (independent of visit duration) elapsed = loop.time() - tick_start remaining = current_delay - elapsed if self.on_progress: await self.on_progress(self.visit_count, current_delay) if remaining > 0: await asyncio.sleep(remaining) except asyncio.CancelledError: logger.info("Visit scheduler cancelled") raise return { "total_visits": self.visit_count, "successful": self.successful, "failed": self.failed, "avg_reading_time": total_reading_time / self.successful if self.successful else 0 }