Full Code of haziq-exe/TikTokAutoUploader for AI

master c409405b7f97 cached
11 files
79.7 KB
21.6k tokens
51 symbols
1 requests
Download .txt
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_<accountname>.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:
<p align="center">
  <img src="READMEimage/Captcha1.gif" alt="" width="250" loop=infinite/>
</p>

<p align="center">
  <img src="READMEimage/2ndCaptcha.gif" alt="" width="250" loop=infinite/>
</p>

#### 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
================================================
<div align="center">
  <h1>tiktokautouploader</h1>
</div>

### 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'```)

<p align="center">
  <img src="READMEimage/READMEGIF.gif" alt="" width="900"/>
</p>

## 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 = "<b>📊 Upload Status</b>\n"
    stats_msg += "<b>===========================</b>\n"
    stats_msg += f"<b>🎥 Currently Uploading:</b> {curr_vid if upld_inp else 'N/A'}\n"
    stats_msg += f"<b>📅 Last Upload Time:</b> {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"<b>📅 Next Upload Time:</b> {time.strftime('%Y-%m-%d, %I:%M:%S %p', time.localtime(nxt_upld)) if nxt_upld else 'N/A'}\n"
    stats_msg += f"<b>⏳ Next Upload In:</b> {time_left() if not upld_inp else 'Uploading now'}\n"
    stats_msg += f"<b>⏳ Uploaded Since:</b> {timeafterupload()}\n"
    stats_msg += f"<b>📂 Videos Left:</b> {vids_left}\n"
    stats_msg += f"<b>🔄 Last Uploaded Video:</b> {last_vid if last_vid else 'N/A'}\n"
    stats_msg += f"<b>🚀 Uploaded To:</b> {last_acc if last_acc else 'N/A'}\n"
    stats_msg += f"<b>🔥 Upload Streak:</b> {upld_strk}\n"
    stats_msg += "<b>===========================</b>\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 = "<b>📢 Timer Skipped!</b>\n"
    message += "<b>===========================</b>\n"
    message += "<b>⏩ Starting next upload...</b>\n"
    message += "<b>===========================</b>\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"
Download .txt
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
Download .txt
SYMBOL INDEX (51 symbols across 3 files)

FILE: TelegramAutomation/Fancy_Upload.py
  function send_msg (line 42) | def send_msg(message: str):
  function time_left (line 46) | def time_left():
  function timeafterupload (line 56) | def timeafterupload():
  function update_stats (line 66) | def update_stats():
  function status_cmd (line 83) | async def status_cmd(update: Update, context: CallbackContext):
  function console_timer (line 88) | def console_timer():
  function skip_cmd (line 106) | async def skip_cmd(update: Update, context: CallbackContext):
  function upld_vid (line 119) | def upld_vid(video):
  function upld_all (line 141) | def upld_all():
  function start_bot (line 154) | def start_bot():
  function main (line 162) | def main():

