Skip to content

Commit 6cc524e

Browse files
authored
Merge pull request #10 from wkaisertexas/qol
Quality of life changes
2 parents a518d81 + 4416569 commit 6cc524e

9 files changed

Lines changed: 245 additions & 27 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Customized items
22
test.py
3+
*.txt
4+
*.mp4
35

46
# Byte-compiled / optimized / DLL files
57
__pycache__/

README.md

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,28 @@
22

33
![Forks](https://img.shields.io/github/forks/wkaisertexas/tiktok-uploader)
44
![Stars](https://img.shields.io/github/stars/wkaisertexas/tiktok-uploader)
5-
![Forks](https://img.shields.io/github/watchers/wkaisertexas/tiktok-uploader)
5+
![Watchers](https://img.shields.io/github/watchers/wkaisertexas/tiktok-uploader)
66

77
> A **Selenium**-based automated **TikTok** video uploader
88
9+
# Table of Contents
10+
- [Installation](#installation)
11+
- [MacOS, Windows and Linux](#macos-windows-and-linux)
12+
- [Downloading from PyPI (Recommended)](#downloading-from-pypi-recommended)
13+
- [Building from source](#building-from-source)
14+
- [Usage](#usage)
15+
- [💻 Commmand Line Interface (CLI)](#💻-commmand-line-interface-cli)
16+
- [⬆️ Uploading Videos](#️⬆️-uploading-videos)
17+
- [🫵 Mentions and Hashtags](#🫵-mentions-and-hashtags)
18+
- [🪡 Stitches, Duets and Comments](#🪡-stitches-duets-and-comments)
19+
- [🔐 Authentication](#🔐-authentication)
20+
- [👀 Browser Selection](#👀-browser-selection)
21+
- [🚲 Custom WebDriver Driver Options](#🚲-custom-webdriver-driver-options)
22+
- [🤯 Headless Browsers](#🤯-headless-browsers)
23+
- [🔨 Initial Setup](#🔨-initial-setup)
24+
- [♻️ Examples](#♻️-examples)
25+
- [📝 Notes](#📝-notes)
26+
- [Accounts made with](#accounts-made-using-tiktok-uploader)
927
# Installation
1028

1129
A prequisite to using this program is the installation of a [Selenium-compatable](https://www.selenium.dev/documentation/webdriver/getting_started/install_drivers/) web browser. [Google Chrome](https://www.google.com/chrome/) is recommended.
@@ -18,21 +36,21 @@ Install Python 3 or greater from [python.org](https://www.python.org/downloads/)
1836

1937
Install `tiktok-uploader` using `pip`
2038

21-
```bash
22-
$ pip install tiktok-uploader
39+
```console
40+
pip install tiktok-uploader
2341
```
2442

2543
## Building from source
2644

2745
Installing from source allows greater flexability to modify the module's code to extend default behavior.
2846

2947
First, `clone` and move into the repository. Next, install `hatch`, the build tool used for this project [^1]. Then, `build` the projet. Finally, `install` the project with the `-e` or editable flag.
30-
```bash
31-
$ git clone https://github.com/wkaisertexas/tiktok-uploader.git
32-
$ cd tiktok-uploader
33-
$ pip install hatch
34-
$ hatch build
35-
$ pip install -e .
48+
```console
49+
git clone https://github.com/wkaisertexas/tiktok-uploader.git
50+
cd tiktok-uploader
51+
pip install hatch
52+
hatch build
53+
pip install -e .
3654
```
3755

3856
# Usage
@@ -43,11 +61,8 @@ While TikTok is strict about login in from Selenium, simply copying your session
4361

4462
Using the CLI is as simple as calling `tiktok-uploader` with your videos: `path` (-v), `description`(-d) and `cookies` (-c)
4563

46-
```bash
47-
$ tiktok-uploader \
48-
-v video.mp4 \
49-
-d "this is my escaped \"description\"" \
50-
-c cookies.txt
64+
```console
65+
tiktok-uploader -v video.mp4 -d "this is my escaped \"description\"" -c cookies.txt
5166
```
5267

5368
```python
@@ -75,7 +90,7 @@ auth = AuthBackend(cookies='cookies.txt')
7590
upload_videos(videos=videos, auth=auth)
7691
```
7792

78-
## ⬆️ Uploading videos
93+
## ⬆️ Uploading Videos
7994

8095
This library revolves around the `upload_videos` function which takes in a list of videos which have **filenames** and **descriptions** and are passed as follows:
8196

@@ -210,17 +225,21 @@ On intial startup, you **may** be prompted to install the correct driver for you
210225

211226
# ♻️ Examples
212227

228+
[Basic Upload Example](exmples/basic_upload.py) is a simple automation which uses `upload_video`.
229+
230+
[Series Upload Example](examples/series_upload.py) is a automation which uploads the same video multiple times using `upload_videos`.
231+
213232
[Scheduled Uploader Example](examples/example_series_upload.py) is an automation which is based off this package. Videos are read from a CSV file using [Pandas](https://pandas.pydata.org). A video upload attempt is made and **if and only if** it is successful will the video be marked as uploaded.
214233

215-
## 📝 Notes
234+
# 📝 Notes
216235

217236
This bot is not fool proof. Though I have not gotten an official ban, when the video will fail to upload after too many uploads. When testing, waiting several hours was sufficient to fix this problem. For this reason, please thing of this more as a scheduled uploader for TikTok videos, rather than a spam bot.
218237

219238
> Please think of this package as more of a scheduled uploader for TikTok videos, rather than a spam bot
220239
221-
## Accounts made using `tiktok-uploader`
240+
# Accounts made using `tiktok-uploader`
222241

223242
- [@C_Span](https://www.tiktok.com/@c_span?lang=en) - A split-screen channel with mobile games below featuring clips from C-Span's YouTube channel
224-
- [@habit_track](https://www.tiktok.com/@habit_track?lang=en) - A generic Dhar Mann TikTok bot
243+
- [@habit_track](https://www.tiktok.com/@habit_track?lang=en) - A Reddit to TikTok bot for a data science project
225244

226245
[^1]: If interested in Hatch, checkout the [website](https://hatch.pypa.io/latest/build/)

examples/basic_upload.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Gets a video from the internet and uplaods it"""
2+
3+
import urllib.request
4+
5+
from tiktok_uploader.upload import upload_video
6+
7+
URL = "https://raw.githubusercontent.com/wkaisertexas/wkaisertexas.github.io/main/upload.mp4"
8+
FILENAME = "upload.mp4"
9+
10+
if __name__ == "__main__":
11+
# download random video
12+
urllib.request.urlretrieve(URL, FILENAME)
13+
14+
# upload video to TikTok
15+
upload_video(FILENAME,
16+
description="This is a video I just downloaded",
17+
cookies="cookies.txt")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
Uploads multiple videos downloaded from the internet
3+
"""
4+
from tiktok_uploader.upload import upload_videos
5+
6+
import urllib.request
7+
8+
URL = "https://raw.githubusercontent.com/wkaisertexas/wkaisertexas.github.io/main/upload.mp4"
9+
FILENAME = "upload.mp4"
10+
11+
videos = [
12+
{
13+
"path": "upload.mp4",
14+
"description": "This is the first upload"
15+
},
16+
{
17+
"filename": "upload.mp4",
18+
"desc": "This is my description"
19+
}
20+
]
21+
22+
if __name__ == "__main__":
23+
# download random video
24+
urllib.request.urlretrieve(URL, FILENAME)
25+
26+
# authentication backend
27+
auth = AuthBackend(cookies="cookies.txt")
28+
29+
# upload video to TikTok
30+
upload_videos(videos, auth=auth)

src/tiktok_uploader/auth.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,19 @@ class InsufficientAuth(Exception):
196196
"""
197197
Raised when there is insufficient authentication
198198
"""
199+
200+
# sets the exception message
201+
def __init__(self, message="""
202+
Insufficient authentication:
203+
204+
> TikTok uses cookies to keep track of the user's authentication or session.
205+
206+
Either:
207+
- Use a cookies file passed as the `cookies` argument
208+
- easily obtained using https://github.com/kairi003/Get-cookies.txt-LOCALLY
209+
- Use a cookies list passed as the `cookies_list` argument
210+
- can be obtained from your browser's developer tools under storage -> cookies
211+
- only the `sessionid` cookie is required
212+
"""):
213+
self.message = message
214+
super().__init__(self.message)

src/tiktok_uploader/config.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
headless = false
44
quit_on_end = true
55

6+
# Messing around with inputs
7+
valid_path_names = ["path", "filename", "video", "video_path"]
8+
valid_descriptions = ["description", "desc", "caption"]
9+
610
# Selenium Webdriver Waits
711
implicit_wait = 3 # seconds
812
explicit_wait = 60 # seconds

src/tiktok_uploader/upload.py

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
upload_videos : Uploads multiple TikTok videos
88
"""
99
from os.path import abspath, exists
10+
from typing import List
1011
import time
1112

1213
from selenium.webdriver.common.by import By
@@ -75,9 +76,7 @@ def upload_videos(videos: list = None, auth: AuthBackend = None, browser='chrome
7576
failed : list
7677
A list of videos which failed to upload
7778
"""
78-
if not videos:
79-
print("No videos were provided")
80-
return
79+
videos = _convert_videos_dict(videos)
8180

8281
if not browser_agent: # user-specified browser agent
8382
driver = get_browser(name=browser, headless=headless, *args, **kwargs)
@@ -182,7 +181,6 @@ def _set_description(driver, description: str) -> None:
182181
while description:
183182
nearest_mention = description.find('@')
184183
nearest_hash = description.find('#')
185-
print("description: ", description)
186184

187185
if nearest_mention == 0 or nearest_hash == 0:
188186
desc.send_keys('@' if nearest_mention == 0 else '#')
@@ -191,8 +189,6 @@ def _set_description(driver, description: str) -> None:
191189
time.sleep(config['implicit_wait'])
192190

193191
name = description[1:].split(' ')[0]
194-
print("name: ", name)
195-
# TODO: sending keys directly for mentions may not work
196192
if nearest_mention == 0: # @ case
197193
mention_xpath = config['selectors']['upload']['mention_box']
198194
condition = EC.presence_of_element_located((By.XPATH, mention_xpath))
@@ -205,7 +201,6 @@ def _set_description(driver, description: str) -> None:
205201

206202
if nearest_mention == 0: # @ case
207203
mention_xpath = config['selectors']['upload']['mentions'].format('@' + name)
208-
print(mention_xpath)
209204
condition = EC.presence_of_element_located((By.XPATH, mention_xpath))
210205
else:
211206
hashtag_xpath = config['selectors']['upload']['hashtags'].format(name)
@@ -236,8 +231,6 @@ def _clear(element) -> None:
236231
element
237232
The text box to clear
238233
"""
239-
# element.send_keys(Keys.CONTROL + 'a')
240-
# element.send_keys(Keys.DELETE)
241234

242235
element.send_keys(2 * len(element.text) * Keys.BACKSPACE) # margin of safety
243236

@@ -381,3 +374,65 @@ def _get_splice_index(nearest_mention: int, nearest_hashtag: int, description: s
381374
return nearest_hashtag
382375
else:
383376
return min(nearest_mention, nearest_hashtag)
377+
378+
def _convert_videos_dict(videos_list_of_dictionaries) -> List:
379+
"""
380+
Takes in a videos dictionary and converts it.
381+
382+
This allows the user to use the wrong stuff and thing to just work
383+
"""
384+
if not videos_list_of_dictionaries:
385+
raise RuntimeError("No videos to upload")
386+
387+
valid_path = config['valid_path_names']
388+
valid_description = config['valid_descriptions']
389+
390+
correct_path = valid_path[0]
391+
correct_description = valid_description[0]
392+
393+
def intersection(lst1, lst2):
394+
""" return the intersection of two lists """
395+
return list(set(lst1) & set(lst2))
396+
397+
return_list = []
398+
for elem in videos_list_of_dictionaries:
399+
# preprocesses the dictionary
400+
elem = {k.strip().lower(): v for k, v in elem.items()}
401+
402+
keys = elem.keys()
403+
path_intersection = intersection(valid_path, keys)
404+
description_interesection = intersection(valid_description, keys)
405+
406+
if path_intersection:
407+
# we have a path
408+
path = elem[path_intersection.pop()]
409+
410+
if not _check_valid_path(path):
411+
raise RuntimeError("Invalid path: " + path)
412+
413+
elem[correct_path] = path
414+
else:
415+
# iterates over the elem and find a key which is a path with a valid extension
416+
for _, value in elem.items():
417+
if _check_valid_path(value):
418+
elem[correct_path] = value
419+
break
420+
else:
421+
# no valid path found
422+
raise RuntimeError("Path not found in dictionary: " + str(elem))
423+
424+
if description_interesection:
425+
# we have a description
426+
elem[correct_description] = elem[description_interesection.pop()]
427+
else:
428+
# iterates over the elem and finds a description which is not a valid path
429+
for _, value in elem.items():
430+
if not _check_valid_path(value):
431+
elem[correct_description] = value
432+
break
433+
else:
434+
elem[correct_description] = '' # null description is fine
435+
436+
return_list.append(elem)
437+
438+
return return_list

tests/test_upload.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Tests uploader
3+
"""
4+
5+
from tiktok_uploader.upload import _convert_videos_dict
6+
7+
import os
8+
9+
from pytest import raises
10+
11+
# before each create a file called test.mp4 and test.jpg
12+
FILENAME = 'test.mp4'
13+
14+
def setup_function():
15+
"""
16+
Creates a dummy file
17+
"""
18+
with open(FILENAME, 'w', encoding='utf-8') as file:
19+
file.write('test')
20+
21+
# delete the file after each test
22+
def teardown_function():
23+
"""
24+
Deletes the dummy file
25+
"""
26+
os.remove(FILENAME)
27+
28+
def test_convert_videos_dict_good():
29+
"""Tests the videos dictionary with the good names"""
30+
good_dict = {
31+
"path": FILENAME,
32+
"description": "this is my description",
33+
}
34+
35+
array = _convert_videos_dict([good_dict])
36+
37+
assert array[0]['path'] == FILENAME
38+
assert array[0]['description'] == "this is my description"
39+
40+
def test_convert_videos_dict_wrong_names():
41+
"""
42+
Tests the videos dictionary with the wrong names
43+
"""
44+
wrong_dict = {
45+
'video': FILENAME,
46+
'desc': 'this is my description',
47+
}
48+
49+
array = _convert_videos_dict([wrong_dict])
50+
51+
assert array[0]['path'] == FILENAME
52+
assert array[0]['description'] == "this is my description"
53+
54+
def test_convert_videos_bad():
55+
"""
56+
Tests the videos dictionary with the wrong dictionaries
57+
"""
58+
bad_dict = {
59+
'nothing': 'asfs',
60+
'wrong': 'wrong',
61+
}
62+
with raises(RuntimeError):
63+
_convert_videos_dict([bad_dict])
64+
65+
def test_convert_videos_filename():
66+
"""
67+
Tests the videos dictionary with the wrong dictionaries
68+
"""
69+
bad_dict = {
70+
'nothing': FILENAME,
71+
}
72+
array = _convert_videos_dict([bad_dict])
73+
74+
assert array[0]['path'] == FILENAME
75+
assert array[0]['description'] == ""

0 commit comments

Comments
 (0)