Fix automation timezone bug
This commit is contained in:
parent
918dbad88f
commit
d97b3d1846
3 changed files with 46 additions and 27 deletions
|
|
@ -17,11 +17,23 @@ import re
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger("automation_engine")
|
logger = get_logger("automation_engine")
|
||||||
|
|
||||||
|
def _utcnow():
|
||||||
|
"""Return current UTC time as timezone-aware datetime."""
|
||||||
|
return datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
def _utcnow_str():
|
||||||
|
"""Return current UTC time as naive string for DB storage (consistent with SQLite CURRENT_TIMESTAMP)."""
|
||||||
|
return datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
def _utc_after(seconds):
|
||||||
|
"""Return UTC time N seconds from now as naive string for DB storage."""
|
||||||
|
return (datetime.now(timezone.utc) + timedelta(seconds=seconds)).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
SYSTEM_AUTOMATIONS = [
|
SYSTEM_AUTOMATIONS = [
|
||||||
{
|
{
|
||||||
'name': 'Auto-Process Wishlist',
|
'name': 'Auto-Process Wishlist',
|
||||||
|
|
@ -161,8 +173,8 @@ class AutomationEngine:
|
||||||
if existing:
|
if existing:
|
||||||
# Only reset next_run for timer-based triggers that have an initial delay
|
# Only reset next_run for timer-based triggers that have an initial delay
|
||||||
if spec.get('initial_delay') is not None:
|
if spec.get('initial_delay') is not None:
|
||||||
next_run = (datetime.now() + timedelta(seconds=spec['initial_delay'])).strftime('%Y-%m-%d %H:%M:%S')
|
nr = _utc_after(spec['initial_delay'])
|
||||||
self.db.update_automation(existing['id'], next_run=next_run)
|
self.db.update_automation(existing['id'], next_run=nr)
|
||||||
logger.info(f"System automation '{spec['name']}' next_run reset to {spec['initial_delay']}s from now")
|
logger.info(f"System automation '{spec['name']}' next_run reset to {spec['initial_delay']}s from now")
|
||||||
else:
|
else:
|
||||||
logger.info(f"System automation '{spec['name']}' ready (event-based)")
|
logger.info(f"System automation '{spec['name']}' ready (event-based)")
|
||||||
|
|
@ -173,8 +185,8 @@ class AutomationEngine:
|
||||||
if not auto or not auto.get('enabled') or not auto.get('next_run'):
|
if not auto or not auto.get('enabled') or not auto.get('next_run'):
|
||||||
return 0
|
return 0
|
||||||
try:
|
try:
|
||||||
next_run = datetime.strptime(auto['next_run'], '%Y-%m-%d %H:%M:%S')
|
next_run = datetime.strptime(auto['next_run'], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
|
||||||
remaining = (next_run - datetime.now()).total_seconds()
|
remaining = (next_run - _utcnow()).total_seconds()
|
||||||
return max(0, int(remaining))
|
return max(0, int(remaining))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -561,20 +573,22 @@ class AutomationEngine:
|
||||||
try:
|
try:
|
||||||
trigger_config = json.loads(auto.get('trigger_config') or '{}')
|
trigger_config = json.loads(auto.get('trigger_config') or '{}')
|
||||||
if trigger_type == 'daily_time':
|
if trigger_type == 'daily_time':
|
||||||
# Next run is tomorrow at the configured time
|
# Next run is tomorrow at the configured time (compute delay from local time, store as UTC)
|
||||||
time_str = trigger_config.get('time', '00:00')
|
time_str = trigger_config.get('time', '00:00')
|
||||||
hour, minute = map(int, time_str.split(':'))
|
hour, minute = map(int, time_str.split(':'))
|
||||||
target = datetime.now().replace(hour=hour, minute=minute, second=0, microsecond=0) + timedelta(days=1)
|
now_local = datetime.now()
|
||||||
next_run_str = target.strftime('%Y-%m-%d %H:%M:%S')
|
target = now_local.replace(hour=hour, minute=minute, second=0, microsecond=0) + timedelta(days=1)
|
||||||
|
next_run_str = _utc_after((target - now_local).total_seconds())
|
||||||
elif trigger_type == 'weekly_time':
|
elif trigger_type == 'weekly_time':
|
||||||
time_str = trigger_config.get('time', '00:00')
|
time_str = trigger_config.get('time', '00:00')
|
||||||
hour, minute = map(int, time_str.split(':'))
|
hour, minute = map(int, time_str.split(':'))
|
||||||
|
now_local = datetime.now()
|
||||||
target = self._next_weekly_occurrence(hour, minute, trigger_config.get('days', []))
|
target = self._next_weekly_occurrence(hour, minute, trigger_config.get('days', []))
|
||||||
next_run_str = target.strftime('%Y-%m-%d %H:%M:%S')
|
next_run_str = _utc_after((target - now_local).total_seconds())
|
||||||
else:
|
else:
|
||||||
delay = self._calc_delay_seconds(trigger_config)
|
delay = self._calc_delay_seconds(trigger_config)
|
||||||
if delay:
|
if delay:
|
||||||
next_run_str = (datetime.now() + timedelta(seconds=delay)).strftime('%Y-%m-%d %H:%M:%S')
|
next_run_str = _utc_after(delay)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -623,14 +637,14 @@ class AutomationEngine:
|
||||||
auto = self.db.get_automation(automation_id)
|
auto = self.db.get_automation(automation_id)
|
||||||
if auto and auto.get('next_run'):
|
if auto and auto.get('next_run'):
|
||||||
try:
|
try:
|
||||||
next_run = datetime.strptime(auto['next_run'], '%Y-%m-%d %H:%M:%S')
|
next_run = datetime.strptime(auto['next_run'], '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone.utc)
|
||||||
remaining = (next_run - datetime.now()).total_seconds()
|
remaining = (next_run - _utcnow()).total_seconds()
|
||||||
if remaining > 0:
|
if remaining > 0:
|
||||||
delay = remaining
|
delay = remaining
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
next_run_str = (datetime.now() + timedelta(seconds=delay)).strftime('%Y-%m-%d %H:%M:%S')
|
next_run_str = _utc_after(delay)
|
||||||
self.db.update_automation(automation_id, next_run=next_run_str)
|
self.db.update_automation(automation_id, next_run=next_run_str)
|
||||||
|
|
||||||
timer = threading.Timer(delay, self.run_automation, args=(automation_id,))
|
timer = threading.Timer(delay, self.run_automation, args=(automation_id,))
|
||||||
|
|
@ -650,14 +664,14 @@ class AutomationEngine:
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
hour, minute = 0, 0
|
hour, minute = 0, 0
|
||||||
|
|
||||||
now = datetime.now()
|
now_local = datetime.now()
|
||||||
target = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
target = now_local.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||||
if target <= now:
|
if target <= now_local:
|
||||||
target += timedelta(days=1)
|
target += timedelta(days=1)
|
||||||
|
|
||||||
delay = (target - now).total_seconds()
|
delay = (target - now_local).total_seconds()
|
||||||
|
|
||||||
next_run_str = target.strftime('%Y-%m-%d %H:%M:%S')
|
next_run_str = _utc_after(delay)
|
||||||
self.db.update_automation(automation_id, next_run=next_run_str)
|
self.db.update_automation(automation_id, next_run=next_run_str)
|
||||||
|
|
||||||
timer = threading.Timer(delay, self.run_automation, args=(automation_id,))
|
timer = threading.Timer(delay, self.run_automation, args=(automation_id,))
|
||||||
|
|
@ -680,7 +694,7 @@ class AutomationEngine:
|
||||||
target = self._next_weekly_occurrence(hour, minute, config.get('days', []))
|
target = self._next_weekly_occurrence(hour, minute, config.get('days', []))
|
||||||
delay = (target - datetime.now()).total_seconds()
|
delay = (target - datetime.now()).total_seconds()
|
||||||
|
|
||||||
next_run_str = target.strftime('%Y-%m-%d %H:%M:%S')
|
next_run_str = _utc_after(delay)
|
||||||
self.db.update_automation(automation_id, next_run=next_run_str)
|
self.db.update_automation(automation_id, next_run=next_run_str)
|
||||||
|
|
||||||
timer = threading.Timer(delay, self.run_automation, args=(automation_id,))
|
timer = threading.Timer(delay, self.run_automation, args=(automation_id,))
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ from core.web_scan_manager import WebScanManager
|
||||||
from core.lyrics_client import lyrics_client
|
from core.lyrics_client import lyrics_client
|
||||||
from database.music_database import get_database
|
from database.music_database import get_database
|
||||||
from services.sync_service import PlaylistSyncService
|
from services.sync_service import PlaylistSyncService
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
from core.matching_engine import MusicMatchingEngine
|
from core.matching_engine import MusicMatchingEngine
|
||||||
from beatport_unified_scraper import BeatportUnifiedScraper
|
from beatport_unified_scraper import BeatportUnifiedScraper
|
||||||
|
|
@ -968,11 +968,11 @@ def _register_automation_handlers():
|
||||||
state = automation_progress_states.get(aid)
|
state = automation_progress_states.get(aid)
|
||||||
if state:
|
if state:
|
||||||
started_at = state.get('started_at')
|
started_at = state.get('started_at')
|
||||||
finished_at = state.get('finished_at') or datetime.now().isoformat()
|
finished_at = state.get('finished_at') or datetime.now(timezone.utc).isoformat()
|
||||||
log_entries = list(state.get('log', []))
|
log_entries = list(state.get('log', []))
|
||||||
else:
|
else:
|
||||||
started_at = datetime.now().isoformat()
|
started_at = datetime.now(timezone.utc).isoformat()
|
||||||
finished_at = datetime.now().isoformat()
|
finished_at = datetime.now(timezone.utc).isoformat()
|
||||||
log_entries = []
|
log_entries = []
|
||||||
|
|
||||||
# Compute duration
|
# Compute duration
|
||||||
|
|
@ -1181,7 +1181,7 @@ def _init_automation_progress(automation_id, automation_name, action_type):
|
||||||
'progress': 0, 'phase': 'Starting...', 'current_item': '',
|
'progress': 0, 'phase': 'Starting...', 'current_item': '',
|
||||||
'processed': 0, 'total': 0,
|
'processed': 0, 'total': 0,
|
||||||
'log': [{'type': 'info', 'text': f'Starting {automation_name}'}],
|
'log': [{'type': 'info', 'text': f'Starting {automation_name}'}],
|
||||||
'started_at': datetime.now().isoformat(),
|
'started_at': datetime.now(timezone.utc).isoformat(),
|
||||||
'finished_at': None,
|
'finished_at': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1202,7 +1202,7 @@ def _update_automation_progress(automation_id, **kwargs):
|
||||||
state[k] = v
|
state[k] = v
|
||||||
# Immediate emit on finish so frontend gets final state without waiting for loop
|
# Immediate emit on finish so frontend gets final state without waiting for loop
|
||||||
if kwargs.get('status') in ('finished', 'error'):
|
if kwargs.get('status') in ('finished', 'error'):
|
||||||
state['finished_at'] = datetime.now().isoformat()
|
state['finished_at'] = datetime.now(timezone.utc).isoformat()
|
||||||
try:
|
try:
|
||||||
socketio.emit('automation:progress', {str(automation_id): dict(state)})
|
socketio.emit('automation:progress', {str(automation_id): dict(state)})
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
||||||
|
|
@ -43515,15 +43515,20 @@ function _autoFormatNotify(type) {
|
||||||
if (type === 'fire_signal') return '\u26A1 Signal';
|
if (type === 'fire_signal') return '\u26A1 Signal';
|
||||||
return type || '';
|
return type || '';
|
||||||
}
|
}
|
||||||
|
function _autoParseUTC(ts) {
|
||||||
|
// If timestamp already has timezone info (+00:00 or Z), parse as-is; otherwise append Z to treat as UTC
|
||||||
|
if (/[Zz]$/.test(ts) || /[+-]\d{2}:\d{2}$/.test(ts)) return new Date(ts).getTime();
|
||||||
|
return new Date(ts + 'Z').getTime();
|
||||||
|
}
|
||||||
function _autoTimeAgo(ts) {
|
function _autoTimeAgo(ts) {
|
||||||
if (!ts) return 'Never';
|
if (!ts) return 'Never';
|
||||||
const d = (Date.now() - new Date(ts + 'Z').getTime()) / 1000;
|
const d = (Date.now() - _autoParseUTC(ts)) / 1000;
|
||||||
if (d < 60) return 'just now'; if (d < 3600) return Math.floor(d/60) + 'm ago';
|
if (d < 60) return 'just now'; if (d < 3600) return Math.floor(d/60) + 'm ago';
|
||||||
if (d < 86400) return Math.floor(d/3600) + 'h ago'; return Math.floor(d/86400) + 'd ago';
|
if (d < 86400) return Math.floor(d/3600) + 'h ago'; return Math.floor(d/86400) + 'd ago';
|
||||||
}
|
}
|
||||||
function _autoTimeUntil(ts) {
|
function _autoTimeUntil(ts) {
|
||||||
if (!ts) return '';
|
if (!ts) return '';
|
||||||
const d = (new Date(ts + 'Z').getTime() - Date.now()) / 1000;
|
const d = (_autoParseUTC(ts) - Date.now()) / 1000;
|
||||||
if (d <= 0) return 'soon'; if (d < 60) return 'in ' + Math.ceil(d) + 's';
|
if (d <= 0) return 'soon'; if (d < 60) return 'in ' + Math.ceil(d) + 's';
|
||||||
if (d < 3600) return 'in ' + Math.ceil(d/60) + 'm'; if (d < 86400) return 'in ' + Math.round(d/3600) + 'h';
|
if (d < 3600) return 'in ' + Math.ceil(d/60) + 'm'; if (d < 86400) return 'in ' + Math.round(d/3600) + 'h';
|
||||||
return 'in ' + Math.round(d/86400) + 'd';
|
return 'in ' + Math.round(d/86400) + 'd';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue