Initial commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Todd
2026-03-29 22:42:55 -04:00
commit 0d7b2b1aab
389 changed files with 280296 additions and 0 deletions

491
modules/activity_status.py Normal file
View 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