2026-07-18 10:20:43 +08:00
|
|
|
from __future__ import annotations
|
2026-07-10 07:25:18 +08:00
|
|
|
|
2026-07-18 10:20:43 +08:00
|
|
|
import re
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
from module.config.config import AzurLaneConfig
|
2026-07-10 07:25:18 +08:00
|
|
|
from module.logger import logger
|
|
|
|
|
from module.smart_mgmt.downtime_fetcher import DowntimeFetcher, FetchError
|
2026-07-18 10:20:43 +08:00
|
|
|
from module.smart_mgmt.fetcher_test import DowntimeFetcherTest
|
2026-07-10 07:25:18 +08:00
|
|
|
|
|
|
|
|
|
2026-07-18 10:20:43 +08:00
|
|
|
DOWNTIME_FORMAT_RE = re.compile(
|
|
|
|
|
r'(\d{4})-(\d{1,2})-(\d{1,2}) '
|
|
|
|
|
r'(\d{1,2}):(\d{2})~(\d{1,2}):(\d{2})',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_downtime(window: str) -> Optional[datetime]:
|
|
|
|
|
"""
|
|
|
|
|
Args:
|
|
|
|
|
window (str): Window string in format 'YYYY-MM-DD HH:MM~HH:MM'.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Optional[datetime]:
|
|
|
|
|
End datetime parsed from the window, or None if no pattern matched.
|
|
|
|
|
"""
|
|
|
|
|
match = DOWNTIME_FORMAT_RE.search(window)
|
|
|
|
|
if not match:
|
|
|
|
|
logger.warning(f'Failed to parse downtime window: {window}')
|
|
|
|
|
return None
|
|
|
|
|
year, month, day = int(match.group(1)), int(match.group(2)), int(match.group(3))
|
|
|
|
|
end_hour, end_minute = int(match.group(6)), int(match.group(7))
|
|
|
|
|
end_dt = datetime(year, month, day, end_hour, end_minute)
|
|
|
|
|
logger.info(f'Parsed downtime window: end={end_dt:%Y-%m-%d %H:%M}')
|
|
|
|
|
return end_dt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def update_downtime(config: AzurLaneConfig, force_refresh=False) -> bool:
|
|
|
|
|
"""Fetch downtime and write config."""
|
|
|
|
|
try:
|
|
|
|
|
payload = DowntimeFetcher().run(force_refresh=force_refresh)
|
|
|
|
|
except FetchError as e:
|
|
|
|
|
# Expected operational failure degrade
|
|
|
|
|
logger.warning(f'Failed to fetch downtime: {e}')
|
|
|
|
|
return False
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# Unexpected programming error; full traceback for diagnosis
|
|
|
|
|
logger.exception(f'Unexpected error: {e}')
|
|
|
|
|
return False
|
|
|
|
|
with config.multi_set():
|
|
|
|
|
config.cross_set('DowntimeMgmt.Downtime.Window', payload.get('window', ''))
|
|
|
|
|
config.cross_set('DowntimeMgmt.Downtime.Title', payload.get('title', ''))
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_downtime_end(config: AzurLaneConfig) -> Optional[datetime]:
|
|
|
|
|
task = config.task.command
|
|
|
|
|
if not config.cross_get(f'DowntimeMgmt.DowntimeStrategy.Manage{task}', False):
|
|
|
|
|
return None
|
|
|
|
|
if config.cross_get('DowntimeMgmt.DowntimeStrategy.AutoFetch', False):
|
|
|
|
|
if config.SERVER == 'cn':
|
|
|
|
|
update_downtime(config)
|
2026-07-10 07:25:18 +08:00
|
|
|
else:
|
2026-07-18 10:20:43 +08:00
|
|
|
logger.warning('Auto-fetch skipped as fetcher is for CN only, use window from config')
|
|
|
|
|
else:
|
|
|
|
|
logger.info('Auto-fetch disabled, use window from config')
|
|
|
|
|
window = config.cross_get('DowntimeMgmt.Downtime.Window', None)
|
|
|
|
|
if not window:
|
|
|
|
|
return None
|
|
|
|
|
return parse_downtime(window)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_downtime_fetch(config: AzurLaneConfig):
|
|
|
|
|
# Tool task entry is invoked with config bound to Alas by default,
|
|
|
|
|
# rebind to DowntimeFetch.
|
|
|
|
|
config.init_task('DowntimeFetch')
|
|
|
|
|
if config.DowntimeFetch_FetcherTest:
|
|
|
|
|
DowntimeFetcherTest().run()
|
|
|
|
|
else:
|
|
|
|
|
if config.SERVER != 'cn':
|
|
|
|
|
logger.warning('Downtime fetch targets CN Bilibili notices, data may be irrelevant on other servers')
|
|
|
|
|
update_downtime(config, force_refresh=True)
|