Added CaptchaAI as an anti-captcha provider

This commit is contained in:
B.Shahin
2026-06-30 15:33:48 +03:00
committed by GitHub
parent 3b8094fce7
commit 8e4563f25e
6 changed files with 239 additions and 5 deletions

View File

@@ -155,7 +155,7 @@ validators = [
Validator('general.days_to_upgrade_subs', must_exist=True, default=7, is_type_of=int, gte=0, lte=30),
Validator('general.upgrade_manual', must_exist=True, default=True, is_type_of=bool),
Validator('general.anti_captcha_provider', must_exist=True, default=None, is_type_of=(NoneType, str),
is_in=[None, 'anti-captcha', 'death-by-captcha']),
is_in=[None, 'anti-captcha', 'death-by-captcha', 'captchaai']),
Validator('general.wanted_search_frequency', must_exist=True, default=6, is_type_of=int,
is_in=[6, 12, 24, 168, ONE_HUNDRED_YEARS_IN_HOURS]),
Validator('general.wanted_search_frequency_movie', must_exist=True, default=6, is_type_of=int,
@@ -385,6 +385,9 @@ validators = [
Validator('deathbycaptcha.username', must_exist=True, default='', is_type_of=str, cast=str),
Validator('deathbycaptcha.password', must_exist=True, default='', is_type_of=str, cast=str),
# captchaai section
Validator('captchaai.captchaai_key', must_exist=True, default='', is_type_of=str),
# napisy24 section
Validator('napisy24.username', must_exist=True, default='', is_type_of=str, cast=str),
Validator('napisy24.password', must_exist=True, default='', is_type_of=str, cast=str),
@@ -753,7 +756,8 @@ def save_settings(settings_items):
os.environ["SZ_HI_EXTENSION"] = value or ""
if key in ['settings-general-anti_captcha_provider', 'settings-anticaptcha-anti_captcha_key',
'settings-deathbycaptcha-username', 'settings-deathbycaptcha-password']:
'settings-deathbycaptcha-username', 'settings-deathbycaptcha-password',
'settings-captchaai-captchaai_key']:
configure_captcha = True
if key in ['update_schedule', 'settings-general-use_sonarr', 'settings-general-use_radarr',
@@ -981,6 +985,9 @@ def configure_captcha_func():
os.environ["ANTICAPTCHA_CLASS"] = 'DeathByCaptchaProxyLess'
os.environ["ANTICAPTCHA_ACCOUNT_KEY"] = str(':'.join(
{settings.deathbycaptcha.username, settings.deathbycaptcha.password}))
elif settings.general.anti_captcha_provider == 'captchaai' and settings.captchaai.captchaai_key != "":
os.environ["ANTICAPTCHA_CLASS"] = 'CaptchaAIProxyLess'
os.environ["ANTICAPTCHA_ACCOUNT_KEY"] = str(settings.captchaai.captchaai_key)
else:
os.environ["ANTICAPTCHA_CLASS"] = ''

View File

@@ -5,6 +5,8 @@ import os
import time
import logging
import json
import base64
import requests
from subliminal.cache import region
from dogpile.cache.api import NO_VALUE
from python_anticaptcha import AnticaptchaClient, NoCaptchaTaskProxylessTask, NoCaptchaTask, ImageToTextTask, \
@@ -255,6 +257,206 @@ class DBCPitcher(DBCProxyLessPitcher):
return payload
@registry.register
class CaptchaAIProxyLessPitcher(Pitcher):
name = "CaptchaAIProxyLess"
source = "captchaai.com"
host = "https://ocr.captchaai.com"
timeout = 180
def __init__(self, website_name, website_url, website_key, tries=3, host=None, *args, **kwargs):
super(CaptchaAIProxyLessPitcher, self).__init__(website_name, website_url, website_key, tries=tries, *args,
**kwargs)
self.host = host or self.host
def get_client(self):
# CaptchaAI exposes a 2Captcha-compatible HTTP API (in.php/res.php), so a
# plain requests session is all that is needed.
return requests.Session()
def get_job(self):
# The reCAPTCHA task is created in _throw() through the in.php endpoint.
pass
@property
def in_params(self):
return {
"key": self.client_key,
"method": "userrecaptcha",
"googlekey": self.website_key,
"pageurl": self.website_url,
"json": 1,
}
def _throw(self):
self.client = self.get_client()
for i in range(self.tries):
try:
resp = self.client.post("%s/in.php" % self.host, data=self.in_params, timeout=30)
payload = resp.json()
if payload.get("status") != 1:
request = payload.get("request")
if request == "ERROR_ZERO_BALANCE":
logger.error("%s: No balance left on captcha solving service. Exiting", self.website_name)
return
elif request == "ERROR_NO_SLOT_AVAILABLE":
logger.info("%s: No captcha solving slot available, retrying", self.website_name)
time.sleep(5.0)
continue
elif request in ("ERROR_KEY_DOES_NOT_EXIST", "ERROR_WRONG_USER_KEY"):
logger.error("%s: Bad CaptchaAI API key", self.website_name)
return
logger.error("%s: CaptchaAI returned an error: %s", self.website_name, request)
if i >= self.tries - 1:
return
continue
job_id = payload.get("request")
deadline = time.time() + self.timeout
while time.time() < deadline:
time.sleep(5.0)
res = self.client.get(
"%s/res.php" % self.host,
params={"key": self.client_key, "action": "get", "id": job_id, "json": 1},
timeout=30,
)
res_payload = res.json()
if res_payload.get("status") == 1:
self.success = True
return res_payload.get("request")
if res_payload.get("request") == "CAPCHA_NOT_READY":
continue
logger.error("%s: CaptchaAI returned an error: %s", self.website_name,
res_payload.get("request"))
break
logger.info("%s: Captcha solving timed out, retrying", self.website_name)
except Exception:
if i >= self.tries - 1:
logger.error("%s: Captcha solving finally failed. Exiting", self.website_name)
return
logger.info("%s: Captcha solving failed, retrying", self.website_name)
time.sleep(5.0)
@registry.register
class CaptchaAIPitcher(CaptchaAIProxyLessPitcher):
name = "CaptchaAI"
proxy = None
needs_proxy = True
proxy_type = "HTTP"
user_agent = None
def __init__(self, *args, **kwargs):
self.proxy = kwargs.pop("proxy", None)
self.user_agent = kwargs.pop("user_agent", None)
# cookies/is_invisible are accepted for call-site compatibility but unused.
kwargs.pop("cookies", None)
kwargs.pop("is_invisible", None)
super(CaptchaAIPitcher, self).__init__(*args, **kwargs)
@property
def in_params(self):
params = super(CaptchaAIPitcher, self).in_params
if self.proxy:
params.update({"proxy": self.proxy, "proxytype": self.proxy_type})
if self.user_agent:
params.update({"userAgent": self.user_agent})
return params
@registry.register
class CaptchaAIImageToTextPitcher(Pitcher):
name = "CaptchaAIImageToText"
source = "captchaai.com"
host = "https://ocr.captchaai.com"
timeout = 180
image_fp = None
def __init__(self, website_name, image_fp, tries=3, client_key=None, *args, **kwargs):
super().__init__(website_name, tries, client_key, *args, **kwargs)
self.tries = tries
self.client_key = client_key or os.environ.get("ANTICAPTCHA_ACCOUNT_KEY")
if not self.client_key:
raise Exception("CaptchaAI key not given, exiting")
self.website_name = website_name
self.image_fp = image_fp
self.success = False
self.solve_time = None
def get_client(self):
# CaptchaAI exposes a 2Captcha-compatible HTTP API (in.php/res.php), so a
# plain requests session is all that is needed.
return requests.Session()
def get_job(self):
# The image task is created in _throw() through the in.php endpoint.
pass
@property
def in_params(self):
# Read the image bytes and submit them as base64 (method=base64).
try:
self.image_fp.seek(0)
except (AttributeError, ValueError):
pass
body = base64.b64encode(self.image_fp.read()).decode("ascii")
return {
"key": self.client_key,
"method": "base64",
"body": body,
"json": 1,
}
def _throw(self):
self.client = self.get_client()
for i in range(self.tries):
try:
resp = self.client.post("%s/in.php" % self.host, data=self.in_params, timeout=30)
payload = resp.json()
if payload.get("status") != 1:
request = payload.get("request")
if request == "ERROR_ZERO_BALANCE":
logger.error("%s: No balance left on captcha solving service. Exiting", self.website_name)
return
elif request == "ERROR_NO_SLOT_AVAILABLE":
logger.info("%s: No captcha solving slot available, retrying", self.website_name)
time.sleep(5.0)
continue
elif request in ("ERROR_KEY_DOES_NOT_EXIST", "ERROR_WRONG_USER_KEY"):
logger.error("%s: Bad CaptchaAI API key", self.website_name)
return
logger.error("%s: CaptchaAI returned an error: %s", self.website_name, request)
if i >= self.tries - 1:
return
continue
job_id = payload.get("request")
deadline = time.time() + self.timeout
while time.time() < deadline:
time.sleep(5.0)
res = self.client.get(
"%s/res.php" % self.host,
params={"key": self.client_key, "action": "get", "id": job_id, "json": 1},
timeout=30,
)
res_payload = res.json()
if res_payload.get("status") == 1:
self.success = True
return res_payload.get("request")
if res_payload.get("request") == "CAPCHA_NOT_READY":
continue
logger.error("%s: CaptchaAI returned an error: %s", self.website_name,
res_payload.get("request"))
break
logger.info("%s: Captcha solving timed out, retrying", self.website_name)
except Exception:
if i >= self.tries - 1:
logger.error("%s: Captcha solving finally failed. Exiting", self.website_name)
return
logger.info("%s: Captcha solving failed, retrying", self.website_name)
time.sleep(5.0)
@registry.register
class AntiCaptchaImageToTextPitcher(Pitcher):
name = "AntiCaptchaImageToText"

View File

@@ -124,9 +124,14 @@ class ZimukuProvider(Provider):
return img_fp
fp = bmp_to_image(image_content)
pitcher = pitchers.get_pitcher("AntiCaptchaImageToText" if
os.environ["ANTICAPTCHA_CLASS"] == 'AntiCaptchaProxyLess' else
"DeathByCaptchaImageToText")("Zimuku", fp)
anticaptcha_class = os.environ["ANTICAPTCHA_CLASS"]
if anticaptcha_class == 'AntiCaptchaProxyLess':
image_pitcher = "AntiCaptchaImageToText"
elif anticaptcha_class == 'CaptchaAIProxyLess':
image_pitcher = "CaptchaAIImageToText"
else:
image_pitcher = "DeathByCaptchaImageToText"
pitcher = pitchers.get_pitcher(image_pitcher)("Zimuku", fp)
return pitcher.throw()
i = -1

View File

@@ -62,6 +62,17 @@ const SettingsProvidersView: FunctionComponent = () => {
</Anchor>
<Message>Link to subscribe</Message>
</CollapseBox>
<CollapseBox
settingKey="settings-general-anti_captcha_provider"
on={(value) => value === "captchaai"}
>
<Text
label="Account Key"
settingKey="settings-captchaai-captchaai_key"
></Text>
<Anchor href="https://captchaai.com">CaptchaAI.com</Anchor>
<Message>Link to subscribe</Message>
</CollapseBox>
</Section>
<Section header="Integrations">
<ProviderView

View File

@@ -9,4 +9,8 @@ export const antiCaptchaOption: SelectorOption<string>[] = [
label: "Death by Captcha",
value: "death-by-captcha",
},
{
label: "CaptchaAI",
value: "captchaai",
},
];

View File

@@ -12,6 +12,7 @@ interface Settings {
// Anitcaptcha
anticaptcha: Settings.Anticaptcha;
deathbycaptcha: Settings.DeathByCaptche;
captchaai: Settings.CaptchaAI;
// Providers
opensubtitlescom: Settings.OpenSubtitlesCom;
addic7ed: Settings.Addic7ed;
@@ -216,6 +217,10 @@ declare namespace Settings {
password?: string;
}
interface CaptchaAI {
captchaai_key?: string;
}
// Providers
interface BaseProvider {