kairee e2e4dc3336 fix(telegram): dedupe candidates before sending
Deduplicate candidates by aid at the notifier boundary so duplicate queue entries cannot produce repeated message bodies and button rows. The discovery layer already deduped by aid; this adds defense in depth with a regression test.
2026-10-31 21:55:10 +08:00

bili-fav

咻咻满 Reaction 视频数据看板与收藏夹管理工具。

项目以 B 站年度收藏夹为可信视频来源,补充关键词发现和 Telegram 人工确认,持续采集 reaction 视频的七项数据与咻咻满粉丝数,并提供本地数据看板。现有的收藏夹排序工具仍保留并作为一键加入后的排序能力基础。

当前状态:P0(数据底座与收藏夹同步)、P1(快照与粉丝追踪)、P3(Telegram 与一键加入)已实施;P2 待开始。

当前已可用:扫码授权、收藏夹读取、排序计划生成、执行与校验;SQLite 建库迁移、收藏夹首次导入与增量同步、失效识别、黑名单种子;每日七项视频快照、粉丝数快照、失败占位、断点续跑与日备份;搜索发现、候选池、Telegram 通知与收藏/忽略/拉黑回调。

详细业务口径、接口验证和风险记录见 project.md;开发架构与实施计划见 DEVELOPMENT.md。

功能概览

功能 状态 说明
收藏夹排序 可用 同一 UP 连续,组内按发布时间倒序,组间按该 UP 最早视频发布时间倒序。
扫码授权与 Cookie 续期 可用 本地生成二维码;更新身份 Cookie 时保留设备指纹。
收藏夹同步与首次导入 可用(P0) 监控已配置年度夹,自动发现人工新收藏的视频与失效资源。
视频日快照 可用(P1) 逐条采集播放、点赞、投币、收藏、转发、评论、弹幕。
粉丝追踪 可用(P1) 每日记录咻咻满等配置 UP 的粉丝数。
搜索发现与候选池 可用(P3) 每小时搜索前 2 页,分类打分后写入候选池。
Telegram 通知与一键加入 可用(P3) 收藏 / 忽略 / 拉黑按钮,deal + sort 回读校验。
本地数据看板 规划(P2) 总览、榜单、视频/UP 详情、粉丝曲线、候选池和系统状态。

核心工作流

关键词搜索(补漏) ──► 候选池 / Telegram 人工确认 ──► 收藏夹 + 视频库
                                                      │
人工加入年度收藏夹 ──► 收藏夹同步(真相源)───────────┤
                                                      ▼
                                              每日指标与粉丝快照
                                                      ▼
                                                   本地看板
  • 收录范围:只收录他人观看、试听或点评咻咻满内容的 reaction 视频;最终是否收录由人工确认。
  • 收藏夹是真相源:手工加入已配置年度夹的视频自动入库;关键词搜索仅负责发现候选。
  • 黑名单仅作用于发现:已收藏或已入库的黑名单 UP 视频不会被清理,也会继续追踪。
  • 年度夹不自动创建或切换:同一 UP 的新视频优先进入该 UP 最早视频所在的收藏夹;容量接近上限时预警,失败时告警,不擅自改夹。
  • 数据不回填:B 站不提供任意日期的历史指标;新视频从首次入库日起开始积累快照。

本地开发环境

项目默认使用 Python 3.14 和 uv。本地开发、测试和现有排序工具的运行均不依赖 Docker、Node.js、数据库服务或宿主机 cron。

# 在项目根目录初始化或同步环境
uv sync --all-groups

# 确认锁定的解释器版本
uv run python --version

# 查看现有 CLI 帮助
uv run bili-fav help

完整的 uv 使用方式、依赖规范、架构与开发约定见 DEVELOPMENT.md。

P0:数据底座与收藏夹同步

P0 已实现 SQLite 迁移、配置热加载、B 站只读客户端、收藏夹首次导入与增量同步、infos 全量元数据刷新、失效识别、容量预警和黑名单种子。所有请求均为只读,不会修改收藏夹。

# 建库、执行迁移、写入配置文件夹与黑名单种子(离线)
uv run python app.py init

# 首次导入或增量同步配置中的年度收藏夹(只读联网)
uv run python app.py sync

