Repository: haziq-exe/TikTokAutoUploader Branch: master Commit: c409405b7f97 Files: 11 Total size: 79.7 KB Directory structure: gitextract_2zvyzhdu/ ├── .gitignore ├── AGENT.md ├── Documentation.md ├── LICENSE.md ├── README.md ├── TelegramAutomation/ │ ├── Fancy_Upload.py │ └── README.md └── tiktokautouploader/ ├── Js_assets/ │ ├── login.js │ └── package.json ├── __init__.py └── function.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /.DS_Store /build /dist /tiktokautouploader.egg-info .history ================================================ FILE: AGENT.md ================================================ # AGENT.md — tiktokautouploader This file provides context for AI coding agents working on this repository. --- ## Project Overview `tiktokautouploader` is a Python library that automates uploading and scheduling videos to TikTok via browser automation. It uses [Phantomwright](https://pypi.org/project/phantomwright/) (a patched Playwright fork) as its browser engine for bot-detection evasion, and calls into Node.js/JavaScript assets for the initial login flow. **PyPI package:** `tiktokautouploader` **Primary entry point:** `tiktokautouploader/function.py` → `upload_tiktok()` **Public API surface:** `tiktokautouploader/__init__.py` (exports only `upload_tiktok`) --- ## Repository Structure ``` tiktokautouploader/ ├── __init__.py # Exports upload_tiktok ├── function.py # Core upload logic — all Python automation lives here └── Js_assets/ ├── login.js # Node.js script: opens browser for first-time login & saves cookies └── package.json # JS deps: playwright, playwright-extra, puppeteer-extra-plugin-stealth TelegramAutomation/ ├── Fancy_Upload.py # Standalone community script: folder-based uploads with Telegram bot control └── README.md README.md DOCUMENTATION.md # Full parameter reference for upload_tiktok() ``` --- ## Setup & Installation ### Python dependencies ```bash pip install tiktokautouploader ``` All Python deps (`phantomwright`, `requests`, `Pillow`, `inference`) install automatically. ### Browser binaries (required once after install) ```bash phantomwright_driver install chromium ``` ### Node.js (required) Node.js must be installed and `npm` must be on PATH. The JS dependencies (`playwright`, `playwright-extra`, `puppeteer-extra-plugin-stealth`) self-install on first function call — no manual step needed. --- ## Key Behaviours Agents Must Know ### Cookie-based authentication - On **first use per account**, the library spawns a visible browser via `login.js` and prompts the user to log in manually. - Cookies are saved to `TK_cookies_.json` in the **current working directory**. - On subsequent runs, cookies are read from that file. Expired cookies trigger re-login automatically. - Never delete or move cookie files during a session. ### JS asset auto-install - `install_js_dependencies()` checks for `Js_assets/node_modules/` and runs `npm install` if absent. - This adds ~20–30 seconds on the very first run only. ### Captcha solving - Captchas are solved automatically via Roboflow inference (`inference_sdk`). - Two captcha types are supported (see `DOCUMENTATION.md`). - The solver downloads a temporary `captcha_image.jpg` to the working directory and removes it after solving. ### Error exits - The codebase uses `sys.exit()` extensively for hard failures (bad proxy, unsolvable captcha, upload timeout, etc.). - When adding new failure paths, prefer `sys.exit("DESCRIPTIVE MESSAGE")` over raising exceptions, to stay consistent with the existing pattern. --- ## Core Function Signature ```python upload_tiktok( video: str, # Path to video file description: str, # Caption text (no hashtags here) accountname: str, # Account name — determines which cookie file is used hashtags=None, # List of hashtag strings e.g. ['#fun', '#viral'] sound_name=None, # TikTok sound name to search for or find in favorites sound_aud_vol='mix', # 'mix' | 'main' | 'background' schedule=None, # 'HH:MM' in local time, minute must be multiple of 5 day=None, # Day-of-month integer (requires schedule) copyrightcheck=False, suppressprint=False, headless=True, stealth=False, # Extra delays on top of Phantomwright's always-on evasion proxy=None, # {'server': '...', 'username': '...', 'password': '...'} search_mode='search', # 'search' | 'favorites' ) -> str # Returns 'Completed' or 'Error' ``` --- ## Internal Architecture `function.py` is decomposed into focused private helpers — do not inline them back into `upload_tiktok()`: | Helper | Responsibility | |---|---| | `_load_or_create_cookies()` | Read cookies file or trigger login flow | | `_make_stealth_context()` | Launch Phantomwright browser + apply stealth | | `_goto_with_retry()` | Navigate with 2-attempt retry | | `_wait_for_upload_or_captcha()` | Poll until upload UI or captcha appears | | `_solve_captcha_if_needed()` | Roboflow-based captcha solver | | `_set_video_input()` | File input injection | | `_add_description_and_hashtags()` | Type description + click hashtag suggestions | | `_wait_for_upload_ready()` | Wait for TikTok to finish processing the video | | `_apply_schedule()` | Interact with date/time picker | | `_add_sound_from_upload_page()` | Open Sounds panel, search/select, adjust volume | | `_run_upload_copyright_check()` | Toggle copyright check and wait for result | | `_submit_upload()` | Click Post/Schedule and confirm success | Selector constants for all major UI elements are defined at the top of `function.py` as module-level strings. When TikTok's UI changes, update these constants first. --- ## CSS Selectors & Fragility The automation relies on TikTok Studio's DOM structure. These are the most likely breakage points: - `CAPTCHA_*` selectors — captcha UI changes frequently - `SCHEDULE_DAY_ICON_SELECTOR` / `SCHEDULE_TIME_ICON_SELECTOR` — SVG path-based selectors, will break on icon updates - `SOUND_VOLUME_ICON_*` selectors — base64 SVG `src` attributes, highly fragile - `DRAFT_EDIT_ICON_SELECTOR` — SVG path-based When fixing broken selectors, set `headless=False` to observe the live DOM and update the relevant constant at the top of `function.py`. --- ## Adding New Features - New upload options should be added as optional keyword arguments to `upload_tiktok()` with safe defaults. - New browser interaction steps belong in their own `_helper_function()` following the existing decomposition pattern. - If a feature requires JS, add it to `Js_assets/` and invoke via `subprocess.run(["node", ...])`. - Update `DOCUMENTATION.md` and the parameter table in `README.md` for any new parameters. --- ## Telegram Automation (`TelegramAutomation/`) `Fancy_Upload.py` is a **standalone community script**, not part of the library. It imports `upload_tiktok` from the installed package. Do not modify `function.py` to accommodate Telegram-specific behaviour — keep concerns separated. --- ## Do Not - Do not add `print()` statements outside the `suppressprint` guard (`if not suppressprint: print(...)`) - Do not change `sys.exit()` error handling to exceptions without updating all callers - Do not store state between calls — `upload_tiktok()` is stateless by design - Do not hardcode account names, cookie paths, or video paths in library code - Do not commit `TK_cookies_*.json` files — they contain session credentials ================================================ FILE: Documentation.md ================================================ # tiktokautouploader Documentation This document provides detailed information about the parameters and usage of the `upload_tiktok` function in the **tiktokautouploader** library. The function is designed to automate the process of uploading or scheduling videos to TikTok with additional features such as adding TikTok sounds, hashtags, and conducting copyright checks. ### Key Sections: - **Parameter Explanations**: Provides detailed descriptions of each parameter, including the valid options and their effects. - **Initialization Info**: Details instances that occur during first run of function - **Important Notes**: Highlights important account recommendations and limitations related to TikTok accounts and scheduling. - **Supported Captchas**: Showcases the Captchas the code is able to solve - **Runtime**: Provides an estimate of how much runtime is added by different parameters - **Example Usage**: Demonstrates a practical example of how to use the function. ## Function: `upload_tiktok` ### Parameters - **`video`** (str) - The input path for your video file that you want to upload to TikTok. - **`description`** (str) - The description text that will accompany the video when uploaded. hashtags included in description will NOT work, must be included in `hashtags` parameter - **`accountname`** (str) - The name of the account you want to post on. - **NOTE:** When uploading to an account for the FIRST TIME ONLY, you will be prompted to log-in, once you log-in your cookies will be stored and you will not need to log-in to that account again. Read INITIALIZATION section for more info. - **`hashtags`** (list of str, optional, default: None) - An array of hashtag strings (e.g., `['#example', '#fun']`) to be added to the video description. - **`sound_name`** (str, optional, default: None) - The name of the TikTok sound that you want to use for the video. This sound will be applied during the upload. - NOTE: please be specific with sound name (include sound creator name also if possible) - **`sound_aud_vol`** (str, optional, default: `'mix'`) - Determines the volume mix between the TikTok sound and the original video audio. Accepts one of the following options: - `'mix'`: The TikTok sound and original audio will have a 50/50 split. - `'background'`: The original audio will be louder, and the TikTok sound will be faintly heard in the background. - `'main'`: The TikTok sound will be louder, and the original audio will be faintly heard in the background. - Defaults to `'mix'` if invalid option chosen - **`schedule`** (str, optional, default: None) - The time you want the video to be uploaded. The format should be `HH:MM`, and the minute (`MM`) must be a multiple of 5. The scheduled time must be at least 15 minutes later than the current local time (unless scheduling for a different day). The time should be in your local time zone. - **`day`** (int, optional, default: None) (requires `schedule` != None) - If you want to schedule the video for a different day, this parameter specifies the day of the current month on which to upload the video. i.e: If current day is Sept 3rd, day=5 will upload video on Sept 5th - NOTE: You will also need to specify time of upload in `schedule` parameter or else `day` won't work **Important**: - You can only schedule a maximum of 240 hours (10 days) in advance. - If scheduling for the next month, you can only schedule within the first 2 days of the next month (as long as they are also within 10 days of the current date). i.e: If current day is Sept 30th, day=2 will upload on Oct 2nd, 4 WILL NOT WORK. - **`copyrightcheck`** (bool, optional, default: `False`) - If set to `True`, the function will conduct a copyright check on TikTok before uploading. If the check fails, the code execution will stop. - **`stealth`** (bool, optional, default: `False`) - If set to `True`, the function will wait a couple of seconds between each operation to make it harder for TikTok to detect automation use. - **`suppressprint`** (bool, optional, default: `False`) - Suppresses print messages that indicate the progress of the video upload. It is recommended to set this to `False` when first running the code to see progress and ensure everything works correctly. - **`headless`** (bool, optional, default: True) - Runs the code in headless mode, when set to `False` you can see the code execute in the browser, recommended to set this to `False` if code is not working as intended in order to more clearly see what the issue exactly is - **`proxy`** (dict, optional, default: None) - Allows user to run the code on a proxy server - Must be a dictionary with "server" key that has a string of proxy server IP address - Optionally can also include "username" and "password" keys for authentication - feature was contributed by KryvMykyta - **`search_mode`** (str, optional, default: `'search'`) - Determines how the function looks up the sound specified in `sound_name`. Accepts one of the following options: - `'search'`: Searches TikTok for the sound by name. This is the default behaviour. - `'favorites'`: Looks for the sound in your TikTok account's saved favorites instead of searching. - NOTE: `sound_name` must be provided for this parameter to have any effect. If using `'favorites'`, make sure the sound is actually saved to your account beforehand. ## Initialization Info - **During FIRST RUN:** - Javascript dependencies will be automatically downloaded, once downloaded it will not attempt to download it again unless the files get deleted. - Runtime might be a 20-30 seconds longer than usual, this is due to libraries being built. Runtime should return to normal after first run - **When uploading to an account for the FIRST TIME:** - You will be asked to log-in to TikTok, your cookies from your log-in will then be stored in a file called `TK_cookies_(youraccountname).json`. You will not need to log-in to that account again after that. ## Important Notes **VERY IMPORTANT: Use this tool at your own risk, as automated uploading may violate TikTok's Terms of Service** - **TikTok Account Recommendations**: - It is recommended to have a TikTok account with at least a few weeks of history built up for the best results. - **Scheduling Limitations**: - The function allows scheduling up to 240 hours (10 days) in advance. - If you need to schedule a video for the next month, the video can only be uploaded within the first 2 days of that month (as long as these days are also within 10 days from the current date). ## Supported Captchas: ### Captcha solver currently supports Captchas of type:

