├── .github
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .gitignore
├── :w
├── README.md
├── post_login_click.png
├── pyproject.toml
├── pyrightconfig.json
├── src
├── tiktok_captcha_solver.egg-info
│ ├── PKG-INFO
│ ├── SOURCES.txt
│ ├── dependency_links.txt
│ ├── requires.txt
│ └── top_level.txt
└── tiktok_captcha_solver
│ ├── __init__.py
│ ├── api.py
│ ├── asyncplaywrightsolver.py
│ ├── asyncsolver.py
│ ├── captchatype.py
│ ├── download_crx.py
│ ├── downloader.py
│ ├── geometry.py
│ ├── launcher.py
│ ├── models.py
│ ├── nodriversolver.py
│ ├── playwrightsolver.py
│ ├── selectors.py
│ ├── seleniumsolver.py
│ ├── solver.py
│ └── tests
│ ├── __init__.py
│ ├── __pycache__
│ ├── __init__.cpython-312.pyc
│ ├── test_all.cpython-312-pytest-8.2.0.pyc
│ ├── test_api.cpython-312-pytest-8.2.0.pyc
│ ├── test_asyncplaywrightsolver.cpython-312-pytest-8.2.0.pyc
│ ├── test_downloader.cpython-312-pytest-8.2.0.pyc
│ ├── test_playwrightsolver.cpython-312-pytest-8.2.0.pyc
│ ├── test_sadcaptcha.cpython-312-pytest-8.2.0.pyc
│ └── test_seleniumsolver.cpython-312-pytest-8.2.0.pyc
│ ├── new_puzzle.html
│ ├── new_rotate.html
│ ├── new_shapes.html
│ ├── piece_mobile.jpg
│ ├── puzzle_mobile.jpg
│ ├── puzzle_video_search.html
│ ├── test_api.py
│ ├── test_asyncplaywrightsolver.py
│ ├── test_download_crx.py
│ ├── test_downloader.py
│ ├── test_launcher.py
│ ├── test_playwrightsolver.py
│ ├── test_seleniumsolver.py
│ └── unknown_challenge.html
├── test.html
├── test.jpg
├── test.png
└── tiktok_captcha_solver.egg-info
├── PKG-INFO
├── SOURCES.txt
├── dependency_links.txt
├── requires.txt
└── top_level.txt
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | # Help us help you! Providing detailed information will help us solve the issue quickly with minimal back-and-forth. Make sure your issue includes the following information, or it may be closed automatically:
11 |
12 | 1. Paste the URL you're trying to access here: [PASTE URL HERE]
13 | 2. Paste your code inside the triple quotes below:
14 | ```py
15 |
16 | ```
17 | 3. Paste the HTML of the page in the triple quotes below (this is the output of `driver.page_source` on Selenium or `page.content()` on Playwright):
18 | ```html
19 |
20 | ```
21 | 4. Paste the full console logs set where log_level is set to DEBUG into the triple quotes below (if you're not sure how to do this, instructions are below):
22 | ```
23 |
24 | ```
25 |
26 | # How to set log_level to DEBUG:
27 | Add the following lines to the top of your python script:
28 | ```py
29 | import logging
30 | logging.basicConfig(level=logging.DEBUG)
31 | ```
32 |
33 | # Pull request
34 | **Please consider cloning this repository and making adjustments as needed to the code. Often times issues can be fixed very quickly by making very small adjustments to the code.**
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv/**
2 | dist/**
3 | *.pyc
4 | .DS_Store
5 | videso/**
6 |
--------------------------------------------------------------------------------
/:w:
--------------------------------------------------------------------------------
1 | import logging
2 | import tempfile
3 | from typing import Any
4 | import io
5 | import os
6 | import zipfile
7 | import requests
8 |
9 | from selenium.webdriver import ChromeOptions
10 | from selenium import webdriver
11 | import undetected_chromedriver as uc
12 | import nodriver
13 |
14 | from playwright import sync_api
15 | from playwright import async_api
16 |
17 | LOGGER = logging.getLogger(__name__)
18 |
19 | async def make_nodriver_solver(
20 | api_key: str,
21 | **nodriver_start_kwargs: dict[str, Any]
22 | ) -> uc.Chrome:
23 | """Create an selenium chromedriver patched with SadCaptcha.
24 |
25 | Args:
26 | api_key (str): SadCaptcha API key
27 | nodriver_start_args: Keyword arguments for nodriver.start()
28 | """
29 | ext_dir = download_extension_to_unpacked()
30 | _patch_extension_file_with_key(ext_dir.name, api_key)
31 | add_extension_argument = f'--load-extension={ext_dir.name}'
32 | if "browser_args" in nodriver_start_kwargs.keys():
33 | nodriver_start_kwargs["browser_args"].append(add_extension_argument)
34 | chrome = webdriver.Chrome(options=config, **webdriver_chrome_kwargs)
35 | LOGGER.debug("created new undetected chromedriver patched with sadcaptcha")
36 | return chrome
37 |
38 | def make_selenium_solver(
39 | api_key: str,
40 | options: ChromeOptions | None = None,
41 | no_warn: bool = False,
42 | **webdriver_chrome_kwargs
43 | ) -> uc.Chrome:
44 | """Create an selenium chromedriver patched with SadCaptcha.
45 |
46 | Args:
47 | api_key (str): SadCaptcha API key
48 | options (ChromeOptions | None): Options to launch webdriver.Chrome with
49 | webdriver_chrome_kwargs: keyword arguments for call to webdriver.Chrome
50 | """
51 | if not no_warn:
52 | LOGGER.warning("Selenium, playwright, and undetected chromedriver are no longer recommended for scraping. A better option is to use nodriver. To use nodriver, use the command 'make_nodriver_solver()' instead. To disable this warning, set the kwarg no_warn=True.")
53 | if options is None:
54 | options = ChromeOptions()
55 | ext_dir = download_extension_to_unpacked()
56 | _patch_extension_file_with_key(ext_dir.name, api_key)
57 | options.add_argument(f'--load-extension={ext_dir.name}')
58 | chrome = webdriver.Chrome(options=options, **webdriver_chrome_kwargs)
59 | LOGGER.debug("created new undetected chromedriver patched with sadcaptcha")
60 | return chrome
61 |
62 | def make_undetected_chromedriver_solver(
63 | api_key: str,
64 | options: ChromeOptions | None = None,
65 | no_warn: bool = False,
66 | **uc_chrome_kwargs
67 | ) -> uc.Chrome:
68 | """Create an undetected chromedriver patched with SadCaptcha.
69 |
70 | Args:
71 | api_key (str): SadCaptcha API key
72 | options (ChromeOptions | None): Options to launch uc.Chrome with
73 | uc_chrome_kwargs: keyword arguments for call to uc.Chrome
74 | """
75 | if not no_warn:
76 | LOGGER.warning("Selenium, playwright, and undetected chromedriver are no longer recommended for scraping. A better option is to use nodriver. To use nodriver, use the command 'make_nodriver_solver()' instead. To disable this warning, set the kwarg no_warn=True.")
77 | if options is None:
78 | options = ChromeOptions()
79 | ext_dir = download_extension_to_unpacked()
80 | _patch_extension_file_with_key(ext_dir.name, api_key)
81 | verify_api_key_injection(ext_dir.name, api_key)
82 | options.add_argument(f'--load-extension={ext_dir.name}')
83 | chrome = uc.Chrome(options=options, **uc_chrome_kwargs)
84 | # keep the temp dir alive for the lifetime of the driver
85 | chrome._sadcaptcha_tmpdir = ext_dir # ← prevents garbage collection
86 | LOGGER.debug("created new undetected chromedriver patched with sadcaptcha")
87 | return chrome
88 |
89 | def make_playwright_solver_context(
90 | playwright: sync_api.Playwright,
91 | api_key: str,
92 | user_data_dir: str | None = None,
93 | no_warn: bool = False,
94 | **playwright_context_kwargs
95 | ) -> sync_api.BrowserContext:
96 | """Create a playwright context patched with SadCaptcha.
97 |
98 | Args:
99 | playwright (playwright.sync_api.playwright) - Playwright instance
100 | api_key (str): SadCaptcha API key
101 | user_data_dir (str | None): User data dir that is passed to playwright.chromium.launch_persistent_context. If None, a temporary directory will be used.
102 | **playwright_context_kwargs: Keyword args which will be passed to playwright.chromium.launch_persistent_context()
103 | """
104 | if not no_warn:
105 | LOGGER.warning("Selenium, playwright, and undetected chromedriver are no longer recommended for scraping. A better option is to use nodriver. To use nodriver, use the command 'make_nodriver_solver()' instead. To disable this warning, set the kwarg no_warn=True.")
106 | ext_dir = download_extension_to_unpacked()
107 | if user_data_dir is None:
108 | user_data_dir_tempdir = tempfile.TemporaryDirectory()
109 | user_data_dir = user_data_dir_tempdir.name
110 | _patch_extension_file_with_key(ext_dir.name, api_key)
111 | playwright_context_kwargs = _prepare_pw_context_args(playwright_context_kwargs, ext_dir.name)
112 | ctx = playwright.chromium.launch_persistent_context(
113 | user_data_dir,
114 | **playwright_context_kwargs
115 | )
116 | ctx._sadcaptcha_tmpdir = ext_dir # keep reference
117 | LOGGER.debug("created patched playwright context")
118 | return ctx
119 |
120 | async def make_async_playwright_solver_context(
121 | async_playwright: async_api.Playwright,
122 | api_key: str,
123 | user_data_dir: str | None = None,
124 | **playwright_context_kwargs
125 | ) -> async_api.BrowserContext:
126 | """Create a async playwright context patched with SadCaptcha.
127 |
128 | Args:
129 | playwright (playwright.async_api.playwright) - Playwright instance
130 | api_key (str): SadCaptcha API key
131 | user_data_dir (str | None): User data dir that is passed to playwright.chromium.launch_persistent_context. If None, a temporary directory will be used.
132 | **playwright_context_kwargs: Keyword args which will be passed to playwright.chromium.launch_persistent_context()
133 | """
134 | ext_dir = download_extension_to_unpacked()
135 | if user_data_dir is None:
136 | user_data_dir_tempdir = tempfile.TemporaryDirectory()
137 | user_data_dir = user_data_dir_tempdir.name
138 | _patch_extension_file_with_key(ext_dir.name, api_key)
139 | playwright_context_kwargs = _prepare_pw_context_args(playwright_context_kwargs, ext_dir.name)
140 | ctx = await async_playwright.chromium.launch_persistent_context(
141 | user_data_dir,
142 | **playwright_context_kwargs
143 | )
144 | ctx._sadcaptcha_tmpdir = ext_dir # keep reference
145 | LOGGER.debug("created patched async playwright context")
146 | return ctx
147 |
148 | def _prepare_pw_context_args(
149 | playwright_context_kwargs: dict[str, Any],
150 | ext: str
151 | ) -> dict[str, Any]:
152 | if "args" in playwright_context_kwargs.keys():
153 | playwright_context_kwargs["args"] = playwright_context_kwargs["args"] + [
154 | f"--disable-extensions-except={ext}",
155 | f"--load-extension={ext}",
156 | ]
157 | else:
158 | playwright_context_kwargs["args"] = [
159 | f"--disable-extensions-except={ext}",
160 | f"--load-extension={ext}",
161 | '--disable-blink-features=AutomationControlled',
162 | '--no-sandbox',
163 | '--disable-web-security',
164 | '--disable-infobars',
165 | '--start-maximized',
166 | ]
167 | if playwright_context_kwargs.get("headless") == True:
168 | if "--headless=new" not in playwright_context_kwargs["args"]:
169 | _ = playwright_context_kwargs["args"].append("--headless=new")
170 | playwright_context_kwargs["headless"] = None
171 | LOGGER.debug("Removed headless=True and added --headless=new launch arg")
172 | LOGGER.debug("prepared playwright context kwargs")
173 | return playwright_context_kwargs
174 |
175 | def download_extension_to_unpacked() -> tempfile.TemporaryDirectory:
176 | """
177 | Download the SadCaptcha Chrome extension from GitHub and return an unpacked
178 | TemporaryDirectory that can be passed to Playwright / Chrome.
179 | """
180 | repo_zip_url = (
181 | "https://codeload.github.com/gbiz123/sadcaptcha-chrome-extensino/zip/refs/heads/master"
182 | )
183 |
184 | LOGGER.debug("Downloading SadCaptcha extension from %s", repo_zip_url)
185 | resp = requests.get(repo_zip_url, timeout=30)
186 | resp.raise_for_status()
187 |
188 | tmp_dir = tempfile.TemporaryDirectory(prefix="sadcaptcha_ext_")
189 | with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:
190 | # GitHub zips have a single top‑level folder → strip it
191 | root_prefix = zf.namelist()[0].split("/")[0] + "/"
192 | for member in zf.namelist():
193 | if member.endswith("/"):
194 | continue
195 | rel_path = member[len(root_prefix) :]
196 | if not rel_path:
197 | continue
198 | dest_file = os.path.join(tmp_dir.name, rel_path)
199 | os.makedirs(os.path.dirname(dest_file), exist_ok=True)
200 | with zf.open(member) as src, open(dest_file, "wb") as dst:
201 | dst.write(src.read())
202 |
203 | LOGGER.debug("Extension unpacked to %s", tmp_dir.name)
204 | return tmp_dir
205 |
206 | def _patch_extension_file_with_key(extension_dir: str, api_key: str) -> None:
207 | script_path = os.path.join(extension_dir, "script.js")
208 | try:
209 | with open(script_path, "r", encoding="utf-8") as f:
210 | script = f.read()
211 |
212 | original_script = script
213 | script = patch_extension_script_with_key(script, api_key)
214 |
215 | # Verify replacement happened
216 | if script == original_script:
217 | LOGGER.warning("API key pattern not found in script.js")
218 |
219 | with open(script_path, "w", encoding="utf-8") as f:
220 | f.write(script)
221 |
222 | LOGGER.debug("Successfully patched extension file with API key")
223 | except Exception as e:
224 | LOGGER.error(f"Failed to patch extension with API key: {e}")
225 | raise
226 |
227 | def patch_extension_script_with_key(script: str, api_key: str) -> str:
228 | script = script.replace('localStorage.getItem("sadCaptchaKey")', f"\"{api_key}\";")
229 | LOGGER.debug("patched extension script with api key")
230 | return script
231 |
232 | def verify_api_key_injection(extension_dir, api_key):
233 | script_path = os.path.join(extension_dir, "script.js")
234 |
235 | # Check if file exists and contains your API key
236 | if os.path.exists(script_path):
237 | with open(script_path, "r", encoding="utf-8") as f:
238 | content = f.read()
239 | if f'"{api_key}";' in content:
240 | LOGGER.info(f"SUCCESS: API key found in script.js")
241 | return True
242 | else:
243 | LOGGER.warning(f"FAILURE: API key not found in script.js")
244 | else:
245 | LOGGER.error(f"FAILURE: script.js not found at {script_path}")
246 | return False
247 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TikTok Captcha Solver API
2 | This project is the [SadCaptcha TikTok Captcha Solver](https://www.sadcaptcha.com?ref=ghclientrepo) API client.
3 | The purpose is to make integrating SadCaptcha into your Nodriver, Selenium, Playwright, or Async Playwright app as simple as one line of code.
4 | Instructions for integrating with Selenium, Playwright, and Async Playwright are described below in their respective sections.
5 | This API also works on mobile devices (Appium, etc.).
6 |
7 | This tool works on both TikTok and Douyin and can solve any of the four captcha challenges pictured below:
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ## Requirements
18 | - Python >= 3.10
19 | - **If using Nodriver** - Google chrome installe don system. This is the recommended method.
20 | - **If using Selenium** - Selenium properly installed and in `PATH`
21 | - **If using Playwright** - Playwright must be properly installed with `playwright install`
22 | - **If using mobile** - Appium and opencv must be properly installed
23 | - **Stealth plugin** - You must use the appropriate `stealth` plugin for whichever browser automation framework you are using.
24 | - For Selenium, you can use [undetected-chromedriver](https://github.com/ultrafunkamsterdam/undetected-chromedriver)
25 | - For Playwright, you can use [playwright-stealth](https://pypi.org/project/playwright-stealth/)
26 |
27 | ## Installation
28 | This project can be installed with `pip`. Just run the following command:
29 | ```
30 | pip install tiktok-captcha-solver
31 | ```
32 |
33 | ## Note for NodeJS users
34 | For users automating in NodeJS or another programming language, the recommended method is to download the chrome extension from the
35 | chrome web store, unzip the file, patch the script.js file with your API key, and load it into your browser.
36 | This will save you a lot of time implementing the API on your own.
37 |
38 | ## Note on running headless
39 | To run in headless mode, you need to use the launch arg `headless=new` or `headless=chrome` as a launch arg.
40 | Instructions to do this are in their own respective sections.
41 | Another option is to use [Xvfb](https://www.x.org/archive/X11R7.7/doc/man/man1/Xvfb.1.xhtml) with `headless=True` to spoof a graphical environment.
42 |
43 | ## Nodriver Client (Recommended)
44 | Nodriver is the latest advancement in undetected automation technology, and is the recommended method for using SadCaptcha.
45 | Import the function `make_nodriver_solver`
46 | This function will create an noddriver instance patched with the tiktok Captcha Solver chrome extension.
47 | The extension will automatically detect and solve the captcha in the background, and there is nothing further you need to do.
48 |
49 | ```py
50 | from tiktok_captcha_solver.launcher import make_nodriver_solver
51 |
52 | async def main():
53 | launch_args = ["--headless=chrome"] # If running headless, use this option, or headless=new
54 | api_key = "YOUR_API_KEY_HERE"
55 | # NOTE: Keyword arguments passed to make_nodriver_solver() are directly passed to nodriver.start()!
56 | driver = await make_nodriver_solver(api_key, browser_args=launch_args) # Returns nodriver browser
57 | # ... [The rest of your code that accesses tiktok goes here]
58 | # Now tiktok captchas will be automatically solved!
59 | ```
60 | All keyword arguments passed to `make_nodriver_solver()` are passed directly to `nodriver.start()`.
61 |
62 | ## Selenium Client
63 | Import the function `make_undetected_chromedriver_solver`
64 | This function will create an undetected chromedriver instance patched with the tiktok Captcha Solver chrome extension.
65 | The extension will automatically detect and solve the captcha in the background, and there is nothing further you need to do.
66 |
67 | ```py
68 | from tiktok_captcha_solver import make_undetected_chromedriver_solver
69 | from selenium_stealth import stealth
70 | from selenium.webdriver import ChromeOptions
71 | import undetected_chromedriver as uc
72 |
73 | chrome_options = ChromeOptions()
74 | # chrome_options.add_argument("--headless=chrome") # If running headless, use this option
75 |
76 | api_key = "YOUR_API_KEY_HERE"
77 | driver = make_undetected_chromedriver_solver(api_key, options=options) # Returns uc.Chrome instance
78 | stealth(driver) # Add stealth if needed
79 | # ... [The rest of your code that accesses tiktok goes here]
80 |
81 | # Now tiktok captchas will be automatically solved!
82 | ```
83 | You may also pass `ChromeOptions` to `make_undetected_chromedriver_solver()`, as well as keyword arguments for `uc.Chrome()`.
84 |
85 | ## Playwright Client
86 | Import the function `make_playwright_solver_context`
87 | This function will create a playwright BrowserContext instance patched with the tiktok Captcha Solver chrome extension.
88 | The extension will automatically detect and solve the captcha in the background, and there is nothing further you need to do.
89 |
90 | ```py
91 | from tiktok_captcha_solver import make_playwright_solver_context
92 | from playwright.sync_api import sync_playwright
93 | from playwright_stealth import stealth_sync, StealthConfig
94 |
95 | launch_args = ["--headless=chrome"] # or --headless=new if that doesn't work
96 |
97 | api_key = "YOUR_API_KEY_HERE"
98 | with sync_playwright() as p:
99 | # Keyword arguments are passed to p.chromium.launch_persistent_context()
100 | # Returns playwright BrowserContext instance
101 | context = make_playwright_solver_context(p, api_key, args=launch_args)
102 |
103 | # If using playwright_stealth, you need to use this StealthConfig to avoid the white screen:
104 | page = context.new_page()
105 | stealth_config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
106 | stealth_sync(page, stealth_config)
107 |
108 | # ... [The rest of your code that accesses tiktok goes here]
109 |
110 | # Now tiktok captchas will be automatically solved!
111 | ```
112 | You may also pass keyword args to this function, which will be passed directly to playwright's call to `playwright.chromium.launch_persistent_context()`.
113 | By default, the user data directory is a tempory directory that is deleted at the end of runtime.
114 |
115 | ## Async Playwright Client
116 | Import the function `make_async_playwright_solver_context`
117 | This function will create an async playwright BrowserContext instance patched with the tiktok Captcha Solver chrome extension.
118 | The extension will automatically detect and solve the captcha in the background, and there is nothing further you need to do.
119 |
120 | ```py
121 | import asyncio
122 | from playwright.async_api import async_playwright
123 | from tiktok_captcha_solver import make_async_playwright_solver_context
124 | from playwright_stealth import stealth_ssync, StealthConfig
125 |
126 | # Need this arg if running headless
127 | launch_args = ["--headless=chrome"] # or --headless=new if that doesn't work
128 |
129 | async def main():
130 | api_key = "YOUR_API_KEY_HERE"
131 | async with async_playwright() as p:
132 | # Keyword arguments are passed to p.chromium.launch_persistent_context()
133 | # Returns playwright BrowserContext instance
134 | context = await make_async_playwright_solver_context(p, api_key, args=launch_args)
135 |
136 | # If using playwright_stealth, you need to use this StealthConfig to avoid the white screen:
137 | page = await context.new_page()
138 | stealth_config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
139 | stealth_async(page, stealth_config)
140 |
141 | # ... [The rest of your code that accesses tiktok goes here]
142 |
143 | asyncio.run(main())
144 |
145 | # Now tiktok captchas will be automatically solved!
146 | ```
147 | You may also pass keyword args to this function, which will be passed directly to playwright's call to `playwright.chromium.launch_persistent_context()`.
148 | By default, the user data directory is a tempory directory that is deleted at the end of runtime.
149 |
150 | ## Mobile (Appium)
151 | Currently there is no premade solver for Mobile/appium, but you can implement the API with relative ease.
152 | The idea is that you take a screenshot using the mobile driver, crop the images, and then send the images to the API.
153 | Once you've done that, you can consume the response.
154 | Here is a working example for Puzzle and Rotate captcha.
155 | keep in mind, you will need to adjust the `captcha_box` and `offset_x` varaibles according to your particular mobile device.
156 |
157 | ### Puzzle slide
158 | ```py
159 | from PIL import Image, ImageDraw
160 | import base64
161 | import requests
162 |
163 | # SOLVING PUZZLE CAPTCHA
164 | BASE_URL = 'https://www.sadcaptcha.com/api/v1'
165 | LICENSE_KEY = ''
166 | puzzle_url = f'{BASE_URL}/puzzle?licenseKey={LICENSE_KEY}'
167 |
168 | def solve_puzzle():
169 | # Screenshot of page
170 | driver.save_screenshot('puzzle.png')
171 | full_image = Image.open('puzzle.png')
172 |
173 | # Full puzzle image - adjust box to your device
174 | captcha_box1 = (165, 1175, 303, 1330)
175 | captcha_image1 = full_image.crop(captcha_box1)
176 |
177 | # Draw circle over left side to occlude the puzzle piece in the main image
178 | draw = ImageDraw.Draw(captcha_image1)
179 | draw.ellipse([(0, 0), (captcha_image1.width / 4, captcha_image1.height)], fill="blue", outline="blue")
180 | captcha_image1.save('puzzle_screenshot.png')
181 |
182 | # Puzzle piece image - adjust box to your device
183 | captcha_box2 = (300, 945, 1016, 1475)
184 | captcha_image2 = full_image.crop(captcha_box2)
185 | captcha_image2.save('puzzle_screenshot1.png')
186 |
187 |
188 | with open('puzzle_screenshot.png', 'rb') as f:
189 | puzzle = base64.b64encode(f.read()).decode()
190 | with open('puzzle_screenshot1.png', 'rb') as f:
191 | piece = base64.b64encode(f.read()).decode()
192 |
193 | data = {
194 | 'puzzleImageB64': puzzle,
195 | 'pieceImageB64': piece
196 | }
197 |
198 | r = requests.post(puzzle_url, json=data)
199 |
200 | slide_x_proportion = r.json().get('slideXProportion')
201 |
202 | offset_x = 46 + (46 * float(slide_x_proportion))
203 |
204 | driver.swipe(start_x=55, start_y=530, end_x=55 + int(offset_x), end_y=530, duration=1000)
205 | time.sleep(3)
206 | ```
207 | The number `46` in my equation comes from the distance between the captcha image and the side of the screen, which is why you add it to the value `offset_x`. `start_x` is supposed to be the center of the puzzle piece. Similarly, `530` is supposed to be the center of the puzzle piece as well.
208 |
209 | ### Rotate
210 | ```py
211 | # SOLVING ROTATE CAPTCHA
212 | BASE_URL = 'https://www.sadcaptcha.com/api/v1'
213 | LICENSE_KEY = ''
214 | rotate_url = f'{BASE_URL}/rotate?licenseKey={LICENSE_KEY}'
215 |
216 | def solve_rotate():
217 | driver.save_screenshot('full_screenshot.png')
218 |
219 | full_image = Image.open('full_screenshot.png')
220 |
221 | captcha_box1 = (415, 1055, 755, 1395)
222 | captcha_image1 = full_image.crop(captcha_box1)
223 |
224 | mask = Image.new('L', captcha_image1.size, 0)
225 | draw = ImageDraw.Draw(mask)
226 | circle_bbox = (0, 0, captcha_image1.size[0], captcha_image1.size[1])
227 | draw.ellipse(circle_bbox, fill=255)
228 |
229 | captcha_image1.putalpha(mask)
230 | captcha_image1.save('captcha_image_circular.png')
231 |
232 | captcha_box2 = (318, 958, 852, 1492)
233 | captcha_image2 = full_image.crop(captcha_box2)
234 |
235 | mask2 = Image.new('L', captcha_image2.size, 0)
236 | draw = ImageDraw.Draw(mask2)
237 | draw.ellipse((captcha_box1[0] - captcha_box2[0], captcha_box1[1] - captcha_box2[1],
238 | captcha_box1[2] - captcha_box2[0], captcha_box1[3] - captcha_box2[1]), fill=255)
239 |
240 | captcha_image_with_hole = captcha_image2.copy()
241 | captcha_image_with_hole.paste((0, 0, 0, 0), (0, 0), mask2)
242 | captcha_image_with_hole.save('captcha_image_with_hole.png')
243 |
244 | # inner and outer images should be cropped to the edges of the circle, without whitespace on the edges
245 | with open('captcha_image_with_hole.png', 'rb') as f:
246 | outer = base64.b64encode(f.read()).decode('utf-8')
247 | with open('captcha_image_circular.png', 'rb') as f:
248 | inner = base64.b64encode(f.read()).decode('utf-8')
249 |
250 | data = {
251 | 'outerImageB64': outer,
252 | 'innerImageB64': inner
253 | }
254 |
255 | r = requests.post(rotate_url, json=data)
256 | r.raise_for_status()
257 | response = r.json()
258 | angle = response.get('angle', 0)
259 |
260 | # calculate where the button needs to be dragged to
261 | # 55 is the width of the slide button
262 | # 286 in the example is the width of the entire bar.
263 | # These values may vary based on your device!
264 | slide_button_width = 55
265 | slide_bar_width = 286
266 | result = ((slide_bar_width - slide_button_width) * angle) / 360
267 | start_x = 55
268 | start_y = 530
269 | offset_x = result
270 |
271 | driver.swipe(start_x, start_y, start_x + int(offset_x), start_y, duration=1000)
272 | ```
273 |
274 |
275 | ## API Client
276 | If you are not using Selenium or Playwright, you can still import and use the API client to help you make calls to SadCaptcha
277 | ```py
278 | from tiktok_captcha_solver import ApiClient
279 |
280 | api_key = "YOUR_API_KEY_HERE"
281 | client = ApiClient(api_key)
282 |
283 | # Rotate
284 | res = client.rotate("base64 encoded outer", "base64 encoded inner")
285 |
286 | # Puzzle
287 | res = client.puzzle("base64 encoded puzzle", "base64 encoded piece")
288 |
289 | # Shapes
290 | res = client.shapes("base64 encoded shapes image")
291 |
292 | # Icon (Video upload)
293 | res = client.icon("Which of these objects... ?", base64 encoded icon image")
294 | ```
295 |
296 | ## Troubleshooting
297 | ### Captcha solved but still says Verification failed?
298 | This common problem is due to your browser settings.
299 | If using Selenium, you must use `undetected_chromedriver` with the **default** settings.
300 | If you are using Playwright, you must use the `playwright_stealth` package with the **default** settings.
301 | Do not change the user agent, or modify any other browser characteristics as this is easily detected and flagged as suspicious behavior.
302 |
303 | ## Contact
304 | To contact us, make an accout and reach out through the contact form or message us on Telegram.
305 | - Homepage: https://www.sadcaptcha.com/
306 | - Telegram @toughdata
307 |
308 | ## The SadCaptcha Team
309 | - [Michael P](https://github.com/michaelzeboth) - Python Client and Chrome Extension Maintainer
310 | - [Greg B](https://github.com/gbiz123) - Full Stack and Algorithm Developer
311 |
--------------------------------------------------------------------------------
/post_login_click.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/post_login_click.png
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | # [build-system]
2 | requires = ["setuptools"]
3 | build-backend = "setuptools.build_meta" # If not defined, then legacy behavior can happen.
4 |
5 | [project]
6 | name = "tiktok-captcha-solver"
7 | version = "0.8.5"
8 |
9 | description = "This package integrates with Selenium or Playwright to solve any TikTok captcha in one line of code."
10 | readme = "README.md"
11 | requires-python = ">=3.10"
12 |
13 | keywords = ["tiktok", "captcha", "solver", "selenium", "rotate", "puzzle", "3d"]
14 |
15 | authors = [
16 | {name = "Toughdata LLC", email = "greg@toughdata.net" }
17 | ]
18 |
19 | classifiers = [
20 | "Intended Audience :: Developers",
21 | "Topic :: Software Development :: Build Tools",
22 | "Programming Language :: Python :: 3.10",
23 | "Programming Language :: Python :: 3.11",
24 | "Programming Language :: Python :: 3.12",
25 | "Programming Language :: Python :: 3 :: Only",
26 | ]
27 |
28 | dependencies = [
29 | "selenium",
30 | "webdriver-manager",
31 | "pydantic",
32 | "requests",
33 | "pytest",
34 | "pytest-asyncio",
35 | "playwright",
36 | "playwright-stealth",
37 | "undetected_chromedriver",
38 | "setuptools",
39 | "nodriver"
40 | ]
41 |
42 | [project.urls]
43 | "Homepage" = "https://www.sadcaptcha.com"
44 | "Source" = "https://github.com/gbiz123/tiktok-captcha-solver/"
45 |
--------------------------------------------------------------------------------
/pyrightconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "typeCheckingMode": "basic"
3 | }
4 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver.egg-info/PKG-INFO:
--------------------------------------------------------------------------------
1 | Metadata-Version: 2.4
2 | Name: tiktok-captcha-solver
3 | Version: 0.8.4
4 | Summary: This package integrates with Selenium or Playwright to solve any TikTok captcha in one line of code.
5 | Author-email: Toughdata LLC
6 | Project-URL: Homepage, https://www.sadcaptcha.com
7 | Project-URL: Source, https://github.com/gbiz123/tiktok-captcha-solver/
8 | Keywords: tiktok,captcha,solver,selenium,rotate,puzzle,3d
9 | Classifier: Intended Audience :: Developers
10 | Classifier: Topic :: Software Development :: Build Tools
11 | Classifier: Programming Language :: Python :: 3.10
12 | Classifier: Programming Language :: Python :: 3.11
13 | Classifier: Programming Language :: Python :: 3.12
14 | Classifier: Programming Language :: Python :: 3 :: Only
15 | Requires-Python: >=3.10
16 | Description-Content-Type: text/markdown
17 | Requires-Dist: selenium
18 | Requires-Dist: webdriver-manager
19 | Requires-Dist: pydantic
20 | Requires-Dist: requests
21 | Requires-Dist: pytest
22 | Requires-Dist: pytest-asyncio
23 | Requires-Dist: playwright
24 | Requires-Dist: playwright-stealth
25 | Requires-Dist: undetected_chromedriver
26 | Requires-Dist: setuptools
27 | Requires-Dist: nodriver
28 |
29 | # TikTok Captcha Solver API
30 | This project is the [SadCaptcha TikTok Captcha Solver](https://www.sadcaptcha.com?ref=ghclientrepo) API client.
31 | The purpose is to make integrating SadCaptcha into your Selenium, Playwright, or Async Playwright app as simple as one line of code.
32 | Instructions for integrating with Selenium, Playwright, and Async Playwright are described below in their respective sections.
33 | This API also works on mobile devices (Appium, etc.).
34 |
35 | This tool works on both TikTok and Douyin and can solve any of the four captcha challenges pictured below:
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | ## Requirements
46 | - Python >= 3.10
47 | - **If using Nodriver** - Google chrome installe don system. This is the recommended method.
48 | - **If using Selenium** - Selenium properly installed and in `PATH`
49 | - **If using Playwright** - Playwright must be properly installed with `playwright install`
50 | - **If using mobile** - Appium and opencv must be properly installed
51 | - **Stealth plugin** - You must use the appropriate `stealth` plugin for whichever browser automation framework you are using.
52 | - For Selenium, you can use [undetected-chromedriver](https://github.com/ultrafunkamsterdam/undetected-chromedriver)
53 | - For Playwright, you can use [playwright-stealth](https://pypi.org/project/playwright-stealth/)
54 |
55 | ## Installation
56 | This project can be installed with `pip`. Just run the following command:
57 | ```
58 | pip install tiktok-captcha-solver
59 | ```
60 |
61 | ## Note for NodeJS users
62 | For users automating in NodeJS or another programming language, the recommended method is to download the chrome extension from the
63 | chrome web store, unzip the file, patch the script.js file with your API key, and load it into your browser.
64 | This will save you a lot of time implementing the API on your own.
65 |
66 | ## Note on running headless
67 | To run in headless mode, you need to use the launch arg `headless=new` or `headless=chrome` as a launch arg.
68 | Instructions to do this are in their own respective sections.
69 | Another option is to use [Xvfb](https://www.x.org/archive/X11R7.7/doc/man/man1/Xvfb.1.xhtml) with `headless=True` to spoof a graphical environment.
70 |
71 | ## Nodriver Client (Recommended)
72 | Nodriver is the latest advancement in undetected automation technology, and is the recommended method for using SadCaptcha.
73 | Import the function `make_nodriver_solver`
74 | This function will create an noddriver instance patched with the tiktok Captcha Solver chrome extension.
75 | The extension will automatically detect and solve the captcha in the background, and there is nothing further you need to do.
76 |
77 | ```py
78 | from tiktok_captcha_solver import make_nodriver_solver
79 |
80 | launch_args = ["--headless=chrome"] # If running headless, use this option, or headless=new
81 |
82 | api_key = "YOUR_API_KEY_HERE"
83 | # NOTE: Keyword arguments passed to make_nodriver_solver() are directly passed to nodriver.start()!
84 | driver = make_nodriver_solver(api_key, browser_args=launch_args) # Returns nodriver browser
85 | # ... [The rest of your code that accesses tiktok goes here]
86 |
87 | # Now tiktok captchas will be automatically solved!
88 | ```
89 | All keyword arguments passed to `make_nodriver_solver()` are passed directly to `nodriver.start()`.
90 |
91 | ## Selenium Client
92 | Import the function `make_undetected_chromedriver_solver`
93 | This function will create an undetected chromedriver instance patched with the tiktok Captcha Solver chrome extension.
94 | The extension will automatically detect and solve the captcha in the background, and there is nothing further you need to do.
95 |
96 | ```py
97 | from tiktok_captcha_solver import make_undetected_chromedriver_solver
98 | from selenium_stealth import stealth
99 | from selenium.webdriver import ChromeOptions
100 | import undetected_chromedriver as uc
101 |
102 | chrome_options = ChromeOptions()
103 | # chrome_options.add_argument("--headless=chrome") # If running headless, use this option
104 |
105 | api_key = "YOUR_API_KEY_HERE"
106 | driver = make_undetected_chromedriver_solver(api_key, options=options) # Returns uc.Chrome instance
107 | stealth(driver) # Add stealth if needed
108 | # ... [The rest of your code that accesses tiktok goes here]
109 |
110 | # Now tiktok captchas will be automatically solved!
111 | ```
112 | You may also pass `ChromeOptions` to `make_undetected_chromedriver_solver()`, as well as keyword arguments for `uc.Chrome()`.
113 |
114 | ## Playwright Client
115 | Import the function `make_playwright_solver_context`
116 | This function will create a playwright BrowserContext instance patched with the tiktok Captcha Solver chrome extension.
117 | The extension will automatically detect and solve the captcha in the background, and there is nothing further you need to do.
118 |
119 | ```py
120 | from tiktok_captcha_solver import make_playwright_solver_context
121 | from playwright.sync_api import sync_playwright
122 | from playwright_stealth import stealth_sync, StealthConfig
123 |
124 | launch_args = ["--headless=chrome"] # or --headless=new if that doesn't work
125 |
126 | api_key = "YOUR_API_KEY_HERE"
127 | with sync_playwright() as p:
128 | # Keyword arguments are passed to p.chromium.launch_persistent_context()
129 | # Returns playwright BrowserContext instance
130 | context = make_playwright_solver_context(p, api_key, args=launch_args)
131 |
132 | # If using playwright_stealth, you need to use this StealthConfig to avoid the white screen:
133 | page = context.new_page()
134 | stealth_config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
135 | stealth_sync(page, stealth_config)
136 |
137 | # ... [The rest of your code that accesses tiktok goes here]
138 |
139 | # Now tiktok captchas will be automatically solved!
140 | ```
141 | You may also pass keyword args to this function, which will be passed directly to playwright's call to `playwright.chromium.launch_persistent_context()`.
142 | By default, the user data directory is a tempory directory that is deleted at the end of runtime.
143 |
144 | ## Async Playwright Client
145 | Import the function `make_async_playwright_solver_context`
146 | This function will create an async playwright BrowserContext instance patched with the tiktok Captcha Solver chrome extension.
147 | The extension will automatically detect and solve the captcha in the background, and there is nothing further you need to do.
148 |
149 | ```py
150 | import asyncio
151 | from playwright.async_api import async_playwright
152 | from tiktok_captcha_solver import make_async_playwright_solver_context
153 | from playwright_stealth import stealth_ssync, StealthConfig
154 |
155 | # Need this arg if running headless
156 | launch_args = ["--headless=chrome"] # or --headless=new if that doesn't work
157 |
158 | async def main():
159 | api_key = "YOUR_API_KEY_HERE"
160 | async with async_playwright() as p:
161 | # Keyword arguments are passed to p.chromium.launch_persistent_context()
162 | # Returns playwright BrowserContext instance
163 | context = await make_async_playwright_solver_context(p, api_key, args=launch_args)
164 |
165 | # If using playwright_stealth, you need to use this StealthConfig to avoid the white screen:
166 | page = await context.new_page()
167 | stealth_config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
168 | stealth_async(page, stealth_config)
169 |
170 | # ... [The rest of your code that accesses tiktok goes here]
171 |
172 | asyncio.run(main())
173 |
174 | # Now tiktok captchas will be automatically solved!
175 | ```
176 | You may also pass keyword args to this function, which will be passed directly to playwright's call to `playwright.chromium.launch_persistent_context()`.
177 | By default, the user data directory is a tempory directory that is deleted at the end of runtime.
178 |
179 | ## Mobile (Appium)
180 | Currently there is no premade solver for Mobile/appium, but you can implement the API with relative ease.
181 | The idea is that you take a screenshot using the mobile driver, crop the images, and then send the images to the API.
182 | Once you've done that, you can consume the response.
183 | Here is a working example for Puzzle and Rotate captcha.
184 | keep in mind, you will need to adjust the `captcha_box` and `offset_x` varaibles according to your particular mobile device.
185 |
186 | ### Puzzle slide
187 | ```py
188 | from PIL import Image, ImageDraw
189 | import base64
190 | import requests
191 |
192 | # SOLVING PUZZLE CAPTCHA
193 | BASE_URL = 'https://www.sadcaptcha.com/api/v1'
194 | LICENSE_KEY = ''
195 | puzzle_url = f'{BASE_URL}/puzzle?licenseKey={LICENSE_KEY}'
196 |
197 | def solve_puzzle():
198 | # Screenshot of page
199 | driver.save_screenshot('puzzle.png')
200 | full_image = Image.open('puzzle.png')
201 |
202 | # Full puzzle image - adjust box to your device
203 | captcha_box1 = (165, 1175, 303, 1330)
204 | captcha_image1 = full_image.crop(captcha_box1)
205 |
206 | # Draw circle over left side to occlude the puzzle piece in the main image
207 | draw = ImageDraw.Draw(captcha_image1)
208 | draw.ellipse([(0, 0), (captcha_image1.width / 4, captcha_image1.height)], fill="blue", outline="blue")
209 | captcha_image1.save('puzzle_screenshot.png')
210 |
211 | # Puzzle piece image - adjust box to your device
212 | captcha_box2 = (300, 945, 1016, 1475)
213 | captcha_image2 = full_image.crop(captcha_box2)
214 | captcha_image2.save('puzzle_screenshot1.png')
215 |
216 |
217 | with open('puzzle_screenshot.png', 'rb') as f:
218 | puzzle = base64.b64encode(f.read()).decode()
219 | with open('puzzle_screenshot1.png', 'rb') as f:
220 | piece = base64.b64encode(f.read()).decode()
221 |
222 | data = {
223 | 'puzzleImageB64': puzzle,
224 | 'pieceImageB64': piece
225 | }
226 |
227 | r = requests.post(puzzle_url, json=data)
228 |
229 | slide_x_proportion = r.json().get('slideXProportion')
230 |
231 | offset_x = 46 + (46 * float(slide_x_proportion))
232 |
233 | driver.swipe(start_x=55, start_y=530, end_x=55 + int(offset_x), end_y=530, duration=1000)
234 | time.sleep(3)
235 | ```
236 | The number `46` in my equation comes from the distance between the captcha image and the side of the screen, which is why you add it to the value `offset_x`. `start_x` is supposed to be the center of the puzzle piece. Similarly, `530` is supposed to be the center of the puzzle piece as well.
237 |
238 | ### Rotate
239 | ```py
240 | # SOLVING ROTATE CAPTCHA
241 | BASE_URL = 'https://www.sadcaptcha.com/api/v1'
242 | LICENSE_KEY = ''
243 | rotate_url = f'{BASE_URL}/rotate?licenseKey={LICENSE_KEY}'
244 |
245 | def solve_rotate():
246 | driver.save_screenshot('full_screenshot.png')
247 |
248 | full_image = Image.open('full_screenshot.png')
249 |
250 | captcha_box1 = (415, 1055, 755, 1395)
251 | captcha_image1 = full_image.crop(captcha_box1)
252 |
253 | mask = Image.new('L', captcha_image1.size, 0)
254 | draw = ImageDraw.Draw(mask)
255 | circle_bbox = (0, 0, captcha_image1.size[0], captcha_image1.size[1])
256 | draw.ellipse(circle_bbox, fill=255)
257 |
258 | captcha_image1.putalpha(mask)
259 | captcha_image1.save('captcha_image_circular.png')
260 |
261 | captcha_box2 = (318, 958, 852, 1492)
262 | captcha_image2 = full_image.crop(captcha_box2)
263 |
264 | mask2 = Image.new('L', captcha_image2.size, 0)
265 | draw = ImageDraw.Draw(mask2)
266 | draw.ellipse((captcha_box1[0] - captcha_box2[0], captcha_box1[1] - captcha_box2[1],
267 | captcha_box1[2] - captcha_box2[0], captcha_box1[3] - captcha_box2[1]), fill=255)
268 |
269 | captcha_image_with_hole = captcha_image2.copy()
270 | captcha_image_with_hole.paste((0, 0, 0, 0), (0, 0), mask2)
271 | captcha_image_with_hole.save('captcha_image_with_hole.png')
272 |
273 | # inner and outer images should be cropped to the edges of the circle, without whitespace on the edges
274 | with open('captcha_image_with_hole.png', 'rb') as f:
275 | outer = base64.b64encode(f.read()).decode('utf-8')
276 | with open('captcha_image_circular.png', 'rb') as f:
277 | inner = base64.b64encode(f.read()).decode('utf-8')
278 |
279 | data = {
280 | 'outerImageB64': outer,
281 | 'innerImageB64': inner
282 | }
283 |
284 | r = requests.post(rotate_url, json=data)
285 | r.raise_for_status()
286 | response = r.json()
287 | angle = response.get('angle', 0)
288 |
289 | # calculate where the button needs to be dragged to
290 | # 55 is the width of the slide button
291 | # 286 in the example is the width of the entire bar.
292 | # These values may vary based on your device!
293 | slide_button_width = 55
294 | slide_bar_width = 286
295 | result = ((slide_bar_width - slide_button_width) * angle) / 360
296 | start_x = 55
297 | start_y = 530
298 | offset_x = result
299 |
300 | driver.swipe(start_x, start_y, start_x + int(offset_x), start_y, duration=1000)
301 | ```
302 |
303 |
304 | ## API Client
305 | If you are not using Selenium or Playwright, you can still import and use the API client to help you make calls to SadCaptcha
306 | ```py
307 | from tiktok_captcha_solver import ApiClient
308 |
309 | api_key = "YOUR_API_KEY_HERE"
310 | client = ApiClient(api_key)
311 |
312 | # Rotate
313 | res = client.rotate("base64 encoded outer", "base64 encoded inner")
314 |
315 | # Puzzle
316 | res = client.puzzle("base64 encoded puzzle", "base64 encoded piece")
317 |
318 | # Shapes
319 | res = client.shapes("base64 encoded shapes image")
320 |
321 | # Icon (Video upload)
322 | res = client.icon("Which of these objects... ?", base64 encoded icon image")
323 | ```
324 |
325 | ## Troubleshooting
326 | ### Captcha solved but still says Verification failed?
327 | This common problem is due to your browser settings.
328 | If using Selenium, you must use `undetected_chromedriver` with the **default** settings.
329 | If you are using Playwright, you must use the `playwright_stealth` package with the **default** settings.
330 | Do not change the user agent, or modify any other browser characteristics as this is easily detected and flagged as suspicious behavior.
331 |
332 | ## Contact
333 | To contact us, make an accout and reach out through the contact form or message us on Telegram.
334 | - Homepage: https://www.sadcaptcha.com/
335 | - Telegram @toughdata
336 |
337 | ## The SadCaptcha Team
338 | - [Michael P](https://github.com/michaelzeboth) - Python Client and Chrome Extension Maintainer
339 | - [Greg B](https://github.com/gbiz123) - Full Stack and Algorithm Developer
340 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver.egg-info/SOURCES.txt:
--------------------------------------------------------------------------------
1 | README.md
2 | pyproject.toml
3 | src/tiktok_captcha_solver/__init__.py
4 | src/tiktok_captcha_solver/api.py
5 | src/tiktok_captcha_solver/asyncplaywrightsolver.py
6 | src/tiktok_captcha_solver/asyncsolver.py
7 | src/tiktok_captcha_solver/captchatype.py
8 | src/tiktok_captcha_solver/download_crx.py
9 | src/tiktok_captcha_solver/downloader.py
10 | src/tiktok_captcha_solver/geometry.py
11 | src/tiktok_captcha_solver/launcher.py
12 | src/tiktok_captcha_solver/models.py
13 | src/tiktok_captcha_solver/nodriversolver.py
14 | src/tiktok_captcha_solver/playwrightsolver.py
15 | src/tiktok_captcha_solver/selectors.py
16 | src/tiktok_captcha_solver/seleniumsolver.py
17 | src/tiktok_captcha_solver/solver.py
18 | src/tiktok_captcha_solver.egg-info/PKG-INFO
19 | src/tiktok_captcha_solver.egg-info/SOURCES.txt
20 | src/tiktok_captcha_solver.egg-info/dependency_links.txt
21 | src/tiktok_captcha_solver.egg-info/requires.txt
22 | src/tiktok_captcha_solver.egg-info/top_level.txt
23 | src/tiktok_captcha_solver/tests/__init__.py
24 | src/tiktok_captcha_solver/tests/test_api.py
25 | src/tiktok_captcha_solver/tests/test_asyncplaywrightsolver.py
26 | src/tiktok_captcha_solver/tests/test_download_crx.py
27 | src/tiktok_captcha_solver/tests/test_downloader.py
28 | src/tiktok_captcha_solver/tests/test_launcher.py
29 | src/tiktok_captcha_solver/tests/test_playwrightsolver.py
30 | src/tiktok_captcha_solver/tests/test_seleniumsolver.py
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver.egg-info/dependency_links.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver.egg-info/requires.txt:
--------------------------------------------------------------------------------
1 | selenium
2 | webdriver-manager
3 | pydantic
4 | requests
5 | pytest
6 | pytest-asyncio
7 | playwright
8 | playwright-stealth
9 | undetected_chromedriver
10 | setuptools
11 | nodriver
12 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver.egg-info/top_level.txt:
--------------------------------------------------------------------------------
1 | tiktok_captcha_solver
2 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/__init__.py:
--------------------------------------------------------------------------------
1 | from .seleniumsolver import SeleniumSolver
2 | from .playwrightsolver import PlaywrightSolver
3 | from .asyncplaywrightsolver import AsyncPlaywrightSolver
4 | from .api import ApiClient
5 | from .launcher import make_playwright_solver_context, make_undetected_chromedriver_solver, make_async_playwright_solver_context
6 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/api.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import logging
3 |
4 | from .models import ProportionalPoint, ShapesCaptchaResponse, RotateCaptchaResponse, PuzzleCaptchaResponse, IconCaptchaResponse
5 |
6 | class ApiClient:
7 |
8 | _PUZZLE_URL: str
9 | _ROTATE_URL: str
10 | _SHAPES_URL: str
11 | _ICON_URL: str
12 |
13 | def __init__(self, api_key: str) -> None:
14 | self._PUZZLE_URL = "https://www.sadcaptcha.com/api/v1/puzzle?licenseKey=" + api_key
15 | self._ROTATE_URL = "https://www.sadcaptcha.com/api/v1/rotate?licenseKey=" + api_key
16 | self._SHAPES_URL = "https://www.sadcaptcha.com/api/v1/shapes?licenseKey=" + api_key
17 | self._ICON_URL = "https://www.sadcaptcha.com/api/v1/icon?licenseKey=" + api_key
18 |
19 | def rotate(self, outer_b46: str, inner_b64: str) -> RotateCaptchaResponse:
20 | """Slide the slider to rotate the images"""
21 | data = {
22 | "outerImageB64": outer_b46,
23 | "innerImageB64": inner_b64
24 | }
25 | resp = requests.post(self._ROTATE_URL, json=data)
26 | result = resp.json()
27 | logging.debug("Got API response")
28 | return RotateCaptchaResponse(angle=result.get("angle"))
29 |
30 | def puzzle(self, puzzle_b64: str, piece_b64: str) -> PuzzleCaptchaResponse:
31 | """Slide the puzzle piece"""
32 | data = {
33 | "puzzleImageB64": puzzle_b64,
34 | "pieceImageB64": piece_b64
35 | }
36 | resp = requests.post(self._PUZZLE_URL, json=data)
37 | result = resp.json()
38 | logging.debug("Got API response")
39 | return PuzzleCaptchaResponse(slide_x_proportion=result.get("slideXProportion"))
40 |
41 | def shapes(self, image_b64: str) -> ShapesCaptchaResponse:
42 | """Click the two matching points"""
43 | data = { "imageB64": image_b64 }
44 | resp = requests.post(self._SHAPES_URL, json=data)
45 | result = resp.json()
46 | logging.debug("Got API response")
47 | return ShapesCaptchaResponse(
48 | point_one_proportion_x=result.get("pointOneProportionX"),
49 | point_one_proportion_y=result.get("pointOneProportionY"),
50 | point_two_proportion_x=result.get("pointTwoProportionX"),
51 | point_two_proportion_y=result.get("pointTwoProportionY")
52 | )
53 |
54 | def icon(self, challenge_text: str, image_b64: str) -> IconCaptchaResponse:
55 | """Which of these objects has a... type captcha. Shown at video upload."""
56 | data = { "challenge": challenge_text, "imageB64": image_b64 }
57 | resp = requests.post(self._ICON_URL, json=data)
58 | result = resp.json()
59 | logging.debug("Got API response")
60 | resp = IconCaptchaResponse(proportional_points=[])
61 | for point in result.get("proportionalPoints"):
62 | resp.proportional_points.append(
63 | ProportionalPoint(
64 | proportion_x=point.get("proportionX"),
65 | proportion_y=point.get("proportionY")
66 | )
67 | )
68 | return resp
69 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/asyncplaywrightsolver.py:
--------------------------------------------------------------------------------
1 | """This class handles the captcha solving for playwright users"""
2 |
3 | import logging
4 | from optparse import Values
5 | import random
6 | from typing import Any
7 | from playwright.async_api import FloatRect, Page, expect
8 | from playwright.async_api import TimeoutError
9 | from undetected_chromedriver.reactor import asyncio
10 |
11 | from . import selectors
12 | from .captchatype import CaptchaType
13 | from .asyncsolver import AsyncSolver
14 | from .api import ApiClient
15 | from .downloader import fetch_image_b64_async_page
16 | from .geometry import compute_pixel_fraction, compute_rotate_slide_distance, get_translateX_from_style
17 |
18 | class AsyncPlaywrightSolver(AsyncSolver):
19 |
20 | client: ApiClient
21 | page: Page
22 |
23 | def __init__(
24 | self,
25 | page: Page,
26 | sadcaptcha_api_key: str,
27 | headers: dict[str, Any] | None = None,
28 | proxy: str | None = None,
29 | mouse_step_size: int = 1,
30 | mouse_step_delay_ms: int = 10
31 | ) -> None:
32 | self.page = page
33 | self.client = ApiClient(sadcaptcha_api_key)
34 | self.headers = headers
35 | self.proxy = proxy
36 | self.mouse_step_size = mouse_step_size
37 | self.mouse_step_delay_ms = mouse_step_delay_ms
38 |
39 | async def captcha_is_present(self, timeout: int = 15) -> bool:
40 | if self.page_is_douyin():
41 | try:
42 | douyin_locator = self.page.frame_locator(selectors.DouyinPuzzle.FRAME).locator("*")
43 | await expect(douyin_locator.first).not_to_have_count(0)
44 | except (TimeoutError, AssertionError):
45 | return False
46 | else:
47 | try:
48 | tiktok_locator = self.page.locator(f"{selectors.Wrappers.V1}, {selectors.Wrappers.V2}")
49 | await expect(tiktok_locator.first).to_be_visible(timeout=timeout * 1000)
50 | logging.debug("v1 or v2 tiktok selector present")
51 | except (TimeoutError, AssertionError):
52 | return False
53 | return True
54 |
55 | async def captcha_is_not_present(self, timeout: int = 15) -> bool:
56 | if self.page_is_douyin():
57 | try:
58 | douyin_locator = self.page.frame_locator(selectors.DouyinPuzzle.FRAME).locator("*")
59 | await expect(douyin_locator.first).to_have_count(0)
60 | except (TimeoutError, AssertionError):
61 | return False
62 | else:
63 | try:
64 | tiktok_locator = self.page.locator(f"{selectors.Wrappers.V1}, {selectors.Wrappers.V2}")
65 | await expect(tiktok_locator.first).to_have_count(0, timeout=timeout * 1000)
66 | logging.debug("v1 or v2 tiktok selector not present")
67 | except (TimeoutError, AssertionError):
68 | return False
69 | return True
70 |
71 | async def identify_captcha(self) -> CaptchaType:
72 | for _ in range(60):
73 | try:
74 | if await self._any_selector_in_list_present([selectors.PuzzleV1.UNIQUE_IDENTIFIER]):
75 | logging.debug("detected puzzle")
76 | return CaptchaType.PUZZLE_V1
77 | if await self._any_selector_in_list_present([selectors.PuzzleV2.UNIQUE_IDENTIFIER]):
78 | logging.debug("detected puzzle v2")
79 | return CaptchaType.PUZZLE_V2
80 | elif await self._any_selector_in_list_present([selectors.RotateV1.UNIQUE_IDENTIFIER]):
81 | logging.debug("detected rotate v1")
82 | return CaptchaType.ROTATE_V1
83 | elif await self._any_selector_in_list_present([selectors.RotateV2.UNIQUE_IDENTIFIER]):
84 | logging.debug("detected rotate v2")
85 | return CaptchaType.ROTATE_V2
86 | if await self._any_selector_in_list_present([selectors.ShapesV1.UNIQUE_IDENTIFIER]):
87 | img_url = await self._get_image_url(selectors.ShapesV1.IMAGE)
88 | if "/icon" in img_url:
89 | logging.debug("detected icon v1")
90 | return CaptchaType.ICON_V1
91 | elif "/3d" in img_url:
92 | logging.debug("detected shapes v1")
93 | return CaptchaType.SHAPES_V1
94 | else:
95 | logging.warn("did not see '/3d' in image source url but returning shapes v1 anyways")
96 | return CaptchaType.SHAPES_V1
97 | if await self._any_selector_in_list_present([selectors.ShapesV2.UNIQUE_IDENTIFIER]):
98 | img_url = await self._get_image_url(selectors.ShapesV2.IMAGE)
99 | if "/icon" in img_url:
100 | logging.debug("detected icon v2")
101 | return CaptchaType.ICON_V2
102 | elif "/3d" in img_url:
103 | logging.debug("detected shapes v2")
104 | return CaptchaType.SHAPES_V2
105 | else:
106 | logging.warn("did not see '/3d' in image source url but returning shapes v2 anyways")
107 | return CaptchaType.SHAPES_V2
108 | else:
109 | await asyncio.sleep(0.5)
110 | except Exception as e:
111 | logging.debug(f"Exception occurred identifying captcha: {str(e)}. Trying again")
112 | await asyncio.sleep(0.5)
113 | raise ValueError("Neither puzzle, shapes, or rotate captcha was present.")
114 |
115 | def page_is_douyin(self) -> bool:
116 | if "douyin" in self.page.url:
117 | logging.debug("page is douyin")
118 | return True
119 | logging.debug("page is tiktok")
120 | return False
121 |
122 | async def solve_shapes(self, retries: int = 3) -> None:
123 | for _ in range(retries):
124 | if not await self._any_selector_in_list_present([selectors.ShapesV1.IMAGE]):
125 | logging.debug("Went to solve shapes but #captcha-verify-image was not present")
126 | return
127 | image = await fetch_image_b64_async_page(await self._get_image_url(selectors.ShapesV1.IMAGE), self.page)
128 | solution = self.client.shapes(image)
129 | image_element = self.page.locator(selectors.ShapesV1.IMAGE)
130 | bounding_box = await image_element.bounding_box()
131 | if not bounding_box:
132 | raise AttributeError("Image element was found but had no bounding box")
133 | await self._click_proportional(bounding_box, solution.point_one_proportion_x, solution.point_one_proportion_y)
134 | await self._click_proportional(bounding_box, solution.point_two_proportion_x, solution.point_two_proportion_y)
135 | await self.page.locator(selectors.ShapesV1.SUBMIT_BUTTON).click()
136 | if await self.captcha_is_not_present(timeout=5):
137 | return
138 | else:
139 | await asyncio.sleep(5)
140 |
141 | async def solve_shapes_v2(self, retries: int = 3) -> None:
142 | for _ in range(retries):
143 | if not await self._any_selector_in_list_present([selectors.ShapesV2.IMAGE]):
144 | logging.debug("Went to solve shapes but image was not present")
145 | return
146 | image = await fetch_image_b64_async_page(await self._get_image_url(selectors.ShapesV2.IMAGE), self.page)
147 | solution = self.client.shapes(image)
148 | image_element = self.page.locator(selectors.ShapesV2.IMAGE)
149 | bounding_box = await image_element.bounding_box()
150 | if not bounding_box:
151 | raise AttributeError("Image element was found but had no bounding box")
152 | await self._click_proportional(bounding_box, solution.point_one_proportion_x, solution.point_one_proportion_y)
153 | await self._click_proportional(bounding_box, solution.point_two_proportion_x, solution.point_two_proportion_y)
154 | await self.page.locator(selectors.ShapesV2.SUBMIT_BUTTON).click()
155 | if await self.captcha_is_not_present(timeout=5):
156 | return
157 | else:
158 | await asyncio.sleep(5)
159 |
160 | async def solve_rotate(self, retries: int = 3) -> None:
161 | for _ in range(retries):
162 | if not await self._any_selector_in_list_present([selectors.RotateV1.INNER]):
163 | logging.debug("Went to solve rotate but whirl-inner-img was not present")
164 | return
165 |
166 | outer_url, inner_url, slide_bar_width, slide_button_width = await asyncio.gather(
167 | self._get_image_url(selectors.RotateV1.OUTER),
168 | self._get_image_url(selectors.RotateV1.INNER),
169 | )
170 |
171 | outer, inner = await asyncio.gather(
172 | fetch_image_b64_async_page(outer_url, self.page),
173 | fetch_image_b64_async_page(inner_url, self.page)
174 | )
175 |
176 | solution = self.client.rotate(outer, inner)
177 | logging.debug(f"Solution angle: {solution}")
178 | slide_bar_width, slide_button_width = await asyncio.gather(
179 | self._get_element_width(selectors.RotateV1.SLIDE_BAR),
180 | self._get_element_width(selectors.RotateV1.SLIDER_DRAG_BUTTON)
181 | )
182 | distance = compute_rotate_slide_distance(solution.angle, slide_bar_width, slide_button_width)
183 | logging.debug(f"Solution distance: {distance}")
184 | await self._drag_element_horizontal(selectors.RotateV1.SLIDER_DRAG_BUTTON, distance)
185 | if await self.captcha_is_not_present(timeout=5):
186 | return
187 | else:
188 | await asyncio.sleep(5)
189 |
190 | async def solve_rotate_v2(self, retries: int = 3) -> None:
191 | for _ in range(retries):
192 | if not await self._any_selector_in_list_present([selectors.RotateV2.INNER]):
193 | logging.debug("Went to solve rotate but whirl-inner-img was not present")
194 | return
195 |
196 | outer_url, inner_url, = await asyncio.gather(
197 | self._get_image_url(selectors.RotateV2.OUTER),
198 | self._get_image_url(selectors.RotateV2.INNER),
199 | )
200 |
201 | outer, inner = await asyncio.gather(
202 | fetch_image_b64_async_page(outer_url, self.page),
203 | fetch_image_b64_async_page(inner_url, self.page)
204 | )
205 |
206 | solution = self.client.rotate(outer, inner)
207 | logging.debug(f"Solution angle: {solution}")
208 | slide_bar_width, slide_button_width = await asyncio.gather(
209 | self._get_element_width(selectors.RotateV2.SLIDE_BAR),
210 | self._get_element_width(selectors.RotateV2.SLIDER_DRAG_BUTTON)
211 | )
212 | distance = compute_rotate_slide_distance(solution.angle, slide_bar_width, slide_button_width)
213 | logging.debug(f"Solution distance: {distance}")
214 | await self._drag_element_horizontal(selectors.RotateV2.SLIDER_DRAG_BUTTON, distance)
215 | if await self.captcha_is_not_present(timeout=5):
216 | return
217 | else:
218 | await self.page.click(selectors.RotateV2.REFRESH_BUTTON)
219 | logging.debug("clicked refresh button")
220 | await asyncio.sleep(3)
221 |
222 | async def solve_puzzle(self, retries: int = 3) -> None:
223 | for _ in range(retries):
224 | if not await self._any_selector_in_list_present([selectors.PuzzleV1.PIECE]):
225 | logging.debug("Went to solve puzzle but piece image was not present")
226 | return
227 | puzzle_url, piece_url = await asyncio.gather(self._get_image_url(selectors.PuzzleV1.PUZZLE), self._get_image_url(selectors.PuzzleV1.PIECE))
228 |
229 | puzzle, piece = await asyncio.gather(
230 | fetch_image_b64_async_page(puzzle_url, self.page),
231 | fetch_image_b64_async_page(piece_url, self.page)
232 | )
233 |
234 | solution = self.client.puzzle(puzzle, piece)
235 | puzzle_width = await self._get_element_width(selectors.PuzzleV1.PUZZLE)
236 | distance = compute_pixel_fraction(solution.slide_x_proportion, puzzle_width)
237 | await self._drag_element_horizontal(selectors.PuzzleV1.SLIDER_DRAG_BUTTON, distance)
238 | if await self.captcha_is_not_present(timeout=5):
239 | return
240 | else:
241 | await asyncio.sleep(5)
242 |
243 | async def solve_puzzle_v2(self, retries: int = 3) -> None:
244 | for _ in range(retries):
245 | if not await self._any_selector_in_list_present([selectors.PuzzleV2.PIECE]):
246 | logging.debug("Went to solve puzzle but piece image was not present")
247 | return
248 |
249 | puzzle_url, piece_url = await asyncio.gather(
250 | self._get_image_url(selectors.PuzzleV2.PUZZLE),
251 | self._get_image_url(selectors.PuzzleV2.PIECE),
252 | )
253 |
254 | puzzle, piece = await asyncio.gather(
255 | fetch_image_b64_async_page(puzzle_url, self.page),
256 | fetch_image_b64_async_page(piece_url, self.page)
257 | )
258 |
259 | solution = self.client.puzzle(puzzle, piece)
260 | puzzle_width = await self._get_element_width(selectors.PuzzleV2.PUZZLE)
261 | distance = compute_pixel_fraction(solution.slide_x_proportion, puzzle_width)
262 | logging.debug("distance = " + str(distance))
263 | await self._drag_ele_until_watched_ele_has_translateX(
264 | selectors.PuzzleV2.SLIDER_DRAG_BUTTON,
265 | selectors.PuzzleV2.PIECE_IMAGE_CONTAINER,
266 | distance
267 | )
268 | if await self.captcha_is_not_present(timeout=5):
269 | return
270 | else:
271 | await asyncio.sleep(5)
272 |
273 | async def solve_icon(self) -> None:
274 | if not await self._any_selector_in_list_present([selectors.IconV1.IMAGE]):
275 | logging.debug("Went to solve icon captcha but #captcha-verify-image was not present")
276 | return
277 |
278 | challenge, image_url = await asyncio.gather(
279 | self._get_element_text(selectors.IconV1.TEXT),
280 | self._get_image_url(selectors.IconV1.IMAGE)
281 | )
282 | image = await fetch_image_b64_async_page(image_url, self.page) # 이미지는 다운로드 필요
283 | solution = self.client.icon(challenge, image)
284 | image_element = self.page.locator(selectors.IconV1.IMAGE)
285 | bounding_box = await image_element.bounding_box()
286 | if not bounding_box:
287 | raise AttributeError("Image element was found but had no bounding box")
288 | for point in solution.proportional_points:
289 | await self._click_proportional(bounding_box, point.proportion_x, point.proportion_y)
290 | await self.page.locator(selectors.IconV1.SUBMIT_BUTTON).click()
291 |
292 | async def solve_icon_v2(self) -> None:
293 | if not await self._any_selector_in_list_present([selectors.IconV2.IMAGE]):
294 | logging.debug("Went to solve icon captcha but #captcha-verify-image was not present")
295 | return
296 |
297 | challenge, image_url = await asyncio.gather(
298 | self._get_element_text(selectors.IconV2.TEXT),
299 | self._get_image_url(selectors.IconV2.IMAGE)
300 | )
301 | image = await fetch_image_b64_async_page(image_url, self.page)
302 | solution = self.client.icon(challenge, image)
303 | image_element = self.page.locator(selectors.IconV2.IMAGE)
304 | bounding_box = await image_element.bounding_box()
305 | if not bounding_box:
306 | raise AttributeError("Image element was found but had no bounding box")
307 | for point in solution.proportional_points:
308 | await self._click_proportional(bounding_box, point.proportion_x, point.proportion_y)
309 | await self.page.locator(selectors.IconV2.SUBMIT_BUTTON).click()
310 |
311 | async def solve_douyin_puzzle(self) -> None:
312 | puzzle_url, piece_url = await asyncio.gather(
313 | self._get_douyin_puzzle_image_url(),
314 | self._get_douyin_piece_image_url()
315 | )
316 |
317 | puzzle, piece = await asyncio.gather(
318 | fetch_image_b64_async_page(puzzle_url, self.page),
319 | fetch_image_b64_async_page(piece_url, self.page)
320 | )
321 |
322 | solution = self.client.puzzle(puzzle, piece)
323 | distance = await self._compute_douyin_puzzle_slide_distance(solution.slide_x_proportion)
324 | await self._drag_element_horizontal(".captcha-slider-btn", distance, frame_selector=selectors.DouyinPuzzle.FRAME)
325 |
326 | async def _get_douyin_puzzle_image_url(self) -> str:
327 | e = self.page.frame_locator(selectors.DouyinPuzzle.FRAME).locator("#captcha_verify_image")
328 | url = await e.get_attribute("src")
329 | if not url:
330 | raise ValueError("Puzzle image URL was None")
331 | return url
332 |
333 | async def _compute_douyin_puzzle_slide_distance(self, proportion_x: float) -> int:
334 | e = self.page.frame_locator(selectors.DouyinPuzzle.FRAME).locator(selectors.DouyinPuzzle.PUZZLE)
335 | box = await e.bounding_box()
336 | if box:
337 | return int(proportion_x * box["width"])
338 | raise AttributeError("#captcha-verify-image was found but had no bouding box")
339 |
340 | async def _get_douyin_piece_image_url(self) -> str:
341 | e = self.page.frame_locator(selectors.DouyinPuzzle.FRAME).locator(selectors.DouyinPuzzle.PIECE)
342 | url = await e.get_attribute("src")
343 | if not url:
344 | raise ValueError("Piece image URL was None")
345 | return url
346 |
347 | async def _get_element_text(self, selector: str) -> str:
348 | challenge_element = self.page.locator(selector)
349 | text = await challenge_element.text_content()
350 | if not text:
351 | raise ValueError("selector was found but did not have any text.")
352 | return text
353 |
354 | async def _get_element_width(self, selector: str) -> int:
355 | e = self.page.locator(selector)
356 | box = await e.bounding_box()
357 | if box:
358 | return int(box["width"])
359 | raise AttributeError("element was found but had no bouding box")
360 |
361 | async def _get_image_url(self, selector: str) -> str:
362 | e = self.page.locator(selector)
363 | url = await e.get_attribute("src")
364 | if not url:
365 | raise ValueError("image URL was None")
366 | return url
367 |
368 | async def _click_proportional(
369 | self,
370 | bounding_box: FloatRect,
371 | proportion_x: float,
372 | proportion_y: float
373 | ) -> None:
374 | """Click an element inside its bounding box at a point defined by the proportions of x and y
375 | to the width and height of the entire element
376 |
377 | Args:
378 | element: FloatRect to click inside
379 | proportion_x: float from 0 to 1 defining the proportion x location to click
380 | proportion_y: float from 0 to 1 defining the proportion y location to click
381 | """
382 | x_origin = bounding_box["x"]
383 | y_origin = bounding_box["y"]
384 | x_offset = (proportion_x * bounding_box["width"])
385 | y_offset = (proportion_y * bounding_box["height"])
386 | await self.page.mouse.move(x_origin + x_offset, y_origin + y_offset)
387 | await asyncio.sleep(random.randint(1, 10) / 11)
388 | await self.page.mouse.down()
389 | await asyncio.sleep(0.001337)
390 | await self.page.mouse.up()
391 | await asyncio.sleep(random.randint(1, 10) / 11)
392 |
393 | async def _drag_ele_until_watched_ele_has_translateX(self, drag_ele_selector: str, watch_ele_selector: str, target_translateX: int) -> None:
394 | """This method drags the element drag_ele_selector until the translateX value of watch_ele_selector is equal to translateX_target.
395 | This is necessary because there is a small difference between the amount the puzzle piece slides and
396 | the amount of pixels the drag element has been dragged in TikTok puzzle v2."""
397 | drag_ele = self.page.locator(drag_ele_selector)
398 | watch_ele = self.page.locator(watch_ele_selector)
399 | style = await watch_ele.get_attribute("style")
400 | if not style:
401 | raise ValueError("element had no attribut style: " + watch_ele_selector)
402 | current_translateX = get_translateX_from_style(style)
403 | drag_ele_box = await drag_ele.bounding_box()
404 | if not drag_ele_box:
405 | raise AttributeError("element had no bounding box: " + drag_ele_selector)
406 | start_x = (drag_ele_box["x"] + (drag_ele_box["width"] / 1.337))
407 | start_y = (drag_ele_box["y"] + (drag_ele_box["height"] / 1.337))
408 | await self.page.mouse.move(start_x, start_y)
409 | await asyncio.sleep(random.randint(1, 10) / 11)
410 | await self.page.mouse.down()
411 | current_x = start_x
412 | while current_translateX <= target_translateX:
413 | current_x = current_x + self.mouse_step_size
414 | await self.page.mouse.move(current_x, start_y)
415 | await self.page.wait_for_timeout(self.mouse_step_delay_ms)
416 | style = await watch_ele.get_attribute("style")
417 | if not style:
418 | raise ValueError("element had no attribut style: " + watch_ele_selector)
419 | current_translateX = get_translateX_from_style(style)
420 | await asyncio.sleep(0.3)
421 | await self.page.mouse.up()
422 |
423 |
424 | async def _drag_element_horizontal(self, css_selector: str, x_offset: int, frame_selector: str | None = None) -> None:
425 | if frame_selector:
426 | e = self.page.frame_locator(frame_selector).locator(css_selector)
427 | else:
428 | e = self.page.locator(css_selector)
429 | box = await e.bounding_box()
430 | if not box:
431 | raise AttributeError("Element had no bounding box")
432 | start_x = (box["x"] + (box["width"] / 1.337))
433 | start_y = (box["y"] + (box["height"] / 1.337))
434 | await self.page.mouse.move(start_x, start_y)
435 | await asyncio.sleep(random.randint(1, 10) / 11)
436 | await self.page.mouse.down()
437 | for pixel in range(0, x_offset + 5, self.mouse_step_size):
438 | await self.page.mouse.move(start_x + pixel, start_y)
439 | await self.page.wait_for_timeout(self.mouse_step_delay_ms)
440 | await asyncio.sleep(0.25)
441 | for pixel in range(-5, 2):
442 | await self.page.mouse.move(start_x + x_offset - pixel, start_y + pixel) # overshoot back
443 | await self.page.wait_for_timeout(self.mouse_step_delay_ms / 2)
444 | await asyncio.sleep(0.2)
445 | await self.page.mouse.move(start_x + x_offset, start_y, steps=75)
446 | await asyncio.sleep(0.3)
447 | await self.page.mouse.up()
448 |
449 | async def _any_selector_in_list_present(self, selectors: list[str]) -> bool:
450 | for selector in selectors:
451 | for ele in await self.page.locator(selector).all():
452 | if await ele.is_visible():
453 | logging.debug("Detected selector: " + selector + " from list " + ", ".join(selectors))
454 | return True
455 | logging.debug("No selector in list found: " + ", ".join(selectors))
456 | return False
457 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/asyncsolver.py:
--------------------------------------------------------------------------------
1 | """Abstract base class for Tiktok Captcha Async Solvers"""
2 |
3 | import asyncio
4 | from abc import ABC, abstractmethod
5 |
6 | from undetected_chromedriver import logging
7 |
8 | from .captchatype import CaptchaType
9 |
10 | class AsyncSolver(ABC):
11 |
12 | async def solve_captcha_if_present(self, captcha_detect_timeout: int = 15, retries: int = 3) -> None:
13 | """Solves any captcha that is present, if one is detected
14 |
15 | Args:
16 | captcha_detect_timeout: return if no captcha is detected in this many seconds
17 | retries: number of times to retry captcha
18 | """
19 | for _ in range(retries):
20 | if not await self.captcha_is_present(captcha_detect_timeout):
21 | logging.debug("Captcha is not present")
22 | return
23 | if self.page_is_douyin():
24 | logging.debug("Solving douyin puzzle")
25 | try:
26 | await self.solve_douyin_puzzle()
27 | except ValueError as e:
28 | logging.debug("Douyin puzzle was not ready, trying again in 5 seconds")
29 | else:
30 | match await self.identify_captcha():
31 | case CaptchaType.PUZZLE_V1:
32 | logging.debug("Detected puzzle v1")
33 | await self.solve_puzzle()
34 | case CaptchaType.PUZZLE_V2:
35 | logging.debug("Detected puzzle v2")
36 | await self.solve_puzzle_v2()
37 | case CaptchaType.ROTATE_V1:
38 | logging.debug("Detected rotate v1")
39 | await self.solve_rotate()
40 | case CaptchaType.ROTATE_V2:
41 | logging.debug("Detected rotate v2")
42 | await self.solve_rotate_v2()
43 | case CaptchaType.SHAPES_V1:
44 | logging.debug("Detected shapes v2")
45 | await self.solve_shapes()
46 | case CaptchaType.SHAPES_V2:
47 | logging.debug("Detected shapes v2")
48 | await self.solve_shapes_v2()
49 | case CaptchaType.ICON_V1:
50 | logging.debug("Detected icon v1")
51 | await self.solve_icon()
52 | case CaptchaType.ICON_V2:
53 | logging.debug("Detected icon v2")
54 | await self.solve_icon_v2()
55 | if await self.captcha_is_not_present(timeout=5):
56 | return
57 | else:
58 | await asyncio.sleep(5)
59 |
60 | @abstractmethod
61 | async def captcha_is_present(self, timeout: int = 15) -> bool:
62 | pass
63 |
64 | @abstractmethod
65 | async def captcha_is_not_present(self, timeout: int = 15) -> bool:
66 | pass
67 |
68 | @abstractmethod
69 | async def identify_captcha(self) -> CaptchaType:
70 | pass
71 |
72 | @abstractmethod
73 | def page_is_douyin(self) -> bool:
74 | pass
75 |
76 | @abstractmethod
77 | async def solve_shapes(self) -> None:
78 | pass
79 |
80 | @abstractmethod
81 | async def solve_shapes_v2(self) -> None:
82 | pass
83 |
84 | @abstractmethod
85 | async def solve_rotate(self) -> None:
86 | pass
87 |
88 | @abstractmethod
89 | async def solve_rotate_v2(self) -> None:
90 | pass
91 |
92 | @abstractmethod
93 | async def solve_puzzle(self) -> None:
94 | pass
95 |
96 | @abstractmethod
97 | async def solve_puzzle_v2(self) -> None:
98 | pass
99 |
100 | @abstractmethod
101 | async def solve_icon(self) -> None:
102 | pass
103 |
104 | @abstractmethod
105 | async def solve_icon_v2(self) -> None:
106 | pass
107 |
108 | @abstractmethod
109 | async def solve_douyin_puzzle(self) -> None:
110 | pass
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/captchatype.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | class CaptchaType(Enum):
4 | ROTATE_V1 = 1
5 | ROTATE_V2 = 2
6 | PUZZLE_V1 = 3
7 | PUZZLE_V2 = 4
8 | SHAPES_V1 = 5
9 | SHAPES_V2 = 6
10 | ICON_V1 = 7
11 | ICON_V2 = 8
12 | DOUYIN_PUZZLE = 9
13 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/download_crx.py:
--------------------------------------------------------------------------------
1 | import os
2 | from collections.abc import Generator
3 | import zipfile
4 | from contextlib import contextmanager
5 | from io import BufferedWriter
6 | import tempfile
7 | import requests
8 | import logging
9 |
10 | LOGGER = logging.getLogger(__name__)
11 |
12 | # https://stackoverflow.com/questions/7184793/how-to-download-a-crx-file-from-the-chrome-web-store-for-a-given-id
13 | EXTENSION_ID = "colmpcmlmokfplanmjmnnahkkpgmmbjl"
14 | CHROME_EXT_DOWNLOAD_URL = f"https://clients2.google.com/service/update2/crx?response=redirect&prodversion=126.0.6478.270&acceptformat=crx2,crx3&x=id%3D{EXTENSION_ID}%26uc"
15 |
16 | def download_extension_to_unpacked() -> tempfile.TemporaryDirectory:
17 | with download_extension_to_tempfile() as f:
18 | temp_dir = tempfile.TemporaryDirectory()
19 | with zipfile.ZipFile(f.name, "r") as zip_file:
20 | zip_file.extractall(temp_dir.name)
21 | LOGGER.debug("extracted crx to directory: " + temp_dir.name)
22 | return temp_dir
23 |
24 |
25 | @contextmanager
26 | def download_extension_to_tempfile() -> Generator[BufferedWriter, None, None]:
27 | r = requests.get(CHROME_EXT_DOWNLOAD_URL)
28 | LOGGER.debug("downloaded chrome extension from " + CHROME_EXT_DOWNLOAD_URL)
29 | tf = open(os.path.join(tempfile.gettempdir(), os.urandom(24).hex()), "wb")
30 | _ = tf.write(r.content)
31 | LOGGER.debug("wrote chrome extension to temp file at: " + tf.name)
32 | try:
33 | yield tf
34 | finally:
35 | tf.close()
36 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/downloader.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Any
3 | from playwright.sync_api import Page as SyncPage
4 | from playwright.async_api import Page as AsyncPage
5 | import requests
6 | import base64
7 |
8 | from selenium.webdriver.chrome.webdriver import WebDriver
9 |
10 |
11 | def fetch_image_b64(
12 | url: str,
13 | driver: WebDriver | None = None,
14 | sync_page: SyncPage | None = None,
15 | headers: dict[str, Any] | None = None,
16 | proxy: str | None = None
17 | ) -> str:
18 | """Fetch an image from URL and return as base64 encoded string
19 |
20 | If just the URL is passed, it will be feched with python requests.
21 | If driver is passed, chromedriver will be used to evaluate javascript to download from blob
22 | If sync_page is passed, sync playwright will be used to evaluate javascript to download from blob
23 | """
24 | if not any([sync_page, driver]):
25 | logging.debug(f"fetching {url} with requests")
26 | if proxy:
27 | proxies = {"http": proxy, "https": proxy}
28 | else:
29 | proxies = None
30 | r = requests.get(url, headers=headers, proxies=proxies)
31 | image = base64.b64encode(r.content).decode()
32 | logging.debug(f"Got image from {url} as B64: {image[0:20]}...")
33 | return image
34 | if driver:
35 | logging.debug(f"fetching {url} with selenium script execute")
36 | return driver.execute_script(_make_selenium_fetch_image_code(url))
37 | if sync_page:
38 | logging.debug(f"fetching {url} with sync playwright javascript evaluate")
39 | return sync_page.evaluate(_make_playwright_fetch_image_code(url))
40 |
41 | async def fetch_image_b64_async_page(url: str, async_page: AsyncPage) -> str:
42 | logging.debug(f"fetching {url} with async playwright javascript evaluate")
43 | return await async_page.evaluate(_make_playwright_fetch_image_code(url))
44 |
45 | def _make_playwright_fetch_image_code(image_source: str) -> str:
46 | """prepare javascript on page to fetch image from blob url.
47 | This is necessary because you can't fetch a blob with requests."""
48 | return """async () => {
49 | function getBase64StringFromDataURL(dataUrl){
50 | return dataUrl.replace('data:', '').replace(/^.+,/, '')
51 | }
52 | async function fetchImageBase64(imageSource) {
53 | let res = await fetch(imageSource)
54 | let img = await res.blob()
55 | let reader = new FileReader()
56 | reader.readAsDataURL(img)
57 | return new Promise(resolve => {
58 | reader.onloadend = () => {
59 | resolve(getBase64StringFromDataURL(reader.result))
60 | }
61 | })
62 | }
63 | let imgB64 = await fetchImageBase64([IMAGE_SOURCE]);
64 | return getBase64StringFromDataURL(imgB64);
65 | }
66 | """.replace("[IMAGE_SOURCE]", f"\"{image_source}\"")
67 |
68 | def _make_selenium_fetch_image_code(image_source: str) -> str:
69 | """prepare javascript on page to fetch image from blob url.
70 | This is necessary because you can't fetch a blob with requests."""
71 | return """function getBase64StringFromDataURL(dataUrl){
72 | return dataUrl.replace('data:', '').replace(/^.+,/, '')
73 | }
74 | async function fetchImageBase64(imageSource) {
75 | let res = await fetch(imageSource)
76 | let img = await res.blob()
77 | let reader = new FileReader()
78 | reader.readAsDataURL(img)
79 | return new Promise(resolve => {
80 | reader.onloadend = () => {
81 | resolve(getBase64StringFromDataURL(reader.result))
82 | }
83 | })
84 | }
85 | let imgB64 = await fetchImageBase64([IMAGE_SOURCE]);
86 | return getBase64StringFromDataURL(imgB64);
87 | """.replace("[IMAGE_SOURCE]", f"\"{image_source}\"")
88 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/geometry.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | def compute_rotate_slide_distance(angle: int, slide_bar_width: float, slide_button_width: float) -> int:
4 | return int(((slide_bar_width - slide_button_width) * angle) / 360)
5 |
6 |
7 | def compute_pixel_fraction(proportion_x: float, container_width: float) -> int:
8 | return int(proportion_x * container_width)
9 |
10 |
11 | def get_translateX_from_style(style: str) -> int:
12 | translate_x_match = re.search(r"(?<=translateX\()\d+", style)
13 | if not translate_x_match:
14 | raise ValueError("did not find translateX in style: " + style)
15 | return int(translate_x_match.group())
16 |
17 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/launcher.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import tempfile
3 | from typing import Any
4 | import io
5 | import os
6 | import zipfile
7 | import requests
8 |
9 | from selenium.webdriver import ChromeOptions
10 | from selenium import webdriver
11 | import undetected_chromedriver as uc
12 | import nodriver
13 |
14 | from playwright import sync_api
15 | from playwright import async_api
16 |
17 | LOGGER = logging.getLogger(__name__)
18 |
19 | async def make_nodriver_solver(
20 | api_key: str,
21 | **nodriver_start_kwargs
22 | ) -> nodriver.Browser:
23 | """Create a nodriver Browser patched with SadCaptcha.
24 |
25 | Args:
26 | api_key (str): SadCaptcha API key
27 | nodriver_start_args: Keyword arguments for nodriver.start()
28 | """
29 | ext_dir = download_extension_to_unpacked()
30 | _patch_extension_file_with_key(ext_dir.name, api_key)
31 | load_extension_argument = f'--load-extension={ext_dir.name}'
32 | disable_extensions_except_argument = f'--disable-extensions-except-{ext_dir.name}'
33 | browser_args = nodriver_start_kwargs.get("browser_args")
34 | if isinstance(browser_args, list):
35 | nodriver_start_kwargs["browser_args"].append(load_extension_argument)
36 | nodriver_start_kwargs["browser_args"].append(disable_extensions_except_argument)
37 | LOGGER.debug("Appended add extension argument to browser args: " + load_extension_argument)
38 | LOGGER.debug("Appended add extension argument to browser args: " + disable_extensions_except_argument)
39 | else:
40 | nodriver_start_kwargs["browser_args"] = [load_extension_argument, disable_extensions_except_argument]
41 | LOGGER.debug("Set browser arg to " + load_extension_argument)
42 | chrome = await nodriver.start(**nodriver_start_kwargs)
43 | LOGGER.debug("created new nodriver Browser patched with sadcaptcha")
44 | return chrome
45 |
46 | def make_selenium_solver(
47 | api_key: str,
48 | options: ChromeOptions | None = None,
49 | no_warn: bool = False,
50 | **webdriver_chrome_kwargs
51 | ) -> uc.Chrome:
52 | """Create an selenium chromedriver patched with SadCaptcha.
53 |
54 | Args:
55 | api_key (str): SadCaptcha API key
56 | options (ChromeOptions | None): Options to launch webdriver.Chrome with
57 | webdriver_chrome_kwargs: keyword arguments for call to webdriver.Chrome
58 | """
59 | if not no_warn:
60 | LOGGER.warning("Selenium, playwright, and undetected chromedriver are no longer recommended for scraping. A better option is to use nodriver. To use nodriver, use the command 'make_nodriver_solver()' instead. To disable this warning, set the kwarg no_warn=True.")
61 | if options is None:
62 | options = ChromeOptions()
63 | ext_dir = download_extension_to_unpacked()
64 | _patch_extension_file_with_key(ext_dir.name, api_key)
65 | options.add_argument(f'--load-extension={ext_dir.name}')
66 | chrome = webdriver.Chrome(options=options, **webdriver_chrome_kwargs)
67 | LOGGER.debug("created new undetected chromedriver patched with sadcaptcha")
68 | return chrome
69 |
70 | def make_undetected_chromedriver_solver(
71 | api_key: str,
72 | options: ChromeOptions | None = None,
73 | no_warn: bool = False,
74 | **uc_chrome_kwargs
75 | ) -> uc.Chrome:
76 | """Create an undetected chromedriver patched with SadCaptcha.
77 |
78 | Args:
79 | api_key (str): SadCaptcha API key
80 | options (ChromeOptions | None): Options to launch uc.Chrome with
81 | uc_chrome_kwargs: keyword arguments for call to uc.Chrome
82 | """
83 | if not no_warn:
84 | LOGGER.warning("Selenium, playwright, and undetected chromedriver are no longer recommended for scraping. A better option is to use nodriver. To use nodriver, use the command 'make_nodriver_solver()' instead. To disable this warning, set the kwarg no_warn=True.")
85 | if options is None:
86 | options = ChromeOptions()
87 | ext_dir = download_extension_to_unpacked()
88 | _patch_extension_file_with_key(ext_dir.name, api_key)
89 | verify_api_key_injection(ext_dir.name, api_key)
90 | options.add_argument(f'--load-extension={ext_dir.name}')
91 | chrome = uc.Chrome(options=options, **uc_chrome_kwargs)
92 | # keep the temp dir alive for the lifetime of the driver
93 | chrome._sadcaptcha_tmpdir = ext_dir # ← prevents garbage collection
94 | LOGGER.debug("created new undetected chromedriver patched with sadcaptcha")
95 | return chrome
96 |
97 | def make_playwright_solver_context(
98 | playwright: sync_api.Playwright,
99 | api_key: str,
100 | user_data_dir: str | None = None,
101 | no_warn: bool = False,
102 | **playwright_context_kwargs
103 | ) -> sync_api.BrowserContext:
104 | """Create a playwright context patched with SadCaptcha.
105 |
106 | Args:
107 | playwright (playwright.sync_api.playwright) - Playwright instance
108 | api_key (str): SadCaptcha API key
109 | user_data_dir (str | None): User data dir that is passed to playwright.chromium.launch_persistent_context. If None, a temporary directory will be used.
110 | **playwright_context_kwargs: Keyword args which will be passed to playwright.chromium.launch_persistent_context()
111 | """
112 | if not no_warn:
113 | LOGGER.warning("Selenium, playwright, and undetected chromedriver are no longer recommended for scraping. A better option is to use nodriver. To use nodriver, use the command 'make_nodriver_solver()' instead. To disable this warning, set the kwarg no_warn=True.")
114 | ext_dir = download_extension_to_unpacked()
115 | if user_data_dir is None:
116 | user_data_dir_tempdir = tempfile.TemporaryDirectory()
117 | user_data_dir = user_data_dir_tempdir.name
118 | _patch_extension_file_with_key(ext_dir.name, api_key)
119 | playwright_context_kwargs = _prepare_pw_context_args(playwright_context_kwargs, ext_dir.name)
120 | ctx = playwright.chromium.launch_persistent_context(
121 | user_data_dir,
122 | **playwright_context_kwargs
123 | )
124 | ctx._sadcaptcha_tmpdir = ext_dir # keep reference
125 | LOGGER.debug("created patched playwright context")
126 | return ctx
127 |
128 | async def make_async_playwright_solver_context(
129 | async_playwright: async_api.Playwright,
130 | api_key: str,
131 | user_data_dir: str | None = None,
132 | **playwright_context_kwargs
133 | ) -> async_api.BrowserContext:
134 | """Create a async playwright context patched with SadCaptcha.
135 |
136 | Args:
137 | playwright (playwright.async_api.playwright) - Playwright instance
138 | api_key (str): SadCaptcha API key
139 | user_data_dir (str | None): User data dir that is passed to playwright.chromium.launch_persistent_context. If None, a temporary directory will be used.
140 | **playwright_context_kwargs: Keyword args which will be passed to playwright.chromium.launch_persistent_context()
141 | """
142 | ext_dir = download_extension_to_unpacked()
143 | if user_data_dir is None:
144 | user_data_dir_tempdir = tempfile.TemporaryDirectory()
145 | user_data_dir = user_data_dir_tempdir.name
146 | _patch_extension_file_with_key(ext_dir.name, api_key)
147 | playwright_context_kwargs = _prepare_pw_context_args(playwright_context_kwargs, ext_dir.name)
148 | ctx = await async_playwright.chromium.launch_persistent_context(
149 | user_data_dir,
150 | **playwright_context_kwargs
151 | )
152 | ctx._sadcaptcha_tmpdir = ext_dir # keep reference
153 | LOGGER.debug("created patched async playwright context")
154 | return ctx
155 |
156 | def _prepare_pw_context_args(
157 | playwright_context_kwargs: dict[str, Any],
158 | ext: str
159 | ) -> dict[str, Any]:
160 | if "args" in playwright_context_kwargs.keys():
161 | playwright_context_kwargs["args"] = playwright_context_kwargs["args"] + [
162 | f"--disable-extensions-except={ext}",
163 | f"--load-extension={ext}",
164 | ]
165 | else:
166 | playwright_context_kwargs["args"] = [
167 | f"--disable-extensions-except={ext}",
168 | f"--load-extension={ext}",
169 | '--disable-blink-features=AutomationControlled',
170 | '--no-sandbox',
171 | '--disable-web-security',
172 | '--disable-infobars',
173 | '--start-maximized',
174 | ]
175 | if playwright_context_kwargs.get("headless") == True:
176 | if "--headless=new" not in playwright_context_kwargs["args"]:
177 | _ = playwright_context_kwargs["args"].append("--headless=new")
178 | playwright_context_kwargs["headless"] = None
179 | LOGGER.debug("Removed headless=True and added --headless=new launch arg")
180 | LOGGER.debug("prepared playwright context kwargs")
181 | return playwright_context_kwargs
182 |
183 | def download_extension_to_unpacked() -> tempfile.TemporaryDirectory:
184 | """
185 | Download the SadCaptcha Chrome extension from GitHub and return an unpacked
186 | TemporaryDirectory that can be passed to Playwright / Chrome.
187 | """
188 | repo_zip_url = (
189 | "https://codeload.github.com/gbiz123/sadcaptcha-chrome-extensino/zip/refs/heads/master"
190 | )
191 |
192 | LOGGER.debug("Downloading SadCaptcha extension from %s", repo_zip_url)
193 | resp = requests.get(repo_zip_url, timeout=30)
194 | resp.raise_for_status()
195 |
196 | tmp_dir = tempfile.TemporaryDirectory(prefix="sadcaptcha_ext_")
197 | with zipfile.ZipFile(io.BytesIO(resp.content)) as zf:
198 | # GitHub zips have a single top‑level folder → strip it
199 | root_prefix = zf.namelist()[0].split("/")[0] + "/"
200 | for member in zf.namelist():
201 | if member.endswith("/"):
202 | continue
203 | rel_path = member[len(root_prefix) :]
204 | if not rel_path:
205 | continue
206 | dest_file = os.path.join(tmp_dir.name, rel_path)
207 | os.makedirs(os.path.dirname(dest_file), exist_ok=True)
208 | with zf.open(member) as src, open(dest_file, "wb") as dst:
209 | dst.write(src.read())
210 |
211 | LOGGER.debug("Extension unpacked to %s", tmp_dir.name)
212 | return tmp_dir
213 |
214 | def _patch_extension_file_with_key(extension_dir: str, api_key: str) -> None:
215 | script_path = os.path.join(extension_dir, "script.js")
216 | try:
217 | with open(script_path, "r", encoding="utf-8") as f:
218 | script = f.read()
219 |
220 | original_script = script
221 | script = patch_extension_script_with_key(script, api_key)
222 |
223 | # Verify replacement happened
224 | if script == original_script:
225 | LOGGER.warning("API key pattern not found in script.js")
226 |
227 | with open(script_path, "w", encoding="utf-8") as f:
228 | f.write(script)
229 |
230 | LOGGER.debug("Successfully patched extension file with API key")
231 | except Exception as e:
232 | LOGGER.error(f"Failed to patch extension with API key: {e}")
233 | raise
234 |
235 | def patch_extension_script_with_key(script: str, api_key: str) -> str:
236 | script = script.replace('localStorage.getItem("sadCaptchaKey")', f"\"{api_key}\";")
237 | LOGGER.debug("patched extension script with api key")
238 | return script
239 |
240 | def verify_api_key_injection(extension_dir, api_key):
241 | script_path = os.path.join(extension_dir, "script.js")
242 |
243 | # Check if file exists and contains your API key
244 | if os.path.exists(script_path):
245 | with open(script_path, "r", encoding="utf-8") as f:
246 | content = f.read()
247 | if f'"{api_key}";' in content:
248 | LOGGER.info(f"SUCCESS: API key found in script.js")
249 | return True
250 | else:
251 | LOGGER.warning(f"FAILURE: API key not found in script.js")
252 | else:
253 | LOGGER.error(f"FAILURE: script.js not found at {script_path}")
254 | return False
255 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/models.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 | class ShapesCaptchaResponse(BaseModel):
4 | point_one_proportion_x: float
5 | point_one_proportion_y: float
6 | point_two_proportion_x: float
7 | point_two_proportion_y: float
8 |
9 | class ProportionalPoint(BaseModel):
10 | proportion_x: float
11 | proportion_y: float
12 |
13 | class IconCaptchaResponse(BaseModel):
14 | proportional_points: list[ProportionalPoint]
15 |
16 | class RotateCaptchaResponse(BaseModel):
17 | angle: int
18 |
19 | class PuzzleCaptchaResponse(BaseModel):
20 | slide_x_proportion: float
21 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/nodriversolver.py:
--------------------------------------------------------------------------------
1 | """This class handles the captcha solving for selenium users"""
2 |
3 | import time
4 | from typing import Literal
5 |
6 | from selenium.webdriver import ActionChains, Chrome
7 | from selenium.webdriver.common.actions.action_builder import ActionBuilder
8 | from selenium.webdriver.common.by import By
9 | from selenium.webdriver.remote.webelement import WebElement
10 | from undetected_chromedriver import logging
11 | from undetected_chromedriver.patcher import random
12 |
13 | from nodriver import Tab
14 |
15 | from tiktok_captcha_solver.asyncsolver import AsyncSolver
16 |
17 | from .api import ApiClient
18 | from .downloader import fetch_image_b64
19 | from .solver import Solver
20 |
21 |
22 | class NodriverSolver(AsyncSolver):
23 |
24 | client: ApiClient
25 | tab: Tab
26 |
27 | def __init__(
28 | self,
29 | tab: Tab,
30 | sadcaptcha_api_key: str,
31 | headers: dict | None = None,
32 | proxy: str | None = None
33 | ) -> None:
34 | self.tab = tab
35 | self.client = ApiClient(sadcaptcha_api_key)
36 | self.headers = headers
37 | self.proxy = proxy
38 |
39 | async def captcha_is_present(self, timeout: int = 15) -> bool:
40 | for _ in range(timeout * 2):
41 | if await self._any_selector_in_list_present(self.captcha_wrappers):
42 | print("Captcha detected")
43 | return True
44 | time.sleep(0.5)
45 | logging.debug("Captcha not found")
46 | return False
47 |
48 | async def captcha_is_not_present(self, timeout: int = 15) -> bool:
49 | for _ in range(timeout * 2):
50 | if len(self.tab.find_elements(By.CSS_SELECTOR, self.captcha_wrappers[0])) == 0:
51 | print("Captcha detected")
52 | return True
53 | time.sleep(0.5)
54 | logging.debug("Captcha not found")
55 | return False
56 |
57 | async def identify_captcha(self) -> Literal["puzzle", "shapes", "rotate"]:
58 | for _ in range(15):
59 | if self._any_selector_in_list_present(self.puzzle_selectors):
60 | return "puzzle"
61 | elif self._any_selector_in_list_present(self.rotate_selectors):
62 | return "rotate"
63 | elif self._any_selector_in_list_present(self.shapes_selectors):
64 | return "shapes"
65 | else:
66 | time.sleep(2)
67 | raise ValueError("Neither puzzle, shapes, or rotate captcha was present.")
68 |
69 | async def solve_shapes(self) -> None:
70 | if not self._any_selector_in_list_present(["#captcha-verify-image"]):
71 | logging.debug("Went to solve puzzle but #captcha-verify-image was not present")
72 | return
73 | image = fetch_image_b64(self._get_shapes_image_url(), headers=self.headers, proxy=self.proxy)
74 | solution = self.client.shapes(image)
75 | image_element = self.tab.find_element(By.CSS_SELECTOR, "#captcha-verify-image")
76 | self._click_proportional(image_element, solution.point_one_proportion_x, solution.point_one_proportion_y)
77 | self._click_proportional(image_element, solution.point_two_proportion_x, solution.point_two_proportion_y)
78 | self.tab.find_element(By.CSS_SELECTOR, ".verify-captcha-submit-button").click()
79 |
80 | async def solve_rotate(self) -> None:
81 | if not self._any_selector_in_list_present(["[data-testid=whirl-inner-img]"]):
82 | logging.debug("Went to solve rotate but whirl-inner-img was not present")
83 | return
84 | outer = fetch_image_b64(self._get_rotate_outer_image_url(), headers=self.headers, proxy=self.proxy)
85 | inner = fetch_image_b64(self._get_rotate_inner_image_url(), headers=self.headers, proxy=self.proxy)
86 | solution = self.client.rotate(outer, inner)
87 | distance = self._compute_rotate_slide_distance(solution.angle)
88 | self._drag_element_horizontal(".secsdk-captcha-drag-icon", distance)
89 |
90 | async def solve_puzzle(self) -> None:
91 | if not self._any_selector_in_list_present(["#captcha-verify-image"]):
92 | logging.debug("Went to solve puzzle but #captcha-verify-image was not present")
93 | return
94 | puzzle = fetch_image_b64(self._get_puzzle_image_url(), headers=self.headers, proxy=self.proxy)
95 | piece = fetch_image_b64(self._get_piece_image_url(), headers=self.headers, proxy=self.proxy)
96 | solution = self.client.puzzle(puzzle, piece)
97 | distance = self._compute_puzzle_slide_distance(solution.slide_x_proportion)
98 | self._drag_element_horizontal(".secsdk-captcha-drag-icon", distance)
99 |
100 | async def _compute_rotate_slide_distance(self, angle: int) -> int:
101 | slide_length = self._get_slide_length()
102 | icon_length = self._get_slide_icon_length()
103 | return int(((slide_length - icon_length) * angle) / 360)
104 |
105 | async def _compute_puzzle_slide_distance(self, proportion_x: float) -> int:
106 | e = self.tab.find_element(By.CSS_SELECTOR, "#captcha-verify-image")
107 | return int(proportion_x * e.size["width"])
108 |
109 | async def _get_slide_length(self) -> int:
110 | e = self.tab.find_element(By.CSS_SELECTOR, ".captcha_verify_slide--slidebar")
111 | return e.size['width']
112 |
113 | async def _get_slide_icon_length(self) -> int:
114 | e = self.tab.find_element(By.CSS_SELECTOR, ".secsdk-captcha-drag-icon")
115 | return e.size['width']
116 |
117 | async def _get_rotate_inner_image_url(self) -> str:
118 | e = self.tab.find_element(By.CSS_SELECTOR, "[data-testid=whirl-inner-img]")
119 | url = e.get_attribute("src")
120 | if not url:
121 | raise ValueError("Inner image URL was None")
122 | return url
123 |
124 | async def _get_rotate_outer_image_url(self) -> str:
125 | e = self.tab.find_element(By.CSS_SELECTOR, "[data-testid=whirl-outer-img]")
126 | url = e.get_attribute("src")
127 | if not url:
128 | raise ValueError("Outer image URL was None")
129 | return url
130 |
131 | async def _get_puzzle_image_url(self) -> str:
132 | e = self.tab.find_element(By.CSS_SELECTOR, "#captcha-verify-image")
133 | url = e.get_attribute("src")
134 | if not url:
135 | raise ValueError("Puzzle image URL was None")
136 | return url
137 |
138 | async def _get_piece_image_url(self) -> str:
139 | e = self.tab.find_element(By.CSS_SELECTOR, ".captcha_verify_img_slide")
140 | url = e.get_attribute("src")
141 | if not url:
142 | raise ValueError("Piece image URL was None")
143 | return url
144 |
145 | async def _get_shapes_image_url(self) -> str:
146 | e = self.tab.find_element(By.CSS_SELECTOR, "#captcha-verify-image")
147 | url = e.get_attribute("src")
148 | if not url:
149 | raise ValueError("Shapes image URL was None")
150 | return url
151 |
152 | async def _click_proportional(
153 | self,
154 | element: WebElement,
155 | proportion_x: float,
156 | proportion_y: float
157 | ) -> None:
158 | """Click an element inside its bounding box at a point defined by the proportions of x and y
159 | to the width and height of the entire element
160 |
161 | Args:
162 | element: WebElement to click inside
163 | proportion_x: float from 0 to 1 defining the proportion x location to click
164 | proportion_y: float from 0 to 1 defining the proportion y location to click
165 | """
166 | x_origin = element.location["x"]
167 | y_origin = element.location["y"]
168 | x_offset = (proportion_x * element.size["width"])
169 | y_offset = (proportion_y * element.size["height"])
170 | action = ActionBuilder(self.tab)
171 | action.pointer_action \
172 | .move_to_location(x_origin + x_offset, y_origin + y_offset) \
173 | .pause(random.randint(1, 10) / 11) \
174 | .click() \
175 | .pause(random.randint(1, 10) / 11)
176 | action.perform()
177 |
178 | async def _drag_element_horizontal(self, css_selector: str, x: int) -> None:
179 | e = self.tab.find_element(By.CSS_SELECTOR, css_selector)
180 | actions = ActionChains(self.tab, duration=0)
181 | actions.click_and_hold(e)
182 | time.sleep(0.1)
183 | for _ in range(0, x - 15):
184 | actions.move_by_offset(1, 0)
185 | for _ in range(0, 20):
186 | actions.move_by_offset(1, 0)
187 | actions.pause(0.01)
188 | actions.pause(0.7)
189 | for _ in range(0, 5):
190 | actions.move_by_offset(-1, 0)
191 | actions.pause(0.05)
192 | actions.pause(0.1)
193 | actions.release().perform()
194 |
195 | async def _any_selector_in_list_present(self, selectors: list[str]) -> bool:
196 | for selector in selectors:
197 | for ele in self.tab.find_elements(By.CSS_SELECTOR, selector):
198 | if ele.is_displayed():
199 | logging.debug("Detected selector: " + selector + " from list " + ", ".join(selectors))
200 | return True
201 | logging.debug("No selector in list found: " + ", ".join(selectors))
202 | return False
203 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/playwrightsolver.py:
--------------------------------------------------------------------------------
1 | """This class handles the captcha solving for playwright users"""
2 |
3 | import logging
4 | import random
5 | import time
6 | from typing import Any
7 |
8 | from playwright.sync_api import FloatRect, Page, expect
9 | from playwright.sync_api import TimeoutError
10 |
11 | from . import selectors
12 | from .captchatype import CaptchaType
13 | from .solver import Solver
14 | from .api import ApiClient
15 | from .downloader import fetch_image_b64
16 | from .geometry import compute_pixel_fraction, compute_rotate_slide_distance, get_translateX_from_style
17 |
18 | class PlaywrightSolver(Solver):
19 |
20 | client: ApiClient
21 | page: Page
22 |
23 | def __init__(
24 | self,
25 | page: Page,
26 | sadcaptcha_api_key: str,
27 | headers: dict[str, Any] | None = None,
28 | proxy: str | None = None,
29 | mouse_step_size: int = 1,
30 | mouse_step_delay_ms: int = 10
31 | ) -> None:
32 | self.page = page
33 | self.client = ApiClient(sadcaptcha_api_key)
34 | self.headers = headers
35 | self.proxy = proxy
36 | self.mouse_step_size = mouse_step_size
37 | self.mouse_step_delay_ms = mouse_step_delay_ms
38 |
39 | def captcha_is_present(self, timeout: int = 15) -> bool:
40 | if self.page_is_douyin():
41 | try:
42 | douyin_locator = self.page.frame_locator(selectors.DouyinPuzzle.FRAME).locator("*")
43 | expect(douyin_locator.first).not_to_have_count(0)
44 | except (TimeoutError, AssertionError):
45 | return False
46 | else:
47 | try:
48 | tiktok_locator = self.page.locator(f"{selectors.Wrappers.V1}, {selectors.Wrappers.V2}")
49 | expect(tiktok_locator.first).to_be_visible(timeout=timeout * 1000)
50 | logging.debug("v1 or v2 tiktok selector present")
51 | except (TimeoutError, AssertionError):
52 | return False
53 | return True
54 |
55 | def captcha_is_not_present(self, timeout: int = 15) -> bool:
56 | if self.page_is_douyin():
57 | try:
58 | douyin_locator = self.page.frame_locator(selectors.DouyinPuzzle.FRAME).locator("*")
59 | expect(douyin_locator.first).to_have_count(0)
60 | except (TimeoutError, AssertionError):
61 | return False
62 | else:
63 | try:
64 | tiktok_locator = self.page.locator(f"{selectors.Wrappers.V1}, {selectors.Wrappers.V2}")
65 | expect(tiktok_locator.first).to_have_count(0, timeout=timeout * 1000)
66 | logging.debug("v1 or v2 tiktok selector not present")
67 | except (TimeoutError, AssertionError):
68 | return False
69 | return True
70 |
71 | def identify_captcha(self) -> CaptchaType:
72 | for _ in range(60):
73 | try:
74 | if self._any_selector_in_list_present([selectors.PuzzleV1.UNIQUE_IDENTIFIER]):
75 | logging.debug("detected puzzle")
76 | return CaptchaType.PUZZLE_V1
77 | if self._any_selector_in_list_present([selectors.PuzzleV2.UNIQUE_IDENTIFIER]):
78 | logging.debug("detected puzzle v2")
79 | return CaptchaType.PUZZLE_V2
80 | elif self._any_selector_in_list_present([selectors.RotateV1.UNIQUE_IDENTIFIER]):
81 | logging.debug("detected rotate v1")
82 | return CaptchaType.ROTATE_V1
83 | elif self._any_selector_in_list_present([selectors.RotateV2.UNIQUE_IDENTIFIER]):
84 | logging.debug("detected rotate v2")
85 | return CaptchaType.ROTATE_V2
86 | if self._any_selector_in_list_present([selectors.ShapesV1.UNIQUE_IDENTIFIER]):
87 | img_url = self._get_image_url(selectors.ShapesV1.IMAGE)
88 | if "/icon" in img_url:
89 | logging.debug("detected icon v1")
90 | return CaptchaType.ICON_V1
91 | elif "/3d" in img_url:
92 | logging.debug("detected shapes v1")
93 | return CaptchaType.SHAPES_V1
94 | else:
95 | logging.warn("did not see '/3d' in image source url but returning shapes v1 anyways")
96 | return CaptchaType.SHAPES_V1
97 | if self._any_selector_in_list_present([selectors.ShapesV2.UNIQUE_IDENTIFIER]):
98 | img_url = self._get_image_url(selectors.ShapesV2.IMAGE)
99 | if "/icon" in img_url:
100 | logging.debug("detected icon v2")
101 | return CaptchaType.ICON_V2
102 | elif "/3d" in img_url:
103 | logging.debug("detected shapes v2")
104 | return CaptchaType.SHAPES_V2
105 | else:
106 | logging.warn("did not see '/3d' in image source url but returning shapes v2 anyways")
107 | return CaptchaType.SHAPES_V2
108 | else:
109 | time.sleep(0.5)
110 | except Exception as e:
111 | logging.debug(f"Exception occurred identifying captcha: {str(e)}. Trying again")
112 | continue
113 | raise ValueError("Neither puzzle, shapes, or rotate captcha was present.")
114 |
115 | def page_is_douyin(self) -> bool:
116 | if "douyin" in self.page.url:
117 | logging.debug("page is douyin")
118 | return True
119 | logging.debug("page is tiktok")
120 | return False
121 |
122 | def solve_shapes(self, retries: int = 3) -> None:
123 | for _ in range(retries):
124 | if not self._any_selector_in_list_present([selectors.ShapesV1.IMAGE]):
125 | logging.debug("Went to solve shapes but #captcha-verify-image was not present")
126 | return
127 | image = fetch_image_b64(self._get_image_url(selectors.ShapesV1.IMAGE), sync_page=self.page)
128 | solution = self.client.shapes(image)
129 | image_element = self.page.locator(selectors.ShapesV1.IMAGE)
130 | bounding_box = image_element.bounding_box()
131 | if not bounding_box:
132 | raise AttributeError("Image element was found but had no bounding box")
133 | self._click_proportional(bounding_box, solution.point_one_proportion_x, solution.point_one_proportion_y)
134 | self._click_proportional(bounding_box, solution.point_two_proportion_x, solution.point_two_proportion_y)
135 | self.page.locator(selectors.ShapesV1.SUBMIT_BUTTON).click()
136 | if self.captcha_is_not_present(timeout=5):
137 | return
138 | else:
139 | time.sleep(3)
140 |
141 | def solve_shapes_v2(self, retries: int = 3) -> None:
142 | for _ in range(retries):
143 | if not self._any_selector_in_list_present([selectors.ShapesV2.IMAGE]):
144 | logging.debug("Went to solve shapes but image was not present")
145 | return
146 | image = fetch_image_b64(self._get_image_url(selectors.ShapesV2.IMAGE), sync_page=self.page)
147 | solution = self.client.shapes(image)
148 | image_element = self.page.locator(selectors.ShapesV2.IMAGE)
149 | bounding_box = image_element.bounding_box()
150 | if not bounding_box:
151 | raise AttributeError("Image element was found but had no bounding box")
152 | self._click_proportional(bounding_box, solution.point_one_proportion_x, solution.point_one_proportion_y)
153 | self._click_proportional(bounding_box, solution.point_two_proportion_x, solution.point_two_proportion_y)
154 | self.page.locator(selectors.ShapesV2.SUBMIT_BUTTON).click()
155 | if self.captcha_is_not_present(timeout=5):
156 | return
157 | else:
158 | time.sleep(3)
159 |
160 | def solve_rotate(self, retries: int = 3) -> None:
161 | for _ in range(retries):
162 | if not self._any_selector_in_list_present([selectors.RotateV1.INNER]):
163 | logging.debug("Went to solve rotate but whirl-inner-img was not present")
164 | return
165 | outer = fetch_image_b64(self._get_image_url(selectors.RotateV1.OUTER), sync_page=self.page)
166 | inner = fetch_image_b64(self._get_image_url(selectors.RotateV1.INNER), sync_page=self.page)
167 | solution = self.client.rotate(outer, inner)
168 | logging.debug(f"Solution angle: {solution}")
169 | slide_bar_width = self._get_element_width(selectors.RotateV1.SLIDE_BAR)
170 | slider_button_width = self._get_element_width(selectors.RotateV1.SLIDER_DRAG_BUTTON)
171 | distance = compute_rotate_slide_distance(solution.angle, slide_bar_width, slider_button_width)
172 | logging.debug(f"Solution distance: {distance}")
173 | self._drag_element_horizontal(selectors.RotateV1.SLIDER_DRAG_BUTTON, distance)
174 | if self.captcha_is_not_present(timeout=5):
175 | return
176 | else:
177 | time.sleep(3)
178 |
179 | def solve_rotate_v2(self, retries: int = 3) -> None:
180 | for _ in range(retries):
181 | if not self._any_selector_in_list_present([selectors.RotateV2.INNER]):
182 | logging.debug("Went to solve rotate but whirl-inner-img was not present")
183 | return
184 | outer = fetch_image_b64(self._get_image_url(selectors.RotateV2.OUTER), sync_page=self.page)
185 | inner = fetch_image_b64(self._get_image_url(selectors.RotateV2.INNER), sync_page=self.page)
186 | solution = self.client.rotate(outer, inner)
187 | logging.debug(f"Solution angle: {solution}")
188 | slide_bar_width = self._get_element_width(selectors.RotateV2.SLIDE_BAR)
189 | slider_button_width = self._get_element_width(selectors.RotateV2.SLIDER_DRAG_BUTTON)
190 | distance = compute_rotate_slide_distance(solution.angle, slide_bar_width, slider_button_width)
191 | logging.debug(f"Solution distance: {distance}")
192 | self._drag_element_horizontal(selectors.RotateV2.SLIDER_DRAG_BUTTON, distance)
193 | if self.captcha_is_not_present(timeout=5):
194 | return
195 | else:
196 | self.page.click(selectors.RotateV2.REFRESH_BUTTON)
197 | logging.debug("clicked refresh button")
198 | time.sleep(3)
199 |
200 | def solve_puzzle(self, retries: int = 3) -> None:
201 | for _ in range(retries):
202 | if not self._any_selector_in_list_present([selectors.PuzzleV1.PIECE]):
203 | logging.debug("Went to solve puzzle but piece image was not present")
204 | return
205 | puzzle = fetch_image_b64(self._get_image_url(selectors.PuzzleV1.PUZZLE), sync_page=self.page)
206 | piece = fetch_image_b64(self._get_image_url(selectors.PuzzleV1.PIECE), sync_page=self.page)
207 | solution = self.client.puzzle(puzzle, piece)
208 | puzzle_width = self._get_element_width(selectors.PuzzleV1.PUZZLE)
209 | distance = compute_pixel_fraction(solution.slide_x_proportion, puzzle_width)
210 | self._drag_element_horizontal(selectors.PuzzleV1.SLIDER_DRAG_BUTTON, distance)
211 | if self.captcha_is_not_present(timeout=5):
212 | return
213 | else:
214 | time.sleep(3)
215 |
216 |
217 | def solve_puzzle_v2(self, retries: int = 3) -> None:
218 | for _ in range(retries):
219 | if not self._any_selector_in_list_present([selectors.PuzzleV2.PIECE]):
220 | logging.debug("Went to solve puzzle but piece image was not present")
221 | return
222 | puzzle = fetch_image_b64(self._get_image_url(selectors.PuzzleV2.PUZZLE), sync_page=self.page)
223 | piece = fetch_image_b64(self._get_image_url(selectors.PuzzleV2.PIECE), sync_page=self.page)
224 | solution = self.client.puzzle(puzzle, piece)
225 | puzzle_width = self._get_element_width(selectors.PuzzleV2.PUZZLE)
226 | distance = compute_pixel_fraction(solution.slide_x_proportion, puzzle_width)
227 | logging.debug("distance = " + str(distance))
228 | self._drag_ele_until_watched_ele_has_translateX(
229 | selectors.PuzzleV2.SLIDER_DRAG_BUTTON,
230 | selectors.PuzzleV2.PIECE_IMAGE_CONTAINER,
231 | distance
232 | )
233 | if self.captcha_is_not_present(timeout=5):
234 | return
235 | else:
236 | time.sleep(3)
237 |
238 | def solve_icon(self) -> None:
239 | if not self._any_selector_in_list_present([selectors.IconV1.IMAGE]):
240 | logging.debug("Went to solve icon captcha but #captcha-verify-image was not present")
241 | return
242 | challenge = self._get_element_text(selectors.IconV1.TEXT)
243 | image = fetch_image_b64(self._get_image_url(selectors.IconV1.IMAGE), sync_page=self.page)
244 | solution = self.client.icon(challenge, image)
245 | image_element = self.page.locator(selectors.IconV1.IMAGE)
246 | bounding_box = image_element.bounding_box()
247 | if not bounding_box:
248 | raise AttributeError("Image element was found but had no bounding box")
249 | for point in solution.proportional_points:
250 | self._click_proportional(bounding_box, point.proportion_x, point.proportion_y)
251 | self.page.locator(selectors.IconV1.SUBMIT_BUTTON).click()
252 |
253 | def solve_icon_v2(self) -> None:
254 | if not self._any_selector_in_list_present([selectors.IconV2.IMAGE]):
255 | logging.debug("Went to solve icon captcha but #captcha-verify-image was not present")
256 | return
257 | challenge = self._get_element_text(selectors.IconV2.TEXT)
258 | image = fetch_image_b64(selectors.IconV2.IMAGE, sync_page=self.page)
259 | solution = self.client.icon(challenge, image)
260 | image_element = self.page.locator(selectors.IconV2.IMAGE)
261 | bounding_box = image_element.bounding_box()
262 | if not bounding_box:
263 | raise AttributeError("Image element was found but had no bounding box")
264 | for point in solution.proportional_points:
265 | self._click_proportional(bounding_box, point.proportion_x, point.proportion_y)
266 | self.page.locator(selectors.IconV2.SUBMIT_BUTTON).click()
267 |
268 | def solve_douyin_puzzle(self) -> None:
269 | puzzle = fetch_image_b64(self._get_douyin_puzzle_image_url(), sync_page=self.page)
270 | piece = fetch_image_b64(self._get_douyin_piece_image_url(), sync_page=self.page)
271 | solution = self.client.puzzle(puzzle, piece)
272 | distance = self._compute_douyin_puzzle_slide_distance(solution.slide_x_proportion)
273 | self._drag_element_horizontal(".captcha-slider-btn", distance, frame_selector=selectors.DouyinPuzzle.FRAME)
274 |
275 | def _get_douyin_puzzle_image_url(self) -> str:
276 | e = self.page.frame_locator(selectors.DouyinPuzzle.FRAME).locator(selectors.DouyinPuzzle.PUZZLE)
277 | url = e.get_attribute("src")
278 | if not url:
279 | raise ValueError("Puzzle image URL was None")
280 | return url
281 |
282 | def _compute_douyin_puzzle_slide_distance(self, proportion_x: float) -> int:
283 | e = self.page.frame_locator(selectors.DouyinPuzzle.FRAME).locator(selectors.DouyinPuzzle.PUZZLE)
284 | box = e.bounding_box()
285 | if box:
286 | return int(proportion_x * box["width"])
287 | raise AttributeError("#captcha-verify-image was found but had no bouding box")
288 |
289 | def _get_douyin_piece_image_url(self) -> str:
290 | e = self.page.frame_locator(selectors.DouyinPuzzle.FRAME).locator(selectors.DouyinPuzzle.PIECE)
291 | url = e.get_attribute("src")
292 | if not url:
293 | raise ValueError("Piece image URL was None")
294 | return url
295 |
296 | def _get_element_text(self, selector: str) -> str:
297 | challenge_element = self.page.locator(selector)
298 | text = challenge_element.text_content()
299 | if not text:
300 | raise ValueError("element was found but did not have any text.")
301 | return text
302 |
303 | def _get_element_width(self, selector: str) -> int:
304 | e = self.page.locator(selector)
305 | box = e.bounding_box()
306 | if box:
307 | return int(box["width"])
308 | raise AttributeError("element was found but had no bouding box")
309 |
310 | def _get_image_url(self, selector: str) -> str:
311 | e = self.page.locator(selector)
312 | url = e.get_attribute("src")
313 | if not url:
314 | raise ValueError("image URL was None")
315 | return url
316 |
317 | def _click_proportional(
318 | self,
319 | bounding_box: FloatRect,
320 | proportion_x: float,
321 | proportion_y: float
322 | ) -> None:
323 | """Click an element inside its bounding box at a point defined by the proportions of x and y
324 | to the width and height of the entire element
325 |
326 | Args:
327 | element: FloatRect to click inside
328 | proportion_x: float from 0 to 1 defining the proportion x location to click
329 | proportion_y: float from 0 to 1 defining the proportion y location to click
330 | """
331 | x_origin = bounding_box["x"]
332 | y_origin = bounding_box["y"]
333 | x_offset = (proportion_x * bounding_box["width"])
334 | y_offset = (proportion_y * bounding_box["height"])
335 | self.page.mouse.move(x_origin + x_offset, y_origin + y_offset)
336 | time.sleep(random.randint(1, 10) / 11)
337 | self.page.mouse.down()
338 | time.sleep(0.001337)
339 | self.page.mouse.up()
340 | time.sleep(random.randint(1, 10) / 11)
341 |
342 | def _drag_ele_until_watched_ele_has_translateX(self, drag_ele_selector: str, watch_ele_selector: str, target_translateX: int) -> None:
343 | """This method drags the element drag_ele_selector until the translateX value of watch_ele_selector is equal to translateX_target.
344 | This is necessary because there is a small difference between the amount the puzzle piece slides and
345 | the amount of pixels the drag element has been dragged in TikTok puzzle v2."""
346 | drag_ele = self.page.locator(drag_ele_selector)
347 | watch_ele = self.page.locator(watch_ele_selector)
348 | style = watch_ele.get_attribute("style")
349 | if not style:
350 | raise ValueError("element had no attribut style: " + watch_ele_selector)
351 | current_translateX = get_translateX_from_style(style)
352 | drag_ele_box = drag_ele.bounding_box()
353 | if not drag_ele_box:
354 | raise AttributeError("element had no bounding box: " + drag_ele_selector)
355 | start_x = (drag_ele_box["x"] + (drag_ele_box["width"] / 1.337))
356 | start_y = (drag_ele_box["y"] + (drag_ele_box["height"] / 1.337))
357 | self.page.mouse.move(start_x, start_y)
358 | time.sleep(random.randint(1, 10) / 11)
359 | self.page.mouse.down()
360 | current_x = start_x
361 | while current_translateX != target_translateX:
362 | current_x = current_x + self.mouse_step_size
363 | self.page.mouse.move(current_x, start_y)
364 | self.page.wait_for_timeout(self.mouse_step_delay_ms)
365 | style = watch_ele.get_attribute("style")
366 | if not style:
367 | raise ValueError("element had no attribut style: " + watch_ele_selector)
368 | current_translateX = get_translateX_from_style(style)
369 | time.sleep(0.3)
370 | self.page.mouse.up()
371 |
372 | def _drag_element_horizontal(self, css_selector: str, x_offset: int, frame_selector: str | None = None) -> None:
373 | if frame_selector:
374 | e = self.page.frame_locator(frame_selector).locator(css_selector)
375 | else:
376 | e = self.page.locator(css_selector)
377 | box = e.bounding_box()
378 | if not box:
379 | raise AttributeError("Element had no bounding box")
380 | start_x = (box["x"] + (box["width"] / 1.337))
381 | start_y = (box["y"] + (box["height"] / 1.337))
382 | self.page.mouse.move(start_x, start_y)
383 | time.sleep(random.randint(1, 10) / 11)
384 | self.page.mouse.down()
385 | for pixel in range(0, x_offset + 5, self.mouse_step_size):
386 | self.page.mouse.move(start_x + pixel, start_y)
387 | self.page.wait_for_timeout(self.mouse_step_delay_ms)
388 | time.sleep(0.25)
389 | for pixel in range(-5, 2):
390 | self.page.mouse.move(start_x + x_offset - pixel, start_y + pixel) # overshoot back
391 | self.page.wait_for_timeout(self.mouse_step_delay_ms / 2)
392 | time.sleep(0.2)
393 | self.page.mouse.move(start_x + x_offset, start_y, steps=75)
394 | time.sleep(0.3)
395 | self.page.mouse.up()
396 |
397 | def _any_selector_in_list_present(self, selectors: list[str]) -> bool:
398 | for selector in selectors:
399 | for ele in self.page.locator(selector).all():
400 | if ele.is_visible():
401 | logging.debug("Detected selector: " + selector + " from list " + ", ".join(selectors))
402 | return True
403 | logging.debug("No selector in list found: " + ", ".join(selectors))
404 | return False
405 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/selectors.py:
--------------------------------------------------------------------------------
1 | class Wrappers:
2 | V1 = ".captcha-disable-scroll"
3 | V2 = ".captcha-verify-container"
4 |
5 | class RotateV1:
6 | INNER = "[data-testid=whirl-inner-img]"
7 | OUTER = "[data-testid=whirl-outer-img]"
8 | SLIDE_BAR = ".captcha_verify_slide--slidebar"
9 | SLIDER_DRAG_BUTTON = ".secsdk-captcha-drag-icon"
10 | UNIQUE_IDENTIFIER = ".captcha-disable-scroll [data-testid=whirl-inner-img]"
11 |
12 | class RotateV2:
13 | INNER = ".captcha-verify-container > div > div > div > img.cap-absolute"
14 | OUTER = ".captcha-verify-container > div > div > div > img:first-child"
15 | SLIDE_BAR = ".captcha-verify-container > div > div > div.cap-w-full > div.cap-rounded-full"
16 | SLIDER_DRAG_BUTTON = ".captcha-verify-container div[draggable=true]"
17 | UNIQUE_IDENTIFIER = ".captcha-verify-container > div > div > div > img.cap-absolute"
18 | REFRESH_BUTTON = "#captcha_refresh_button"
19 |
20 | class PuzzleV1:
21 | PIECE = "img.captcha_verify_img_slide"
22 | PUZZLE = "#captcha-verify-image"
23 | SLIDER_DRAG_BUTTON = ".secsdk-captcha-drag-icon"
24 | UNIQUE_IDENTIFIER = ".captcha-disable-scroll img.captcha_verify_img_slide"
25 |
26 | class PuzzleV2:
27 | PIECE = ".captcha-verify-container .cap-absolute img"
28 | PUZZLE = "#captcha-verify-image"
29 | SLIDER_DRAG_BUTTON = ".secsdk-captcha-drag-icon"
30 | PIECE_IMAGE_CONTAINER = ".captcha-verify-container div[draggable=true]:has(img[draggable=false])"
31 | UNIQUE_IDENTIFIER = ".captcha-verify-container #captcha-verify-image"
32 |
33 | class ShapesV1:
34 | IMAGE = "#captcha-verify-image"
35 | SUBMIT_BUTTON = ".verify-captcha-submit-button"
36 | UNIQUE_IDENTIFIER = ".captcha-disable-scroll .verify-captcha-submit-button"
37 |
38 | class ShapesV2:
39 | IMAGE = ".captcha-verify-container div.cap-relative img"
40 | SUBMIT_BUTTON = ".captcha-verify-container .cap-relative button.cap-w-full"
41 | UNIQUE_IDENTIFIER = ".captcha-verify-container .cap-relative button.cap-w-full"
42 |
43 | class IconV1:
44 | IMAGE = "#captcha-verify-image"
45 | SUBMIT_BUTTON = ".verify-captcha-submit-button"
46 | TEXT = ".captcha_verify_bar"
47 | UNIQUE_IDENTIFIER = ".captcha-disable-scroll .verify-captcha-submit-button"
48 |
49 | class IconV2:
50 | IMAGE = ".captcha-verify-container div.cap-relative img"
51 | SUBMIT_BUTTON = ".captcha-verify-container .cap-relative button.cap-w-full"
52 | TEXT = ".captcha-verify-container > div > div > span"
53 | UNIQUE_IDENTIFIER = ".captcha-verify-container .cap-relative button.cap-w-full"
54 |
55 | class DouyinPuzzle:
56 | FRAME = "#captcha_container > iframe"
57 | PUZZLE = "#captcha_verify_image"
58 | PIECE = "#captcha-verify_img_slide"
59 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/seleniumsolver.py:
--------------------------------------------------------------------------------
1 | """This class handles the captcha solving for selenium users"""
2 |
3 | from platform import release
4 | import time
5 | from typing import Any
6 |
7 | from selenium.webdriver import ActionChains, Chrome
8 | from selenium.webdriver.common.actions.action_builder import ActionBuilder
9 | from selenium.webdriver.common.by import By
10 | from selenium.webdriver.remote.webelement import WebElement
11 | from undetected_chromedriver import logging
12 | from undetected_chromedriver.patcher import random
13 |
14 | from . import selectors
15 | from .geometry import compute_pixel_fraction, compute_rotate_slide_distance, get_translateX_from_style
16 | from .captchatype import CaptchaType
17 | from .api import ApiClient
18 | from .downloader import fetch_image_b64
19 | from .solver import Solver
20 |
21 |
22 | class SeleniumSolver(Solver):
23 |
24 | client: ApiClient
25 | chromedriver: Chrome
26 |
27 | def __init__(
28 | self,
29 | chromedriver: Chrome,
30 | sadcaptcha_api_key: str,
31 | headers: dict[str, Any] | None = None,
32 | proxy: str | None = None,
33 | mouse_step_size: int = 1,
34 | mouse_step_delay_ms: int = 10
35 | ) -> None:
36 | self.chromedriver = chromedriver
37 | self.client = ApiClient(sadcaptcha_api_key)
38 | self.headers = headers
39 | self.proxy = proxy
40 | self.mouse_step_size = mouse_step_size
41 | self.mouse_step_delay_s = mouse_step_delay_ms / 1000
42 |
43 | def captcha_is_present(self, timeout: int = 15) -> bool:
44 | for _ in range(timeout * 2):
45 | if self.page_is_douyin():
46 | if self._any_selector_in_list_present([selectors.DouyinPuzzle.FRAME]):
47 | return True
48 | else:
49 | if self._any_selector_in_list_present([selectors.Wrappers.V1]):
50 | logging.debug("Captcha detected v1")
51 | return True
52 | if self._any_selector_in_list_present([selectors.Wrappers.V2]):
53 | logging.debug("Captcha detected v2")
54 | return True
55 | time.sleep(0.5)
56 | logging.debug("Captcha not found")
57 | return False
58 |
59 | def captcha_is_not_present(self, timeout: int = 15) -> bool:
60 | for _ in range(timeout * 2):
61 | if self.page_is_douyin():
62 | if len(self.chromedriver.find_elements(By.CSS_SELECTOR, selectors.DouyinPuzzle.FRAME)) == 0:
63 | logging.debug("Captcha detected")
64 | return True
65 | else:
66 | if len(self.chromedriver.find_elements(By.CSS_SELECTOR, selectors.Wrappers.V1)) == 0 and \
67 | len(self.chromedriver.find_elements(By.CSS_SELECTOR, selectors.Wrappers.V2)) == 0:
68 | logging.debug("Captcha not present")
69 | return True
70 | time.sleep(0.5)
71 | logging.debug("Captcha not found")
72 | return False
73 |
74 | def identify_captcha(self) -> CaptchaType:
75 | for _ in range(60):
76 | try:
77 | if self._any_selector_in_list_present([selectors.PuzzleV1.UNIQUE_IDENTIFIER]):
78 | logging.debug("detected puzzle")
79 | return CaptchaType.PUZZLE_V1
80 | elif self._any_selector_in_list_present([selectors.PuzzleV2.UNIQUE_IDENTIFIER]):
81 | logging.debug("detected puzzle v2")
82 | return CaptchaType.PUZZLE_V2
83 | elif self._any_selector_in_list_present([selectors.RotateV1.UNIQUE_IDENTIFIER]):
84 | logging.debug("detected rotate v1")
85 | return CaptchaType.ROTATE_V1
86 | elif self._any_selector_in_list_present([selectors.RotateV2.UNIQUE_IDENTIFIER]):
87 | logging.debug("detected rotate v2")
88 | return CaptchaType.ROTATE_V2
89 | elif self._any_selector_in_list_present([selectors.ShapesV1.UNIQUE_IDENTIFIER]):
90 | img_url = self._get_image_url(selectors.ShapesV1.IMAGE)
91 | if "/icon" in img_url:
92 | logging.debug("detected icon v1")
93 | return CaptchaType.ICON_V1
94 | elif "/3d" in img_url:
95 | logging.debug("detected shapes v1")
96 | return CaptchaType.SHAPES_V1
97 | else:
98 | logging.warn("did not see '/3d' in image source url but returning shapes v1 anyways")
99 | return CaptchaType.SHAPES_V1
100 | elif self._any_selector_in_list_present([selectors.ShapesV2.UNIQUE_IDENTIFIER]):
101 | img_url = self._get_image_url(selectors.ShapesV2.IMAGE)
102 | if "/icon" in img_url:
103 | logging.debug("detected icon v2")
104 | return CaptchaType.ICON_V2
105 | elif "/3d" in img_url:
106 | logging.debug("detected shapes v2")
107 | return CaptchaType.SHAPES_V2
108 | else:
109 | logging.warning("did not see '/3d' in image source url but returning shapes v2 anyways")
110 | return CaptchaType.SHAPES_V2
111 | else:
112 | time.sleep(0.5)
113 | except Exception as e:
114 | logging.debug(f"Exception occurred identifying captcha: {str(e)}. Trying again")
115 | time.sleep(0.5)
116 | raise ValueError("Neither puzzle, shapes, or rotate captcha was present.")
117 |
118 | def page_is_douyin(self) -> bool:
119 | if "douyin" in self.chromedriver.current_url:
120 | logging.debug("page is douyin")
121 | return True
122 | logging.debug("page is tiktok")
123 | return False
124 |
125 | def solve_shapes(self) -> None:
126 | if not self._any_selector_in_list_present([selectors.ShapesV1.IMAGE]):
127 | logging.debug("Went to solve puzzle but #captcha-verify-image was not present")
128 | return
129 | image = fetch_image_b64(self._get_image_url(selectors.ShapesV1.IMAGE), driver=self.chromedriver)
130 | solution = self.client.shapes(image)
131 | image_element = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.ShapesV1.IMAGE)
132 | self._click_proportional(image_element, solution.point_one_proportion_x, solution.point_one_proportion_y)
133 | self._click_proportional(image_element, solution.point_two_proportion_x, solution.point_two_proportion_y)
134 | self.chromedriver.find_element(By.CSS_SELECTOR, selectors.ShapesV1.SUBMIT_BUTTON).click()
135 |
136 | def solve_shapes_v2(self) -> None:
137 | if not self._any_selector_in_list_present([selectors.ShapesV2.IMAGE]):
138 | logging.debug("Went to solve puzzle but #captcha-verify-image was not present")
139 | return
140 | image = fetch_image_b64(self._get_image_url(selectors.ShapesV2.IMAGE), driver=self.chromedriver)
141 | solution = self.client.shapes(image)
142 | image_element = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.ShapesV2.IMAGE)
143 | self._click_proportional(image_element, solution.point_one_proportion_x, solution.point_one_proportion_y)
144 | self._click_proportional(image_element, solution.point_two_proportion_x, solution.point_two_proportion_y)
145 | self.chromedriver.find_element(By.CSS_SELECTOR, selectors.ShapesV2.SUBMIT_BUTTON).click()
146 |
147 | def solve_rotate(self) -> None:
148 | if not self._any_selector_in_list_present([selectors.RotateV1.INNER]):
149 | logging.debug("Went to solve rotate but whirl-inner-img was not present")
150 | return
151 | outer = fetch_image_b64(self._get_image_url(selectors.RotateV1.OUTER), driver=self.chromedriver)
152 | inner = fetch_image_b64(self._get_image_url(selectors.RotateV1.INNER), driver=self.chromedriver)
153 | solution = self.client.rotate(outer, inner)
154 | slide_bar_width = self._get_element_width(selectors.RotateV1.SLIDE_BAR)
155 | slider_button_width = self._get_element_width(selectors.RotateV1.SLIDER_DRAG_BUTTON)
156 | distance = compute_rotate_slide_distance(solution.angle, slide_bar_width, slider_button_width)
157 | self._drag_element_horizontal(selectors.RotateV1.SLIDER_DRAG_BUTTON, distance)
158 |
159 | def solve_rotate_v2(self) -> None:
160 | if not self._any_selector_in_list_present([selectors.RotateV2.INNER]):
161 | logging.debug("Went to solve rotate but whirl-inner-img was not present")
162 | return
163 | outer = fetch_image_b64(self._get_image_url(selectors.RotateV2.OUTER), driver=self.chromedriver)
164 | inner = fetch_image_b64(self._get_image_url(selectors.RotateV2.INNER), driver=self.chromedriver)
165 | solution = self.client.rotate(outer, inner)
166 | logging.debug("angle solution: " + str(solution.angle))
167 | slide_bar_width = self._get_element_width(selectors.RotateV2.SLIDE_BAR)
168 | slider_button_width = self._get_element_width(selectors.RotateV2.SLIDER_DRAG_BUTTON)
169 | distance = compute_rotate_slide_distance(solution.angle, slide_bar_width, slider_button_width)
170 | logging.debug("slide distance: " + str(distance))
171 | self._drag_ele_until_watched_ele_has_translateX(
172 | selectors.RotateV2.SLIDER_DRAG_BUTTON,
173 | selectors.RotateV2.SLIDER_DRAG_BUTTON,
174 | distance
175 | )
176 |
177 | def solve_puzzle(self) -> None:
178 | if not self._any_selector_in_list_present([selectors.PuzzleV1.PIECE]):
179 | logging.debug("Went to solve puzzle but #captcha-verify-image was not present")
180 | return
181 | puzzle = fetch_image_b64(self._get_image_url(selectors.PuzzleV1.PUZZLE), driver=self.chromedriver)
182 | piece = fetch_image_b64(self._get_image_url(selectors.PuzzleV1.PIECE), driver=self.chromedriver)
183 | solution = self.client.puzzle(puzzle, piece)
184 | puzzle_width = self._get_element_width(selectors.PuzzleV1.PUZZLE)
185 | distance = compute_pixel_fraction(solution.slide_x_proportion, puzzle_width)
186 | self._drag_element_horizontal(selectors.PuzzleV1.SLIDER_DRAG_BUTTON, distance)
187 |
188 | def solve_puzzle_v2(self) -> None:
189 | if not self._any_selector_in_list_present([selectors.PuzzleV2.PIECE]):
190 | logging.debug("Went to solve puzzle but #captcha-verify-image was not present")
191 | return
192 | puzzle = fetch_image_b64(self._get_image_url(selectors.PuzzleV2.PUZZLE), driver=self.chromedriver)
193 | piece = fetch_image_b64(self._get_image_url(selectors.PuzzleV2.PIECE), driver=self.chromedriver)
194 | solution = self.client.puzzle(puzzle, piece)
195 | puzzle_width = self._get_element_width(selectors.PuzzleV2.PUZZLE)
196 | distance = compute_pixel_fraction(solution.slide_x_proportion, puzzle_width)
197 | self._drag_ele_until_watched_ele_has_translateX(
198 | selectors.PuzzleV2.SLIDER_DRAG_BUTTON,
199 | selectors.PuzzleV2.PIECE_IMAGE_CONTAINER,
200 | distance
201 | )
202 |
203 | def solve_icon(self) -> None:
204 | if not self._any_selector_in_list_present([selectors.ShapesV1.IMAGE]):
205 | logging.debug("Went to solve icon but #captcha-verify-image was not present")
206 | return
207 | challenge = self._get_element_text(selectors.IconV1.TEXT)
208 | image = fetch_image_b64(self._get_image_url(selectors.IconV1.IMAGE), driver=self.chromedriver)
209 | solution = self.client.icon(challenge, image)
210 | image_element = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.IconV1.IMAGE)
211 | for point in solution.proportional_points:
212 | self._click_proportional(image_element, point.proportion_x, point.proportion_y)
213 | self.chromedriver.find_element(By.CSS_SELECTOR, selectors.IconV1.SUBMIT_BUTTON).click()
214 |
215 | def solve_icon_v2(self) -> None:
216 | if not self._any_selector_in_list_present([selectors.ShapesV2.IMAGE]):
217 | logging.debug("Went to solve icon but #captcha-verify-image was not present")
218 | return
219 | challenge = self._get_element_text(selectors.IconV2.TEXT)
220 | image = fetch_image_b64(self._get_image_url(selectors.IconV2.IMAGE), driver=self.chromedriver)
221 | solution = self.client.icon(challenge, image)
222 | image_element = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.IconV2.IMAGE)
223 | for point in solution.proportional_points:
224 | self._click_proportional(image_element, point.proportion_x, point.proportion_y)
225 | self.chromedriver.find_element(By.CSS_SELECTOR, selectors.IconV2.SUBMIT_BUTTON).click()
226 |
227 | def solve_douyin_puzzle(self) -> None:
228 | puzzle = fetch_image_b64(self._get_douyin_puzzle_image_url(), driver=self.chromedriver)
229 | piece = fetch_image_b64(self._get_douyin_piece_image_url(), driver=self.chromedriver)
230 | solution = self.client.puzzle(puzzle, piece)
231 | distance = self._compute_douyin_puzzle_slide_distance(solution.slide_x_proportion)
232 | self._drag_element_horizontal(".captcha-slider-btn", distance, frame_selector=selectors.DouyinPuzzle.FRAME)
233 |
234 | def _get_element_text(self, selector: str) -> str:
235 | challenge_element = self.chromedriver.find_element(By.CSS_SELECTOR, selector)
236 | text = challenge_element.text
237 | if not text:
238 | raise ValueError("element was found but did not have any text.")
239 | return text
240 |
241 | def _get_element_width(self, selector: str) -> int:
242 | e = self.chromedriver.find_element(By.CSS_SELECTOR, selector)
243 | return e.size['width']
244 |
245 | def _get_image_url(self, selector: str) -> str:
246 | e = self.chromedriver.find_element(By.CSS_SELECTOR, selector)
247 | url = e.get_attribute("src")
248 | if not url:
249 | raise ValueError("image URL was None")
250 | return url
251 |
252 | def _get_douyin_puzzle_image_url(self) -> str:
253 | frame = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.DouyinPuzzle.FRAME)
254 | self.chromedriver.switch_to.frame(frame)
255 | try:
256 | e = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.DouyinPuzzle.PUZZLE)
257 | url = e.get_attribute("src")
258 | if not url:
259 | raise ValueError("Puzzle image URL was None")
260 | return url
261 | finally:
262 | self.chromedriver.switch_to.default_content()
263 |
264 | def _compute_douyin_puzzle_slide_distance(self, proportion_x: float) -> int:
265 | frame = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.DouyinPuzzle.FRAME)
266 | self.chromedriver.switch_to.frame(frame)
267 | try:
268 | e = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.DouyinPuzzle.PUZZLE)
269 | return int(proportion_x * e.size["width"])
270 | finally:
271 | self.chromedriver.switch_to.default_content()
272 |
273 | def _get_douyin_piece_image_url(self) -> str:
274 | frame = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.DouyinPuzzle.FRAME)
275 | self.chromedriver.switch_to.frame(frame)
276 | try:
277 | e = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.DouyinPuzzle.PIECE)
278 | url = e.get_attribute("src")
279 | if not url:
280 | raise ValueError("Piece image URL was None")
281 | return url
282 | finally:
283 | self.chromedriver.switch_to.default_content()
284 |
285 | def _click_proportional(
286 | self,
287 | element: WebElement,
288 | proportion_x: float,
289 | proportion_y: float
290 | ) -> None:
291 | """Click an element inside its bounding box at a point defined by the proportions of x and y
292 | to the width and height of the entire element
293 |
294 | Args:
295 | element: WebElement to click inside
296 | proportion_x: float from 0 to 1 defining the proportion x location to click
297 | proportion_y: float from 0 to 1 defining the proportion y location to click
298 | """
299 | x_origin = element.location["x"]
300 | y_origin = element.location["y"]
301 | x_offset = (proportion_x * element.size["width"])
302 | y_offset = (proportion_y * element.size["height"])
303 | action = ActionBuilder(self.chromedriver)
304 | action.pointer_action \
305 | .move_to_location(x_origin + x_offset, y_origin + y_offset) \
306 | .pause(random.randint(1, 10) / 11) \
307 | .click() \
308 | .pause(random.randint(1, 10) / 11)
309 | action.perform()
310 |
311 | def _drag_ele_until_watched_ele_has_translateX(self, drag_ele_selector: str, watch_ele_selector: str, target_translateX: int) -> None:
312 | """This method drags the element drag_ele_selector until the translateX value of watch_ele_selector is equal to translateX_target.
313 | This is necessary because there is a small difference between the amount the puzzle piece slides and
314 | the amount of pixels the drag element has been dragged in TikTok puzzle v2."""
315 | drag_ele = self.chromedriver.find_element(By.CSS_SELECTOR, drag_ele_selector)
316 | watch_ele = self.chromedriver.find_element(By.CSS_SELECTOR, watch_ele_selector)
317 | style = watch_ele.get_attribute("style")
318 | if not style:
319 | raise ValueError("element had no attribut style: " + watch_ele_selector)
320 | current_translateX = get_translateX_from_style(style)
321 | actions = ActionChains(self.chromedriver, duration=0)
322 | # Start with a slight move_by_offset() because for some reason running perform()
323 | # right after click_and_hold() does nothing
324 | actions.click_and_hold(drag_ele) \
325 | .move_by_offset(10, 0) \
326 | .perform()
327 | time.sleep(0.1)
328 | while current_translateX <= target_translateX:
329 | actions.move_by_offset(self.mouse_step_size, 0) \
330 | .pause(self.mouse_step_delay_s) \
331 | .perform()
332 | style = watch_ele.get_attribute("style")
333 | if not style:
334 | raise ValueError("element had no attribute style: " + watch_ele_selector)
335 | current_translateX = get_translateX_from_style(style)
336 | time.sleep(0.3)
337 | actions.release().perform()
338 |
339 | def _drag_element_horizontal(self, css_selector: str, x: int, frame_selector: str | None = None) -> None:
340 | try:
341 | if frame_selector:
342 | frame = self.chromedriver.find_element(By.CSS_SELECTOR, selectors.DouyinPuzzle.FRAME)
343 | self.chromedriver.switch_to.frame(frame)
344 | e = self.chromedriver.find_element(By.CSS_SELECTOR, css_selector)
345 | else:
346 | e = self.chromedriver.find_element(By.CSS_SELECTOR, css_selector)
347 | actions = ActionChains(self.chromedriver, duration=0)
348 | actions.click_and_hold(e)
349 | time.sleep(0.1)
350 | for _ in range(0, x - 15, self.mouse_step_size):
351 | actions.move_by_offset(1, 0)
352 | actions.pause(self.mouse_step_delay_s)
353 | for _ in range(0, 20):
354 | actions.move_by_offset(1, 0)
355 | actions.pause(self.mouse_step_delay_s / 2)
356 | actions.pause(0.7)
357 | for _ in range(0, 5):
358 | actions.move_by_offset(-1, 0)
359 | actions.pause(self.mouse_step_delay_s)
360 | actions.pause(0.5)
361 | actions.release().perform()
362 | finally:
363 | self.chromedriver.switch_to.default_content()
364 |
365 | def _any_selector_in_list_present(self, selectors: list[str]) -> bool:
366 | for selector in selectors:
367 | for ele in self.chromedriver.find_elements(By.CSS_SELECTOR, selector):
368 | if ele.is_displayed():
369 | logging.debug("Detected selector: " + selector + " from list " + ", ".join(selectors))
370 | return True
371 | logging.debug("No selector in list found: " + ", ".join(selectors))
372 | return False
373 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/solver.py:
--------------------------------------------------------------------------------
1 | """Abstract base class for Tiktok Captcha Solvers"""
2 |
3 | import time
4 | from abc import ABC, abstractmethod
5 |
6 | from undetected_chromedriver import logging
7 |
8 | from .captchatype import CaptchaType
9 |
10 | class Solver(ABC):
11 |
12 | def solve_captcha_if_present(self, captcha_detect_timeout: int = 15, retries: int = 3) -> None:
13 | """Solves any captcha that is present, if one is detected
14 |
15 | Args:
16 | captcha_detect_timeout: return if no captcha is detected in this many seconds
17 | retries: number of times to retry captcha
18 | """
19 | for _ in range(retries):
20 | if not self.captcha_is_present(captcha_detect_timeout):
21 | logging.debug("Captcha is not present")
22 | return
23 | if self.page_is_douyin():
24 | logging.debug("Solving douyin puzzle")
25 | try:
26 | self.solve_douyin_puzzle()
27 | except ValueError as e:
28 | logging.debug("Douyin puzzle was not ready, trying again in 5 seconds")
29 | else:
30 | match self.identify_captcha():
31 | case CaptchaType.PUZZLE_V1:
32 | logging.debug("Detected puzzle v1")
33 | self.solve_puzzle()
34 | case CaptchaType.PUZZLE_V2:
35 | logging.debug("Detected puzzle v2")
36 | self.solve_puzzle_v2()
37 | case CaptchaType.ROTATE_V1:
38 | logging.debug("Detected rotate v1")
39 | self.solve_rotate()
40 | case CaptchaType.ROTATE_V2:
41 | logging.debug("Detected rotate v2")
42 | self.solve_rotate_v2()
43 | case CaptchaType.SHAPES_V1:
44 | logging.debug("Detected shapes v2")
45 | self.solve_shapes()
46 | case CaptchaType.SHAPES_V2:
47 | logging.debug("Detected shapes v2")
48 | self.solve_shapes_v2()
49 | case CaptchaType.ICON_V1:
50 | logging.debug("Detected icon v1")
51 | self.solve_icon()
52 | case CaptchaType.ICON_V2:
53 | logging.debug("Detected icon v2")
54 | self.solve_icon_v2()
55 | if self.captcha_is_not_present(timeout=5):
56 | return
57 | else:
58 | time.sleep(5)
59 |
60 | @abstractmethod
61 | def captcha_is_present(self, timeout: int = 15) -> bool:
62 | pass
63 |
64 | @abstractmethod
65 | def captcha_is_not_present(self, timeout: int = 15) -> bool:
66 | pass
67 |
68 | @abstractmethod
69 | def identify_captcha(self) -> CaptchaType:
70 | pass
71 |
72 | @abstractmethod
73 | def page_is_douyin(self) -> bool:
74 | pass
75 |
76 | @abstractmethod
77 | def solve_shapes(self) -> None:
78 | pass
79 |
80 | @abstractmethod
81 | def solve_shapes_v2(self) -> None:
82 | pass
83 |
84 | @abstractmethod
85 | def solve_rotate(self) -> None:
86 | pass
87 |
88 | @abstractmethod
89 | def solve_rotate_v2(self) -> None:
90 | pass
91 |
92 | @abstractmethod
93 | def solve_puzzle(self) -> None:
94 | pass
95 |
96 | @abstractmethod
97 | def solve_puzzle_v2(self) -> None:
98 | pass
99 |
100 | @abstractmethod
101 | def solve_icon(self) -> None:
102 | pass
103 |
104 | @abstractmethod
105 | def solve_icon_v2(self) -> None:
106 | pass
107 |
108 | @abstractmethod
109 | def solve_douyin_puzzle(self) -> None:
110 | pass
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/__init__.py
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/__pycache__/__init__.cpython-312.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/__pycache__/__init__.cpython-312.pyc
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/__pycache__/test_all.cpython-312-pytest-8.2.0.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/__pycache__/test_all.cpython-312-pytest-8.2.0.pyc
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/__pycache__/test_api.cpython-312-pytest-8.2.0.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/__pycache__/test_api.cpython-312-pytest-8.2.0.pyc
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/__pycache__/test_asyncplaywrightsolver.cpython-312-pytest-8.2.0.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/__pycache__/test_asyncplaywrightsolver.cpython-312-pytest-8.2.0.pyc
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/__pycache__/test_downloader.cpython-312-pytest-8.2.0.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/__pycache__/test_downloader.cpython-312-pytest-8.2.0.pyc
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/__pycache__/test_playwrightsolver.cpython-312-pytest-8.2.0.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/__pycache__/test_playwrightsolver.cpython-312-pytest-8.2.0.pyc
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/__pycache__/test_sadcaptcha.cpython-312-pytest-8.2.0.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/__pycache__/test_sadcaptcha.cpython-312-pytest-8.2.0.pyc
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/__pycache__/test_seleniumsolver.cpython-312-pytest-8.2.0.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/__pycache__/test_seleniumsolver.cpython-312-pytest-8.2.0.pyc
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/piece_mobile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/piece_mobile.jpg
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/puzzle_mobile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gbiz123/tiktok-captcha-solver/d00f63555df1ee56bb9a7de3bc86fae954b212db/src/tiktok_captcha_solver/tests/puzzle_mobile.jpg
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/test_api.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import os
3 | import logging
4 |
5 | from ..downloader import fetch_image_b64
6 | from ..api import ApiClient
7 | from tiktok_captcha_solver.models import IconCaptchaResponse, PuzzleCaptchaResponse, RotateCaptchaResponse, ShapesCaptchaResponse
8 |
9 | api_client = ApiClient(os.environ["API_KEY"])
10 |
11 | def test_rotate(caplog):
12 | caplog.set_level(logging.DEBUG)
13 | inner = fetch_image_b64("https://raw.githubusercontent.com/gbiz123/sadcaptcha-code-examples/master/images/inner_tt.png")
14 | outer = fetch_image_b64("https://raw.githubusercontent.com/gbiz123/sadcaptcha-code-examples/master/images/outer_tt.png")
15 | res = api_client.rotate(outer, inner)
16 | assert isinstance(res, RotateCaptchaResponse)
17 |
18 |
19 | def test_puzzle():
20 | piece = fetch_image_b64("https://raw.githubusercontent.com/gbiz123/sadcaptcha-code-examples/master/images/piece.png")
21 | puzzle = fetch_image_b64("https://raw.githubusercontent.com/gbiz123/sadcaptcha-code-examples/master/images/puzzle.jpg")
22 | res = api_client.puzzle(puzzle, piece)
23 | assert isinstance(res, PuzzleCaptchaResponse)
24 |
25 |
26 | def test_shapes():
27 | with open("/home/gregb/ToughdataLLC/SadCaptcha/sadcaptcha-image-processor/src/test/resources/tiktok3d.png", "rb") as image_file:
28 | image = base64.b64encode(image_file.read()).decode()
29 | res = api_client.shapes(image)
30 | assert isinstance(res, ShapesCaptchaResponse)
31 |
32 | def test_icon():
33 | with open("/home/gregb/ToughdataLLC/SadCaptcha/sadcaptcha-image-processor/src/test/resources/tiktokicon.jpg", "rb") as image_file:
34 | challenge = "Which of these objects has a brim?"
35 | image = base64.b64encode(image_file.read()).decode()
36 | res = api_client.icon(challenge, image)
37 | assert isinstance(res, IconCaptchaResponse)
38 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/test_asyncplaywrightsolver.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | import os
4 |
5 | from playwright.async_api import Page, async_playwright, expect
6 | from playwright_stealth import stealth_async, StealthConfig
7 | import pytest
8 |
9 | from tiktok_captcha_solver.captchatype import CaptchaType
10 |
11 | from ..asyncplaywrightsolver import AsyncPlaywrightSolver
12 |
13 | async def open_tiktkok_login(page: Page) -> None:
14 | await page.goto("https://www.tiktok.com/login/phone-or-email/email")
15 | await asyncio.sleep(10)
16 | write_username = page.locator('xpath=//input[contains(@name,"username")]')
17 | await write_username.type(os.environ["TIKTOK_USERNAME"]);
18 | await asyncio.sleep(2);
19 | write_password = page.get_by_placeholder('Password')
20 | await write_password.type(os.environ["TIKTOK_PASSWORD"]);
21 | await asyncio.sleep(2)
22 | login_btn = await page.locator('//button[contains(@data-e2e,"login-button")]').click();
23 | await asyncio.sleep(8)
24 |
25 | async def open_tiktok_search(page: Page) -> None:
26 | search_query = "davidteather"
27 | await page.goto(f"https://www.tiktok.com/@therock")
28 |
29 | # @pytest.mark.asyncio
30 | # async def test_does_not_false_positive(caplog):
31 | # caplog.set_level(logging.DEBUG)
32 | # async with async_playwright() as p:
33 | # browser = await p.chromium.launch(headless=False)
34 | # page = await browser.new_page()
35 | # config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
36 | # await stealth_async(page, config)
37 | # await page.goto("https://www.tiktok.com/login/phone-or-email/email")
38 | # sadcaptcha = AsyncPlaywrightSolver(page, os.environ["API_KEY"])
39 | # assert await sadcaptcha.captcha_is_present(timeout=5) == False
40 | #
41 | # @pytest.mark.asyncio
42 | # async def test_solve_at_scroll(caplog) -> None:
43 | # caplog.set_level(logging.DEBUG)
44 | # async with async_playwright() as p:
45 | # browser = await p.chromium.launch(headless=False)
46 | # page = await browser.new_page()
47 | # config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
48 | # # stealth_sync(page, config)
49 | # await page.goto("https://www.tiktok.com/")
50 | # input("Trigger a captcha...")
51 | # sadcaptcha = AsyncPlaywrightSolver(page, os.environ["API_KEY"])
52 | # await sadcaptcha.solve_captcha_if_present(captcha_detect_timeout=10)
53 | #
54 | #
55 | # @pytest.mark.asyncio
56 | # async def test_shapes_v2_is_detected(caplog):
57 | # caplog.set_level(logging.DEBUG)
58 | # async with async_playwright() as p:
59 | # browser = await p.chromium.launch(headless=False)
60 | # page = await browser.new_page()
61 | # config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
62 | # await stealth_async(page, config)
63 | # await page.goto("file:///home/gregb/ToughdataLLC/SadCaptcha/tiktok-captcha-solver/src/tiktok_captcha_solver/tests/new_shapes.html")
64 | # sadcaptcha = AsyncPlaywrightSolver(page, os.environ["API_KEY"])
65 | # assert await sadcaptcha.identify_captcha() == CaptchaType.SHAPES_V2
66 | #
67 | # @pytest.mark.asyncio
68 | # async def test_rotate_v2_is_detected(caplog):
69 | # caplog.set_level(logging.DEBUG)
70 | # async with async_playwright() as p:
71 | # browser = await p.chromium.launch(headless=False)
72 | # page = await browser.new_page()
73 | # config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
74 | # await stealth_async(page, config)
75 | # await page.goto("file:///home/gregb/ToughdataLLC/SadCaptcha/tiktok-captcha-solver/src/tiktok_captcha_solver/tests/new_rotate.html")
76 | # sadcaptcha = AsyncPlaywrightSolver(page, os.environ["API_KEY"])
77 | # assert await sadcaptcha.identify_captcha() == CaptchaType.ROTATE_V2
78 | #
79 | #
80 | # @pytest.mark.asyncio
81 | # async def test_puzzle_v2_is_detected(caplog):
82 | # caplog.set_level(logging.DEBUG)
83 | # async with async_playwright() as p:
84 | # browser = await p.chromium.launch(headless=False)
85 | # page = await browser.new_page()
86 | # config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
87 | # await stealth_async(page, config)
88 | # await page.goto("file:///home/gregb/ToughdataLLC/SadCaptcha/tiktok-captcha-solver/src/tiktok_captcha_solver/tests/new_puzzle.html")
89 | # sadcaptcha = AsyncPlaywrightSolver(page, os.environ["API_KEY"])
90 | # assert await sadcaptcha.identify_captcha() == CaptchaType.PUZZLE_V2
91 |
92 |
93 | @pytest.mark.asyncio
94 | async def test_solve_captcha_at_login(caplog):
95 | proxy = {
96 | "server": "pr.oxylabs.io:7777",
97 | "username": "customer-toughdata-cc-us",
98 | "password": "Toughproxies_123"
99 | }
100 | caplog.set_level(logging.DEBUG)
101 | async with async_playwright() as p:
102 | browser = await p.chromium.launch(headless=False, proxy=proxy)
103 | page = await browser.new_page()
104 | config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
105 | await stealth_async(page, config)
106 | await open_tiktkok_login(page)
107 | sadcaptcha = AsyncPlaywrightSolver(page, os.environ["API_KEY"])
108 | await sadcaptcha.solve_captcha_if_present()
109 | await expect(page.locator("css=#header-more-menu-icon")).to_be_visible(timeout=30000)
110 |
111 | # @pytest.mark.asyncio
112 | # async def test_solve_captcha_at_login_with_proxy(caplog):
113 | # caplog.set_level(logging.DEBUG)
114 | # async with async_playwright() as p:
115 | # browser = await p.chromium.launch(headless=False)
116 | # page = await browser.new_page()
117 | # config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
118 | # await stealth_async(page, config)
119 | # await open_tiktkok_login(page)
120 | # sadcaptcha = AsyncPlaywrightSolver(page, os.environ["API_KEY"], proxy=os.environ["PROXY"])
121 | # await sadcaptcha.solve_captcha_if_present()
122 | # await expect(page.locator("css=#header-more-menu-icon")).to_be_visible(timeout=30000)
123 |
124 | @pytest.mark.asyncio
125 | async def test_solve_captcha_at_search(caplog):
126 | caplog.set_level(logging.DEBUG)
127 | async with async_playwright() as p:
128 | browser = await p.chromium.launch(headless=False)
129 | page = await browser.new_page()
130 | config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
131 | await stealth_async(page, config)
132 | await open_tiktok_search(page)
133 | sadcaptcha = AsyncPlaywrightSolver(page, os.environ["API_KEY"])
134 | await sadcaptcha.solve_captcha_if_present()
135 |
136 | @pytest.mark.asyncio
137 | async def test_detect_douyin(caplog):
138 | caplog.set_level(logging.DEBUG)
139 | async with async_playwright() as p:
140 | browser = await p.chromium.launch(headless=False)
141 | page = await browser.new_page()
142 | await page.goto("https://www.douyin.com")
143 | sadcaptcha = AsyncPlaywrightSolver(page, os.environ["API_KEY"])
144 | assert sadcaptcha.page_is_douyin()
145 |
146 | @pytest.mark.asyncio
147 | async def test_solve_douyin_puzzle(caplog):
148 | caplog.set_level(logging.DEBUG)
149 | async with async_playwright() as p:
150 | browser = await p.chromium.launch(headless=False)
151 | page = await browser.new_page()
152 | await page.goto("https://www.douyin.com")
153 | await page.goto("https://www.douyin.com/discover")
154 | sadcaptcha = AsyncPlaywrightSolver(page, os.environ["API_KEY"])
155 | await sadcaptcha.solve_captcha_if_present()
156 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/test_download_crx.py:
--------------------------------------------------------------------------------
1 | from tiktok_captcha_solver.download_crx import download_extension_to_tempfile, download_extension_to_unpacked
2 | import logging
3 | import os
4 |
5 | def test_download_extension_to_unpac(caplog):
6 | caplog.set_level(logging.DEBUG)
7 | with download_extension_to_unpacked() as ext:
8 | assert os.path.isdir(ext)
9 |
10 | def test_download_extension_to_tempfile(caplog):
11 | caplog.set_level(logging.DEBUG)
12 | with download_extension_to_tempfile() as ext:
13 | assert os.path.isfile(ext.name)
14 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/test_downloader.py:
--------------------------------------------------------------------------------
1 | from ..downloader import fetch_image_b64
2 |
3 |
4 | def test_download_image_b64():
5 | result = fetch_image_b64("https://fastly.picsum.photos/id/237/536/354.jpg")
6 | assert len(result) > 1
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/test_launcher.py:
--------------------------------------------------------------------------------
1 | import random
2 | import time
3 | import os
4 | from playwright.sync_api import Page, sync_playwright
5 | from playwright.async_api import async_playwright
6 | from playwright_stealth import stealth_sync, stealth_async, StealthConfig
7 |
8 | import pytest
9 | from selenium.webdriver import ChromeOptions
10 | from tiktok_captcha_solver import make_async_playwright_solver_context, make_playwright_solver_context, make_undetected_chromedriver_solver
11 | from tiktok_captcha_solver.launcher import make_nodriver_solver, make_selenium_solver
12 | from tiktok_captcha_solver.playwrightsolver import PlaywrightSolver
13 |
14 | proxy = {
15 | # "server": "216.173.104.197:6334",
16 | # "server": "185.216.106.238:6315",
17 | # "server": "23.27.75.226:6306"
18 | # "server": "206.232.75.209:6779"
19 | # "server": "206.232.75.84:6654"
20 | "server": "185.216.106.238:6315"
21 | # "server": "185.15.178.3:5687"
22 | # "server": "2.57.30.223:7299"
23 | # "server": "2.57.30.49:7125"
24 | }
25 |
26 | # proxy = None
27 | def test_launch_uc_solver():
28 | options = ChromeOptions()
29 | # _ = options.add_argument("--proxy-server=2.57.30.49:7125")
30 | #options.add_argument("--headless=")
31 | solver = make_selenium_solver(
32 | os.environ["API_KEY"],
33 | options=options
34 | )
35 | # solver.get("https://affiliate-us.tiktok.com/connection/creator?shop_region=US")
36 | solver.get("https://www.tiktok.com")
37 | input("waiting for enter")
38 | solver.close()
39 |
40 | def open_tiktkok_login(page: Page) -> None:
41 | _ = page.goto("https://www.tiktok.com/login/phone-or-email/email")
42 | print("opened tiktok login")
43 | # time.sleep(10)
44 | write_username = page.locator('xpath=//input[contains(@name,"username")]')
45 | write_username.type(os.environ["TIKTOK_USERNAME"]);
46 | time.sleep(2);
47 | # write_password = page.locator('xpath=//input[contains(@type,"password")]')
48 | write_password = page.get_by_placeholder('Password')
49 | write_password.type(os.environ["TIKTOK_PASSWORD"]);
50 | print("typed credentials")
51 | time.sleep(2)
52 | page.locator('//button[contains(@data-e2e,"login-button")]').click();
53 | print("pressed login button")
54 | # time.sleep(5)
55 | _ = page.screenshot(path="post_login_click.png")
56 | time.sleep(15)
57 | #
58 | # def test_launch_browser_with_crx_headless():
59 | # with sync_playwright() as p:
60 | # ctx = make_playwright_solver_context(
61 | # p,
62 | # os.environ["API_KEY"],
63 | # headless=True,
64 | # proxy=proxy,
65 | # user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
66 | # args=["--headless=chrome"],
67 | # record_video_dir="videos/"
68 | # )
69 | # page = ctx.new_page()
70 | # stealth_config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
71 | # stealth_sync(page, stealth_config)
72 | # open_tiktkok_login(page)
73 | # assert not PlaywrightSolver(page, os.environ["API_KEY"]).captcha_is_present()
74 | # ctx.close()
75 | #
76 | # def test_launch_browser_with_crx():
77 | # with sync_playwright() as p:
78 | # ctx = make_playwright_solver_context(
79 | # p,
80 | # os.environ["API_KEY"],
81 | # headless=False,
82 | # proxy=proxy,
83 | # # user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
84 | # )
85 | # page = ctx.new_page()
86 | # stealth_config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
87 | # stealth_sync(page, stealth_config)
88 | # _ = page.goto("https://tiktok.com")
89 | # time.sleep(2)
90 | # _ = page.locator("div[data-e2e=\"explore-item\"]").first.click()
91 | # input("waiting for enter")
92 | #
93 | # @pytest.mark.asyncio
94 | # async def test_launch_browser_with_asyncpw():
95 | # async with async_playwright() as p:
96 | # ctx = await make_async_playwright_solver_context(
97 | # p,
98 | # os.environ["API_KEY"],
99 | # headless=False,
100 | # user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
101 | # proxy=proxy
102 | # )
103 | # page = await ctx.new_page()
104 | # stealth_config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
105 | # await stealth_async(page, stealth_config)
106 | # _ = await page.goto("https://tiktok.com")
107 | # x, y = random.randint(0, 50), random.randint(0, 50)
108 | # a, b = random.randint(1, 50), random.randint(100, 200)
109 | #
110 | # await page.mouse.move(x, y)
111 | # await page.wait_for_load_state("networkidle")
112 | # await page.mouse.move(a, b)
113 | #
114 | # time.sleep(2)
115 | # _ = await page.locator("div[data-e2e=\"explore-item\"]").first.click()
116 | # input("waiting for enter")
117 |
118 | @pytest.mark.asyncio
119 | async def test_launch_browser_with_nodriver():
120 | ctx = await make_nodriver_solver(
121 | os.environ["API_KEY"],
122 | headless=False,
123 | user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
124 | proxy=proxy["server"]
125 | )
126 | page = await ctx.get("https://tiktok.com")
127 | input("waiting for enter")
128 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/test_playwrightsolver.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import time
3 | import os
4 |
5 | from playwright.sync_api import Page, sync_playwright, expect
6 | from playwright_stealth import stealth_sync, StealthConfig
7 |
8 | from tiktok_captcha_solver.captchatype import CaptchaType
9 |
10 | from ..playwrightsolver import PlaywrightSolver
11 |
12 |
13 | def open_tiktkok_login(page: Page) -> None:
14 | page.goto("https://www.tiktok.com/login/phone-or-email/email")
15 | time.sleep(10)
16 | write_username = page.locator('xpath=//input[contains(@name,"username")]')
17 | write_username.type(os.environ["TIKTOK_USERNAME"]);
18 | time.sleep(2);
19 | # write_password = page.locator('xpath=//input[contains(@type,"password")]')
20 | write_password = page.get_by_placeholder('Password')
21 | write_password.type(os.environ["TIKTOK_PASSWORD"]);
22 | time.sleep(2)
23 | login_btn = page.locator('//button[contains(@data-e2e,"login-button")]').click();
24 | time.sleep(8)
25 |
26 | def open_tiktok_search(page: Page) -> None:
27 | search_query = "davidteather"
28 | page.goto(f"https://www.tiktok.com/search/user?q={search_query}&t=1715558822399")
29 |
30 | def test_solve_at_scroll(caplog) -> None:
31 | caplog.set_level(logging.DEBUG)
32 | with sync_playwright() as p:
33 | browser = p.chromium.launch(headless=False)
34 | page = browser.new_page()
35 | config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
36 | # stealth_sync(page, config)
37 | page.goto("https://www.tiktok.com/")
38 | input("Trigger a captcha...")
39 | sadcaptcha = PlaywrightSolver(page, os.environ["API_KEY"])
40 | sadcaptcha.solve_captcha_if_present(captcha_detect_timeout=1)
41 |
42 | def test_does_not_false_positive(caplog):
43 | caplog.set_level(logging.DEBUG)
44 | with sync_playwright() as p:
45 | browser = p.chromium.launch(headless=False)
46 | page = browser.new_page()
47 | config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
48 | stealth_sync(page, config)
49 | page.goto("https://www.tiktok.com/login/phone-or-email/email")
50 | sadcaptcha = PlaywrightSolver(page, os.environ["API_KEY"])
51 | assert sadcaptcha.captcha_is_present(timeout=5) == False
52 |
53 | def test_shapes_v2_is_detected(caplog):
54 | caplog.set_level(logging.DEBUG)
55 | with sync_playwright() as p:
56 | browser = p.chromium.launch(headless=False)
57 | page = browser.new_page()
58 | page.goto("file:///home/gregb/ToughdataLLC/SadCaptcha/tiktok-captcha-solver/src/tiktok_captcha_solver/tests/new_shapes.html")
59 | sadcaptcha = PlaywrightSolver(page, os.environ["API_KEY"])
60 | assert sadcaptcha.identify_captcha() == CaptchaType.SHAPES_V2
61 |
62 | def test_rotate_v2_is_detected(caplog):
63 | caplog.set_level(logging.DEBUG)
64 | with sync_playwright() as p:
65 | browser = p.chromium.launch(headless=False)
66 | page = browser.new_page()
67 | page.goto("file:///home/gregb/ToughdataLLC/SadCaptcha/tiktok-captcha-solver/src/tiktok_captcha_solver/tests/new_rotate.html")
68 | sadcaptcha = PlaywrightSolver(page, os.environ["API_KEY"])
69 | assert sadcaptcha.identify_captcha() == CaptchaType.ROTATE_V2
70 |
71 |
72 | def test_puzzle_v2_is_detected(caplog):
73 | caplog.set_level(logging.DEBUG)
74 | with sync_playwright() as p:
75 | browser = p.chromium.launch(headless=False)
76 | page = browser.new_page()
77 | page.goto("file:///home/gregb/ToughdataLLC/SadCaptcha/tiktok-captcha-solver/src/tiktok_captcha_solver/tests/new_puzzle.html")
78 | sadcaptcha = PlaywrightSolver(page, os.environ["API_KEY"])
79 | assert sadcaptcha.identify_captcha() == CaptchaType.PUZZLE_V2
80 |
81 | def test_solve_captcha_at_login(caplog):
82 | caplog.set_level(logging.DEBUG)
83 | with sync_playwright() as p:
84 | browser = p.chromium.launch(headless=False)
85 | page = browser.new_page()
86 | config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
87 | # stealth_sync(page, config)
88 | open_tiktkok_login(page)
89 | sadcaptcha = PlaywrightSolver(page, os.environ["API_KEY"])
90 | sadcaptcha.solve_captcha_if_present()
91 | page.locator("css=#header-more-menu-icon").is_visible()
92 | expect(page.locator("css=#header-more-menu-icon")).to_be_visible(timeout=30000)
93 |
94 | # def test_solve_captcha_at_login_with_proxy(caplog):
95 | # caplog.set_level(logging.DEBUG)
96 | # with sync_playwright() as p:
97 | # browser = p.chromium.launch(headless=False)
98 | # page = browser.new_page()
99 | # config = StealthConfig(navigator_languages=False, navigator_vendor=False, navigator_user_agent=False)
100 | # stealth_sync(page, config)
101 | # open_tiktkok_login(page)
102 | # sadcaptcha = PlaywrightSolver(page, os.environ["API_KEY"], proxy=os.environ["PROXY"])
103 | # sadcaptcha.solve_captcha_if_present()
104 | # page.locator("css=#header-more-menu-icon").is_visible()
105 | # expect(page.locator("css=#header-more-menu-icon")).to_be_visible(timeout=30000)
106 | #
107 | # def test_solve_captcha_at_search(caplog):
108 | # caplog.set_level(logging.DEBUG)
109 | # with sync_playwright() as p:
110 | # browser = p.chromium.launch(headless=False)
111 | # page = browser.new_page()
112 | # open_tiktok_search(page)
113 | # sadcaptcha = PlaywrightSolver(page, os.environ["API_KEY"])
114 | # sadcaptcha.solve_captcha_if_present()
115 |
116 | # def test_detect_douyin(caplog):
117 | # caplog.set_level(logging.DEBUG)
118 | # with sync_playwright() as p:
119 | # browser = p.chromium.launch(headless=False)
120 | # page = browser.new_page()
121 | # page.goto("https://www.douyin.com")
122 | # sadcaptcha = PlaywrightSolver(page, os.environ["API_KEY"])
123 | # assert sadcaptcha.page_is_douyin()
124 | #
125 | # def test_solve_douyin_puzzle(caplog):
126 | # caplog.set_level(logging.DEBUG)
127 | # with sync_playwright() as p:
128 | # browser = p.chromium.launch(headless=False)
129 | # page = browser.new_page()
130 | # page.goto("https://www.douyin.com")
131 | # page.goto("https://www.douyin.com/discover")
132 | # sadcaptcha = PlaywrightSolver(page, os.environ["API_KEY"])
133 | # sadcaptcha.solve_captcha_if_present()
134 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/test_seleniumsolver.py:
--------------------------------------------------------------------------------
1 | import random
2 | import time
3 | import logging
4 | import os
5 |
6 | from selenium.webdriver.chrome.service import Service
7 | from selenium.webdriver.chrome.webdriver import WebDriver
8 | from webdriver_manager.chrome import ChromeDriverManager
9 | from selenium import webdriver
10 | from selenium.webdriver.common.by import By
11 | import undetected_chromedriver as uc
12 |
13 | from tiktok_captcha_solver.captchatype import CaptchaType
14 |
15 | from ..seleniumsolver import SeleniumSolver
16 |
17 |
18 |
19 | def make_driver() -> uc.Chrome:
20 | options = uc.ChromeOptions()
21 | # options.add_argument('--proxy-server=http://pr.oxylabs.io:7777')
22 | options.add_argument('--ignore-certificate-errors')
23 | options.binary_location = "/usr/bin/google-chrome-stable"
24 | return uc.Chrome(
25 | service=ChromeDriverManager().install(),
26 | headless=False,
27 | use_subprocess=False,
28 | options=options,
29 | browser_executable_path="/usr/bin/google-chrome-stable"
30 | )
31 |
32 | def make_driver_normal() -> WebDriver:
33 | options = webdriver.ChromeOptions()
34 | # options.add_argument('--proxy-server=http://pr.oxylabs.io:7777')
35 | options.add_argument('--ignore-certificate-errors')
36 | options.binary_location = "/usr/bin/google-chrome-stable"
37 | options.headless = False
38 | return webdriver.Chrome(
39 | service=Service(ChromeDriverManager().install()),
40 | options=options,
41 | )
42 |
43 |
44 | ### TODO
45 | # Make unit tests for static HTML files.
46 | # TikTok is way too finicky and testing is annoying!
47 |
48 | def open_tiktkok_login(driver: uc.Chrome) -> None:
49 | driver.get("https://www.tiktok.com/login/phone-or-email/email")
50 | time.sleep(10)
51 | write_username = driver.find_element(By.XPATH, '//input[contains(@name,"username")]');
52 | write_username.click()
53 | for char in os.environ["TIKTOK_USERNAME"]:
54 | write_username.send_keys(char)
55 | time.sleep(0.001)
56 | # time.sleep(random.random())
57 |
58 | time.sleep(2);
59 | write_password = driver.find_element(By.XPATH, '//input[contains(@type,"password")]');
60 | write_password.click()
61 | for char in os.environ["TIKTOK_PASSWORD"]:
62 | write_password.send_keys(char)
63 | time.sleep(0.001)
64 | # time.sleep(random.random())
65 | time.sleep(2)
66 | login_btn = driver.find_element(By.XPATH, '//button[contains(@data-e2e,"login-button")]').click();
67 | time.sleep(8)
68 |
69 | def open_tiktkok_search(driver: uc.Chrome) -> None:
70 | search_query = "davidteather"
71 | driver.get(f"https://www.tiktok.com/@therock")
72 |
73 | def test_solve_at_scroll(caplog) -> None:
74 | caplog.set_level(logging.DEBUG)
75 | driver = make_driver_normal()
76 | driver.get("https://www.tiktok.com/")
77 | input("Trigger a captcha...")
78 | sadcaptcha = SeleniumSolver(driver, os.environ["API_KEY"], mouse_step_size=2)
79 | with open("src/tiktok_captcha_solver/tests/puzzle_video_search.html", "w") as f:
80 | f.write(driver.page_source)
81 | sadcaptcha.solve_captcha_if_present()
82 |
83 | # def test_does_not_false_positive():
84 | # driver = make_driver()
85 | # try:
86 | # driver.get("https://www.tiktok.com/login/phone-or-email/email")
87 | # sadcaptcha = SeleniumSolver(driver, os.environ["API_KEY"])
88 | # assert sadcaptcha.captcha_is_present(timeout=5) == False
89 | # finally:
90 | # driver.quit()
91 | #
92 |
93 | # def test_shapes_v2_is_detected():
94 | # driver = make_driver()
95 | # try:
96 | # driver.get("file:///home/gregb/ToughdataLLC/SadCaptcha/tiktok-captcha-solver/src/tiktok_captcha_solver/tests/new_shapes.html")
97 | # sadcaptcha = SeleniumSolver(driver, os.environ["API_KEY"])
98 | # assert sadcaptcha.identify_captcha() == CaptchaType.SHAPES_V2
99 | # finally:
100 | # driver.quit()
101 | #
102 | #
103 | # def test_rotate_v2_is_detected():
104 | # driver = make_driver()
105 | # try:
106 | # driver.get("file:///home/gregb/ToughdataLLC/SadCaptcha/tiktok-captcha-solver/src/tiktok_captcha_solver/tests/new_rotate.html")
107 | # sadcaptcha = SeleniumSolver(driver, os.environ["API_KEY"])
108 | # assert sadcaptcha.identify_captcha() == CaptchaType.ROTATE_V2
109 | # finally:
110 | # driver.quit()
111 | #
112 | # def test_puzzle_v2_is_detected():
113 | # driver = make_driver()
114 | # try:
115 | # driver.get("file:///home/gregb/ToughdataLLC/SadCaptcha/tiktok-captcha-solver/src/tiktok_captcha_solver/tests/new_puzzle.html")
116 | # sadcaptcha = SeleniumSolver(driver, os.environ["API_KEY"])
117 | # assert sadcaptcha.identify_captcha() == CaptchaType.PUZZLE_V2
118 | # finally:
119 | # driver.quit()
120 | #
121 | # def test_solve_captcha_at_login(caplog):
122 | # caplog.set_level(logging.DEBUG)
123 | # driver = make_driver()
124 | # try:
125 | # open_tiktkok_login(driver)
126 | # sadcaptcha = SeleniumSolver(driver, os.environ["API_KEY"])
127 | # sadcaptcha.solve_captcha_if_present()
128 | # time.sleep(3)
129 | # assert not sadcaptcha.captcha_is_present()
130 | # finally:
131 | # driver.quit()
132 | #
133 | # # def test_solve_captcha_at_login_with_proxy(caplog):
134 | # # caplog.set_level(logging.DEBUG)
135 | # # driver = make_driver()
136 | # # try:
137 | # # open_tiktkok_login(driver)
138 | # # sadcaptcha = SeleniumSolver(driver, os.environ["API_KEY"], proxy=os.environ["PROXY"])
139 | # # sadcaptcha.solve_captcha_if_present()
140 | # # finally:
141 | # # driver.quit()
142 | #
143 | # def test_solve_captcha_at_search(caplog):
144 | # caplog.set_level(logging.DEBUG)
145 | # driver = make_driver_normal()
146 | # open_tiktkok_search(driver)
147 | # sadcaptcha = SeleniumSolver(driver, os.environ["API_KEY"])
148 | # sadcaptcha.solve_captcha_if_present()
149 | # driver.quit()
150 | #
151 | # def test_detect_douyin(caplog):
152 | # caplog.set_level(logging.DEBUG)
153 | # driver = make_driver()
154 | # try:
155 | # driver.get("https://www.douyin.com")
156 | # sadcaptcha = SeleniumSolver(driver, os.environ["API_KEY"])
157 | # assert sadcaptcha.page_is_douyin()
158 | # finally:
159 | # driver.quit()
160 | #
161 | # def test_solve_douyin_puzzle(caplog):
162 | # caplog.set_level(logging.DEBUG)
163 | # driver = webdriver.Chrome(options)
164 | # try:
165 | # driver.get("https://www.douyin.com/discover")
166 | # time.sleep(5)
167 | # sadcaptcha = SeleniumSolver(driver, os.environ["API_KEY"])
168 | # sadcaptcha.solve_captcha_if_present()
169 | # finally:
170 | # driver.quit()
171 |
--------------------------------------------------------------------------------
/src/tiktok_captcha_solver/tests/unknown_challenge.html:
--------------------------------------------------------------------------------
1 |
Select 2 objects that are the same shape
2 |
--------------------------------------------------------------------------------
/test.html:
--------------------------------------------------------------------------------
1 | TikTok recently made some changes to their frontend UI, and this has been causing some users to experience crashes while running the TikTok Captcha Solver Python Client. If you use the Python client, please update to the latest version at your earliest convenience.
2 |
3 |
4 | The command to update to the latest version is pip install tiktok-captcha-solver --upgrade
5 |
--------------------------------------------------------------------------------
/test.jpg:
--------------------------------------------------------------------------------
1 | TikTok
--------------------------------------------------------------------------------
/tiktok_captcha_solver.egg-info/PKG-INFO:
--------------------------------------------------------------------------------
1 | Metadata-Version: 2.1
2 | Name: tiktok-captcha-solver
3 | Version: 0.0.1
4 | Summary: This package integrates with Selenium to solve any TikTok captcha in one line of code.
5 | Author-email: Toughdata LLC
6 | Project-URL: Homepage, https://www.sadcaptcha.com
7 | Project-URL: Source, https://github.com/gbiz123/tiktok-captcha-solver/
8 | Keywords: tiktok,captcha,solver,selenium,rotate,puzzle,3d
9 | Classifier: Intended Audience :: Developers
10 | Classifier: Topic :: Software Development :: Build Tools
11 | Classifier: Programming Language :: Python :: 3.10
12 | Classifier: Programming Language :: Python :: 3.11
13 | Classifier: Programming Language :: Python :: 3.12
14 | Classifier: Programming Language :: Python :: 3 :: Only
15 | Requires-Python: >=3.10
16 | Description-Content-Type: text/markdown
17 | Requires-Dist: selenium
18 | Requires-Dist: webdriver-manager
19 | Requires-Dist: pydantic
20 | Requires-Dist: requests
21 | Requires-Dist: pytest
22 | Requires-Dist: undetected_chromedriver
23 |
24 | # TikTok Captcha Solver API
25 | This project is the [SadCaptcha TikTok Captcha Solver](https://www.sadcaptcha.com?ref=ghclientrepo) API client.
26 | The purpose is to make integrating SadCaptcha into your selenium app as simple as one line of code.
27 |
28 | ## Requirements
29 | - Python >= 3.10
30 | - Selenium properly installed and in `PATH`
31 |
32 | ## Installation
33 | This project can be installed with `pip`. Just run the following command:
34 | ```
35 | pip install tiktok-captcha-solver
36 | ```
37 |
38 | ## Selenium client
39 | Import the package, set up the SadCaptcha class, and call it whenever you need.
40 | This turns the entire captcha detection, solution, retry, and verification process into a single line of code.
41 | It is the recommended method if you are using Selenium.
42 |
43 | ```py
44 | from tiktok_captcha_solver import SadCaptcha
45 | import undetected_chromedriver as uc
46 |
47 | driver = uc.Chrome(headless=False)
48 | api_key = "YOUR_API_KEY_HERE"
49 | sadcaptcha = SadCaptcha(driver, api_key)
50 |
51 | # Selenium code that causes a TikTok captcha...
52 |
53 | sadcaptcha.solve_captcha_if_present()
54 | ```
55 |
56 | That's it!
57 |
58 | ## API Client
59 | If you are not using Selenium, you can still import and use the API client to help you make calls to SadCaptcha
60 | ```py
61 | from tiktok_captcha_solver import ApiClient
62 |
63 | api_key = "YOUR_API_KEY_HERE"
64 | client = ApiClient(api_key)
65 |
66 | # Rotate
67 | res = client.rotate("base64 encoded outer", "base64 encoded inner")
68 |
69 | # Puzzle
70 | res = client.puzzle("base64 encoded puzzle", "base64 encoded piece")
71 |
72 | # Shapes
73 | res = client.shapes("base64 encoded shapes image")
74 | ```
75 |
76 | ## Contact
77 | - Homepage: https://www.sadcaptcha.com/
78 | - Email: info@toughdata.net
79 | - Telegram @toughdata
80 |
--------------------------------------------------------------------------------
/tiktok_captcha_solver.egg-info/SOURCES.txt:
--------------------------------------------------------------------------------
1 | README.md
2 | pyproject.toml
3 | tiktok_captcha_solver/__init__.py
4 | tiktok_captcha_solver/api.py
5 | tiktok_captcha_solver/downloader.py
6 | tiktok_captcha_solver/models.py
7 | tiktok_captcha_solver/sadcaptcha.py
8 | tiktok_captcha_solver.egg-info/PKG-INFO
9 | tiktok_captcha_solver.egg-info/SOURCES.txt
10 | tiktok_captcha_solver.egg-info/dependency_links.txt
11 | tiktok_captcha_solver.egg-info/requires.txt
12 | tiktok_captcha_solver.egg-info/top_level.txt
13 | tiktok_captcha_solver/tests/__init__.py
14 | tiktok_captcha_solver/tests/test_api.py
15 | tiktok_captcha_solver/tests/test_downloader.py
16 | tiktok_captcha_solver/tests/test_sadcaptcha.py
--------------------------------------------------------------------------------
/tiktok_captcha_solver.egg-info/dependency_links.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tiktok_captcha_solver.egg-info/requires.txt:
--------------------------------------------------------------------------------
1 | selenium
2 | webdriver-manager
3 | pydantic
4 | requests
5 | pytest
6 | undetected_chromedriver
7 |
--------------------------------------------------------------------------------
/tiktok_captcha_solver.egg-info/top_level.txt:
--------------------------------------------------------------------------------
1 | tiktok_captcha_solver
2 |
--------------------------------------------------------------------------------