feat(p1): add daily snapshots and fan tracking
Implement P1: seven-metric view snapshots, relation/stat fan snapshots, failure placeholders, resume by daily completion markers, Cookie login warnings, daily SQLite backups, Shanghai daily scheduling, and CLI snapshot/auth-check commands. Real read-only run captured 795/795 videos successfully over 1124s with a verified daily backup.
This commit is contained in:
1 parent
ee289bfa56
commit
5c2ab7b187
20 files changed
+1680
-54
No files matched your search
@@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""上海时区与每日任务时间计算。
|
||||
|
||||
B 站数据口径使用 ``Asia/Shanghai``。中国自 1991 年起不再使用夏令时,因此这里
|
||||
用固定 UTC+8,避免 Windows 上依赖未安装的 ``tzdata`` 包。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
SHANGHAI_TZ = timezone(timedelta(hours=8))
|
||||
_HHMM_RE = re.compile(r"^([01]\d|2[0-3]):([0-5]\d)$")
|
||||
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||
|
||||
|
||||
def shanghai_datetime(ts: float | None = None) -> datetime:
|
||||
return datetime.fromtimestamp(time.time() if ts is None else ts, SHANGHAI_TZ)
|
||||
|
||||
|
||||
def shanghai_date(ts: float | None = None) -> str:
|
||||
return shanghai_datetime(ts).strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def parse_hhmm(value: str) -> tuple[int, int]:
|
||||
match = _HHMM_RE.match(value or "")
|
||||
if not match:
|
||||
raise ValueError(f"时间必须是 HH:MM(24 小时制): {value!r}")
|
||||
return int(match.group(1)), int(match.group(2))
|
||||
|
||||
|
||||
def next_daily_run(now: float, hhmm: str) -> float:
|
||||
"""返回 ``now`` 之后(严格大于)最近的 ``hh:mm`` 上海时间时间戳。"""
|
||||
hour, minute = parse_hhmm(hhmm)
|
||||
current = shanghai_datetime(now)
|
||||
target = current.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
if target.timestamp() <= now:
|
||||
target += timedelta(days=1)
|
||||
return target.timestamp()
|
||||
|
||||
|
||||
def is_date(value: str) -> bool:
|
||||
if not _DATE_RE.match(value or ""):
|
||||
return False
|
||||
try:
|
||||
datetime.strptime(value, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def day_bounds(date: str) -> tuple[float, float]:
|
||||
"""返回上海时区某一天的 [start, end) 时间戳。"""
|
||||
if not is_date(date):
|
||||
raise ValueError(f"日期格式必须是 YYYY-MM-DD: {date!r}")
|
||||
start = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=SHANGHAI_TZ)
|
||||
return start.timestamp(), (start + timedelta(days=1)).timestamp()
|
||||
Reference in new issue
Block a user