lostars
发布于 2024-06-19 / 202 阅读
0

qbittorrent 搜索插件调试

开始之前

调试的插件:bt4gprx ,来自于 非官方插件目录
qbittorrent版本: docker latest 来自于 qbittorrentee
该插件搜索没有任何输出,且qbittorrent日志目录无任何输出,官方仓库相关issue:#20104

马后炮:
downloadtorrentfile.com域名可以直连,部分机场代理是屏蔽torrent关键字的。
docker qbittorrent搜索插件调用的是webui中选项-连接-代理服务器配置的代理,docker环境变量中配置的代理是无法使用的。
qbitttorrent插件安装位置:.../config/qBittorrent/data/nova3/engines 。注意该文件夹下的插件都有缓存,并且在docker容器重启之后会还原所有改动,所以要修改最好停止容器后进行修改或者外部修改之后重新安装插件。
qbittorrent插件可用于调试的程序入口:.../config/qBittorrent/data/nova3/nova2.py。该入口可调试所有已安装插件,使用方法:

# 该调试方式会使用配置于环境变量的代理
nova2.py [engines] [category] [keywords]

debugging

从上面的issue中猜测没有搜索结果很有可能是网络问题,然后是插件本身问题,内部报错或者解析网站失败等等。
将qbittorrent配置文件夹nova3导入vscode直接进行本地调试。使用launch.json进行调试同时本地开启clash代理:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger: Current File",
            "type": "debugpy",
            "request": "launch",
            "program": "nova2.py",
            "console": "integratedTerminal",
          // nova2 调试参数
            "args": ["bt4gprx", "all", "MEYD-909"]
        }
    ]
}

发现无结果输出,分析bt4gprx插件结构:

...
class bt4gprx(object):
    url = "https://bt4gprx.com/"
    name = "bt4gprx"
    supported_categories = {'all': '', 'movies': 'movie/', 'tv': 'movie/', 'music': 'audio/', 'books': 'doc/', 'software': 'app/'}
    def _init_(self):
    class MyHTMLParser(HTMLParser):
    # 搜索入口
    def search(self, term, cat="all"):
    # 分页搜索,搜索入口调用
    def search_page(self, term, pagenumber, cat):
    # 解析磁力链接
    def download_torrent(self, info):
    # 格式化返回符合qbit解析的数据
    def pretty_print_results(self, results):

进一步分析发现最终请求是调用的retrieve_url这个方法,该方法来自于nova3/helpers.py,查看部分源码:

# Some sites blocks default python User-agent
user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0'
headers = {'User-Agent': user_agent}
# SOCKS5 Proxy support
if "sock_proxy" in os.environ and len(os.environ["sock_proxy"].strip()) > 0:
    proxy_str = os.environ["sock_proxy"].strip()
    m = re.match(r"^(?:(?P<username>[^:]+):(?P<password>[^@]+)@)?(?P<host>[^:]+):(?P<port>\w+)$",
                 proxy_str)
    if m is not None:
        socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, m.group('host'),
                              int(m.group('port')), True, m.group('username'), m.group('password'))
        socket.socket = socks.socksocket

发现程序本身是从环境变量中获取了socks代理,且设置了ua,但是ua太过于陈旧,可见该文件估计有很多年没被人改动了。测试发现就算环境变量配置了sock_proxy依旧无法使用代理,不清楚py版本更替,qbit docker内置的py版本是3.10.x,猜测上面的调用方式在py3中可能不生效。
继续debug:

    def search_page(self, term, pagenumber, cat):
        try:
            query = f"{self.url}{self.supported_categories[cat]}search/{term}/byseeders/{pagenumber}"
            parser = self.MyHTMLParser();
            res = retrieve_url(query)
            return parser.feed(res)
        except Exception as e:
            return []

发现此时res变量返回值异常:

'<!DOCTYPE html>\n<html>\n<head>\n<title></title>\n<meta charset="UTF-8" />\n<script type="a9d4a12ffef2772d8ade6b31-text/javascript">\ndocument.cookie = "ge_js_validator_22=1718790079@22@b954a468bd39a1650a0fe9debc7e1476; path=/; max-age=3600;";\nwindow.location.reload();\n</script>\n</head>\n<body>\n<script src="/cdn-cgi/scripts/7d0fa10a/cloudflare-static/rocket-loader.min.js" data-cf-settings="a9d4a12ffef2772d8ade6b31-|49" defer></script></body>\n</html>'

通过ge_js_validator_22和返回值异常可以猜测应该是触发了某种验证,但是本地开了代理且在debug过程中有请求日志产生,Chrome浏览器访问也是正常。那就应该是客户端差异导致的了,联想到上面异常老旧的ua,应该是网站反爬触发,先修改ua。
上面提到最终发请求的是helpers,为了不破坏原有代码结构,在自身插件中配置ua:

