From be75970dca50169853fae860d426f9e7f93e60ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AD=D0=BB=D1=8C=D0=BD=D1=83=D1=80=20=D0=9D=D1=83=D1=80?= =?UTF-8?q?=D1=83=D0=BB=D0=BB=D0=B0=D0=B5=D0=B2?= Date: Sun, 19 Apr 2026 13:13:29 +0300 Subject: [PATCH] fix(nnmclub): handle Cloudflare Turnstile CAPTCHA on login NNMClub added a Cloudflare Turnstile widget to their login form, breaking the previous requests-based POST login. Replace it with a Playwright-driven headless browser flow that fills credentials, waits for Turnstile to auto-complete, submits the form, and extracts the phpBB session cookies. Playwright is already listed in requirements.txt. Co-Authored-By: Claude Sonnet 4.6 --- monitorrent/plugins/trackers/nnmclub.py | 56 ++++++++++++++++++++----- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/monitorrent/plugins/trackers/nnmclub.py b/monitorrent/plugins/trackers/nnmclub.py index 19953903..b8dc2252 100644 --- a/monitorrent/plugins/trackers/nnmclub.py +++ b/monitorrent/plugins/trackers/nnmclub.py @@ -3,7 +3,7 @@ standard_library.install_aliases() from builtins import object import sys -from requests import Session +import asyncio import requests import urllib.request, urllib.parse, urllib.error from sqlalchemy import Column, Integer, String, ForeignKey @@ -85,18 +85,52 @@ def parse_url(self, url): return self._get_title(title) def login(self, username, password): - s = Session() - data = {"username": username, "password": password, "autologin": "on", "login": "%C2%F5%EE%E4"} - login_result = s.post(self._login_url, data, **self.tracker_settings.get_requests_kwargs()) - if login_result.url.startswith(self._login_url): - # TODO get error info (although it shouldn't contain anything useful + cookies = asyncio.run(self._login_with_browser(username, password)) + if u'phpbb2mysql_4_sid' not in cookies: raise NnmClubLoginFailedException(1, "Invalid login or password") - else: - sid = s.cookies[u'phpbb2mysql_4_sid'] - data = s.cookies[u'phpbb2mysql_4_data'] - parsed_data = loads(unquote(data).encode('utf-8')) + self.sid = cookies[u'phpbb2mysql_4_sid'] + data_cookie = cookies.get(u'phpbb2mysql_4_data', '') + if data_cookie: + parsed_data = loads(unquote(data_cookie).encode('utf-8')) self.user_id = parsed_data[u'userid'.encode('utf-8')].decode('utf-8') - self.sid = sid + + async def _login_with_browser(self, username, password): + from playwright.async_api import async_playwright, TimeoutError as PlaywrightTimeoutError + + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + context = await browser.new_context() + page = await context.new_page() + + await page.goto(self._login_url) + await page.fill('input[name="username"]', username) + await page.fill('input[name="password"]', password) + + # Wait for Cloudflare Turnstile to auto-complete and inject its token + try: + await page.wait_for_function( + "() => { const el = document.querySelector('[name=\"cf-turnstile-response\"]'); " + "return el && el.value && el.value.length > 0; }", + timeout=30000 + ) + except PlaywrightTimeoutError: + pass # Proceed anyway; Turnstile may have completed silently + + await page.click('input[name="login"]') + + try: + await page.wait_for_url( + lambda url: not url.startswith(self._login_url), + timeout=15000 + ) + except PlaywrightTimeoutError: + await browser.close() + raise NnmClubLoginFailedException(2, "Login failed: CAPTCHA not solved or invalid credentials") + + all_cookies = await context.cookies() + await browser.close() + + return {c['name']: c['value'] for c in all_cookies} def verify(self): cookies = self.get_cookies()