# 按配置周期常驻运行收藏夹同步 + 每日快照;--once 只跑一次到期任务
uv run python app.py run
uv run python app.py run --once

# 强制全量刷新元数据(每 6 小时自动执行一次)
uv run python app.py sync --full

# 查看库内状态(离线)
uv run python app.py status
  • 数据写入 data/app.db(SQLite WAL);升级已有数据库前会自动备份到 data/backup/;
  • 内置调度器按 schedule.folder_sync_minutes 周期运行,run 常驻、sync 单次;
  • infos 静默缺失或 attr=9 的资源标记为 removed,保留历史;
  • 从收藏夹移除的资源标记为 unfaved,仍保留数据;
  • 黑名单只作为发现阶段的种子写入,不会过滤收藏夹同步;
  • 收藏夹达到 950 条时预警,系统绝不自动换夹;
  • 远端返回 0 条但本地仍有记录时默认按异常跳过差集,确认收藏夹确实被清空后才使用 --allow-empty-folder。

P1:每日快照与粉丝追踪

P1 已实现每日七项视频指标快照、咻咻满粉丝数快照、失败占位、断点续跑、Cookie 失效告警和 SQLite 日备份。

# 采集当日快照与粉丝数(只读联网,约 18~20 分钟 / 795 条)
uv run python app.py snapshot

# 忽略当日完成标记,重新采集
uv run python app.py snapshot --force

# 分别为只读和写入账号扫码授权(默认 read)
uv run python app.py auth
uv run python app.py auth --account write

# 检查指定账号或两套账号的登录态
uv run python app.py auth-check --account read
uv run python app.py auth-check --account all
  • snapshot 对 active / unfaved 视频逐条请求 view,每条请求间隔 1.0~1.6 秒;
  • 成功写入 snapshot 的七项指标;同日 ok=1 的记录直接跳过,可断点续跑;
  • 当天已完成后新入库或从 dropped 恢复的视频会立即补采当天快照,不必等到次日;
  • 视频不可访问(-404 / 62002 / 62012)标记为 removed;
  • 其他错误重试 3 次,仍失败写入 ok=0 和错误信息,不写伪造的零值;
  • 粉丝数写入 up_stat,失败同样写 ok=0;续跑时已成功的 (stat_date, mid) 会跳过,不会被失败结果覆盖;
  • 当日全部视频与粉丝采集成功后写入完成标记;服务启动时若当天未完成会自动补跑;
  • 每次采集后生成 data/backup/app-YYYYMMDD.db,按 storage.backup_keep_days 清理旧日备份;
  • 备份失败时 snapshot 返回 1 便于任务告警;再次执行只重试备份,不重复请求 B 站;
  • Cookie 失效只告警,不阻塞匿名 view 采集;P0/P1 只使用 bilibili.read。

首次真实全量采集结果:795 条视频全部成功、0 失败、0 移除,耗时约 19 分钟;粉丝 follower=2767270;日备份为 data/backup/app-20261031.db。

P3:搜索发现与 Telegram 一键加入

P3 已实现 WBI 搜索发现、候选池、Telegram 通知与按钮回调,以及使用写账号完成的 deal / sort 加入流程。

# 执行一次发现;--no-notify 只写候选池,不发 Telegram
uv run python app.py discover
uv run python app.py discover --no-notify

# 查看候选池(离线)
uv run python app.py candidates
uv run python app.py candidates --decision rejected

# 常驻运行:同步 + 快照 + 发现 + Telegram long polling
uv run python app.py run
  • 搜索使用 x/web-interface/wbi/search/type,WBI 密钥缓存 6 小时,失败时降级旧版 search/type;
  • 每轮按 discover.keywords 搜索前 discover.pages 页,过滤已入库/已见/超时间窗/黑名单/标题不含要求词的结果;
  • 分数规则与 classify 配置一致;score >= notify.min_score 才发送 Telegram;
  • 通知按钮:收藏、忽略、拉黑;回调只接受 telegram.chat_id 对应的私聊用户;
  • 收藏使用 bilibili.write:先查目标夹是否已存在,再 deal 加入、sort 插入同 UP 组头、回读校验位置;
  • 失败保留候选并写 fav_action.state='failed',可直接重试;不自动换夹;
  • 忽略写 candidate.decision='rejected';拉黑写 up_blacklist 并拒绝该 UP 的其他待决策候选;
  • 候选超过 schedule.candidate_ttl_days 且未决策时自动删除,seen_aid 防止重复通知;
  • 人工把候选加入收藏夹后,收藏夹同步会入库并删除同 aid 候选。