import helpers
    helpers.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'}

再次debug,发现正常解析出结果,在qbittorrent docker webui中重新安装插件,发现还是无搜索结果,那就是代理的问题。发现在docker环境变量中配置代理不生效,只有通过qbit webui配置代理生效,能够正常搜索出结果。

如果不想在qbittorrent中配置代理呢?

只能通过修改插件代码了,上面提到所有的请求都是通过helpers.retrieve_url来发送的,为了不破坏代码结构,在当前插件重新定义发送请求方法:

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'}
def get_url(url):
    try:
        http_proxy = os.environ["HTTP_PROXY"]
        https_proxy = os.environ["HTTPS_PROXY"]
    except Exception as e:
        http_proxy = None
        https_proxy = None
    req = urllib.request.Request(url, headers=headers)
    req = urllib.request.Request(url, headers=headers)
    if http_proxy is not None and https_proxy is not None:
        proxies = {'http': http_proxy, 'https': https_proxy}
        opener = urllib.request.build_opener(urllib.request.ProxyHandler(proxies))
        urllib.request.install_opener(opener)

上面代码只贴出了核心改动,其他和helpers.retrieve_url一样。新的请求方法从环境变量中获取http代理,然后设置到请求上。vscode debug launch.json改动,添加了环境变量进行调试,本地clash设置不添加系统代理方便测试。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger: Current File",
            "type": "debugpy",
            "request": "launch",
            "program": "nova2.py",
            "console": "integratedTerminal",
            "args": ["bt4gprx", "all", "MEYD-909"],
            "env": {
                "HTTP_PROXY": "http://localhost:7899",
                "HTTPS_PROXY": "http://localhost:7899"
            }
        }
    ]
}

本地调试通过,docker qbit重新安装插件,取消webui中的代理,在docker compose中配置环境变量进行代理,重启容器,测试通过,搜索正常出结果。

最后

原本是使用Jackett来提供主要搜索的,但是发现Jackett并没有收录bt4g,搜索一圈发现官方已经放弃了支持:issue,这才有了上面的折腾过程。
回顾整个过程,因为不熟悉python,在怎么给py设置代理上花了大量时间,最后还是在官方文档中找到了线索:ProxyHandler,最后在stackoverflow上发现了答案:how-can-i-open-a-website-with-urllib-via-proxy-in-python

后续

使用发现经常出现搜索结果和网页不一致的情况,应该是超时导致的。再次分析插件代码,发现在搜索的时候插件就解析出了所有的磁力链接,那最终的请求次数就是:结果数量 + 分页数量。但是很明显搜索结果页面是不需要磁力链接的,磁力链接可以在qbit结果页面选择下载时进行解析,但这样就需要额外的后端服务来处理这个详情链接。

修改返回结果列表url

最终的返回结果是在pretty_print_results中进行处理,做如下修改:

def gen_server():
    try:
        # 后端处理地址,依旧从环境变量中读取
        gen_server = os.environ["BT4GPRX_GEN_SERVER"]
    except Exception as e:
        return None
    if gen_server is not None:
        return gen_server
      # ......
    def pretty_print_results(self, results):
        sorted_results = sorted(results, key=lambda x: int(x['seeders']), reverse=True)
        gen_server_addr = gen_server()
        for result in sorted_results:
            desc_link = urljoin(self.url, result['href'])
            if gen_server_addr is not None:
                # 如果配置了后端链接则使用后端进行解析,参数为url
                magnet_link = gen_server_addr + "?url=" + urllib.parse.quote_plus(desc_link)
            else:
                # 否则还是进行解析
                magnet_link = self.download_torrent(desc_link)
            temp_result = {
                'name': result['title'],
                'size': result['filesize'],
                'seeds': result['seeders'],
                'leech': result['leechers'],
                'engine_url': self.url,
                'link': magnet_link,
                # 详情url,方便搜索页面右键跳转到站点,key值可以从插件目录中jackett的插件源码中获取到
                'desc_link': desc_link
            }
            prettyPrinter(temp_result)

解析磁力链接后端服务

因为不清楚需要怎样的返回格式,于是查看Jackett插件的实现。Jackett搜索结果页选择任意下载,发现下载链接做了302跳转,直接调用magnet工具。那后端就很简单了,任意起一个web服务,接收url参数,该参数值就是bt4grpx的搜索详情页面,解析磁力链接时同样需要注意上面提到的代理和限制,最后返回只需要重定向到磁力链接即可。
至此,整个插件算是调试的比较好用了,搜索也只需要请求分页数一样的次数,可以正常返回搜索结果,下载也能正确的解析出磁力链接。

Perfect ~