FILE: tiktokautouploader/Js_assets/login.js
  function sleep (line 9) | function sleep(time) {
  function checkForRedirect (line 14) | async function checkForRedirect(page) {

FILE: tiktokautouploader/function.py
  class TikTokUploadError (line 17) | class TikTokUploadError(RuntimeError):
  function check_for_updates (line 49) | def check_for_updates():
  function login_warning (line 62) | def login_warning(accountname):
  function save_cookies (line 66) | def save_cookies(cookies):
  function check_expiry (line 71) | def check_expiry(accountname):
  function run_javascript (line 91) | def run_javascript(proxy_data=None):
  function install_js_dependencies (line 105) | def install_js_dependencies():
  function read_cookies (line 126) | def read_cookies(cookies_path):
  function detect_redirect (line 143) | def detect_redirect(page):
  function understood_Qs (line 156) | def understood_Qs(question):
  function get_image_src (line 192) | def get_image_src(page):
  function download_image (line 197) | def download_image(image_url):
  function run_inference_on_image_tougher (line 205) | def run_inference_on_image_tougher(image_path, object):
  function run_inference_on_image (line 235) | def run_inference_on_image(image_path):
  function convert_to_webpage_coordinates (line 274) | def convert_to_webpage_coordinates(
  function click_on_objects (line 295) | def click_on_objects(page, object_coords):
  function validate_proxy (line 301) | def validate_proxy(proxy):
  function _make_stealth_context (line 331) | def _make_stealth_context(p, headless, proxy):
  function select_sound_from_favorites (line 361) | def select_sound_from_favorites(page, sound_name, sim=None, stealth=Fals...
  function select_sound_from_search (line 442) | def select_sound_from_search(page, sound_name, sim=None, stealth=False):
  function _cookie_file (line 471) | def _cookie_file(accountname):
  function _load_or_create_cookies (line 475) | def _load_or_create_cookies(accountname, proxy):
  function _goto_with_retry (line 499) | def _goto_with_retry(page, url):
  function _wait_for_upload_or_captcha (line 513) | def _wait_for_upload_or_captcha(page):
  function _solve_captcha_if_needed (line 530) | def _solve_captcha_if_needed(page, suppressprint):
  function _set_video_input (line 665) | def _set_video_input(page, video):
  function _add_description_and_hashtags (line 674) | def _add_description_and_hashtags(page, sim, video, description, hashtag...
  function _wait_for_upload_ready (line 730) | def _wait_for_upload_ready(page):
  function _validate_schedule_request (line 745) | def _validate_schedule_request(schedule, day):
  function _normalize_schedule_and_day (line 752) | def _normalize_schedule_and_day(schedule, day):
  function _apply_schedule (line 760) | def _apply_schedule(page, schedule, day, stealth, suppressprint):
  function _adjust_sound_volume_upload (line 840) | def _adjust_sound_volume_upload(page, sound_aud_vol, stealth):
  function _pick_sound (line 872) | def _pick_sound(page, sound_name, sim, stealth, suppressprint, search_mo...
  function _add_sound_from_upload_page (line 889) | def _add_sound_from_upload_page(page, sound_name, sound_aud_vol, sim, st...
  function _run_upload_copyright_check (line 925) | def _run_upload_copyright_check(page, stealth, suppressprint):
  function _submit_upload (line 947) | def _submit_upload(page, schedule, stealth, suppressprint, post_success_...
  function _select_cover_last_frame (line 1004) | def _select_cover_last_frame(page) -> bool:
  function upload_tiktok (line 1090) | def upload_tiktok(
Condensed preview — 11 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (85K chars).
[
  {
    "path": ".gitignore",
    "chars": 61,
    "preview": "/.DS_Store\n/build\n/dist\n/tiktokautouploader.egg-info\n.history"
  },
  {
    "path": "AGENT.md",
    "chars": 6937,
    "preview": "# AGENT.md — tiktokautouploader\n\nThis file provides context for AI coding agents working on this repository.\n\n---\n\n## Pr"
  },
  {
    "path": "Documentation.md",
    "chars": 8377,
    "preview": "# tiktokautouploader Documentation\n\nThis document provides detailed information about the parameters and usage of the `u"
  },
  {
    "path": "LICENSE.md",
    "chars": 1073,
    "preview": "MIT License\n\nCopyright (c) [2024] [HAZIQ KHALID]\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "README.md",
    "chars": 7406,
    "preview": "<div align=\"center\">\n  <h1>tiktokautouploader</h1>\n</div>\n\n### AUTOMATE TIKTOK UPLOADS. USE TRENDING/FAVORITED SOUNDS, A"
  },
  {
    "path": "TelegramAutomation/Fancy_Upload.py",
    "chars": 6903,
    "preview": "import os\r\nimport time\r\nimport shutil\r\nimport random\r\nimport telegram\r\nfrom telegram import Update\r\nfrom telegram.ext im"
  },
  {
    "path": "TelegramAutomation/README.md",
    "chars": 2385,
    "preview": "# Telegram Automation\n\nThis section of the repository contains a standalone script, `Fancy_Upload.py`, which extends the"
  },
  {
    "path": "tiktokautouploader/Js_assets/login.js",
    "chars": 1646,
    "preview": "const { chromium } = require('playwright-extra')\nconst fs = require('fs');\n\nconst stealth = require('puppeteer-extra-plu"
  },
  {
    "path": "tiktokautouploader/Js_assets/package.json",
    "chars": 274,
    "preview": "{\n  \"name\": \"tiktokautouploader-assets\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"postinstall\": \"npx playwright install"
  },
  {
    "path": "tiktokautouploader/__init__.py",
    "chars": 104,
    "preview": "from .function import upload_tiktok, TikTokUploadError\n\n__all__ = ['upload_tiktok', 'TikTokUploadError']"
  },
  {
    "path": "tiktokautouploader/function.py",
    "chars": 46403,
    "preview": "from phantomwright.sync_api import sync_playwright\nfrom phantomwright.stealth import Stealth\nfrom phantomwright.user_sim"
  }
]

About this extraction

This page contains the full source code of the haziq-exe/TikTokAutoUploader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 11 files (79.7 KB), approximately 21.6k tokens, and a symbol index with 51 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!