Intro

从 share.dmhy.org 搜索数据已经是我不再续费大会员之后的主要替代品了,虽然从这里可以检索到很多资源,但如何获取资源本身,也是一个问题。

DMHY 提供的最普遍的下载方式,是通过 Magnet 链接下载的。它有很多优点,也有很多缺点,在我的使用中,最明显的缺点就是:死链接

缺少热度的 Torrent 因为没人做种而变成死种子是很普遍的现象。

但有没有一种办法能尽量避免找到死链接呢?有没有一种方法能将搜索和 Magnet 健康检查结合起来呢?

为了解决这样的问题,我设计了 AnimeSearch

AnimeSearch API

结构化爬取数据

从 DMHY 抓取数据本身并不算太困难,但结构化这些数据比较困难。通常来说,根据可以直接获取的数据,我们定义一个数据结构 TorrentItem,它包含以下字段:

  • published_at: 发布时间
  • category: 分类
  • title: 原始标题
  • fansub: 字幕组
  • anime_title: 动漫标题
  • url: 详情页面链接
  • magnet: 磁力链接
  • size: 文件大小
  • seeders/leechers/downloads: 种子/下载数据
  • uploader: 上传者

DMHY 的搜索功能设计的非常利于我们抓取数据,用法基本就是 https://share.dmhy.org/topics/list?keyword=<keyword>,只要写一个爬虫来按照字段(部分原始字段可以通过解析详情 HTML 获取)抓取就可以了。

但字幕组字段并不是直接可以获取的,上传者是一个替代选项,因为上传者的名称不一定就是字幕组,所以我们用正则来解析约定俗成的 [funsub group] 即可得到字幕组,但解析这个字段的 parse_title() 健壮性不会特别强,因为输入的格式不是总是一致的。

同理,没有设计其他涉及原始标题的解析,比起字幕组这种字段,其它字段不一致的程度更高,所以选择了直接呈现原始标题。

无状态

因为需要部署到无服务器平台,对于服务本身就有一定限制。比如会话是动态创建,动态销毁的,如果需要添加缓存或者存储,也需要同样的无服务器数据库。

不过…一旦涉及到数据库,麻烦就开始多了起来,数据结构设计、生命周期管理、状态和同步…绝对会让复杂度暴增。所以考虑到用户规模很小,以及对上游的压力不大,就将 AnimeSearch API 设计成无状态的了。但为了保持整个服务的无状态,需要它来代理和转发另一个需要实例持久化的服务 MagnetCheck,下文会提到。

在生产环境(Fluid Compute)测试中,内存占用峰值为 261 MB, 使用了不到 0.1 的 vCPU, 最慢可以在 3s 完成请求,具体时间取决于源站。

MagnetCheck API

什么样的Magnet是健康的?

要评估一个 Magnet 的健康情况,我们需要一些指标。由于后端是通过 libtorrent 实现的,考虑到复杂度和成本,设计了以下四个指标:

  • 5s 内可以连接到 DHT 数量
  • 5s 内可以连接的 Peer 数量
  • 获取 metadata 所需时间
  • 总耗时

每个请求的生命周期共计 15s

不过如何通过这些指标评估健康程度,仍旧是一个难题。为了避免评分可能带来的误判问题,API 会返回获得的原始数据,我们的前端也会按原样展示。

libtorrent会话管理和指标采集

在 Python 中可以直接导入 libtorrent, 虽然设计成无状态的更好,但它的特性决定了运行它的实例必须持久化。因为连接到 DHT 需要时间预热,不然是无法在一个生命周期中完成检测任务的。但因为 libtorrent 本身的特性,只需要维持一个 libtorrent 会话即可执行多次查询。

我们用 SessionManager 类来管理 libtorrent 的生命周期,当查询请求从 FastAPI 传入时,使用 validate_magnet_uri() 来检查 Magnet 格式正确,再通过 libtorrent 的 session.add_torrent() 来创建任务,随后通过封装好的函数 run_health_check() 来组合调用 libtorrent 获取需要查询的指标写入 results,最后再清除任务并返回结果。

在每一个 MagnetCheck 实例中,只运行一个全局的 libtorrent 会话,目前运行在 FastAPI Cloud 上,但不活跃的实例会被停止,冷启动性能不理想,在使用量较少时可能会出现 P99 等待时间很差的情况,实测中的最坏情况在 7s 左右,我猜是肥尾分布。但好在实例预热成功之后方差非常小,绝大多数请求可以按时完成。

虽然 run_health_check() 中的绝大部分操作是异步进行的,但并发性能仍然受制于 libtorrent, 预测的单个实例并发性能非常弱,最大并发数在 5~10 之间。但由于实例比较轻,只使用约 55 MB 内存(平均)以及约 0.8 的 vCPU(峰值),可以运行多个实例并使用负载均衡。目前正在提供服务的有两个实例,是动态扩容的。估计公共 API 的最大并发数在 15 左右。

鉴权和代理

由于 MagnetCheck API 非常贵,它本身不能随便调用,所以我们通过简单的 API_KEY 来鉴权请求,通过 AnimeSearch API 代理的请求会携带密钥,其它请求均会返回 401.

关于速率限制,目前没有硬性限制,但客户端会有软限制。在后端也有速率限制,超速会返回 429,但这是在代码内部实现的,如果使用量过大,会切换到网关进行限速。

扩容

如果使用量较大,可以再用一个带有缓存的代理 API 代理并缓存 AnimeSearch 的搜索结果。因为这些 API 本身都是无状态的,组合它们实现高级功能非常简单。

AnimeSearch 前端

为了配合后端的特性,前端对于搜索结果也是不缓存的,每次都需要重新搜索,同时也不能直接分享链接。不过前端对于 Magnet 健康检查是有本地缓存结果的,也可以去 Application 里面删除网站数据重新获取数据,这也是为了降低 MagnetCheck 负载的无奈之举。前端是有限速的,请求频率过高会触发人机验证。

由于没有任何服务端缓存,API 提供的数据也是非常新鲜,因为每次搜索行为都是重新抓取。在使用量不大的情况下应该不会对上游产生太大压力。

整个前端都是 Lovable 生成的,从网站的 ICON 也能看出来,不过性能意外的好。我不是很懂前端,就不说太多了。

尾语

源代码、API文档见 GitHub:naranyinyun/AnimeSearch

如果喜欢,欢迎star

本站前端见:

 

文章也发表在 Nalanyinyun's Library 上:

 

关于本站的邮件订阅服务,参见:

 

感谢阅读、感谢使用。