AutoBangumi 3.1 架构演进:RSS Engine 独立化与 Search Engine 插件化全解析

原创2026-09-26 20:19:101,895 阅读
文章标签:后端前端音视频

AutoBangumi 3.1 架构演进:RSS Engine 独立化与 Search Engine 插件化全解析

AutoBangumi 3.1(2023-08)是一次影响深远的架构升级:后端新增了独立管理 RSS 订阅与种子的 RSS Engine 模块、可插拔的 Search Engine 搜索模块,前端同步引入 i18n、PWA 与 RSS 管理页面,并完成前后端仓库合并与数据库层迁移到 sqlmodel。本文以 docs/changelog/3.1.md 为骨架,结合当前仓库源码逐项还原这些能力的实现细节、API 形态与使用方式,帮助读者理解 AutoBangumi 全自动追番流水线的核心骨架,并能在自己的部署中正确配置订阅、搜索与批量管理。

3.1 版本概况:仓库、发布与文档的三线重构

3.1 版本在功能之外做了三项工程化调整,直接决定了后续版本的开发与维护方式:

  • 合并后端和前端仓库,优化项目目录结构:当前仓库根目录下 backend/(FastAPI + 异步 SQLModel 后端)与 webui/(Vue 3 + Vite 前端)并存,正是这次合并的产物。backend/src/module/ 下按 api/、database/、parser/、rss/、searcher/、downloader/、notification/ 等域组织,前后端共用一套发布流程。
  • 优化版本发布工作流程:仓库内 scripts/generate-beta-notes.sh 等脚本承担了版本说明生成等发布辅助工作,保证 changelog 与版本节奏一致。
  • Wiki 迁移到 VitePress:从 3.1 起文档体系迁移到 VitePress 站点,docs/ 目录中的 config/、deploy/、feature/、changelog/ 等分区即是该迁移后的沉淀。

RSS Engine:让 AutoBangumi 独立接管订阅、去重与投递

3.1 最重要的后端能力是新增 RSS Engine 模块——在此之前的版本依赖外部工具维护 RSS 订阅,3.1 起 AutoBangumi 可以独立更新和管理 RSS 订阅,并将种子直接发送到下载器。该模块由 rss/engine.py 中的 RSSEngine 类与 rss/analyser.py 中的 RSSAnalyser 类共同构成,前者负责订阅生命周期与种子投递,后者负责把种子标题解析为番剧/电影数据。

RSSItem 数据模型:一次订阅的核心字段

每一次 RSS 订阅在数据库中对应一条 RSSItem 记录,定义于 models/rss.py:

字段 类型 默认值 说明
id int 自增主键 订阅 ID
name str 无 订阅名称,未指定时由引擎自动从 RSS 源读取 <channel/title>
url str https://mikanani.me RSS 链接地址(带唯一索引)
aggregate bool False 是否聚合订阅源(多字幕组合并订阅)
parser str "mikan" 标题/海报解析器类型,合法值为 mikan、tmdb、parser
enabled bool True 订阅是否启用
connection_status str 无 最近一次抓取连接状态(healthy / error)
last_checked_at str 无 最近一次检查时间(UTC ISO 8601)
last_error str 无 最近一次抓取错误信息

connection_status、last_checked_at、last_error 三个字段让管理员能在 UI 上直接看到每个订阅的健康状况,而不仅是"能不能下载"。

添加订阅:自动读取标题与去重

RSSEngine.add_rss() 是订阅的入口(engine.py)。当用户不提供 name 时,引擎会通过 RequestContent.get_rss_title()(request_contents.py)请求 XML 并提取 <channel/title> 作为订阅名,避免手动命名。

去重逻辑在 database/rss.py 的 RSSDatabase.add() 中按 URL 判断:URL 已存在时直接返回失败;但若该订阅因删除番剧而被自动停用(enabled=False),重新订阅同一 URL 会被视为恢复更新的意图,自动重新启用——避免出现"番剧静默断更"的隐蔽故障。

刷新流水线:抓取、去重、匹配、投递

