1.1.0 - automations, clean only mode, bug fixes
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
from .models import AutomationRule
|
||||
from .engine import AutomationEngine
|
||||
|
||||
__all__ = ["AutomationRule", "AutomationEngine"]
|
||||
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Tuple
|
||||
|
||||
from logging_utils import get_logger
|
||||
from core.subtitle_processor import SubtitleBlock, parse_srt, format_srt
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def enumerate_srt_files(folders: Iterable[str]) -> List[Path]:
|
||||
files: List[Path] = []
|
||||
for folder in folders:
|
||||
if not folder:
|
||||
continue
|
||||
path = Path(folder)
|
||||
if not path.exists():
|
||||
logger.warning("Automation folder does not exist: %s", folder)
|
||||
continue
|
||||
if not path.is_dir():
|
||||
continue
|
||||
files.extend([p for p in path.rglob("*.srt") if p.is_file()])
|
||||
return files
|
||||
|
||||
|
||||
def remove_lines_matching_patterns(file_path: str, patterns: List[str], dry_run: bool = False) -> Tuple[bool, int]:
|
||||
"""Remove subtitle lines containing any of the provided patterns."""
|
||||
if not patterns:
|
||||
return False, 0
|
||||
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
content = path.read_text(encoding="utf-8", errors="ignore")
|
||||
blocks = parse_srt(content)
|
||||
|
||||
lowered_patterns = [p.lower() for p in patterns if p]
|
||||
removed_lines = 0
|
||||
updated_blocks: List[SubtitleBlock] = []
|
||||
|
||||
for block in blocks:
|
||||
lines = block.text.splitlines()
|
||||
kept_lines = []
|
||||
for line in lines:
|
||||
line_lower = line.lower()
|
||||
if any(pattern in line_lower for pattern in lowered_patterns):
|
||||
removed_lines += 1
|
||||
continue
|
||||
kept_lines.append(line)
|
||||
|
||||
if kept_lines:
|
||||
updated_blocks.append(
|
||||
SubtitleBlock(
|
||||
index=block.index,
|
||||
start_time=block.start_time,
|
||||
end_time=block.end_time,
|
||||
text="\n".join(kept_lines).strip(),
|
||||
)
|
||||
)
|
||||
|
||||
if removed_lines == 0:
|
||||
return False, 0
|
||||
|
||||
renumbered = [
|
||||
SubtitleBlock(i + 1, b.start_time, b.end_time, b.text)
|
||||
for i, b in enumerate(updated_blocks)
|
||||
]
|
||||
|
||||
if not dry_run:
|
||||
path.write_text(format_srt(renumbered), encoding="utf-8")
|
||||
|
||||
return True, removed_lines
|
||||
@@ -0,0 +1,131 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from logging_utils import get_logger
|
||||
from automations.actions import enumerate_srt_files, remove_lines_matching_patterns
|
||||
from automations.models import AutomationRule
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AutomationEngine:
|
||||
"""Scheduler for automation rules."""
|
||||
|
||||
def __init__(self, db_manager):
|
||||
self.db_manager = db_manager
|
||||
self._scheduler = BackgroundScheduler(daemon=True)
|
||||
self._lock = threading.Lock()
|
||||
self._started = False
|
||||
|
||||
def start(self):
|
||||
with self._lock:
|
||||
if self._started:
|
||||
return
|
||||
self._scheduler.start()
|
||||
self._started = True
|
||||
self.reload_rules()
|
||||
logger.info("Automation scheduler started")
|
||||
|
||||
def shutdown(self):
|
||||
with self._lock:
|
||||
if not self._started:
|
||||
return
|
||||
self._scheduler.shutdown(wait=False)
|
||||
self._started = False
|
||||
logger.info("Automation scheduler stopped")
|
||||
|
||||
def reload_rules(self):
|
||||
"""Reload all automation rules from storage."""
|
||||
with self._lock:
|
||||
self._scheduler.remove_all_jobs()
|
||||
rules = self._load_rules()
|
||||
for rule in rules:
|
||||
if not rule.enabled:
|
||||
continue
|
||||
self._schedule_rule(rule)
|
||||
|
||||
def run_rule_now(self, rule_id: str, dry_run: bool = False) -> dict:
|
||||
rule = self._get_rule(rule_id)
|
||||
if not rule:
|
||||
return {"success": False, "error": "Rule not found"}
|
||||
return self._execute_rule(rule, dry_run=dry_run)
|
||||
|
||||
def _load_rules(self) -> List[AutomationRule]:
|
||||
rules_raw = self.db_manager.get_automation_rules()
|
||||
return [AutomationRule.from_dict(r) for r in rules_raw]
|
||||
|
||||
def _get_rule(self, rule_id: str) -> Optional[AutomationRule]:
|
||||
raw = self.db_manager.get_automation_rule(rule_id)
|
||||
return AutomationRule.from_dict(raw) if raw else None
|
||||
|
||||
def _schedule_rule(self, rule: AutomationRule):
|
||||
try:
|
||||
trigger = CronTrigger.from_crontab(rule.schedule)
|
||||
except ValueError as e:
|
||||
logger.error("Invalid cron schedule for rule %s: %s", rule.id, e)
|
||||
return
|
||||
self._scheduler.add_job(
|
||||
self._run_rule_job,
|
||||
trigger=trigger,
|
||||
args=[rule.id],
|
||||
id=f"automation:{rule.id}",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=300,
|
||||
max_instances=1,
|
||||
)
|
||||
|
||||
def _run_rule_job(self, rule_id: str):
|
||||
rule = self._get_rule(rule_id)
|
||||
if not rule or not rule.enabled:
|
||||
return
|
||||
self._execute_rule(rule, dry_run=False)
|
||||
|
||||
def _execute_rule(self, rule: AutomationRule, dry_run: bool) -> dict:
|
||||
files = enumerate_srt_files(rule.target_folders)
|
||||
modified = 0
|
||||
total_removed = 0
|
||||
errors: List[str] = []
|
||||
|
||||
for file_path in files:
|
||||
try:
|
||||
did_modify, removed_lines = remove_lines_matching_patterns(
|
||||
str(file_path),
|
||||
rule.patterns,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
if did_modify:
|
||||
modified += 1
|
||||
total_removed += removed_lines
|
||||
self.db_manager.add_automation_log(
|
||||
rule_id=rule.id,
|
||||
file_path=str(file_path),
|
||||
modified=did_modify,
|
||||
removed_lines=removed_lines,
|
||||
dry_run=dry_run,
|
||||
error_message=None,
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(f"{file_path}: {e}")
|
||||
self.db_manager.add_automation_log(
|
||||
rule_id=rule.id,
|
||||
file_path=str(file_path),
|
||||
modified=False,
|
||||
removed_lines=0,
|
||||
dry_run=dry_run,
|
||||
error_message=str(e),
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"rule_id": rule.id,
|
||||
"files_scanned": len(files),
|
||||
"files_modified": modified,
|
||||
"removed_lines": total_removed,
|
||||
"dry_run": dry_run,
|
||||
"errors": errors,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AutomationRule:
|
||||
id: str
|
||||
name: str
|
||||
schedule: str
|
||||
enabled: bool
|
||||
patterns: List[str]
|
||||
target_folders: List[str]
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "AutomationRule":
|
||||
return AutomationRule(
|
||||
id=str(data.get("id", "")),
|
||||
name=str(data.get("name", "")),
|
||||
schedule=str(data.get("schedule", "")),
|
||||
enabled=bool(data.get("enabled", True)),
|
||||
patterns=list(data.get("patterns", []) or []),
|
||||
target_folders=list(data.get("target_folders", []) or []),
|
||||
)
|
||||
Reference in New Issue
Block a user