feat: add cron for scans
Local Docker Build (Dev) / build-dev (push) Successful in 7s

This commit is contained in:
2025-10-28 17:33:08 -04:00
parent 6d2435a037
commit f11839e005
6 changed files with 1254 additions and 1 deletions
+267
View File
@@ -147,6 +147,48 @@ class NFOGuardDatabase:
)
""")
# Scheduled scans table
cursor.execute("""
CREATE TABLE IF NOT EXISTS scheduled_scans (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
cron_expression VARCHAR(100) NOT NULL,
media_type VARCHAR(20) NOT NULL CHECK (media_type IN ('tv', 'movies', 'both')),
scan_mode VARCHAR(20) NOT NULL CHECK (scan_mode IN ('smart', 'full', 'incomplete')),
specific_paths TEXT,
enabled BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_run_at TIMESTAMP,
next_run_at TIMESTAMP,
run_count INTEGER DEFAULT 0,
created_by VARCHAR(100),
updated_by VARCHAR(100)
)
""")
# Schedule execution history table
cursor.execute("""
CREATE TABLE IF NOT EXISTS schedule_executions (
id SERIAL PRIMARY KEY,
schedule_id INTEGER NOT NULL,
started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
status VARCHAR(50) NOT NULL CHECK (status IN ('running', 'completed', 'failed', 'cancelled')),
media_type VARCHAR(20) NOT NULL,
scan_mode VARCHAR(20) NOT NULL,
items_processed INTEGER DEFAULT 0,
items_skipped INTEGER DEFAULT 0,
items_failed INTEGER DEFAULT 0,
execution_time_seconds INTEGER,
error_message TEXT,
logs TEXT,
triggered_by VARCHAR(100),
FOREIGN KEY (schedule_id) REFERENCES scheduled_scans(id) ON DELETE CASCADE
)
""")
# Create indexes for PostgreSQL
cursor.execute("CREATE INDEX IF NOT EXISTS idx_episodes_imdb ON episodes(imdb_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_episodes_video ON episodes(has_video_file)")
@@ -155,6 +197,11 @@ class NFOGuardDatabase:
cursor.execute("CREATE INDEX IF NOT EXISTS idx_missing_imdb_type ON missing_imdb(media_type)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_missing_imdb_resolved ON missing_imdb(resolved)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_missing_imdb_path ON missing_imdb(file_path)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_scheduled_scans_enabled ON scheduled_scans(enabled)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_scheduled_scans_next_run ON scheduled_scans(next_run_at)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_schedule_executions_schedule ON schedule_executions(schedule_id)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_schedule_executions_status ON schedule_executions(status)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_schedule_executions_started ON schedule_executions(started_at)")
def upsert_series(self, imdb_id: str, path: str, metadata: Optional[Dict] = None):
"""Insert or update series record"""
with self.get_connection() as conn:
@@ -673,6 +720,226 @@ class NFOGuardDatabase:
return deleted_count > 0
# Scheduled Scans Methods
def create_scheduled_scan(self, name: str, description: str, cron_expression: str,
media_type: str, scan_mode: str, specific_paths: str = None,
enabled: bool = True, created_by: str = None) -> int:
"""Create a new scheduled scan"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO scheduled_scans
(name, description, cron_expression, media_type, scan_mode, specific_paths, enabled, created_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", (name, description, cron_expression, media_type, scan_mode, specific_paths, enabled, created_by))
return cursor.fetchone()['id']
def get_scheduled_scans(self, enabled_only: bool = False) -> List[Dict]:
"""Get all scheduled scans"""
with self.get_connection() as conn:
cursor = conn.cursor()
query = "SELECT * FROM scheduled_scans"
if enabled_only:
query += " WHERE enabled = TRUE"
query += " ORDER BY name"
cursor.execute(query)
return cursor.fetchall()
def get_scheduled_scan(self, scan_id: int) -> Optional[Dict]:
"""Get a specific scheduled scan by ID"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM scheduled_scans WHERE id = %s", (scan_id,))
return cursor.fetchone()
def update_scheduled_scan(self, scan_id: int, name: str = None, description: str = None,
cron_expression: str = None, media_type: str = None,
scan_mode: str = None, specific_paths: str = None,
enabled: bool = None, updated_by: str = None) -> bool:
"""Update a scheduled scan"""
with self.get_connection() as conn:
cursor = conn.cursor()
updates = []
params = []
if name is not None:
updates.append("name = %s")
params.append(name)
if description is not None:
updates.append("description = %s")
params.append(description)
if cron_expression is not None:
updates.append("cron_expression = %s")
params.append(cron_expression)
if media_type is not None:
updates.append("media_type = %s")
params.append(media_type)
if scan_mode is not None:
updates.append("scan_mode = %s")
params.append(scan_mode)
if specific_paths is not None:
updates.append("specific_paths = %s")
params.append(specific_paths)
if enabled is not None:
updates.append("enabled = %s")
params.append(enabled)
if updated_by is not None:
updates.append("updated_by = %s")
params.append(updated_by)
updates.append("updated_at = CURRENT_TIMESTAMP")
params.append(scan_id)
if not updates:
return False
query = f"UPDATE scheduled_scans SET {', '.join(updates)} WHERE id = %s"
cursor.execute(query, params)
return cursor.rowcount > 0
def delete_scheduled_scan(self, scan_id: int) -> bool:
"""Delete a scheduled scan and its execution history"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM scheduled_scans WHERE id = %s", (scan_id,))
return cursor.rowcount > 0
def update_scan_next_run(self, scan_id: int, next_run_at: datetime) -> bool:
"""Update the next run time for a scheduled scan"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
UPDATE scheduled_scans
SET next_run_at = %s, updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""", (next_run_at, scan_id))
return cursor.rowcount > 0
def update_scan_last_run(self, scan_id: int, last_run_at: datetime = None) -> bool:
"""Update the last run time and increment run count for a scheduled scan"""
with self.get_connection() as conn:
cursor = conn.cursor()
if last_run_at is None:
last_run_at = datetime.utcnow()
cursor.execute("""
UPDATE scheduled_scans
SET last_run_at = %s, run_count = run_count + 1, updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""", (last_run_at, scan_id))
return cursor.rowcount > 0
# Schedule Execution Methods
def create_schedule_execution(self, schedule_id: int, media_type: str, scan_mode: str,
triggered_by: str = None) -> int:
"""Create a new schedule execution record"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO schedule_executions
(schedule_id, status, media_type, scan_mode, triggered_by)
VALUES (%s, 'running', %s, %s, %s)
RETURNING id
""", (schedule_id, media_type, scan_mode, triggered_by))
return cursor.fetchone()['id']
def update_schedule_execution(self, execution_id: int, status: str = None,
items_processed: int = None, items_skipped: int = None,
items_failed: int = None, error_message: str = None,
logs: str = None) -> bool:
"""Update a schedule execution record"""
with self.get_connection() as conn:
cursor = conn.cursor()
updates = []
params = []
if status is not None:
updates.append("status = %s")
params.append(status)
if status in ['completed', 'failed', 'cancelled']:
updates.append("completed_at = CURRENT_TIMESTAMP")
updates.append("execution_time_seconds = EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - started_at))")
if items_processed is not None:
updates.append("items_processed = %s")
params.append(items_processed)
if items_skipped is not None:
updates.append("items_skipped = %s")
params.append(items_skipped)
if items_failed is not None:
updates.append("items_failed = %s")
params.append(items_failed)
if error_message is not None:
updates.append("error_message = %s")
params.append(error_message)
if logs is not None:
updates.append("logs = %s")
params.append(logs)
if not updates:
return False
params.append(execution_id)
query = f"UPDATE schedule_executions SET {', '.join(updates)} WHERE id = %s"
cursor.execute(query, params)
return cursor.rowcount > 0
def get_schedule_executions(self, schedule_id: int = None, limit: int = 50) -> List[Dict]:
"""Get schedule execution history"""
with self.get_connection() as conn:
cursor = conn.cursor()
query = """
SELECT se.*, ss.name as schedule_name
FROM schedule_executions se
JOIN scheduled_scans ss ON se.schedule_id = ss.id
"""
params = []
if schedule_id is not None:
query += " WHERE se.schedule_id = %s"
params.append(schedule_id)
query += " ORDER BY se.started_at DESC LIMIT %s"
params.append(limit)
cursor.execute(query, params)
return cursor.fetchall()
def get_running_executions(self) -> List[Dict]:
"""Get currently running schedule executions"""
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT se.*, ss.name as schedule_name
FROM schedule_executions se
JOIN scheduled_scans ss ON se.schedule_id = ss.id
WHERE se.status = 'running'
ORDER BY se.started_at DESC
""")
return cursor.fetchall()
def close(self):
"""Close all database connections"""
if hasattr(self._local, 'connection'):