Files
yingshiyun/sync.py
T
yuming b3608f534e feat: 新增视频分析流水线、Web界面、孤儿清理工具;修复健壮性问题
- 新增 analysis/ 模块:MOG2活动检测、YOLO人物跟踪、MediaPipe摔倒检测、Ollama活动打标、缩略图抽取
- 新增 webapp/:Flask事件浏览界面,按需HEVC→H264转码缓存
- 新增 analyze.py:7阶段状态机流水线(scan→backfill→motion→person→fall→activity→thumbnail)
- 新增 cleanup_orphans.py:清理文件已删除的数据库孤儿记录
- 修复:motion/person/activity三阶段加单视频异常兜底,避免批次中断
- 修复:ollama_client加健康检查和3次重试,num_ctx从8192降到2048
- 修复:transcode_cache加fcntl文件锁,防止并发转码同一视频
- 修复:SQLite开WAL模式+busy_timeout,支持读写并发
- 修复:sync.py ffmpeg路径改为shutil.which查找,删除automate.py死代码PYTHON
- 修复:thumbnail失败加3次重试上限,避免无限重试损坏文件
- 新增:requirements.lock锁定版本、README文档
- 更新:.gitignore排除.playwright-mcp、截图、备份文件
2026-09-18 15:52:49 +08:00

159 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
[已废弃,保留作为备选] 萤石CB60 SD卡录像每日同步脚本(开放平台 API 方案)
当前主力下载方案是 automate.py(ADB 自动化模拟器内萤石 App 批量下载)。
本脚本走萤石开放平台 API(open.ys7.com),需要先在开放平台关闭设备码流加密,
且 FLV 回放流拼接的录像不含精确时间戳,因此被 ADB 方案取代。保留仅作备选参考。
"""
import json, os, re, shutil, subprocess, logging
from datetime import datetime
from pathlib import Path
import requests
from config import *
# ====== 路径配置 ======
OUTPUT_DIR = Path(OUTPUT_DIR)
TOKEN_CACHE = Path(TOKEN_CACHE)
LOG_FILE = Path("./sync.log")
# ffmpeg 从 PATH 查找;NAS/Docker 部署时确保 ffmpeg 在 PATH 里即可,无需改代码
FFMPEG = shutil.which("ffmpeg") or "ffmpeg"
BASE = "https://open.ys7.com/api/lapp"
# ====== 日志 ======
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(message)s",
handlers=[
logging.FileHandler(LOG_FILE, encoding="utf-8"),
logging.StreamHandler()
]
)
log = logging.getLogger(__name__)
# ====== Token 管理 ======
def get_token():
if TOKEN_CACHE.exists():
cache = json.loads(TOKEN_CACHE.read_text())
if datetime.now().timestamp() < cache["expire_ts"] - 3600:
log.info(f"使用缓存 Token,有效期至 {cache['expire_str']}")
return cache["token"]
r = requests.post(f"{BASE}/token/get",
data={"appKey": APP_KEY, "appSecret": APP_SECRET})
d = r.json()
if d["code"] != "200":
raise Exception(f"Token 获取失败: {d}")
token = d["data"]["accessToken"]
expire_ts = d["data"]["expireTime"] / 1000
expire_str = datetime.fromtimestamp(expire_ts).strftime("%Y-%m-%d %H:%M:%S")
TOKEN_CACHE.write_text(json.dumps({
"token": token, "expire_ts": expire_ts, "expire_str": expire_str
}))
log.info(f"Token 已刷新,有效期至 {expire_str}")
return token
# ====== 查询录像列表 ======
def get_recording_list(token, start_ms, end_ms):
r = requests.post(f"{BASE}/video/by/time", data={
"accessToken": token,
"deviceSerial": DEVICE_SERIAL,
"channelNo": CHANNEL_NO,
"startTime": start_ms,
"endTime": end_ms,
})
d = r.json()
if d["code"] != "200":
log.warning(f"录像列表查询失败: {d}")
return []
return d.get("data", [])
# ====== 获取 FLV 回放流地址 ======
def get_flv_url(token):
"""
type=2:SD卡回放流,覆盖过去24小时内所有录像片段(服务器拼接成一个 FLV)
protocol=4:HTTP FLV,ffmpeg 可直接下载
"""
r = requests.post(f"{BASE}/v2/live/address/get", data={
"accessToken": token,
"deviceSerial": DEVICE_SERIAL,
"channelNo": CHANNEL_NO,
"quality": 1,
"type": 2,
"protocol": 4,
})
d = r.json()
if d["code"] != "200":
raise Exception(f"FLV 地址获取失败: {d}")
return d["data"]["url"], d["data"]["expireTime"]
# ====== ffmpeg 下载 ======
def download_flv(url, output_path):
output_path.parent.mkdir(parents=True, exist_ok=True)
cmd = [
FFMPEG,
"-i", url,
"-c", "copy",
"-y", str(output_path),
"-v", "warning",
]
log.info(f"开始下载 → {output_path}")
result = subprocess.run(cmd, capture_output=True, text=True)
if not output_path.exists() or output_path.stat().st_size < 1024:
log.error(f"下载失败或文件过小\n{result.stderr[:400]}")
return False
size_kb = output_path.stat().st_size / 1024
log.info(f"下载完成,大小 {size_kb:.0f} KB")
return True
# ====== 主流程 ======
def sync():
log.info("===== 开始同步 =====")
token = get_token()
# 1. 查过去24小时是否有录像
now_ms = int(datetime.now().timestamp() * 1000)
start_ms = now_ms - 86_400_000
recordings = get_recording_list(token, start_ms, now_ms)
if not recordings:
log.info("过去24小时无SD卡录像,退出")
return
log.info(f"发现 {len(recordings)} 段录像")
# 2. 获取 FLV 流(服务器把24小时内所有录像拼接好)
url, expire_time = get_flv_url(token)
log.info(f"FLV URL 有效期至 {expire_time}")
# 3. 文件命名:按 begin 时间戳
begin_match = re.search(r'begin=(\d{14})', url)
begin_str = begin_match.group(1) if begin_match else datetime.now().strftime("%Y%m%d%H%M%S")
date_str = begin_str[:8] # e.g. 20260613
output_path = OUTPUT_DIR / DEVICE_SERIAL / date_str / f"{begin_str}.mp4"
# 4. 已下载则跳过
if output_path.exists() and output_path.stat().st_size > 1024:
log.info(f"已存在 {output_path.name},跳过")
return
# 5. 下载
download_flv(url, output_path)
log.info("===== 同步完成 =====")
if __name__ == "__main__":
sync()