真实 deal / sort 需要用户点击 Telegram 按钮才会触发;离线测试使用契约一致的 Mock 覆盖。

配置

复制样例配置,并填写 B 站账号信息:

Copy-Item config.example.json config.json

config.json 已被 .gitignore 忽略,不得提交。配置必须使用双账号格式:

{
  "bilibili": {
    "read": { "mid": 0, "cookie": "...", "csrf": "..." },
    "write": { "mid": 0, "cookie": "...", "csrf": "..." }
  }
}
  • bilibili.read:年度收藏夹同步、视频数据快照、粉丝数与其他只读查询使用;
  • bilibili.write:后续加入目标收藏夹、回读验证与现有 favsort.py 排序使用;
  • 两个账号的 Cookie、CSRF 与设备指纹严格隔离;顶层 mid / cookie / csrf 不再支持;
  • overrides:可选,为代发或联合投稿视频指定归属 UP;
  • folders:年度收藏夹配置(year + media_id + title),系统不自动建夹;
  • up_blacklist_seed:首次启动写入数据库的 UP 黑名单种子;
  • track_fans:需要每日追踪粉丝数的 UP mid;
  • discover:搜索关键词、页数与时间窗(keywords / pages / window_days);
  • storage:数据库与备份目录;
  • schedule / notify / classify / heat_weights:任务节奏、通知阈值、分类规则和热度权重。

配置每 60 秒检查一次 mtime,凭据、收藏夹、分类规则等变更无需重启即可在下次任务读取;但 schedule.folder_sync_minutes 与 schedule.snapshot_at 在启动时固化,修改后需重启 run 进程。

完整配置口径以 project.md 和 DEVELOPMENT.md 为准。

当前可用:收藏夹排序 CLI

所有命令建议通过 uv run 执行:

# 查看全部命令;无需 config.json
uv run bili-fav help

# 为写账号扫码登录与检查状态(认证入口统一在 app.py)
uv run python app.py auth --account write
uv run python app.py auth-check --account write

# 列出本人创建的收藏夹
uv run bili-fav folders

# 只读:拉取收藏夹,生成排序计划
uv run bili-fav plan <media_id> -v

# 查看或离线重算已有计划
uv run bili-fav show <media_id>
uv run bili-fav replan <media_id>

# 写操作:按计划逐步排序;完成后校验
uv run bili-fav run <media_id>
uv run bili-fav verify <media_id>

plan 会生成 data/plan-<media_id>.json,其中含原始顺序、目标顺序、移动脚本及视频元数据缓存。run 直接使用该计划,不会重复请求 infos;如排序算法变更,可用 replan 离线重算。

排序规则

  1. 同一 UP 必须连续:按 upper.mid 分组,组内按发布时间倒序;发布时间相同则按 avid 升序。
  2. 组间按最早投稿倒序:取该 UP 在当前收藏夹中最早视频的发布时间作为组键,按组键倒序;并列时按组内最小 avid 升序。
  3. 失效或异常资源置底:保持它们原有的相对顺序。
  4. 可配置归属修正:config.json 的 overrides 可为指定视频指定实际归属 UP。

组间规则是有意设计:某个 UP 只要存在较老视频,即使近期继续投稿,整个分组也会靠后。

扫码授权

当接口返回 -101 或 Cookie 即将过期时,使用统一认证入口:

uv run python app.py auth --account read
uv run python app.py auth --account write
uv run python app.py auth-check --account all

授权流程在本地生成二维码图片并等待手机客户端扫码确认。成功后:

  • 更新 SESSDATA、bili_jct、DedeUserID、sid 等身份字段;
  • 保留 buvid3、buvid4、_uuid、buvid_fp 等设备指纹;
  • 将选中账号的 csrf 同步为新的 bili_jct,并从 nav 回写该账号实际 mid;
  • 原子写入 config.json,并保留 config.json.bak;
  • 通过 x/web-interface/nav 验证新登录态,失败时自动回滚;不会修改另一个账号。

