This commit is contained in:
@@ -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'):
|
||||
|
||||
Reference in New Issue
Block a user