diff --git a/bazarr/app/config.py b/bazarr/app/config.py
index 3a2a751f5..4e44ae236 100644
--- a/bazarr/app/config.py
+++ b/bazarr/app/config.py
@@ -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"] = ''
diff --git a/custom_libs/subliminal_patch/pitcher.py b/custom_libs/subliminal_patch/pitcher.py
index d5da50f01..5e2b4533c 100644
--- a/custom_libs/subliminal_patch/pitcher.py
+++ b/custom_libs/subliminal_patch/pitcher.py
@@ -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"
diff --git a/custom_libs/subliminal_patch/providers/zimuku.py b/custom_libs/subliminal_patch/providers/zimuku.py
index ffce3e21d..6c10d21bc 100644
--- a/custom_libs/subliminal_patch/providers/zimuku.py
+++ b/custom_libs/subliminal_patch/providers/zimuku.py
@@ -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
diff --git a/frontend/src/pages/Settings/Providers/index.tsx b/frontend/src/pages/Settings/Providers/index.tsx
index 43966d810..4d13d7af1 100644
--- a/frontend/src/pages/Settings/Providers/index.tsx
+++ b/frontend/src/pages/Settings/Providers/index.tsx
@@ -62,6 +62,17 @@ const SettingsProvidersView: FunctionComponent = () => {
Link to subscribe
+ value === "captchaai"}
+ >
+
+ CaptchaAI.com
+ Link to subscribe
+
[] = [
label: "Death by Captcha",
value: "death-by-captcha",
},
+ {
+ label: "CaptchaAI",
+ value: "captchaai",
+ },
];
diff --git a/frontend/src/types/settings.d.ts b/frontend/src/types/settings.d.ts
index 51f845700..1f163fd4a 100644
--- a/frontend/src/types/settings.d.ts
+++ b/frontend/src/types/settings.d.ts
@@ -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 {