常用选项:

uv run python app.py auth --account write --no-browser
uv run python app.py auth --account read --timeout 180

B 站访问与安全约束

接口 当前用途 身份 Cookie
fav/resource/ids / infos 年度收藏夹同步与排序计划 read 用于 P0;favsort.py 计划使用 write。
view / relation/stat / nav P1 快照与 read 登录态 read。
fav/folder/created/list-all 列出目标自建收藏夹 write。
fav/resource/deal / sort 加入收藏夹、收藏夹排序 write,需要完整 Cookie 和 CSRF。

只读客户端对 ids / infos 默认剥离 read 身份 Cookie,仅保留该账号设备指纹;如果失败才使用完整 read Cookie 重试。写操作一律使用完整 write Cookie 与 CSRF。

x/v3/fav/resource/sort 的实测语义为:

sort=A:2:B:2  →  将 B 插到 A 之后

运行排序时会重新读取实际顺序、逐步回读校验,并在收藏夹内容于执行期间变动时中止,避免用过期计划强制排序。写操作存在风险,请确认计划后再执行;中断后可直接再次运行 run 继续收敛。

测试

测试全部离线运行,外部接口只在 transport 边界用真实响应形状的 Mock 替换:

uv run python tests/test_auth.py            # Cookie 合并、设备指纹、Set-Cookie 解析
uv run python tests/test_moves.py           # 排序移动脚本:随机排列与头部用例
uv run python tests/test_config.py          # 配置加载、校验、ENV 覆盖与热加载
uv run python tests/test_migrations.py      # 迁移、备份、回滚与保留期
uv run python tests/test_database.py        # 单写队列、事务与并发
uv run python tests/test_repository.py      # 收藏夹/视频状态机与黑名单
uv run python tests/test_bilibili_client.py # ids/infos/view/fans/nav 契约与重试
uv run python tests/test_classify.py        # 标题归一化、时长解析与打分
uv run python tests/test_search.py          # WBI 签名、搜索解析与降级
uv run python tests/test_candidates.py      # 候选池、去重、通知与 fav_action
uv run python tests/test_discovery.py       # 发现过滤、候选池与通知批次
uv run python tests/test_fav_position.py    # 新视频插入位置
uv run python tests/test_favorites.py       # 收藏/忽略/拉黑与失败重试
uv run python tests/test_telegram.py        # 消息、分片、长轮询与回调授权
uv run python tests/test_folder_sync.py     # 首次导入、差集、失效识别、容量预警
uv run python tests/test_snapshot.py        # 七项快照、补采、断点续跑、粉丝、备份重试
uv run python tests/test_maintenance.py     # 日备份与保留期
uv run python tests/test_timeutil.py        # 上海时区与每日任务时间
uv run python tests/test_scheduler.py       # 内置调度器到期/失败隔离
uv run python tests/test_provider.py        # 凭据变化重建客户端与配置 revision
uv run python tests/test_favsort_run.py     # 排序参数校验与混合资源类型
uv run python tests/test_logging.py         # JSON 日志脱敏
uv run python tests/test_cli.py             # CLI 离线路径

运行期文件

config.json.bak                最近一次 auth 更新前的配置备份
data/plan-<media_id>.json     排序计划、顺序快照和元数据缓存
data/journal-<media_id>.jsonl 排序执行日志
data/app.db                    SQLite 数据库(收藏夹、视频、快照、粉丝、黑名单)
data/backup/app-YYYYMMDD.db    每日快照后的 SQLite 备份
data/backup/app-pre-v*.db      数据库迁移前的自动备份

文档索引

  • project.md:完整可行性分析、业务口径、接口验证、数据模型草案和风险清单。
  • DEVELOPMENT.md:开发架构、数据库迁移、API、调度、Telegram 与分阶段实施计划。
S
Description
No description provided
Readme
2.6 MiB
0 Stars 2 Watchers 0 Forks
Languages
Python 83.4%
JavaScript 13%
CSS 3.2%
HTML 0.2%
Shell 0.1%
Other 0.1%