RSSEngine.refresh_rss()(engine.py)是整条自动追番流水线的心脏,完整流程为:

  1. 取活跃订阅:未指定 rss_id 时通过 db.rss.search_active() 取全部启用的订阅;指定时只刷新单条。
  2. 并发抓取,同主机限速:按 urlparse(item.url).netloc 把订阅按主机分组,asyncio.gather 并行处理不同站点;同一站点内每条订阅间隔 RSS_PER_HOST_DELAY = 2.0 秒(见 engine.py),避免对同一站点瞬间爆发请求触发 HTTP 429 限流。整体并发由 asyncio.Semaphore(5) 控制。
  3. URL 级去重:db.torrent.check_new()(database/torrent.py)按种子 URL 过滤掉数据库已存在的记录,保证"重复订阅的种子不会重复下载"——即使同一个种子同时出现在多个聚合订阅源里,也只处理一次。
  4. 内存匹配:每轮刷新只加载一次活跃番剧列表(db.bangumi.search_all()),逐种子调用 match_torrent() 匹配番剧。匹配命中且通过番剧 filter 排除规则(用 , 分隔的关键词被编译为正则,见 _get_filter_pattern())的种子才挂上 bangumi_id;被排除的种子不会关联番剧,避免影响后续 offset 建议计算。
  5. 偏好去重:对设置了 preferred_group(首选字幕组)或 preferred_resolution(首选分辨率)的番剧,_select_preference_skips() 会按"内容身份 = 媒体类型 + 规范化季度 + 集数"分组,同一集只保留偏好得分最高、修订号更高的版本,多字幕组不再重复下载同一集;未设置偏好的番剧保持"匹配即下载全部"的旧行为。
  6. 投递下载器:通过 client.add_torrent() 投递到 qBittorrent / aria2 等下载客户端。AddResult.FAILED 的种子不入库(否则按 URL 去重后永远不会重试),下一轮刷新时源中仍在则会重新匹配重试,并发出 DownloadFailureEvent 通知;ADDED 与 DUPLICATE(下载器里已有同一种子)都视为成功。
  7. 状态回写:每轮把 connection_status / last_checked_at / last_error 写回数据库,仅在"健康 → 故障"转换时发送 RssFailureEvent,避免对持续故障的订阅每轮重复轰炸通知。

订阅管理 API:增删改查与手动刷新

RSS Engine 的 API 全部集中在 api/rss.py 的 /rss 路由下(均需登录鉴权):

方法 路径 作用
GET /rss 列出全部订阅
POST /rss/add 添加订阅(自动读取标题、按 URL 去重)
PATCH /rss/update/{rss_id} 更新订阅名称/URL/聚合/解析器/启用状态
PATCH /rss/disable/{rss_id} 禁用单个订阅
POST /rss/disable/many 批量禁用
POST /rss/enable/many 批量启用
DELETE /rss/delete/{rss_id} 删除单个订阅(同时清理引用该订阅的种子记录,避免外键约束报错)
POST /rss/delete/many 批量删除
POST /rss/refresh/all 手动刷新全部订阅
POST /rss/refresh/{rss_id} 手动刷新单个订阅
GET /rss/torrent/{rss_id} 查询某订阅已收录的种子
POST /rss/analysis 分析 RSS 链接,返回解析出的番剧/电影数据
POST /rss/collect 按季节收集整季种子
POST /rss/subscribe 将搜索结果订阅为番剧

其中 /rss/refresh/all 与 /rss/refresh/{rss_id} 即 changelog 中提到的"RSS 订阅手动刷新 API",批量端点(/enable/many、/disable/many、/delete/many)即"批量管理 RSS 订阅 API"。对应数据库层的 enable_batch / disable_batch / delete_list 实现见 database/rss.py。

种子到番剧:解析链路

RSSAnalyser(rss/analyser.py)负责把种子标题解析为 Bangumi 或 Movie:

  • torrents_to_data() 逐种子调用 TitleParser.raw_parser() 提取番剧/电影身份,并按 (episode_type, title_raw, season) 身份去重;
  • official_title_parser() 根据订阅的 parser 字段决定标题与海报补全方式:mikan 解析器从 Mikan 主页抓取官方标题与海报,tmdb 解析器则通过 TMDB 按番剧原名、季度、语言本地化标题与海报;
  • 补全后的官方标题会去除 /:.\ 等路径非法字符(re.sub(r"[/:.\\]", " ", ...)),为后续重命名、落盘路径安全兜底。

Search Engine:插件式搜索引擎把"搜索"变成"订阅"

3.1 的第二个新模块是 Search Engine——"按关键词搜索种子并将结果解析为收集或订阅任务"。核心实现位于 searcher/searcher.py 的 SearchTorrent 类,配合 searcher/provider.py 的站点适配层。

插件式搜索源配置

每个搜索源由 ProviderConfig(URL 模板 + 解析器)描述,默认配置定义在 conf/search_provider.py:

{
  "mikan": { "url": "https://mikanani.me/RSS/Search?searchstr=%s", "parser": "mikan" },
  "anibt": { "url": "https://anibt.net/rss/magnets.xml?q=%s", "parser": "tmdb" },
  "nyaa":  { "url": "https://nyaa.si/?page=rss&q=%s&c=0_0&f=0", "parser": "tmdb" },
  "dmhy":  { "url": "http://dmhy.org/topics/rss/rss.xml?keyword=%s", "parser": "tmdb" }
}
  • URL 中的 %s 是搜索关键词占位符,由 search_url() 用 str.replace(而非正则替换)填入拼接后的关键词,避免关键词中的正则特殊字符破坏模板;
  • parser 字段决定搜索结果如何解析:Mikan 源用 mikan 解析器,其余站点统一用 tmdb 解析器做标题本地化与海报补全;
  • 配置文件位于 config/search_provider.json,未存在时首次运行会自动生成默认配置。save_provider() 保存后立即生效(无需重启),并兼容旧版 {site: url} 字符串格式——旧配置中已有站点缺失 parser 字段时按"mikan 站点用 mikan,其余用 tmdb"补齐。

搜索到订阅的完整链路