#### Note: - These GIFs are just to showcase the project's ability to auto-solve captcha's, this entire process will take place 'under the hood' (unless headless mode is set to `False`). ## Runtime: **Total runtime mostly depends on your WIFI connection, however, here are approximations on how much runtime is added by each parameter** - **Captcha's:** 3 - 10 secs - **Adding Sound:** 3 - 5 secs (```stealth=True``` adds around 8 seconds) - **Scheduling:** 2 - 3 secs (```stealth=True``` adds around 6 seconds) - **Copyright Check:** 2 - 5 secs (```stealth=True``` adds around 2 seconds) - All in all, runtime won't exceed 20 seconds in most cases (unless ```stealth=True```). - **NOTE:** When running for the FIRST TIME ONLY, it may take an extra 20 - 30 seconds at the beginning for the code to start running as JS libraries are being built ## Example Usage Here's a basic example of how to use the `upload_tiktok` function: ```python from tiktokautouploader import upload_tiktok upload_tiktok( video='path/to/your/video.mp4', description='Check out my latest video!', accountname= 'mytiktokaccount', hashtags=['#fun', '#viral'], sound_name='popular_sound', sound_aud_vol='mix', schedule='15:00', day=5, copyrightcheck=True, suppressprint=False ) ``` For more details or if errors persist, please feel free to contact me at haziqmk123@gmail.com or on LinkedIn (on my github profile) ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) [2024] [HAZIQ KHALID] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

tiktokautouploader

### AUTOMATE TIKTOK UPLOADS. USE TRENDING/FAVORITED SOUNDS, ADD WORKING HASHTAGS, SCHEDULE UPLOADS, AUTOSOLVES CAPTCHAS, AND MORE [![PyPI version](https://img.shields.io/pypi/v/tiktokautouploader.svg)](https://pypi.org/project/tiktokautouploader/) WORKING AS OF FEB 2026 (sound_aud_vol issues only, use default ```sound_aud_vol='mix'```)

