-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
db8b126
commit 44c7237
Showing
5 changed files
with
293 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# What is this? | ||
Using _selenium_ and some manual user input, this script automatically downloads all videos of one or more TikTok users. | ||
A rewrite of [TikTokLoader](https://github.com/NicoWeio/TikTokLoader) that... works. | ||
|
||
# Setup and usage | ||
- Create a python 3.12.1 venv in a folder called ".venv" with `py -m venv .\.venv` then enter the venv by running `.\.venv\Scripts\activate` in the terminal | ||
- Run `pip install -r requirements.txt` (specific versions probably not needed but recommended) | ||
- Double click `run.bat` if on windows and follow instructions | ||
- On other OS's, run `main.py` directly and edit the username variable at the bottom of the script (account picker functionality is not supported outside windows) | ||
|
||
# How does it work? | ||
- The script first collects a username or list of several usernames from an accounts.txt file if you want several accounts to be downloaded without having to watch them all | ||
- It then opens a selenium firefox browser and gets the user to manually complete a CAPTCHA test, this convinces TikTok to trust that browser session, so all the rest of the downloading is done by copying the cookies and headers of that browser session to all requests. | ||
- It scrolls to the bottom of a users profile page to load every video, and uses the selenium webdriver built in `find_elements` to get the URL of all videos. | ||
- It then accesses each video one by one, generating a one time access source URL (which is printed to the terminal) and then downloads the video from there using requests (and the verified browser session details) | ||
- The videos are saved in incremental numbers in a subfolder copying the username inputted. | ||
|
||
> [!WARNING] | ||
> THE SCRIPT MAY ONLY BE USED IN ACCORDANCE WITH TIKTOK TERMS OF SERVICE, I AM NOT RESPONSIBLE FOR ANY MISUSE |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import time, os, sys | ||
from colorama import Fore | ||
from snatch import download_all, get_user_videos | ||
|
||
|
||
def typewriter_animation(message): | ||
for char in message: | ||
print(char, end='', flush=True) | ||
time.sleep(0.005) | ||
print() | ||
|
||
|
||
def show_title(instant: bool = False): | ||
os.system("title TikSnatch") | ||
os.system("cls") | ||
print(Fore.GREEN , "") | ||
|
||
title = ''' | ||
████████╗██╗██╗ ██╗███████╗███╗ ██╗ █████╗ ████████╗ ██████╗██╗ ██╗ | ||
╚══██╔══╝██║██║ ██╔╝██╔════╝████╗ ██║██╔══██╗╚══██╔══╝██╔════╝██║ ██║ | ||
██║ ██║█████╔╝ ███████╗██╔██╗ ██║███████║ ██║ ██║ ███████║ | ||
██║ ██║██╔═██╗ ╚════██║██║╚██╗██║██╔══██║ ██║ ██║ ██╔══██║ | ||
██║ ██║██║ ██╗███████║██║ ╚████║██║ ██║ ██║ ╚██████╗██║ ██║ | ||
╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ | ||
''' | ||
if instant: | ||
print(title, end='', flush=True) | ||
else: | ||
typewriter_animation(title) | ||
|
||
|
||
def download_everything(target_username: str, auto_download: bool = False): | ||
|
||
download_dir: str = os.path.join(os.getcwd(), target_username) | ||
if not os.path.exists(download_dir): | ||
os.makedirs(download_dir) | ||
urls: list[str | None] = get_user_videos(target_username) | ||
if urls: | ||
print(Fore.GREEN, '\n[+] Successfully snatched all video urls\n') | ||
download_all(urls, download_dir, auto_download) | ||
|
||
time.sleep(1) | ||
print(Fore.GREEN, '\n[+] Snatch Successfull !\n') | ||
|
||
|
||
def main(): | ||
|
||
print("[+] Enter 'list' to read from file accounts.txt") | ||
target_username = input("[+] Enter Target Username --> ") | ||
if target_username == "list": | ||
if os.path.exists('accounts.txt') == False: | ||
print(Fore.RED, "[+] accounts.txt not found") | ||
sys.exit() | ||
with open('accounts.txt', 'r') as f: | ||
lines = f.readlines() | ||
for line in lines: | ||
if line: | ||
download_everything(line.strip(), True) # REMOVE True TO DISABLE AUTO DOWNLOAD | ||
time.sleep(1) | ||
show_title(instant=True) | ||
|
||
else: | ||
download_everything(target_username, True) # REMOVE True TO DISABLE AUTO DOWNLOAD | ||
|
||
|
||
if __name__ == "__main__": | ||
show_title() | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
attrs==24.2.0 | ||
certifi==2024.8.30 | ||
cffi==1.17.1 | ||
charset-normalizer==3.3.2 | ||
colorama==0.4.6 | ||
h11==0.14.0 | ||
idna==3.10 | ||
markdown-it-py==3.0.0 | ||
mdurl==0.1.2 | ||
outcome==1.3.0.post0 | ||
pycparser==2.22 | ||
Pygments==2.18.0 | ||
PySocks==1.7.1 | ||
requests==2.32.3 | ||
rich==13.8.1 | ||
selenium==4.25.0 | ||
sniffio==1.3.1 | ||
sortedcontainers==2.4.0 | ||
trio==0.26.2 | ||
trio-websocket==0.11.1 | ||
typing_extensions==4.12.2 | ||
urllib3==2.2.3 | ||
websocket-client==1.8.0 | ||
wsproto==1.2.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
@echo off | ||
.\.venv\Scripts\python.exe main.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
import functools | ||
import subprocess | ||
import time | ||
import os | ||
import requests | ||
|
||
from rich.console import Console | ||
from selenium import webdriver | ||
from selenium.webdriver.common.by import By | ||
from selenium.webdriver.support.ui import WebDriverWait | ||
from selenium.webdriver.support import expected_conditions as EC | ||
from selenium.webdriver.firefox.options import Options | ||
|
||
console = Console() | ||
ERRORLEVEL = 0 | ||
|
||
def get_driver(downloading: bool = False, download_dir: str = None): | ||
# Returns a web driver and makes it play nicely with interrupt signals | ||
subprocess_Popen = subprocess.Popen | ||
subprocess.Popen = functools.partial(subprocess_Popen, process_group=0) | ||
if downloading: | ||
profile = webdriver.FirefoxProfile() | ||
profile.set_preference('browser.download.folderList', 2) | ||
profile.set_preference('browser.download.dir', download_dir) | ||
profile.set_preference('browser.helperApps.neverAsk.saveToDisk', 'video/mp4,video/mpeg,video/quicktime,video/x-ms-wmv,video/x-flv,video/webm') | ||
profile.set_preference('browser.download.manager.showWhenStarting', False) | ||
profile.set_preference('browser.download.manager.useWindow', False) | ||
profile.set_preference('browser.download.manager.focusWhenStarting', False) | ||
profile.set_preference('browser.download.manager.alertOnEXEOpen', False) | ||
profile.set_preference('browser.download.manager.showAlertOnComplete', False) | ||
profile.set_preference('browser.download.manager.closeWhenDone', True) | ||
|
||
options = Options() | ||
options.profile = profile | ||
driver = webdriver.Firefox(options=options.profile) | ||
else: | ||
driver = webdriver.Firefox() | ||
subprocess.Popen = subprocess_Popen | ||
return driver | ||
|
||
|
||
def extract_video_url(driver, url) -> str | None: | ||
driver.get(url) | ||
try: | ||
wait = WebDriverWait(driver, 60) | ||
if is_captcha_present(driver): | ||
console.print("[-] Please solve the CAPTCHA in the browser window.", style="yellow") | ||
wait.until_not(EC.presence_of_element_located((By.XPATH, '//div[contains(@class, "captcha")]'))) | ||
wait.until(EC.presence_of_element_located((By.TAG_NAME, 'video'))) | ||
except Exception as e: | ||
console.print(f"[x] Error or timeout while waiting for video element: {e}", style="red") | ||
return None | ||
|
||
# Extract the video URL using JavaScript | ||
video_element = driver.find_element(By.TAG_NAME, 'video') | ||
video_url = driver.execute_script("return arguments[0].currentSrc;", video_element) | ||
|
||
if video_url: | ||
return video_url | ||
else: | ||
console.print("[x] No video URL found in the video element.", style="red") | ||
return None | ||
|
||
def is_captcha_present(driver) -> bool: | ||
# Check for common CAPTCHA elements | ||
captcha_elements = [ | ||
(By.XPATH, '//div[contains(@class, "captcha")]'), | ||
(By.XPATH, '//iframe[contains(@src, "captcha")]'), | ||
] | ||
for by, value in captcha_elements: | ||
try: | ||
driver.find_element(by, value) | ||
return True | ||
except: | ||
continue | ||
return False | ||
|
||
|
||
def get_user_videos(username) -> list[str | None]: | ||
# Create the link from the username | ||
username = username.lstrip('@') | ||
url = f'https://www.tiktok.com/@{username}' | ||
|
||
driver = get_driver() | ||
driver.get(url) | ||
|
||
wait = WebDriverWait(driver, 600) # Keep the webdriver active for up to 10 minutes | ||
|
||
time.sleep(5) # Wait 5 seconds for the page to load | ||
# Check for CAPTCHA | ||
if is_captcha_present(driver): | ||
console.print("[-] Please solve the CAPTCHA in the browser window.", style="yellow") | ||
wait.until_not(EC.presence_of_element_located((By.XPATH, '//div[contains(@class, "captcha")]'))) | ||
|
||
# Scroll to load all videos | ||
SCROLL_PAUSE_TIME = 2 | ||
last_height = driver.execute_script("return document.body.scrollHeight") | ||
while True: | ||
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") | ||
time.sleep(SCROLL_PAUSE_TIME) | ||
|
||
if is_captcha_present(driver): | ||
console.print("[-] Please solve the CAPTCHA in the browser window.", style="yellow") | ||
wait.until_not(EC.presence_of_element_located((By.XPATH, '//div[contains(@class, "captcha")]'))) | ||
new_height = driver.execute_script("return document.body.scrollHeight") | ||
if new_height == last_height: | ||
console.print(f"[+] Successfully loaded all videos", style="green") | ||
break | ||
last_height = new_height | ||
|
||
# Collect video URLs from the user's profile page | ||
video_elements = driver.find_elements(By.XPATH, '//a[contains(@href, "/video/")]') | ||
video_urls = [elem.get_attribute('href') for elem in video_elements] | ||
video_urls = list(set(video_urls)) | ||
driver.quit() | ||
return video_urls | ||
|
||
|
||
def download_all(urls, download_dir: str = None, auto_download: bool =False) -> None: | ||
driver = get_driver() | ||
global filename | ||
filename = 0 | ||
for url in urls: | ||
source_url = extract_video_url(driver, url) | ||
if source_url: | ||
filename += 1 | ||
console.print(f"[+] Extracted source URL: {source_url}", style="green") | ||
download_video(driver, source_url, download_dir if auto_download else None) | ||
else: | ||
console.print("[x] Failed to extract video URL.", style="red") | ||
driver.quit() | ||
|
||
def download_video(driver, source_url, download_dir: str = None): | ||
console.print("[+] Opening in browser...", style="green") | ||
driver.get(source_url) | ||
|
||
if download_dir: | ||
# Automatic download procedure | ||
console.print("[+] Extracting cookies from browser session...", style="green") | ||
selenium_cookies = driver.get_cookies() | ||
session = requests.Session() | ||
for cookie in selenium_cookies: | ||
session.cookies.set(cookie['name'], cookie['value']) | ||
headers = { | ||
'User-Agent': driver.execute_script("return navigator.userAgent;"), | ||
'Referer': source_url, | ||
} | ||
|
||
try: | ||
global filename | ||
file_path = os.path.join(download_dir, f"{filename:04}.mp4") | ||
console.print(f"[+] Downloading video {filename:04} to {download_dir}", style="green") | ||
|
||
response = session.get(source_url, headers=headers, stream=True) | ||
response.raise_for_status() | ||
|
||
with open(file_path, 'wb') as f: | ||
for chunk in response.iter_content(chunk_size=8192): | ||
if chunk: | ||
f.write(chunk) | ||
|
||
console.print(f"[+] Video downloaded successfully to {file_path}", style="green") | ||
return file_path | ||
|
||
except Exception as e: | ||
console.print(f"Error downloading video: {e}", style="red") | ||
return None | ||
else: | ||
# Manual download procedure | ||
console.print("[-] Please use your browser's functionality to download the video.", style="yellow") | ||
input("After downloading the video, press Enter to continue...") | ||
|
||
|
||
|
||
if __name__ == "__main__": | ||
username = "@username" | ||
video_urls = get_user_videos(username) | ||
download_all(video_urls) |