SearchTorrent.analyse_keyword()(searcher.py)是搜索核心:

  1. 用关键词构造 RSSItem(search_url(site, keywords));
  2. 拉取该站点的 RSS 种子列表;
  3. 逐种子解析为 Bangumi/Movie(此处跳过逐条 Mikan 主页抓取与海报下载,保证交互式搜索响应迅速);
  4. 用 special_url() 根据已解析字段(group_name、title_raw、season_raw、subtitle、source、dpi)反查该番剧在站点上的专属订阅链接;
  5. 按专属链接去重后,通过 TMDB 查询(带 512 条上限的 LRU 缓存)补全本地化标题与海报;
  6. 以 json.dumps 逐条 yield,配合 FastAPI 的 EventSourceResponse 实现流式返回,前端边收边渲染。

搜索结果前端选中后调用 /rss/subscribe 落库为正式订阅。api/rss.py 中的订阅端点有一个易踩的细节:前端传上来的是站点名(nyaa/dmhy),而解析器只认识 mikan/tmdb,因此这里会按搜索源配置把站点名映射为其配置的解析器;已是解析器类型的值(包括与站点同名的 mikan)原样透传,不做站点映射。

字幕组特定规则:订阅粒度的专属配置

3.1 支持"为单独的字幕组配置专属规则"。在 AutoBangumi 中,字幕组信息通过 filter 排除规则与番剧的 preferred_group/preferred_resolution 偏好共同作用:

  • 番剧 filter 用 , 分隔多个关键词,刷新时编译为正则过滤掉不需要的字幕组/版本(_get_filter_pattern(),非法正则自动降级为逐词转义的纯文本匹配);
  • preferred_group 与 preferred_resolution 触发按集的偏好去重(_select_preference_skips()),让"同集只留最优版本";
  • RSSEngine.download_bangumi()(engine.py)在手动下载整部番剧时同样套用 filter 与 release_fits_bangumi() 匹配校验。

IPv6 监听支持

changelog 提到"在环境变量中设置 IPV6=1 即可开启 IPv6 监听"。对应实现见 backend/src/main.py:

if os.getenv("IPV6"):
    host = "::"
else:
    host = os.getenv("HOST", "0.0.0.0")

设置 IPV6=1 后 uvicorn 监听地址从默认的 0.0.0.0(仅 IPv4)切换为 ::(IPv6 双栈),适用于纯 IPv6 或双栈网络环境下的部署。

工程化变更:sqlmodel、无缝更新与统一 API

3.1 同时完成了一批影响运行期稳定性的基础设施改造:

  • 数据库结构改用 sqlmodel 管理:全部表模型(RSSItem、Torrent、Bangumi 等)均由 SQLModel 定义,异步会话封装在 Database 门面(database/combine.py)中,每张表对应一个 *Database 仓储类。SQLite 写事务通过 BEGIN IMMEDIATE 串行化(begin_write()),避免并发写者读到不一致状态。
  • 版本管理与无缝更新:数据库迁移与 schema 版本管理集中在 database/migrations.py,配合 update/ 模块在启动时自动执行迁移,实现软件升级时数据无缝衔接。
  • 统一 API 格式:API 响应统一为 APIResponse/ResponseModel(含 msg_en 与 msg_zh 双语消息字段),u_response() 帮助函数统一封装成功/失败返回。
  • API 响应语言选项:双语消息机制配合前端 i18n,不同语言环境的用户看到对应语言的错误提示。
  • 数据库 mock 测试:3.1 起引入数据库层 mock 测试,仓库 backend/test/ 下 test_database*.py、test_engine_wal.py 等即是对该基础设施的持续验证。

前端:i18n、PWA 与 RSS 管理页

3.1 前端的四项变化同样可以在当前仓库中验证:

  • i18n 支持:webui/src/i18n/ 下的 zh-CN.json 与 en.json 即 3.1 引入的双语词条,由 useMyI18n 组合式函数驱动。
  • PWA 支持:webui/public/ 下的 pwa-192.png、pwa-512.png 图标与 webui/vite.config.ts 中的 PWA 插件配置共同提供离线缓存与安装能力。
  • RSS 管理页面:webui/src/pages/index/ 下的订阅管理视图对接 /rss 系列 API,支持列表查看、启停、删除与手动刷新。
  • 搜索顶栏:webui/src/components/search/ 组件对接流式搜索 API,展示实时搜索结果并可一键订阅。

小结

AutoBangumi 3.1 的价值在于把"追番"这件事的关键链路全部收归自身:RSS Engine 让订阅管理、去重、投递形成闭环,Search Engine 让"搜索到订阅"成为一条可配置的插件化流水线,sqlmodel 数据库层与版本迁移机制则为此后每一版的无缝升级奠定了基础。理解这一版的架构,就理解了 AutoBangumi 后续版本"聚合订阅 → 解析匹配 → 偏好去重 → 投递下载"这条核心管线的骨架——无论是排查断更、调优字幕组偏好,还是扩展搜索源,都可以从本文涉及的 engine.py、searcher.py、search_provider.py 与 api/rss.py 出发深入探索。

登录后查看全文
Auto_Bangumi