## Features - **Bypass/Auto Solve Captchas:** Captchas won't slow you down, they get solved automatically. - **Upload with TikTok Sounds:** Add popular TikTok sounds to your videos. Search by name or pull straight from your favorites. - **Schedule Uploads:** Queue videos for a specific time, up to 10 days out. - **Copyright Check:** Run a copyright check before uploading so you're not caught off guard later. - **Add Working Hashtags:** Hashtags that are clickable and actually show up as hashtags instead of text. - **Proxy Support:** Route your uploads through a proxy server of your choice. - **Multiple Accounts:** Handle as many TikTok accounts as you need without losing track of any of them. - **Telegram Integration:** Hook the uploader up to a Telegram bot. Check `/TelegramAutomation` for setup details. - **Phantomwright Stealth Engine:** Bot detection evasion baked in at the browser level — fingerprint spoofing, human-like interactions, and hardened browser flags out of the box. --- ## Bot Detection Evasion with Phantomwright This library uses [**Phantomwright**](https://pypi.org/project/phantomwright/) as its browser engine instead of plain Playwright. Phantomwright is a patched, drop-in Playwright replacement specifically designed to evade bot detection. --- ## Installation ```bash pip install tiktokautouploader ``` > **Already installed?** Make sure you're on the latest version before running anything. --- ## Pre-requisites **Node.js** is required since parts of this package run JavaScript under the hood. Grab it from [nodejs.org](https://nodejs.org/) if you don't have it. The JS dependencies (`playwright`, `playwright-extra`, `puppeteer-extra-plugin-stealth`) install themselves the first time you run the function — just make sure `npm` is in your PATH. **Browser binaries** also need to be installed once (run after installing library): ```bash phantomwright_driver install chromium ``` --- ## Quick-Start > It's worth reading `DOCUMENTATION.md` before diving in. The first time you use the function for an account you'll be asked to log in — this only happens once per account. NOTE: The first time you run the function, it may take a long while to run as JS libraries are built, this only occurs on first run ### Upload with hashtags ```python from tiktokautouploader import upload_tiktok upload_tiktok( video='path/to/your/video.mp4', description='Check out my latest TikTok video!', accountname='mytiktokaccount', hashtags=['#fun', '#viral'] ) ``` ### Upload with a TikTok Sound ```python # Search for a sound by name (default behaviour) upload_tiktok(video=video_path, description=description, accountname=accountname, sound_name='trending_sound', sound_aud_vol='mix') # Pull a sound from your TikTok favorites instead upload_tiktok(video=video_path, description=description, accountname=accountname, sound_name='saved_sound', sound_aud_vol='mix', search_mode='favorites') ``` `sound_aud_vol` controls the balance between your video's original audio and the TikTok sound: `'main'`, `'mix'`, or `'background'`. Check the docs for details. ### Schedule an Upload ```python upload_tiktok(video=video_path, description=description, accountname=accountname, schedule='03:10', day=11) ``` ### Set a Custom Cover Image TikTok's "Upload cover" tab accepts an image but silently discards it server-side — the video always ends up with a random auto-selected frame as the cover regardless of what you upload there. The only reliable approach is TikTok's native cover editor, which lets you pick any frame from the video. The strategy: bake your desired cover image as the **last frame** of the MP4 at encode time, then pass `cover_image=` to `upload_tiktok()`. After the video uploads, the function opens the cover editor and drags the frame slider to the last frame before posting. **Step 1 — append your cover image as the last segment of the video:** ```bash # Encode the static image as a short clip ffmpeg -loop 1 -i cover.png -t 2.5 -vf "scale=1080:1920,setsar=1" \ -c:v libx264 -pix_fmt yuv420p cover_clip.mp4 # Concatenate it onto the end of your main video (concat demuxer) printf "file 'main.mp4'\nfile 'cover_clip.mp4'" > concat.txt ffmpeg -f concat -safe 0 -i concat.txt -c copy final.mp4 ``` **Step 2 — pass `cover_image=` when uploading:** ```python upload_tiktok( video='final.mp4', # last frame = your cover image description='My caption', accountname='myaccount', cover_image='cover.png', # triggers frame slider selection ) ``` `cover_image` defaults to `None` — fully backward compatible. If the cover editor can't be found or the drag fails for any reason, the upload proceeds normally without a custom cover. ### Copyright Check Before Uploading ```python upload_tiktok(video=video_path, description=description, accountname=accountname, hashtags=hashtags, copyrightcheck=True) ``` > The upload will stop if your video fails the copyright check. ### Run Headless with Stealth + Proxy ```python upload_tiktok( video=video_path, description=description, accountname=accountname, headless=True, # no browser window stealth=True, # additional human-like delays on top of baseline evasion suppressprint=True, # no console output proxy={ # optional proxy config — see docs for format 'server': 'http://yourproxy:port', 'username': 'user', 'password': 'pass' } ) ``` --- ## Full Parameter Reference | Parameter | Type | Description | |---|---|---| | `video` | `str` | Path to the video file | | `description` | `str` | Caption for the video | | `accountname` | `str` | Which account to upload on | | `cover_image` | `str` *(opt)* | Path to a PNG/JPG to use as the cover. Must already be baked into the last frame of the video — see above. | | `hashtags` | `list` *(opt)* | List of hashtags to include | | `sound_name` | `str` *(opt)* | Name of the TikTok sound to use | | `sound_aud_vol` | `str` *(opt)* | Audio balance: `'main'`, `'mix'`, or `'background'` | | `schedule` | `str` *(opt)* | Upload time in `HH:MM` (your local time) | | `day` | `int` *(opt)* | Day to schedule the upload for | | `copyrightcheck` | `bool` *(opt)* | Run a copyright check before uploading | | `suppressprint` | `bool` *(opt)* | Silence all progress output from the function | | `headless` | `bool` *(opt)* | Run without a visible browser window | | `stealth` | `bool` *(opt)* | Add extra delays between operations on top of the always-on Phantomwright evasion | | `proxy` | `dict` *(opt)* | Proxy server config — see docs for the expected format | | `search_mode` | `str` *(opt)* | How to find the sound: `'search'` (default) or `'favorites'` | --- ## Dependencies `phantomwright`, `requests`, `Pillow`, `inference` — all installed automatically with the package. --- ================================================ FILE: TelegramAutomation/Fancy_Upload.py ================================================ import os import time import shutil import random import telegram from telegram import Update from telegram.ext import Application, CommandHandler, CallbackContext import threading from tiktokautouploader import upload_tiktok # Path configuration path = r'C:\Path\To\All\Videos\Here' left = os.path.join(path, '!') # "! Folder" will store uploaded videos to keep things organized if not os.path.exists(left): os.makedirs(left) # Create the folder if it doesn't exist # Fetching and sorting video files vids = sorted([f for f in os.listdir(path) if f.endswith('.mp4')], key=lambda f: int(f.split('.')[0])) # Sort videos in ascending numerical order # Video details desc = 'Description here' tags = ['#fyp', '#viral', '#other Tags here'] acct = 'Account name here' # Telegram bot details bot_token = 'Bot token here' group_id = # Group ID here bot = telegram.Bot(token=bot_token) # Global variables for tracking upload state last_vid = "" # Last uploaded video last_acc = "" # Account used for the last upload upld_inp = False # Whether an upload is in progress curr_vid = None # Current video being uploaded nxt_upld = 0 # Timestamp for the next upload upld_strk = 0 # Streak of successful uploads skip_timer = False # Flag to skip the timer last_upload_time = None # Timestamp of the last upload # Send a message to the group using the bot def send_msg(message: str): bot.send_message(chat_id=group_id, text=message) # Calculate the remaining time for the next upload def time_left(): wait_time = max(nxt_upld - time.time(), 0) if wait_time > 0: h = int(wait_time // 3600) m = int((wait_time % 3600) // 60) s = int(wait_time % 60) return f"{h}h {m}m {s}s" return "Ready for next upload" # Calculate time since the last upload def timeafterupload(): if last_upload_time: elapsed_time = time.time() - last_upload_time h = int(elapsed_time // 3600) m = int((elapsed_time % 3600) // 60) s = int(elapsed_time % 60) return f"{h}h {m}m {s}s" return "N/A" # Upload status message def update_stats(): vids_left = len([f for f in os.listdir(path) if f.endswith('.mp4')]) # Count remaining videos stats_msg = "📊 Upload Status\n" stats_msg += "===========================\n" stats_msg += f"🎥 Currently Uploading: {curr_vid if upld_inp else 'N/A'}\n" stats_msg += f"📅 Last Upload Time: {time.strftime('%Y-%m-%d, %I:%M:%S %p', time.localtime(last_upload_time)) if last_upload_time else 'N/A'}\n" stats_msg += f"📅 Next Upload Time: {time.strftime('%Y-%m-%d, %I:%M:%S %p', time.localtime(nxt_upld)) if nxt_upld else 'N/A'}\n" stats_msg += f"⏳ Next Upload In: {time_left() if not upld_inp else 'Uploading now'}\n" stats_msg += f"⏳ Uploaded Since: {timeafterupload()}\n" stats_msg += f"📂 Videos Left: {vids_left}\n" stats_msg += f"🔄 Last Uploaded Video: {last_vid if last_vid else 'N/A'}\n" stats_msg += f"🚀 Uploaded To: {last_acc if last_acc else 'N/A'}\n" stats_msg += f"🔥 Upload Streak: {upld_strk}\n" stats_msg += "===========================\n" return stats_msg # Telegram command: /status async def status_cmd(update: Update, context: CallbackContext): stats_msg = update_stats() await context.bot.send_message(chat_id=update.effective_chat.id, text=stats_msg, parse_mode='HTML') # Console timer for next upload def console_timer(): global skip_timer while True: if skip_timer: # Stop the timer if skip is triggered skip_timer = False break wait_time = max(nxt_upld - time.time(), 0) if wait_time > 0: h = int(wait_time // 3600) m = int((wait_time % 3600) // 60) s = int(wait_time % 60) print(f"Next upload in: {h}h {m}m {s}s", end='\r') time.sleep(1) else: print("Ready for next upload", end='\r') break # Telegram command: /skip async def skip_cmd(update: Update, context: CallbackContext): global skip_timer, nxt_upld skip_timer = True nxt_upld = time.time() # Immediately trigger next upload print("\n------------------\nTimer skipped!\n------------------") message = "📢 Timer Skipped!\n" message += "===========================\n" message += "⏩ Starting next upload...\n" message += "===========================\n" await context.bot.send_message(chat_id=update.effective_chat.id, text=message, parse_mode='HTML') threading.Thread(target=console_timer).start() # Upload video def upld_vid(video): global last_vid, last_acc, upld_inp, curr_vid, nxt_upld, upld_strk, skip_timer, last_upload_time try: upld_inp, curr_vid = True, video vidz = os.path.join(path, video) upload_tiktok(video=vidz, description=desc, accountname=acct, hashtags=tags, sound_name='Swimming', sound_aud_vol='background') print(f"Uploaded {video}\n===========================") last_vid, last_acc, upld_strk = video, acct, upld_strk + 1 shutil.move(vidz, os.path.join(left, video)) # Move video to "!" folder after upload # Randomize the next upload time between 6 and 9 hours to avoid rate limits h, m, s = random.randint(6, 9), random.randint(0, 59), random.randint(0, 59) nxt_upld = time.time() + h * 3600 + m * 60 + s last_upload_time = time.time() threading.Thread(target=console_timer).start() except Exception as e: send_msg(f"Error {video}: {str(e)}") upld_strk = 0 # Reset streak on failure finally: skip_timer = False upld_inp, curr_vid = False, None # Upload all videos in the folder def upld_all(): global skip_timer, nxt_upld for vid in vids: if skip_timer: nxt_upld = time.time() upld_vid(vid) while time.time() < nxt_upld: if skip_timer: # Skip timer if triggered nxt_upld = time.time() break time.sleep(1) # Start the Telegram bot def start_bot(): application = Application.builder().token(bot_token).build() application.add_handler(CommandHandler('status', status_cmd)) # Show current upload status application.add_handler(CommandHandler('stats', status_cmd)) # Alias for /status application.add_handler(CommandHandler('skip', skip_cmd)) # Skip timer for immediate upload application.run_polling() # Main function to start upload and bot def main(): upload_thread = threading.Thread(target=upld_all) upload_thread.start() start_bot() if __name__ == '__main__': main() ================================================ FILE: TelegramAutomation/README.md ================================================ # Telegram Automation This section of the repository contains a standalone script, `Fancy_Upload.py`, which extends the functionality of the `tiktokautouploader` library. The script integrates additional automation features, particularly designed for Telegram-based control. **Please note that `Fancy_Upload.py` is not part of the main `tiktokautouploader` library.** Code written by: t3k-vtx ## Features The `Fancy_Upload.py` script provides the following functionalities: 1. **Folder Uploads:** - Uploads videos from a designated folder to TikTok in ascending order based on file names. 2. **Integration with Telegram Bot:** - Command-based status updates via `/status` or `/stats`. - Timer skipping functionality with the `/skip` command. - Notifications for errors or upload status updates sent to a Telegram group. 3. **Randomized Timer Intervals:** - Ensures uploads occur at randomized intervals between 6 to 9 hours to avoid TikTok rate limits. 4. **Post-Upload Management:** - Moves successfully uploaded videos to a separate folder to keep the upload folder organized. ## How to Use 1. **Clone the Repository:** ```bash git clone https://github.com/TikTokAutoUploader.git ``` 2. **Navigate to the `TelegramAutomation` Folder:** ```bash cd TelegramAutomation ``` 3. **Install Dependencies:** - Ensure you have the `tiktokautouploader` library installed. - Install any additional libraries listed in the `Fancy_Upload.py` script. 4. **Configure Telegram Bot:** - Set up a Telegram bot and obtain the bot token. - Update the `Fancy_Upload.py` script with your Telegram bot token and group chat ID. 5. **Run the Script:** ```bash python Fancy_Upload.py ``` ## Important Notes - **Standalone Script:** `Fancy_Upload.py` is not integrated into the `tiktokautouploader` library. It is a standalone script that utilizes the library's features. - **Customization Required:** Some configurations, such as folder paths and Telegram bot credentials, need to be updated in the script to match your setup. - **Community Contribution:** This script was contributed by a community member (t3k-vtx) and is provided as-is. For issues or suggestions, please raise an issue in the repository. ## Contact For further assistance, feel free to reach out via the repository's Issues section or contact t3k-vtx directly. ================================================ FILE: tiktokautouploader/Js_assets/login.js ================================================ const { chromium } = require('playwright-extra') const fs = require('fs'); const stealth = require('puppeteer-extra-plugin-stealth')() chromium.use(stealth) function sleep(time) { return new Promise(resolve => setTimeout(resolve, time)); } async function checkForRedirect(page) { const currentUrl = page.url(); const pattern = /^https:\/\/www\.tiktok\.com\/foryou/; return pattern.test(currentUrl); } (async () => { const args = process.argv.slice(2); let proxy = null; for (let i = 0; i < args.length; i++) { if (args[i] === '--proxy' && args[i + 1]) { try { proxy = JSON.parse(args[i + 1].replace(/'/g, '"')); // Parse the proxy string as JSON } catch (error) { console.error('Failed to parse proxy argument:', error); } } } const browserOptions = { headless: false, }; if (proxy && proxy.server) { browserOptions.proxy = { server: proxy.server, username: proxy.username || undefined, password: proxy.password || undefined, } } let redirected = false; const browser = await chromium.launch(browserOptions); const page = await browser.newPage(); await page.goto('https://www.tiktok.com/login'); while (!redirected) { redirected = await checkForRedirect(page); if (!redirected) { await sleep(1000); } } sleep(2000) const cookies = await page.context().cookies(); fs.writeFileSync('TK_cookies.json', JSON.stringify(cookies, null, 2)); await browser.close(); })(); ================================================ FILE: tiktokautouploader/Js_assets/package.json ================================================ { "name": "tiktokautouploader-assets", "version": "1.0.0", "scripts": { "postinstall": "npx playwright install chromium" }, "dependencies": { "playwright": "^1.48.2", "playwright-extra": "^4.3.6", "puppeteer-extra-plugin-stealth": "^2.11.2" } } ================================================ FILE: tiktokautouploader/__init__.py ================================================ from .function import upload_tiktok, TikTokUploadError __all__ = ['upload_tiktok', 'TikTokUploadError'] ================================================ FILE: tiktokautouploader/function.py ================================================ from phantomwright.sync_api import sync_playwright from phantomwright.stealth import Stealth from phantomwright.user_simulator import SyncUserSimulator import json import time import subprocess from inference_sdk import InferenceHTTPClient import pkg_resources import requests from PIL import Image import os import warnings warnings.simplefilter("ignore") class TikTokUploadError(RuntimeError): """Raised when a TikTok upload fails or cannot be confirmed. Replaces sys.exit() so callers can catch upload failures without SystemExit propagating to the host process (critical in async and multi-threaded environments). """ pass UPLOAD_URL = "https://www.tiktok.com/tiktokstudio/upload?from=upload&lang=en" DRAFT_URL = "https://www.tiktok.com/tiktokstudio/content?tab=draft" CONTENT_URL = "https://www.tiktok.com/tiktokstudio/content" CAPTCHA_QUESTION_SELECTOR = "div.VerifyBar___StyledDiv-sc-12zaxoy-0.hRJhHT" CAPTCHA_IMAGE_SELECTOR = "img#captcha-verify-image" CAPTCHA_REFRESH_SELECTOR = "span.secsdk_captcha_refresh--text" CAPTCHA_SUCCESS_SELECTOR = "div.captcha_verify_message.captcha_verify_message-pass" CAPTCHA_FAIL_SELECTOR = "div.captcha_verify_message.captcha_verify_message-fail" CAPTCHA_SUBMIT_SELECTOR = "div.verify-captcha-submit-button" SCHEDULE_DAY_ICON_SELECTOR = 'div.TUXTextInputCore-leadingIconWrapper:has(svg > path[d="M15 3a1 1 0 0 0-1 1v3h-1.4c-3.36 0-5.04 0-6.32.65a6 6 0 0 0-2.63 2.63C3 11.56 3 13.24 3 16.6v16.8c0 3.36 0 5.04.65 6.32a6 6 0 0 0 2.63 2.63c1.28.65 2.96.65 6.32.65h22.8c3.36 0 5.04 0 6.32-.65a6 6 0 0 0 2.63-2.63c.65-1.28.65-2.96.65-6.32V16.6c0-3.36 0-5.04-.65-6.32a6 6 0 0 0-2.63-2.63C40.44 7 38.76 7 35.4 7H34V4a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v3H18V4a1 1 0 0 0-1-1h-2Zm-2.4 8H14v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3h12v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3h1.4c1.75 0 2.82 0 3.62.07a5.11 5.11 0 0 1 .86.14h.03a2 2 0 0 1 .88.91 5.11 5.11 0 0 1 .14.86c.07.8.07 1.87.07 3.62v1.9H7v-1.9c0-1.75 0-2.82.07-3.62a5.12 5.12 0 0 1 .14-.86v-.03a2 2 0 0 1 .88-.87l.03-.01a5.11 5.11 0 0 1 .86-.14c.8-.07 1.87-.07 3.62-.07ZM7 22.5h34v10.9c0 1.75 0 2.82-.07 3.62a5.11 5.11 0 0 1-.14.86v.03a2 2 0 0 1-.88.87l-.03.01a5.11 5.11 0 0 1-.86.14c-.8.07-1.87.07-3.62.07H12.6c-1.75 0-2.82 0-3.62-.07a5.11 5.11 0 0 1-.89-.15 2 2 0 0 1-.87-.87l-.01-.03a5.12 5.12 0 0 1-.14-.86C7 36.22 7 35.15 7 33.4V22.5Z"])' SCHEDULE_TIME_ICON_SELECTOR = 'div.TUXTextInputCore-leadingIconWrapper:has(svg > path[d="M24 2a22 22 0 1 0 0 44 22 22 0 0 0 0-44ZM6 24a18 18 0 1 1 36 0 18 18 0 0 1-36 0Z"])' SOUND_VOLUME_ICON_WAIT_SELECTOR = 'img[src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMSAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgNy41MDE2QzAgNi42NzMxNyAwLjY3MTU3MyA2LjAwMTYgMS41IDYuMDAxNkgzLjU3NzA5QzMuODY4MDUgNi4wMDE2IDQuMTQ0NTggNS44NzQ4OCA0LjMzNDU1IDUuNjU0NDlMOC43NDI1NSAwLjU0MDUyQzkuMzQ3OCAtMC4xNjE2NjggMTAuNSAwLjI2NjM3NCAxMC41IDEuMTkzNDFWMTguOTY3MkMxMC41IDE5Ljg3NDUgOS4zODg5NCAyMC4zMTI5IDguNzY5NDIgMTkuNjVMNC4zMzE3OSAxNC45MDIxQzQuMTQyNjkgMTQuNjk5OCAzLjg3ODE2IDE0LjU4NDkgMy42MDEyMiAxNC41ODQ5SDEuNUMwLjY3MTU3MyAxNC41ODQ5IDAgMTMuOTEzNCAwIDEzLjA4NDlWNy41MDE2Wk01Ljg0OTQ1IDYuOTYwMjdDNS4yNzk1NiA3LjYyMTQzIDQuNDQ5OTcgOC4wMDE2IDMuNTc3MDkgOC4wMDE2SDJWMTIuNTg0OUgzLjYwMTIyQzQuNDMyMDMgMTIuNTg0OSA1LjIyNTY0IDEyLjkyOTUgNS43OTI5NSAxMy41MzY0TDguNSAxNi40MzI4VjMuODg1MjJMNS44NDk0NSA2Ljk2MDI3WiIgZmlsbD0iIzE2MTgyMyIgZmlsbC1vcGFjaXR5PSIwLjYiLz4KPHBhdGggZD0iTTEzLjUxNSA3LjE5MTE5QzEzLjM0MjQgNi45NzU1OSAxMy4zMzk5IDYuNjYwNTYgMTMuNTM1MiA2LjQ2NTNMMTQuMjQyMyA1Ljc1ODE5QzE0LjQzNzYgNS41NjI5MyAxNC43NTU4IDUuNTYxNzUgMTQuOTM1NiA1Ljc3MTM2QzE2Ljk5NTkgOC4xNzM2MiAxNi45OTU5IDExLjgyOCAxNC45MzU2IDE0LjIzMDNDMTQuNzU1OCAxNC40Mzk5IDE0LjQzNzYgMTQuNDM4NyAxNC4yNDIzIDE0LjI0MzVMMTMuNTM1MiAxMy41MzY0QzEzLjMzOTkgMTMuMzQxMSAxMy4zNDI0IDEzLjAyNjEgMTMuNTE1IDEyLjgxMDVDMTQuODEzIDExLjE4ODUgMTQuODEzIDguODEzMTIgMTMuNTE1IDcuMTkxMTlaIiBmaWxsPSIjMTYxODIzIiBmaWxsLW9wYWNpdHk9IjAuNiIvPgo8cGF0aCBkPSJNMTYuNzE3MiAxNi43MTgzQzE2LjUyMTkgMTYuNTIzMSAxNi41MjMxIDE2LjIwNzQgMTYuNzA3MiAxNi4wMDE3QzE5LjcyNTcgMTIuNjMgMTkuNzI1NyA3LjM3MTY4IDE2LjcwNzIgNC4wMDAwMUMxNi41MjMxIDMuNzk0MjcgMTYuNTIxOSAzLjQ3ODU4IDE2LjcxNzIgMy4yODMzMkwxNy40MjQzIDIuNTc2MjFDMTcuNjE5NSAyLjM4MDk1IDE3LjkzNyAyLjM4MDIgMTguMTIzMyAyLjU4NDA4QzIxLjkwOTkgNi43MjkyNiAyMS45MDk5IDEzLjI3MjQgMTguMTIzMyAxNy40MTc2QzE3LjkzNyAxNy42MjE1IDE3LjYxOTUgMTcuNjIwNyAxNy40MjQzIDE3LjQyNTVMMTYuNzE3MiAxNi43MTgzWiIgZmlsbD0iIzE2MTgyMyIgZmlsbC1vcGFjaXR5PSIwLjYiLz4KPC9zdmc+Cg=="]' SOUND_VOLUME_ICON_CLICK_UPLOAD_SELECTOR = 'img[src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMSAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgNy41MDE2QzAgNi42NzMxNyAwLjY3MTU3MyA2LjAwMTYgMS41IDYuMDAxNkgzLjU3NzA5QzMuODY4MDUgNi4wMDE2IDQuMTQ0NTggNS44NzQ4OCA0LjMzNDU1IDUuNjU0NDlMOC43NDI1NSAwLjU0MDUyQzkuMzQ3OCAtMC4xNjE2NjggMTAuNSAwLjI2NjM3NCAxMC41IDEuMTkzNDFWMTguOTY3MkMxMC41IDE5Ljg3NDUgOS4zODg5NCAyMC4zMTI5IDguNzY5NDIgMTkuNjVMNC4zMzE3OSAxNC45MDIxQzQuMTQyNjkgMTQuNjk5OCAzLjg3ODE2IDE0LjU4NDkgMy42MDEyMiAxNC41ODQ5SDEuNUMwLjY3MTU3MyAxNC41ODQ5IDAgMTMuOTEzNCAwIDEzLjA4NDlWNy41MDE2Wk01Ljg0OTQ1IDYuOTYwMjdDNS4yNzk1NiA3LjYyMTQzIDQuNDQ5OTcgOC4wMDE2IDMuNTc3MDkgOC4wMDE2SDJWMTIuNTg0OUgzLjYwMTIyQzQuNDMyMDMgMTIuNTg0OSA1LjIyNTY0IDEyLjkyOTUgNS43OTI5NSAxMy41MzY0TDguNSAxNi4wMDAxIDEzLjI3MjQgMTguMTIzMyAxNy40MTc2QzE3LjkzNyAxNy42MjE1IDE3LjYxOTUgMTcuNjIwNyAxNy40MjQzIDE3LjQyNTVMMTYuNzE3MiAxNi43MTgzWiIgZmlsbD0iIzE2MTgyMyIgZmlsbC1vcGFjaXR5PSIwLjYiLz4KPC9zdmc+Cg=="]' SOUND_VOLUME_ICON_CLICK_DRAFT_SELECTOR = 'img[src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMSAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTAgNy41MDE2QzAgNi42NzMxNyAwLjY3MTU3MyA2LjAwMTYgMS41IDYuMDAxNkgzLjU3NzA5QzMuODY4MDUgNi4wMDE2IDQuMTQ0NTggNS44NzQ4OCA0LjMzNDU1IDUuNjU0NDlMOC43NDI1NSAwLjU0MDUyQzkuMzQ3OCAtMC4xNjE2NjggMTAuNSAwLjI2NjM3NCAxMC41IDEuMTkzNDFWMTguOTY3MkMxMC41IDE5Ljg3NDUgOS4zODg5NCAyMC4zMTI5IDguNzY5NDIgMTkuNjVMNC4zMzE3OSAxNC45MDIxQzQuMTQyNjkgMTQuNjk5OCAzLjg3ODE2IDE0LjU4NDkgMy42MDEyMiAxNC41ODQ5SDEuNUMwLjY3MTU3MyAxNC41ODQ5IDAgMTMuOTEzNCAwIDEzLjA4NDlWNy41MDE2Wk01Ljk0OTQ1IDYuOTYwMjdDNS4yNzk1NiA3LjYyMTQzIDQuNDQ5OTcgOC4wMDE2IDMuNTc3MDkgOC4wMDE2SDJWMTIuNTg0OUgzLjYwMTIyQzQuNDMyMDMgMTIuNTg0OSA1LjIyNTY0IDEyLjkyOTUgNS43OTI5NSAxMy41MzY0TDguNSAxNi40MzI4VjMuODg1MjJMNS44NDk0NSA2Ljk2MDI3WiIgZmlsbD0iIzE2MTgyMyIgZmlsbC1vcGFjaXR5PSIwLjYiLz4KPC9zdmc+Cg=="]' DRAFT_EDIT_ICON_SELECTOR = "path[d='M37.37 4.85a4.01 4.01 0 0 0-.99-.79 3 3 0 0 0-2.72 0c-.45.23-.81.6-1 .79a9 9 0 0 1-.04.05l-19.3 19.3c-1.64 1.63-2.53 2.52-3.35 3.47a36 36 0 0 0-4.32 6.16c-.6 1.1-1.14 2.24-2.11 4.33l-.3.6c-.4.75-.84 1.61-.8 2.43a2.5 2.5 0 0 0 2.37 2.36c.82.05 1.68-.4 2.44-.79l.59-.3c2.09-.97 3.23-1.5 4.33-2.11a36 36 0 0 0 6.16-4.32c.95-.82 1.84-1.71 3.47-3.34l19.3-19.3.05-.06a3 3 0 0 0 .78-3.71c-.22-.45-.6-.81-.78-1l-.02-.02-.03-.03-3.67-3.67a8.7 8.7 0 0 1-.06-.05ZM16.2 26.97 35.02 8.15l2.83 2.83L19.03 29.8c-1.7 1.7-2.5 2.5-3.33 3.21a32 32 0 0 1-7.65 4.93 32 32 0 0 1 4.93-7.65c.73-.82 1.51-1.61 3.22-3.32Z']" def check_for_updates(): current_version = pkg_resources.get_distribution("tiktokautouploader").version response = requests.get("https://pypi.org/pypi/tiktokautouploader/json") if response.status_code == 200: latest_version = response.json()["info"]["version"] if current_version != latest_version: print( f"WARNING: You are using version {current_version} of tiktokautouploader, " f"PLEASE UPDATE TO LATEST VERSION {latest_version} FOR BEST EXPERIENCE." ) def login_warning(accountname): print(f"NO COOKIES FILE FOUND FOR ACCOUNT {accountname}, PLEASE LOG-IN TO {accountname} WHEN PROMPTED") def save_cookies(cookies): with open("TK_cookies.json", "w") as file: json.dump(cookies, file, indent=4) def check_expiry(accountname): with open(f"TK_cookies_{accountname}.json", "r") as file: cookies = json.load(file) current_time = int(time.time()) cookies_expire = [] expired = False for cookie in cookies: if cookie["name"] in ["sessionid", "sid_tt", "sessionid_ss", "passport_auth_status"]: expiry = cookie.get("expires") if not expiry: expiry = cookie.get("expirationDate") cookies_expire.append(expiry < current_time) if all(cookies_expire): expired = True return expired def run_javascript(proxy_data=None): js_file_path = pkg_resources.resource_filename(__name__, "Js_assets/login.js") proxy_argument = str(proxy_data) if proxy_data is not None else str({}) try: result = subprocess.run( ["node", js_file_path, "--proxy", proxy_argument], capture_output=True, text=True, ) except Exception as e: raise TikTokUploadError(f"Error while running the JavaScript file, when trying to parse cookies: {e}") return result def install_js_dependencies(): js_dir = pkg_resources.resource_filename(__name__, "Js_assets") node_modules_path = os.path.join(js_dir, "node_modules") if not os.path.exists(node_modules_path): print("JavaScript dependencies not found. Installing...") try: subprocess.run(["npm", "install", "--silent"], cwd=js_dir, check=True) except Exception as e: print("An error occurred during npm installation.") print(f"Error details: {e}") print("Trying to install JavaScript dependencies with shell...") try: subprocess.run(["npm", "install", "--silent"], cwd=js_dir, check=True, shell=True) except Exception as e: print("An error occurred during shell npm installation.") print(f"Error details: {e}") else: time.sleep(0.1) def read_cookies(cookies_path): cookie_read = False try: with open(cookies_path, "r") as cookiefile: cookies = json.load(cookiefile) for cookie in cookies: if cookie.get("sameSite") not in ["Strict", "Lax", "None"]: cookie["sameSite"] = "Lax" cookie_read = True except Exception: raise TikTokUploadError("ERROR: CANT READ COOKIES FILE") return cookies, cookie_read def detect_redirect(page): redirect_detected = False def on_response(response): nonlocal redirect_detected if response.request.redirected_from: redirect_detected = True page.on("response", on_response) return redirect_detected def understood_Qs(question): understood_terms = { "touchdowns": "football", "orange and round": "basketball", "used in hoops": "basketball", "has strings": "guitar", "oval and inflatable": "football", "strumming": "guitar", "bounces": "basketball", "musical instrument": "guitar", "laces": "football", "bands": "guitar", "leather": "football", "leaves": "tree", "pages": "book", "throwing": "football", "tossed in a spiral": "football", "spiky crown": "pineapple", "pigskin": "football", "photography": "camera", "lens": "camera", "grow": "tree", "captures images": "camera", "keeps doctors": "apple", "crown": "pineapple", "driven": "car", } for key in understood_terms.keys(): if key in question: item = understood_terms.get(key) return item return "N.A" def get_image_src(page): image_url = page.get_attribute(CAPTCHA_IMAGE_SELECTOR, "src") return image_url def download_image(image_url): response = requests.get(image_url) image_path = "captcha_image.jpg" with open(image_path, "wb") as f: f.write(response.content) return image_path def run_inference_on_image_tougher(image_path, object): rk = "kyHFbAWkOWfGz8fSEw8O" client = InferenceHTTPClient( api_url="https://detect.roboflow.com", api_key=f"{rk}", ) results = client.infer(image_path, model_id="captcha-2-6ehbe/2") class_names = [] bounding_boxes = [] for obj in results["predictions"]: class_names.append(obj["class"]) bounding_boxes.append( { "x": obj["x"], "y": obj["y"], "width": obj["width"], "height": obj["height"], } ) bounding_box = [] class_to_click = object for i, classes in enumerate(class_names): if classes == class_to_click: bounding_box.append(bounding_boxes[i]) return bounding_box def run_inference_on_image(image_path): rk = "kyHFbAWkOWfGz8fSEw8O" client = InferenceHTTPClient( api_url="https://detect.roboflow.com", api_key=f"{rk}", ) results = client.infer(image_path, model_id="tk-3nwi9/2") class_names = [] bounding_boxes = [] for obj in results["predictions"]: class_names.append(obj["class"]) bounding_boxes.append( { "x": obj["x"], "y": obj["y"], "width": obj["width"], "height": obj["height"], } ) already_written = [] bounding_box = [] class_to_click = [] for i, detected_class in enumerate(class_names): if detected_class in already_written: class_to_click.append(detected_class) bounding_box.append(bounding_boxes[i]) index = already_written.index(detected_class) bounding_box.append(bounding_boxes[index]) already_written.append(detected_class) found = False if len(class_to_click) == 1: found = True return bounding_box, found def convert_to_webpage_coordinates( bounding_boxes, image_x, image_y, image_height_web, image_width_web, image_height_real, image_width_real, ): webpage_coordinates = [] for box in bounding_boxes: x_box = box["x"] y_box = box["y"] rel_x = (x_box * image_width_web) / image_width_real rel_y = (y_box * image_height_web) / image_height_real x_cord = image_x + rel_x y_cord = image_y + rel_y webpage_coordinates.append((x_cord, y_cord)) return webpage_coordinates def click_on_objects(page, object_coords): for (x, y) in object_coords: page.mouse.click(x, y) time.sleep(0.5) def validate_proxy(proxy): if not proxy: return if not isinstance(proxy, dict): raise ValueError("Proxy must be a dictionary.") if "server" not in proxy or not isinstance(proxy["server"], str): raise ValueError("Proxy must contain a 'server' key with a string value.") try: proxies = { "http": f'http://{proxy["server"]}/', "https": f'https://{proxy["server"]}/', } if proxy.get("username"): proxies = { "http": f'http://{proxy.get("username")}:{proxy.get("password")}@{proxy["server"]}/', "https": f'https://{proxy.get("username")}:{proxy.get("password")}@{proxy["server"]}/', } response = requests.get("https://www.google.com", proxies=proxies) if response.status_code == 200: print("Proxy is valid!") else: raise ValueError(f"Proxy test failed with status code: {response.status_code}") except Exception as e: raise ValueError(f"Invalid proxy configuration when trying to simple request: {e}") def _make_stealth_context(p, headless, proxy): stealth = Stealth( navigator_languages_override=("en-US", "en"), ) browser = p.chromium.launch( headless=headless, proxy=proxy, args=[ "--disable-blink-features=AutomationControlled", "--no-sandbox", "--disable-infobars", "--disable-dev-shm-usage", ], ) context = browser.new_context( viewport={"width": 1280, "height": 900}, user_agent=( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0.0.0 Safari/537.36" ), locale="en-US", timezone_id="America/New_York", ) stealth.apply_stealth_sync(context) return browser, context def select_sound_from_favorites(page, sound_name, sim=None, stealth=False, suppressprint=False): """ Selects a sound from the favorites tab by searching through the list. Returns True if sound was found and selected, False otherwise. """ try: if stealth: time.sleep(1) try: page.click('button:has-text("Favorites")') except Exception: try: page.click("button#favourite") except Exception: page.click("div.TUXTabBar-item#favourite button") time.sleep(1) if stealth: time.sleep(1) page.wait_for_selector('div[class*="MusicPanelMusicItem__content"]', timeout=50000) time.sleep(2.5) music_cards = page.locator('div[class*="MusicPanelMusicItem__content"]') card_count = music_cards.count() if not suppressprint: print(f"Found {card_count} favorite sounds, searching for '{sound_name}'...") keywords = sound_name.split() keywords_lower = [kw.lower() for kw in keywords if kw.strip()] if not suppressprint and len(keywords_lower) > 1: print(f"Searching for sounds containing all keywords: {keywords_lower}") found = False for i in range(card_count): try: card = music_cards.nth(i) title_element = card.locator('div[class*="MusicPanelMusicItem__infoBasicTitle"]') title_text = title_element.inner_text() if title_element.count() > 0 else "" other_element = card.locator('div[class*="MusicPanelMusicItem__infoBasicDesc"]') other_text = other_element.inner_text() if other_element.count() > 0 else "" combined_text = f"{title_text} {other_text}".strip().lower() all_keywords_match = all(kw in combined_text for kw in keywords_lower) if all_keywords_match and combined_text: display_title = title_text if title_text else "Unknown" if not suppressprint: print(f"Found matching sound: '{display_title} {other_text}'") if stealth: time.sleep(0.5) if sim: sim.prepare_for_interaction(card) time.sleep(0.3) sim.click(card) else: card.hover() time.sleep(0.3) card.click() card.locator("button").last.click() if stealth: time.sleep(1) found = True break except Exception: continue return found except Exception as e: if not suppressprint: print(f"Error in favorites search: {e}") return False def select_sound_from_search(page, sound_name, sim=None, stealth=False): """ Selects a sound using the search functionality (original behavior). Uses SyncUserSimulator for human-like typing when available. """ search_box = page.get_by_placeholder("Search sounds") if sim: sim.click(search_box) sim.type(search_box, sound_name) else: search_box.click() page.keyboard.type(sound_name) time.sleep(0.2) if stealth: time.sleep(2) page.keyboard.press("Enter") try: page.wait_for_selector("div[class*='MusicPanelMusicItem__operation']") if stealth: time.sleep(0.5) page.locator("div[class*='MusicPanelMusicItem__operation']").first.click() if stealth: time.sleep(1) return True except Exception: return False def _cookie_file(accountname): return f"TK_cookies_{accountname}.json" def _load_or_create_cookies(accountname, proxy): cookie_read = False cookies_path = _cookie_file(accountname) if os.path.exists(cookies_path): cookies, cookie_read = read_cookies(cookies_path=cookies_path) expired = check_expiry(accountname=accountname) if expired: os.remove(cookies_path) print(f"COOKIES EXPIRED FOR ACCOUNT {accountname}, PLEASE LOG-IN AGAIN") cookie_read = False if not cookie_read: install_js_dependencies() login_warning(accountname=accountname) run_javascript(proxy_data=proxy) os.rename("TK_cookies.json", cookies_path) cookies, cookie_read = read_cookies(cookies_path) if not cookie_read: raise TikTokUploadError("ERROR READING COOKIES") return cookies def _goto_with_retry(page, url): retries = 0 while retries < 2: try: page.goto(url, timeout=30000) except Exception: retries += 1 time.sleep(5) if retries == 2: raise TikTokUploadError("ERROR: TIK TOK PAGE FAILED TO LOAD, try again.") else: break def _wait_for_upload_or_captcha(page): detected = False captcha = False while not detected: if page.locator(".upload-text-container").is_visible(): detected = True else: if page.locator(CAPTCHA_QUESTION_SELECTOR).is_visible(): detected = True captcha = True else: time.sleep(0.1) return captcha def _solve_captcha_if_needed(page, suppressprint): image = get_image_src(page) if not image: return if not suppressprint: print("CAPTCHA DETECTED, Attempting to solve") solved = False attempts = 0 old_question = "N.A" question = page.locator(CAPTCHA_QUESTION_SELECTOR).text_content() while not solved: attempts += 1 start_time = time.time() while question == old_question: question = page.locator(CAPTCHA_QUESTION_SELECTOR).text_content() if time.time() - start_time > 2: break if "Select 2 objects that are the same" in question or "Select two objects that are the same" in question: found = False while not found: page.click(CAPTCHA_REFRESH_SELECTOR) image = get_image_src(page) img_path = download_image(image) b_box, found = run_inference_on_image(image_path=img_path) with Image.open(img_path) as img: image_size = img.size imageweb = page.locator("#captcha-verify-image") imageweb.wait_for() box = imageweb.bounding_box() image_x = box["x"] image_y = box["y"] image_height_web = box["height"] image_width_web = box["width"] image_width_real, image_height_real = image_size webpage_coords = convert_to_webpage_coordinates( b_box, image_x, image_y, image_height_web, image_width_web, image_height_real, image_width_real, ) if not webpage_coords: webpage_coords.append((image_x + 50, image_y + 50)) click_on_objects(page, webpage_coords) page.click(CAPTCHA_SUBMIT_SELECTOR) time.sleep(0.5) if attempts > 5: raise TikTokUploadError("FAILED TO SOLVE CAPTCHA") showedup = False while not showedup: if page.locator(CAPTCHA_SUCCESS_SELECTOR).is_visible(): solved = True showedup = True os.remove("captcha_image.jpg") if page.locator(CAPTCHA_FAIL_SELECTOR).is_visible(): showedup = True old_question = question page.click(CAPTCHA_REFRESH_SELECTOR) else: objectclick = understood_Qs(question) while objectclick == "N.A": old_question = question page.click(CAPTCHA_REFRESH_SELECTOR) start_time = time.time() runs = 0 while question == old_question: runs += 1 question = page.locator(CAPTCHA_QUESTION_SELECTOR).text_content() if runs > 1: time.sleep(1) if time.time() - start_time > 2: break objectclick = understood_Qs(question) image = get_image_src(page) img_path = download_image(image) b_box = run_inference_on_image_tougher(image_path=img_path, object=objectclick) with Image.open(img_path) as img: image_size = img.size imageweb = page.locator("#captcha-verify-image") imageweb.wait_for() box = imageweb.bounding_box() image_x = box["x"] image_y = box["y"] image_height_web = box["height"] image_width_web = box["width"] image_width_real, image_height_real = image_size webpage_coords = convert_to_webpage_coordinates( b_box, image_x, image_y, image_height_web, image_width_web, image_height_real, image_width_real, ) if not webpage_coords: webpage_coords.append((image_x + 50, image_y + 50)) click_on_objects(page, webpage_coords) page.click(CAPTCHA_SUBMIT_SELECTOR) time.sleep(1) if attempts > 20: raise TikTokUploadError("FAILED TO SOLVE CAPTCHA") showedup = False while not showedup: if page.locator(CAPTCHA_SUCCESS_SELECTOR).is_visible(): solved = True showedup = True os.remove("captcha_image.jpg") if not suppressprint: print("CAPTCHA SOLVED") if page.locator(CAPTCHA_FAIL_SELECTOR).is_visible(): showedup = True old_question = question page.click(CAPTCHA_REFRESH_SELECTOR) def _set_video_input(page, video): try: page.set_input_files('input[type="file"][accept="video/*"]', f"{video}") except Exception: raise TikTokUploadError( "ERROR: FAILED TO INPUT FILE. Possible Issues: Wifi too slow, file directory wrong, or check documentation to see if captcha is solvable" ) def _add_description_and_hashtags(page, sim, video, description, hashtags, stealth, suppressprint): page.wait_for_selector('div[data-contents="true"]') time.sleep(0.5) if page.locator("button:has-text('Cancel')").is_visible(): print("Tutorial pop-up detected, dismissing...") page.click("button:has-text('Cancel')") if page.locator("button:has-text('Got it')").is_visible(): page.click("button:has-text('Got it')") desc_box = page.locator('div[data-contents="true"]') sim.click(desc_box) if not suppressprint: print( "Entered File, waiting for tiktok to load onto their server, this may take a couple of minutes, depending on your video length" ) time.sleep(0.5) if description is None: raise TikTokUploadError("ERROR: PLEASE INCLUDE A DESCRIPTION") for _ in range(len(video) + 2): page.keyboard.press("Backspace") page.keyboard.press("Delete") time.sleep(0.5) sim.type(desc_box, description) if hashtags is not None: for hashtag in hashtags: if hashtag[0] != "#": hashtag = "#" + hashtag page.keyboard.type(hashtag) time.sleep(0.5) try: if stealth: time.sleep(2) page.click(f'span.hash-tag-topic:has-text("{hashtag}")', timeout=1000) except Exception: try: page.click("span.hash-tag-topic", timeout=1000) except Exception: page.keyboard.press("Backspace") try: page.click("span.hash-tag-topic", timeout=1000) except Exception: if not suppressprint: print(f"Tik tok hashtag not working for {hashtag}, moving onto next") page.keyboard.type(f"{hashtag[-1]} ") if not suppressprint: print("Description and Hashtags added") def _wait_for_upload_ready(page): content_check_btn = page.locator( "div.common-modal-footer > button[data-type='neutral']", has_text="Cancel" ) if content_check_btn.is_visible(): content_check_btn.click() try: page.wait_for_selector('button:has-text("Post")[aria-disabled="false"]', timeout=12000000) except Exception: raise TikTokUploadError( "ERROR: TIK TOK TOOK TOO LONG TO UPLOAD YOUR FILE (>20min). Try again, if issue persists then try a lower file size or different wifi connection" ) def _validate_schedule_request(schedule, day): if (schedule is None) and (day is not None): raise TikTokUploadError( "ERROR: CANT SCHEDULE FOR ANOTHER DAY USING 'day' WITHOUT ALSO INCLUDING TIME OF UPLOAD WITH 'schedule'; PLEASE ALSO INCLUDE TIME WITH 'schedule' PARAMETER" ) def _normalize_schedule_and_day(schedule, day): # Backward-compatible normalization for callers that pass day number via # `schedule` and time string via `day` (e.g. schedule=25, day="12:05"). if isinstance(schedule, int) and isinstance(day, str) and ":" in day: return day, str(schedule) return schedule, day def _apply_schedule(page, schedule, day, stealth, suppressprint): if schedule is None: return try: hour = schedule[0:2] minute = schedule[3:] if (int(minute) % 5) != 0: raise TikTokUploadError( "MINUTE FORMAT ERROR: PLEASE MAKE SURE MINUTE YOU SCHEDULE AT IS A MULTIPLE OF 5 UNTIL 60 (i.e: 40), VIDEO SAVED AS DRAFT" ) except Exception: raise TikTokUploadError( "SCHEDULE TIME ERROR: PLEASE MAKE SURE YOUR SCHEDULE TIME IS A STRING THAT FOLLOWS THE 24H FORMAT 'HH:MM', VIDEO SAVED AS DRAFT" ) page.locator('label:has-text("Schedule")').click() if stealth: time.sleep(2) visible = False while not visible: if page.locator('button:has-text("Allow")').nth(0).is_visible(): if stealth: time.sleep(1) page.locator('button:has-text("Allow")').nth(0).click() visible = True time.sleep(0.1) else: if page.locator("div.TUXTextInputCore-trailingIconWrapper").nth(1).is_visible(): visible = True time.sleep(0.1) if day is not None: if stealth: time.sleep(1) page.locator(SCHEDULE_DAY_ICON_SELECTOR).click() time.sleep(0.2) try: if stealth: time.sleep(1) page.locator(f'span.day.valid:text-is("{day}")').click() except Exception: raise TikTokUploadError( "SCHEDULE DAY ERROR: ERROR WITH SCHEDULED DAY, read documentation for more information on format of day" ) try: time.sleep(0.2) page.locator(SCHEDULE_TIME_ICON_SELECTOR).click() time.sleep(0.2) page.locator( f'.tiktok-timepicker-option-text.tiktok-timepicker-right:text-is("{minute}")' ).scroll_into_view_if_needed() time.sleep(0.2) if stealth: time.sleep(2) page.locator( f'.tiktok-timepicker-option-text.tiktok-timepicker-right:text-is("{minute}")' ).click() time.sleep(0.2) if page.locator("div.tiktok-timepicker-time-picker-container").is_visible(): time.sleep(0.1) else: page.locator(SCHEDULE_TIME_ICON_SELECTOR).click() page.locator( f'.tiktok-timepicker-option-text.tiktok-timepicker-left:text-is("{hour}")' ).scroll_into_view_if_needed() if stealth: time.sleep(2) page.locator( f'.tiktok-timepicker-option-text.tiktok-timepicker-left:text-is("{hour}")' ).click() time.sleep(1) if not suppressprint: print("Done scheduling video") except Exception: raise TikTokUploadError("SCHEDULING ERROR: VIDEO SAVED AS DRAFT") def _adjust_sound_volume_upload(page, sound_aud_vol, stealth): page.wait_for_selector(SOUND_VOLUME_ICON_WAIT_SELECTOR) if stealth: time.sleep(1) page.click(SOUND_VOLUME_ICON_CLICK_UPLOAD_SELECTOR) time.sleep(0.5) sliders = page.locator("input.scaleInput") if sound_aud_vol == "background": slider2 = sliders.nth(1) bounding_box2 = slider2.bounding_box() if bounding_box2: x2 = bounding_box2["x"] + (bounding_box2["width"] * 0.07) y2 = bounding_box2["y"] + bounding_box2["height"] / 2 if stealth: time.sleep(1) page.mouse.click(x2, y2) if sound_aud_vol == "main": slider1 = sliders.nth(0) bounding_box1 = slider1.bounding_box() if bounding_box1: x1 = bounding_box1["x"] + (bounding_box1["width"] * 0.07) y1 = bounding_box1["y"] + bounding_box1["height"] / 2 if stealth: time.sleep(1) page.mouse.click(x1, y1) time.sleep(1) def _pick_sound(page, sound_name, sim, stealth, suppressprint, search_mode): sound_found = False if search_mode == "favorites": sound_found = select_sound_from_favorites( page, sound_name, sim=sim, stealth=stealth, suppressprint=suppressprint, ) else: sound_found = select_sound_from_search(page, sound_name, sim=sim, stealth=stealth) if not sound_found: raise TikTokUploadError(f"ERROR: SOUND '{sound_name}' NOT FOUND") def _add_sound_from_upload_page(page, sound_name, sound_aud_vol, sim, stealth, suppressprint, search_mode): sound_fail = False if sound_name is None: return sound_fail try: if stealth: time.sleep(2) sounds_btn = page.locator("button:has-text('Sounds')").last sim.click(sounds_btn) except Exception: sound_fail = True if sound_fail: return sound_fail time.sleep(1.5) _pick_sound(page, sound_name, sim, stealth, suppressprint, search_mode) if sound_aud_vol != "mix": try: _adjust_sound_volume_upload(page, sound_aud_vol, stealth) except Exception: raise TikTokUploadError("ERROR ADJUSTING SOUND VOLUME: please try again or use the default 'mix'.") page.wait_for_selector("button:has-text('Save')") if stealth: time.sleep(1) page.locator("button:has-text('Save')").first.click() if not suppressprint: print("Added sound") return sound_fail def _run_upload_copyright_check(page, stealth, suppressprint): copy_check_counter = 0 if stealth: time.sleep(1) page.locator('div[data-e2e="copyright_container"] span[data-part="thumb"]').click() while True: time.sleep(2) if page.get_by_text("No issues found.", exact=True).is_visible(): if not suppressprint: print("Copyright check complete") break if page.locator("span:has-text('Copyright issues detected')").is_visible(): raise TikTokUploadError("COPYRIGHT CHECK FAILED: VIDEO SAVED AS DRAFT, COPYRIGHT AUDIO DETECTED FROM TIKTOK") copy_check_counter += 1 if copy_check_counter > 10: print( "COPYRIGHT CHECK TIMEOUT: UNABLE TO CONFIRM IF VIDEO PASSED COPYRIGHT CHECK, CONTINUING TO UPLOAD IN 5 SECONDS." ) break def _submit_upload(page, schedule, stealth, suppressprint, post_success_wait, schedule_success_wait): try: if schedule is None: if stealth: time.sleep(1) try: page.click('button:has-text("Post")[data-e2e="post_video_button"]', timeout=2000) page.wait_for_url(url=CONTENT_URL, timeout=2000) except Exception: page.click('button:has-text("Post")[aria-disabled="false"]', timeout=2000) try: page.wait_for_url(url=CONTENT_URL, timeout=2000) except Exception: print( "POSSIBLE ERROR: Cannot confirm if uploaded successfully, Please check account in a minute or two to confirm" ) return "Error" uploaded = False checks = 0 while not uploaded: if page.locator(':has-text("Leaving the page does not interrupt")').nth(0).is_visible(): time.sleep(post_success_wait) break time.sleep(0.2) checks += 1 if checks == 25: break else: if stealth: time.sleep(1) page.click('button:has-text("Schedule")', timeout=10000) uploaded = False checks = 0 while not uploaded: if page.locator(':has-text("Leaving the page does not interrupt")').nth(0).is_visible(): time.sleep(schedule_success_wait) break time.sleep(0.2) checks += 1 if checks == 25: break if not suppressprint: print("Done uploading video, NOTE: it may take a minute or two to show on TikTok") except Exception: time.sleep(2) raise TikTokUploadError( "POSSIBLE ERROR UPLOADING: Cannot confirm if uploaded successfully, Please check account in a minute or two to confirm." ) time.sleep(1) page.close() return None def _select_cover_last_frame(page) -> bool: """ Open TikTok Studio's cover editor and drag the frame slider to the last frame. The caller must ensure the desired cover image is already the last frame of the MP4 (baked in at encode time). Returns True on success, False on failure (upload proceeds without custom cover). """ # Step 1: Open the cover editor modal try: edit_btn = page.locator('[data-e2e="cover_container"] div.edit-container') if not edit_btn.is_visible(timeout=5000): edit_btn = page.locator('div.edit-container:has-text("Edit cover")') if not edit_btn.is_visible(timeout=3000): return False edit_btn.click() except Exception: return False # Step 2: Wait for the frame slider try: page.wait_for_selector('div.drag-item', timeout=8000) time.sleep(1) except Exception: try: page.keyboard.press("Escape") except Exception: pass return False # Step 3: Drag the slider to the far right (last frame) try: drag_item = page.locator('div.drag-item') container = drag_item.locator('..') container_box = container.bounding_box() drag_box = drag_item.bounding_box() if not container_box or not drag_box: return False target_x = container_box['x'] + container_box['width'] - 4 current_x = drag_box['x'] + drag_box['width'] / 2 current_y = drag_box['y'] + drag_box['height'] / 2 page.mouse.move(current_x, current_y) page.mouse.down() # Move in steps — TikTok ignores instant jumps for i in range(1, 11): page.mouse.move(current_x + (target_x - current_x) * i / 10, current_y) time.sleep(0.05) page.mouse.up() time.sleep(1) except Exception: try: page.keyboard.press("Escape") except Exception: pass return False # Step 4: Confirm try: confirm_btn = page.locator('button:has-text("Confirm")').first confirm_btn.scroll_into_view_if_needed() time.sleep(0.3) try: confirm_btn.click(timeout=5000) except Exception: confirm_btn.evaluate("el => el.click()") time.sleep(1.5) # Wait for modal to close try: page.wait_for_selector('div.drag-item', state="hidden", timeout=5000) except Exception: pass return True except Exception: try: page.keyboard.press("Escape") except Exception: pass return False def upload_tiktok( video: str, description: str, accountname: str, *, cover_image=None, hashtags=None, sound_name=None, sound_aud_vol: str = "mix", schedule=None, day=None, copyrightcheck: bool = False, suppressprint: bool = False, headless: bool = True, stealth: bool = False, proxy=None, search_mode: str = "search", ) -> str: """ UPLOADS VIDEO TO TIKTOK (powered by Phantomwright for bot-detection evasion) -------------------------------------------------------------------------------- video (str) -> path to video to upload description (str) -> description for video accountname (str) -> account to upload on cover_image (str or Path, optional) -> Path to a PNG/JPG to use as the video cover. The image must already be baked into the last frame of the video (see notes below). When provided, the upload flow will open TikTok's cover editor and drag the frame slider to the last frame before posting. Note: TikTok's "Upload cover" tab silently discards uploaded images server-side. This parameter instead uses the native cover editor to select the last frame of the video, which reliably sticks. hashtags (str)(array)(opt) -> hashtags for video sound_name (str)(opt) -> name of tik tok sound to use for video sound_aud_vol (str)(opt) -> volume of tik tok sound, 'main', 'mix' or 'background' schedule (str)(opt) -> format HH:MM, your local time to upload video day (int)(opt) -> day to schedule video for copyrightcheck (bool)(opt) -> include copyright check or not suppressprint (bool)(opt) -> True means function doesnt print anything headless (bool)(opt) -> run in headless mode or not stealth (bool)(opt) -> will wait second(s) before each operation proxy (dict)(opt) -> proxy server to run code on search_mode (str)(opt) -> 'search' or 'favorites' """ try: check_for_updates() except Exception: time.sleep(0.1) try: validate_proxy(proxy) except Exception as e: raise TikTokUploadError(f"Error validating proxy: {e}") if accountname is None: raise TikTokUploadError("PLEASE ENTER NAME OF ACCOUNT TO POST ON, READ DOCUMENTATION FOR MORE INFO") cookies = _load_or_create_cookies(accountname, proxy) with sync_playwright() as p: _, context = _make_stealth_context(p, headless=headless, proxy=proxy) context.add_cookies(cookies) page = context.new_page() sim = SyncUserSimulator(page) if not suppressprint: print(f"Uploading to account '{accountname}'") _goto_with_retry(page, UPLOAD_URL) sim.simulate_browsing(duration_ms=1500) captcha = _wait_for_upload_or_captcha(page) if captcha: _solve_captcha_if_needed(page, suppressprint) _set_video_input(page, video) _add_description_and_hashtags(page, sim, video, description, hashtags, stealth, suppressprint) _wait_for_upload_ready(page) time.sleep(0.2) if not suppressprint: print("Tik tok done loading file onto servers") sim.simulate_browsing(duration_ms=1000) schedule, day = _normalize_schedule_and_day(schedule, day) _validate_schedule_request(schedule, day) _apply_schedule(page, schedule, day, stealth, suppressprint) sound_fail = _add_sound_from_upload_page( page, sound_name, sound_aud_vol, sim, stealth, suppressprint, search_mode, ) if not sound_fail: page.wait_for_selector('div[data-contents="true"]') if copyrightcheck: _run_upload_copyright_check(page, stealth, suppressprint) if cover_image: _select_cover_last_frame(page) time.sleep(0.5) result = _submit_upload( page, schedule, stealth, suppressprint, post_success_wait=0.1, schedule_success_wait=0.2, ) if result == "Error": return "Error" else: try: if stealth: time.sleep(1) page.click('button:has-text("Save draft")', timeout=10000) raise TikTokUploadError("ERROR ADDING SOUND: Video saved as draft, please try again or check documentation for more info") return "Error" except Exception: raise TikTokUploadError("ERROR ADDING SOUND; SAVE AS DRAFT BUTTON NOT FOUND SO VIDEO NOT ADDED AS DRAFT") return "Error" return "Completed"