#!/usr/bin/env python3 """ Activity Status Manager Centralized module for tracking and updating real-time download activity status Stores status in database for reliable, concurrent access Supports: - Single main activity (scheduler) via activity_status table - Multiple background tasks (YouTube monitor, etc.) via background_task_status table """ import json from datetime import datetime from typing import Optional, Dict, Any, List from pathlib import Path from modules.universal_logger import get_logger logger = get_logger('ActivityStatus') class ActivityStatusManager: """Manages real-time activity status updates stored in database""" def __init__(self, unified_db=None): """ Initialize activity status manager Args: unified_db: UnifiedDatabase instance (optional, will create if needed) """ self.db = unified_db if not self.db: from modules.unified_database import UnifiedDatabase self.db = UnifiedDatabase() self._ensure_table() def _ensure_table(self): """Ensure activity_status and background_task_status tables exist""" try: with self.db.get_connection() as conn: cursor = conn.cursor() # Main scheduler activity table (single row) cursor.execute(''' CREATE TABLE IF NOT EXISTS activity_status ( id INTEGER PRIMARY KEY CHECK (id = 1), active INTEGER NOT NULL DEFAULT 0, task_id TEXT, platform TEXT, account TEXT, start_time TEXT, status TEXT, detailed_status TEXT, progress_current INTEGER, progress_total INTEGER, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) ''') # Add account progress columns if missing cursor.execute("PRAGMA table_info(activity_status)") columns = [col[1] for col in cursor.fetchall()] if 'account_current' not in columns: cursor.execute('ALTER TABLE activity_status ADD COLUMN account_current INTEGER') if 'account_total' not in columns: cursor.execute('ALTER TABLE activity_status ADD COLUMN account_total INTEGER') # Insert default row if doesn't exist cursor.execute(''' INSERT OR IGNORE INTO activity_status (id, active) VALUES (1, 0) ''') # Background tasks table (multiple concurrent tasks like YouTube monitor) cursor.execute(''' CREATE TABLE IF NOT EXISTS background_task_status ( task_id TEXT PRIMARY KEY, active INTEGER NOT NULL DEFAULT 0, task_type TEXT, display_name TEXT, start_time TEXT, status TEXT, detailed_status TEXT, progress_current INTEGER, progress_total INTEGER, extra_data TEXT, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() except Exception as e: logger.error(f"Failed to create activity tables: {e}") def start_activity(self, task_id: str, platform: str, account: str, status: str = "Running"): """ Mark activity as started Args: task_id: Unique task identifier platform: Platform name (instagram, snapchat, etc) account: Account/username being processed status: Initial status message """ try: with self.db.get_connection(for_write=True) as conn: cursor = conn.cursor() cursor.execute(''' UPDATE activity_status SET active = 1, task_id = ?, platform = ?, account = ?, start_time = ?, status = ?, detailed_status = NULL, progress_current = NULL, progress_total = NULL, account_current = NULL, account_total = NULL, updated_at = ? WHERE id = 1 ''', (task_id, platform, account, datetime.now().isoformat(), status, datetime.now().isoformat())) conn.commit() except Exception as e: logger.error(f"Failed to start activity: {e}") def update_status(self, detailed_status: str, progress_current: Optional[int] = None, progress_total: Optional[int] = None): """Update detailed status message and progress.""" try: with self.db.get_connection(for_write=True) as conn: cursor = conn.cursor() cursor.execute(''' UPDATE activity_status SET detailed_status = ?, progress_current = COALESCE(?, progress_current), progress_total = COALESCE(?, progress_total), updated_at = ? WHERE id = 1 AND active = 1 ''', (detailed_status, progress_current, progress_total, datetime.now().isoformat())) conn.commit() except Exception as e: logger.error(f"Failed to update status: {e}") def update_account_name(self, account: str): """Update the current account name being processed.""" try: with self.db.get_connection(for_write=True) as conn: cursor = conn.cursor() cursor.execute(''' UPDATE activity_status SET account = ?, updated_at = ? WHERE id = 1 AND active = 1 ''', (account, datetime.now().isoformat())) conn.commit() except Exception as e: logger.error(f"Failed to update account name: {e}") def update_account_progress(self, account_current: int, account_total: int): """Update account-level progress and reset file-level progress for the new account""" try: with self.db.get_connection(for_write=True) as conn: cursor = conn.cursor() cursor.execute(''' UPDATE activity_status SET account_current = ?, account_total = ?, progress_current = NULL, progress_total = NULL, updated_at = ? WHERE id = 1 AND active = 1 ''', (account_current, account_total, datetime.now().isoformat())) conn.commit() except Exception as e: logger.error(f"Failed to update account progress: {e}") def stop_activity(self): """Mark activity as stopped""" try: with self.db.get_connection(for_write=True) as conn: cursor = conn.cursor() cursor.execute(''' UPDATE activity_status SET active = 0, detailed_status = NULL, progress_current = NULL, progress_total = NULL, account_current = NULL, account_total = NULL, updated_at = ? WHERE id = 1 ''', (datetime.now().isoformat(),)) conn.commit() except Exception as e: logger.error(f"Failed to stop activity: {e}") def get_current_activity(self) -> Dict[str, Any]: """ Get current activity status Returns: Dict with activity information """ try: with self.db.get_connection() as conn: cursor = conn.cursor() cursor.execute(''' SELECT active, task_id, platform, account, start_time, status, detailed_status, progress_current, progress_total, account_current, account_total FROM activity_status WHERE id = 1 ''') row = cursor.fetchone() if row: result = { 'active': bool(row[0]), 'task_id': row[1], 'platform': row[2], 'account': row[3], 'start_time': row[4], 'status': row[5] } # Add optional fields only if they exist if row[6]: # detailed_status result['detailed_status'] = row[6] if row[7] is not None and row[8] is not None: # progress result['progress'] = { 'current': row[7], 'total': row[8] } if row[9] is not None and row[10] is not None: # account_progress result['account_progress'] = { 'current': row[9], 'total': row[10] } return result return { 'active': False, 'task_id': None, 'platform': None, 'account': None, 'start_time': None, 'status': None } except Exception as e: logger.error(f"Failed to get current activity: {e}") return { 'active': False, 'task_id': None, 'platform': None, 'account': None, 'start_time': None, 'status': None } # ========================================================================= # BACKGROUND TASK METHODS (for concurrent tasks like YouTube monitor) # ========================================================================= def start_background_task(self, task_id: str, task_type: str, display_name: str, status: str = "Running", extra_data: Dict = None): """ Start a background task (doesn't interfere with main activity). Args: task_id: Unique task identifier (e.g., 'youtube_monitor') task_type: Type of task (e.g., 'youtube_monitor', 'video_processor') display_name: Human-readable name for display status: Initial status message extra_data: Optional extra data to store as JSON """ try: with self.db.get_connection(for_write=True) as conn: cursor = conn.cursor() # Check if task is already running - don't reset if so cursor.execute(''' SELECT active FROM background_task_status WHERE task_id = ? ''', (task_id,)) row = cursor.fetchone() if row and row[0] == 1: # Task already running, just update status without resetting counter logger.debug(f"Background task {task_id} already running, not resetting") return cursor.execute(''' INSERT OR REPLACE INTO background_task_status (task_id, active, task_type, display_name, start_time, status, detailed_status, progress_current, progress_total, extra_data, updated_at) VALUES (?, 1, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?) ''', (task_id, task_type, display_name, datetime.now().isoformat(), status, json.dumps(extra_data) if extra_data else None, datetime.now().isoformat())) conn.commit() except Exception as e: logger.error(f"Failed to start background task {task_id}: {e}") def update_background_task(self, task_id: str, detailed_status: str, progress_current: Optional[int] = None, progress_total: Optional[int] = None, extra_data: Dict = None): """Update a background task's status.""" try: with self.db.get_connection(for_write=True) as conn: cursor = conn.cursor() if extra_data is not None: cursor.execute(''' UPDATE background_task_status SET detailed_status = ?, progress_current = ?, progress_total = ?, extra_data = ?, updated_at = ? WHERE task_id = ? AND active = 1 ''', (detailed_status, progress_current, progress_total, json.dumps(extra_data), datetime.now().isoformat(), task_id)) else: cursor.execute(''' UPDATE background_task_status SET detailed_status = ?, progress_current = ?, progress_total = ?, updated_at = ? WHERE task_id = ? AND active = 1 ''', (detailed_status, progress_current, progress_total, datetime.now().isoformat(), task_id)) conn.commit() except Exception as e: logger.error(f"Failed to update background task {task_id}: {e}") def stop_background_task(self, task_id: str): """Mark a background task as stopped.""" try: with self.db.get_connection(for_write=True) as conn: cursor = conn.cursor() cursor.execute(''' UPDATE background_task_status SET active = 0, updated_at = ? WHERE task_id = ? ''', (datetime.now().isoformat(), task_id)) conn.commit() except Exception as e: logger.error(f"Failed to stop background task {task_id}: {e}") def stop_all_background_tasks(self): """Mark all background tasks as stopped (used on scheduler startup to clear stale state).""" try: with self.db.get_connection(for_write=True) as conn: cursor = conn.cursor() cursor.execute(''' UPDATE background_task_status SET active = 0, updated_at = ? WHERE active = 1 ''', (datetime.now().isoformat(),)) count = cursor.rowcount conn.commit() if count > 0: logger.info(f"Cleared {count} stale background task(s) from previous run") except Exception as e: logger.error(f"Failed to stop all background tasks: {e}") def get_background_task(self, task_id: str) -> Optional[Dict[str, Any]]: """ Get a specific background task's status. Args: task_id: Task identifier Returns: Dict with task information or None """ try: with self.db.get_connection() as conn: cursor = conn.cursor() cursor.execute(''' SELECT task_id, active, task_type, display_name, start_time, status, detailed_status, progress_current, progress_total, extra_data, updated_at FROM background_task_status WHERE task_id = ? ''', (task_id,)) row = cursor.fetchone() if row: result = { 'task_id': row[0], 'active': bool(row[1]), 'task_type': row[2], 'display_name': row[3], 'start_time': row[4], 'status': row[5], 'updated_at': row[10] } if row[6]: # detailed_status result['detailed_status'] = row[6] if row[7] is not None and row[8] is not None: # progress result['progress'] = { 'current': row[7], 'total': row[8] } if row[9]: # extra_data try: result['extra_data'] = json.loads(row[9]) except (json.JSONDecodeError, TypeError, ValueError) as e: logger.debug(f"Failed to parse extra_data for task {task_id}: {e}") result['extra_data'] = {} return result return None except Exception as e: logger.error(f"Failed to get background task {task_id}: {e}") return None def get_active_background_tasks(self) -> List[Dict[str, Any]]: """ Get all active background tasks. Returns: List of active task dictionaries """ try: with self.db.get_connection() as conn: cursor = conn.cursor() cursor.execute(''' SELECT task_id, active, task_type, display_name, start_time, status, detailed_status, progress_current, progress_total, extra_data, updated_at FROM background_task_status WHERE active = 1 ORDER BY start_time DESC ''') tasks = [] for row in cursor.fetchall(): task = { 'task_id': row[0], 'active': bool(row[1]), 'task_type': row[2], 'display_name': row[3], 'start_time': row[4], 'status': row[5], 'updated_at': row[10] } if row[6]: # detailed_status task['detailed_status'] = row[6] if row[7] is not None and row[8] is not None: # progress task['progress'] = { 'current': row[7], 'total': row[8] } if row[9]: # extra_data try: task['extra_data'] = json.loads(row[9]) except (json.JSONDecodeError, TypeError, ValueError): task['extra_data'] = {} tasks.append(task) return tasks except Exception as e: logger.error(f"Failed to get active background tasks: {e}") return [] # Global instance with thread-safe initialization _activity_manager = None _activity_manager_lock = __import__('threading').Lock() def get_activity_manager(unified_db=None): """Get or create global activity manager instance (thread-safe)""" global _activity_manager if _activity_manager is None: with _activity_manager_lock: # Double-check inside lock to prevent race condition if _activity_manager is None: _activity_manager = ActivityStatusManager(unified_db) return _activity_manager