playwright-captcha-solver 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ from .captcha_solver import CaptchaSolver
2
+
3
+ from .sdk_types import (
4
+ AuthToken,
5
+ Credentials,
6
+ Authentication,
7
+ CaptchaSolverOptions,
8
+ ImageChallenge,
9
+ RecaptchaChallenge,
10
+ SolveResult,
11
+ )
12
+
13
+ from .errors import (
14
+ CaptchaSolverError,
15
+ NetworkError,
16
+ TimeoutError,
17
+ )
18
+
19
+ __all__ = [
20
+ "CaptchaSolver",
21
+
22
+ "AuthToken",
23
+ "Credentials",
24
+ "Authentication",
25
+
26
+ "CaptchaSolverOptions",
27
+
28
+ "ImageChallenge",
29
+ "RecaptchaChallenge",
30
+
31
+ "SolveResult",
32
+
33
+ "CaptchaSolverError",
34
+ "NetworkError",
35
+ "TimeoutError",
36
+ ]
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import httpx
6
+
7
+ from .sdk_types import (
8
+ AuthToken,
9
+ Credentials,
10
+ CaptchaSolverOptions,
11
+ )
12
+
13
+ from .errors import (
14
+ CaptchaSolverError,
15
+ NetworkError,
16
+ )
17
+
18
+
19
+ class ApiClient:
20
+ def __init__(self, options: CaptchaSolverOptions):
21
+ self.options = options
22
+
23
+ self._client = httpx.AsyncClient(
24
+ follow_redirects=True,
25
+ timeout=options.timeout or 30,
26
+ )
27
+
28
+ async def close(self) -> None:
29
+ await self._client.aclose()
30
+
31
+ async def request(
32
+ self,
33
+ endpoint: str,
34
+ *,
35
+ method: str = "GET",
36
+ body: Optional[dict] = None,
37
+ authenticated: bool = True,
38
+ files: Optional[dict] = None,
39
+ ) -> httpx.Response:
40
+
41
+ params = None
42
+ data = body
43
+
44
+ if authenticated:
45
+ if files is not None:
46
+ data = self._append_authentication(body)
47
+ else:
48
+ params = self._authentication_query()
49
+
50
+ try:
51
+ response = await self._client.request(
52
+ method=method,
53
+ url=endpoint,
54
+ params=params,
55
+ data=data,
56
+ files=files,
57
+ )
58
+
59
+ except httpx.RequestError as exc:
60
+ raise NetworkError(
61
+ "Unable to reach the captcha service."
62
+ ) from exc
63
+
64
+ self._handle_errors(response)
65
+
66
+ return response
67
+
68
+ def _authentication_query(self) -> dict:
69
+
70
+ auth = self.options.auth
71
+
72
+ if isinstance(auth, AuthToken):
73
+ return {
74
+ "authtoken": auth.authtoken,
75
+ }
76
+
77
+ if isinstance(auth, Credentials):
78
+ return {
79
+ "username": auth.username,
80
+ "password": auth.password,
81
+ }
82
+
83
+ raise TypeError("Unsupported authentication type.")
84
+
85
+ def _append_authentication(
86
+ self,
87
+ body: Optional[dict],
88
+ ) -> dict:
89
+
90
+ body = dict(body or {})
91
+
92
+ auth = self.options.auth
93
+
94
+ if isinstance(auth, AuthToken):
95
+ body["authtoken"] = auth.authtoken
96
+
97
+ elif isinstance(auth, Credentials):
98
+ body["username"] = auth.username
99
+ body["password"] = auth.password
100
+
101
+ else:
102
+ raise TypeError("Unsupported authentication type.")
103
+
104
+ return body
105
+
106
+ @staticmethod
107
+ def _handle_errors(response: httpx.Response) -> None:
108
+
109
+ if response.is_success:
110
+ return
111
+
112
+ messages = {
113
+ 255: "Not logged in. Check your credentials.",
114
+
115
+ 400: (
116
+ "Captcha was rejected by the service. "
117
+ "Check that it is a valid image."
118
+ ),
119
+
120
+ 403: (
121
+ "Access denied. Check your credentials, "
122
+ "balance, or IP address."
123
+ ),
124
+
125
+ 413: (
126
+ "Captcha was rejected by the service. "
127
+ "Check that it is a valid image."
128
+ ),
129
+
130
+ 503: (
131
+ "Service overload. "
132
+ "Please try again later."
133
+ ),
134
+ }
135
+
136
+ raise CaptchaSolverError(
137
+ response.status_code,
138
+ messages.get(
139
+ response.status_code,
140
+ f"Unexpected HTTP status: {response.status_code}",
141
+ ),
142
+ )
@@ -0,0 +1,18 @@
1
+ from .api_client import ApiClient
2
+
3
+
4
+ class CaptchaReporter:
5
+ def __init__(self, api: ApiClient):
6
+ self.api = api
7
+
8
+ async def report(
9
+ self,
10
+ captcha_id: str,
11
+ ) -> None:
12
+
13
+ await self.api.request(
14
+ f"http://api.dbcapi.me/api/captcha/{captcha_id}/report",
15
+ method="POST",
16
+ body={},
17
+ authenticated=True,
18
+ )
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from .api_client import ApiClient
4
+ from .pipeline import Pipeline
5
+
6
+ from .sdk_types import (
7
+ AuthToken,
8
+ Credentials,
9
+ CaptchaSolverOptions,
10
+ SolveResult,
11
+ )
12
+
13
+
14
+ class CaptchaSolver:
15
+ def __init__(self, options: CaptchaSolverOptions):
16
+
17
+ self._validate_options(options)
18
+
19
+ self._api = ApiClient(options)
20
+
21
+ self._pipeline = Pipeline(self._api)
22
+
23
+ async def solve(self, options) -> SolveResult:
24
+ return await self._pipeline.solve(options)
25
+
26
+ async def report(self, captcha_id: str) -> None:
27
+ await self._pipeline.report_captcha(captcha_id)
28
+
29
+ async def close(self) -> None:
30
+ await self._api.close()
31
+
32
+ async def __aenter__(self):
33
+ return self
34
+
35
+ async def __aexit__(self, exc_type, exc, tb):
36
+ await self.close()
37
+
38
+ @staticmethod
39
+ def _validate_options(
40
+ options: CaptchaSolverOptions,
41
+ ) -> None:
42
+
43
+ auth = options.auth
44
+
45
+ if isinstance(auth, AuthToken):
46
+ if not auth.authtoken:
47
+ raise ValueError(
48
+ "Authentication token is required."
49
+ )
50
+ return
51
+
52
+ if isinstance(auth, Credentials):
53
+ if not auth.username:
54
+ raise ValueError(
55
+ "Username is required."
56
+ )
57
+
58
+ if not auth.password:
59
+ raise ValueError(
60
+ "Password is required."
61
+ )
62
+
63
+ return
64
+
65
+ raise TypeError(
66
+ "Unsupported authentication type."
67
+ )
@@ -0,0 +1,127 @@
1
+ import asyncio
2
+ from urllib.parse import parse_qs
3
+
4
+ from playwright.async_api import Locator, Page
5
+
6
+ from .api_client import ApiClient
7
+ from .errors import TimeoutError
8
+
9
+
10
+ class ImageSolver:
11
+ def __init__(self, api: ApiClient):
12
+ self.api = api
13
+
14
+ async def solve(
15
+ self,
16
+ page: Page,
17
+ captcha: Locator,
18
+ ):
19
+ image = await captcha.screenshot(type="png")
20
+
21
+ files = {
22
+ "captchafile": (
23
+ "captcha.png",
24
+ image,
25
+ "image/png",
26
+ )
27
+ }
28
+
29
+ body = {
30
+ "type": "0",
31
+ }
32
+
33
+ response = await self.api.request(
34
+ "http://api.dbcapi.me/api/captcha/",
35
+ method="POST",
36
+ body=body,
37
+ files=files,
38
+ authenticated=True,
39
+ )
40
+
41
+ params = parse_qs(response.text)
42
+
43
+ captcha_id = params.get("captcha", [None])[0]
44
+
45
+ if captcha_id is None:
46
+ raise RuntimeError(
47
+ "Captcha service did not return a captcha ID."
48
+ )
49
+
50
+ return await self.retrieve_token(captcha_id)
51
+
52
+ async def get_token(
53
+ self,
54
+ captcha_id: str,
55
+ ) -> str:
56
+
57
+ response = await self.api.request(
58
+ f"http://api.dbcapi.me/api/captcha/{captcha_id}",
59
+ authenticated=False,
60
+ )
61
+
62
+ return response.text
63
+
64
+ async def get_balance(self) -> float:
65
+
66
+ response = await self.api.request(
67
+ "http://api.dbcapi.me/api/user",
68
+ authenticated=True,
69
+ )
70
+
71
+ params = parse_qs(response.text)
72
+
73
+ balance = params.get("balance", ["0"])[0]
74
+
75
+ return float(balance) / 100
76
+
77
+ async def retrieve_token(
78
+ self,
79
+ captcha_id: str,
80
+ ):
81
+
82
+ print("Solving captcha...")
83
+ print("Captcha ID:", captcha_id)
84
+
85
+ max_attempts = 10
86
+
87
+ for attempt in range(max_attempts):
88
+
89
+ print(
90
+ f"Waiting 3 seconds before attempt {attempt + 1}..."
91
+ )
92
+
93
+ await asyncio.sleep(3)
94
+
95
+ print(
96
+ f"Checking result... Attempt {attempt + 1}"
97
+ )
98
+
99
+ result = await self.get_token(captcha_id)
100
+
101
+ params = parse_qs(result)
102
+
103
+ token = params.get("text", [None])[0]
104
+
105
+ if token and len(token) > 1:
106
+
107
+ balance = await self.get_balance()
108
+
109
+ print("Final response:")
110
+
111
+ return {
112
+ "success": True,
113
+ "data": token,
114
+ "id": captcha_id,
115
+ "balance": balance,
116
+ }
117
+
118
+ if token == "?":
119
+ return {
120
+ "success": False,
121
+ "error": "Empty response.",
122
+ }
123
+
124
+ raise TimeoutError(
125
+ f"Captcha {captcha_id} was not solved "
126
+ f"after {max_attempts} attempts."
127
+ )
@@ -0,0 +1,106 @@
1
+ import time
2
+
3
+ from .api_client import ApiClient
4
+ from .captcha_reporter import CaptchaReporter
5
+ from .image_solver import ImageSolver
6
+ from .recaptcha_solver import RecaptchaSolver
7
+ from .submitter import Submitter
8
+
9
+ from .sdk_types import (
10
+ ImageChallenge,
11
+ RecaptchaChallenge,
12
+ SolveResult,
13
+ )
14
+
15
+
16
+ class Pipeline:
17
+ def __init__(self, api: ApiClient):
18
+
19
+ self.submitter = Submitter()
20
+
21
+ self.image_solver = ImageSolver(api)
22
+
23
+ self.recaptcha_solver = RecaptchaSolver(api)
24
+
25
+ self.captcha_reporter = CaptchaReporter(api)
26
+
27
+ async def report_captcha(
28
+ self,
29
+ captcha_id: str,
30
+ ) -> None:
31
+
32
+ await self.captcha_reporter.report(captcha_id)
33
+
34
+ async def solve(
35
+ self,
36
+ options,
37
+ ) -> SolveResult:
38
+
39
+ started = time.perf_counter()
40
+
41
+ if isinstance(options, ImageChallenge):
42
+
43
+ solution = await self.image_solver.solve(
44
+ options.page,
45
+ options.captcha,
46
+ )
47
+
48
+ if solution["success"]:
49
+
50
+ await self.submitter.submit_image(
51
+ options.page,
52
+ options.input,
53
+ solution["data"],
54
+ )
55
+
56
+ return SolveResult(
57
+ success=True,
58
+ challenge="image",
59
+ duration=int(
60
+ (time.perf_counter() - started) * 1000
61
+ ),
62
+ id=solution["id"],
63
+ balance=solution["balance"],
64
+ )
65
+
66
+ return SolveResult(
67
+ success=False,
68
+ challenge="image",
69
+ duration=int(
70
+ (time.perf_counter() - started) * 1000
71
+ ),
72
+ id=None,
73
+ balance=None,
74
+ )
75
+
76
+ response = await self.recaptcha_solver.solve(
77
+ options.page,
78
+ options.proxy,
79
+ )
80
+
81
+ if response["success"]:
82
+
83
+ await self.submitter.submit_recaptcha(
84
+ options.page,
85
+ response["data"],
86
+ )
87
+
88
+ return SolveResult(
89
+ success=True,
90
+ challenge="recaptcha",
91
+ duration=int(
92
+ (time.perf_counter() - started) * 1000
93
+ ),
94
+ id=response["id"],
95
+ balance=response["balance"],
96
+ )
97
+
98
+ return SolveResult(
99
+ success=False,
100
+ challenge="recaptcha",
101
+ duration=int(
102
+ (time.perf_counter() - started) * 1000
103
+ ),
104
+ id=None,
105
+ balance=None,
106
+ )
@@ -0,0 +1,161 @@
1
+ import asyncio
2
+ from urllib.parse import parse_qs, urlparse
3
+
4
+ from playwright.async_api import Page
5
+
6
+ from .api_client import ApiClient
7
+ from .errors import TimeoutError
8
+
9
+
10
+ class RecaptchaSolver:
11
+ def __init__(self, api: ApiClient):
12
+ self.api = api
13
+
14
+ async def solve(
15
+ self,
16
+ page: Page,
17
+ proxy: str | None = None,
18
+ ):
19
+ captcha = await self.get_captcha(page)
20
+
21
+ body = {
22
+ "type": "4",
23
+ "token_params": (
24
+ f'{{"pageurl":"{captcha["url"]}",'
25
+ f'"googlekey":"{captcha["sitekey"]}"'
26
+ + (
27
+ f',"proxytype":"HTTP","proxy":"{proxy}"'
28
+ if proxy
29
+ else ""
30
+ )
31
+ + "}"
32
+ ),
33
+ }
34
+
35
+ response = await self.api.request(
36
+ "http://api.dbcapi.me/api/captcha/",
37
+ method="POST",
38
+ body=body,
39
+ authenticated=True,
40
+ )
41
+
42
+ params = parse_qs(response.text)
43
+
44
+ captcha_id = params.get("captcha", [None])[0]
45
+
46
+ if captcha_id is None:
47
+ raise RuntimeError(
48
+ "Captcha service did not return a captcha ID."
49
+ )
50
+
51
+ return await self.retrieve_token(captcha_id)
52
+
53
+ async def get_captcha(
54
+ self,
55
+ page: Page,
56
+ ) -> dict:
57
+
58
+ url = page.url
59
+
60
+ iframe = await page.locator(
61
+ 'iframe[title="reCAPTCHA"]'
62
+ ).get_attribute("src")
63
+
64
+ if iframe is None:
65
+ raise RuntimeError(
66
+ "No reCAPTCHA iframe found."
67
+ )
68
+
69
+ parsed = urlparse(iframe)
70
+
71
+ params = parse_qs(parsed.query)
72
+
73
+ sitekey = params.get("k", [None])[0]
74
+
75
+ if sitekey is None:
76
+ raise RuntimeError(
77
+ "Unable to determine site key."
78
+ )
79
+
80
+ return {
81
+ "url": url,
82
+ "sitekey": sitekey,
83
+ }
84
+
85
+ async def get_token(
86
+ self,
87
+ captcha_id: str,
88
+ ) -> str:
89
+
90
+ response = await self.api.request(
91
+ f"http://api.dbcapi.me/api/captcha/{captcha_id}",
92
+ authenticated=False,
93
+ )
94
+
95
+ return response.text
96
+
97
+ async def get_balance(self) -> float:
98
+
99
+ response = await self.api.request(
100
+ "http://api.dbcapi.me/api/user",
101
+ authenticated=True,
102
+ )
103
+
104
+ params = parse_qs(response.text)
105
+
106
+ balance = params.get("balance", ["0"])[0]
107
+
108
+ return float(balance) / 100
109
+
110
+ async def retrieve_token(
111
+ self,
112
+ captcha_id: str,
113
+ ):
114
+
115
+ print("Solving reCAPTCHA...")
116
+ print("Captcha ID:", captcha_id)
117
+
118
+ max_attempts = 24
119
+
120
+ for attempt in range(max_attempts):
121
+
122
+ print(
123
+ f"Waiting 5 seconds before attempt {attempt + 1}..."
124
+ )
125
+
126
+ await asyncio.sleep(5)
127
+
128
+ print(
129
+ f"Checking result... Attempt {attempt + 1}"
130
+ )
131
+
132
+ result = await self.get_token(captcha_id)
133
+
134
+ params = parse_qs(result)
135
+
136
+ token = params.get("text", [None])[0]
137
+
138
+ if token and len(token) > 1:
139
+
140
+ balance = await self.get_balance()
141
+
142
+ print("Final response:")
143
+
144
+ return {
145
+ "success": True,
146
+ "data": token,
147
+ "id": captcha_id,
148
+ "balance": balance,
149
+ }
150
+
151
+ if token == "?":
152
+ return {
153
+ "success": False,
154
+ "error": "Empty response.",
155
+ }
156
+
157
+ raise TimeoutError(
158
+ f"Captcha {captcha_id} "
159
+ f"was not solved after "
160
+ f"{max_attempts} attempts."
161
+ )