491
modules/activity_status.py
Normal file
491
modules/activity_status.py
Normal file
@@ -0,0 +1,491 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user