AutoBangumi 3.1 架构演进:RSS Engine 独立化与 Search Engine 插件化全解析
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)是整条自动追番流水线的心脏,完整流程为:
- 取活跃订阅:未指定
rss_id时通过db.rss.search_active()取全部启用的订阅;指定时只刷新单条。 - 并发抓取,同主机限速:按
urlparse(item.url).netloc把订阅按主机分组,asyncio.gather并行处理不同站点;同一站点内每条订阅间隔RSS_PER_HOST_DELAY = 2.0秒(见 engine.py),避免对同一站点瞬间爆发请求触发 HTTP 429 限流。整体并发由asyncio.Semaphore(5)控制。 - URL 级去重:
db.torrent.check_new()(database/torrent.py)按种子 URL 过滤掉数据库已存在的记录,保证"重复订阅的种子不会重复下载"——即使同一个种子同时出现在多个聚合订阅源里,也只处理一次。 - 内存匹配:每轮刷新只加载一次活跃番剧列表(
db.bangumi.search_all()),逐种子调用match_torrent()匹配番剧。匹配命中且通过番剧filter排除规则(用,分隔的关键词被编译为正则,见_get_filter_pattern())的种子才挂上bangumi_id;被排除的种子不会关联番剧,避免影响后续 offset 建议计算。 - 偏好去重:对设置了
preferred_group(首选字幕组)或preferred_resolution(首选分辨率)的番剧,_select_preference_skips()会按"内容身份 = 媒体类型 + 规范化季度 + 集数"分组,同一集只保留偏好得分最高、修订号更高的版本,多字幕组不再重复下载同一集;未设置偏好的番剧保持"匹配即下载全部"的旧行为。 - 投递下载器:通过
client.add_torrent()投递到 qBittorrent / aria2 等下载客户端。AddResult.FAILED的种子不入库(否则按 URL 去重后永远不会重试),下一轮刷新时源中仍在则会重新匹配重试,并发出DownloadFailureEvent通知;ADDED与DUPLICATE(下载器里已有同一种子)都视为成功。 - 状态回写:每轮把
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)是搜索核心:
- 用关键词构造
RSSItem(search_url(site, keywords)); - 拉取该站点的 RSS 种子列表;
- 逐种子解析为
Bangumi/Movie(此处跳过逐条 Mikan 主页抓取与海报下载,保证交互式搜索响应迅速); - 用
special_url()根据已解析字段(group_name、title_raw、season_raw、subtitle、source、dpi)反查该番剧在站点上的专属订阅链接; - 按专属链接去重后,通过 TMDB 查询(带 512 条上限的 LRU 缓存)补全本地化标题与海报;
- 以
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 出发深入探索。