Skip to content

Commit ca35f0f

Browse files
davidteatherPopesitesmonty-dev
authored
V5.1.0 - Memory Improvement, Jupyter Support (on Linux), Hierarchical Exceptions, (#860)
* Popesites/master (#853) * Add Video().create_time and Video().stats * Exception reorganization & renaming * Update bug_report.md * Memory Improvement & Playwright is not threadsafe(#846) * Update for #846 * Async browser handling * Black formatting & ipynb note * Resize Trendpop * Ensure cleanup on tests Co-authored-by: Popesites <conner@popesites.tech> Co-authored-by: Montel Edwards <m@monteledwards.com> Co-authored-by: davidteather <davidteather@users.noreply.github.com>
1 parent 1c7768f commit ca35f0f

38 files changed

+1014
-2294
lines changed

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ Please add any relevant code that is giving you unexpected results.
1818

1919
Preferably the smallest amount of code to reproduce the issue.
2020

21+
22+
**SET LOGGING LEVEL TO INFO BEFORE POSTING CODE OUTPUT**
23+
```py
24+
import logging
25+
TikTokApi(logging_level=logging.INFO) # SETS LOGGING_LEVEL TO INFO
26+
# Hopefully the info level will help you debug or at least someone else on the issue
2127
```
28+
29+
```py
2230
# Code Goes Here
2331
```
2432

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ test2.py
2626
build
2727
MANIFEST
2828
src
29-
.vscode
29+
.vscode
30+
.env

CITATION.cff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ authors:
55
orcid: "https://orcid.org/0000-0002-9467-4676"
66
title: "TikTokAPI"
77
url: "https://github.com/davidteather/tiktok-api"
8-
version: 5.0.0
9-
date-released: 2022-2-11
8+
version: 5.1.0
9+
date-released: 2022-3-19

README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ If you run into an issue please check the closed issues on the github, although
4646
pip install TikTokApi
4747
python -m playwright install
4848
```
49-
If you would prefer a video walk through of setting up this package I created a currently semi-outdated [YouTube video](https://www.youtube.com/watch?v=-uCt1x8kINQ) just for that.
49+
If you would prefer a video walk through of setting up this package [YouTube video](https://www.youtube.com/watch?v=-uCt1x8kINQ) just for that.
50+
51+
If you want a quick video to listen for [TikTok Live](https://www.youtube.com/watch?v=307ijmA3_lc) events in python.
5052

5153
#### Docker Installation
5254

@@ -73,17 +75,15 @@ Here's a quick bit of code to get the most recent trending videos on TikTok. The
7375
```py
7476
from TikTokApi import TikTokApi
7577

76-
# In your web browser you will need to go to TikTok, check the cookies
77-
# and under www.tiktok.com s_v_web_id should exist, and use that value
78-
# as input to custom_verify_fp
79-
# Or watch https://www.youtube.com/watch?v=-uCt1x8kINQ for a visual
80-
api = TikTokApi(custom_verify_fp="")
81-
82-
for trending_video in api.trending.videos(count=50):
83-
# Prints the author's username of the trending video.
84-
print(trending_video.author.username)
78+
# Watch https://www.youtube.com/watch?v=-uCt1x8kINQ for a brief setup tutorial
79+
with TikTokApi() as api:
80+
for trending_video in api.trending.videos(count=50):
81+
# Prints the author's username of the trending video.
82+
print(trending_video.author.username)
8583
```
8684

85+
**Note**: Jupyter (ipynb) only works on linux, see [microsoft/playwright-python #178](https://github.com/microsoft/playwright-python/issues/178)
86+
8787
To run the example scripts from the repository root, make sure you use the `-m` option on python.
8888
```sh
8989
python -m examples.get_trending
@@ -128,10 +128,10 @@ Here's a few more examples that help illustrate the differences in the flow of t
128128
api = TikTokApi.get_instance()
129129
trending_videos = api.by_trending()
130130

131-
#V5
132-
api = TikTokApi() # .get_instance no longer exists
133-
for trending_video in api.trending.videos():
134-
# do something
131+
#V5.1
132+
with TikTokApi() as api: # .get_instance no longer exists
133+
for trending_video in api.trending.videos():
134+
# do something
135135
```
136136

137137
Where in V4 you had to extract information yourself, the package now handles that for you. So it's much easier to do chained related function calls.

TikTokApi/api/hashtag.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def info_full(self, **kwargs) -> dict:
7979
data = self.parent.get_data(path, **kwargs)
8080

8181
if data["challengeInfo"].get("challenge") is None:
82-
raise TikTokNotFoundError("Challenge {} does not exist".format(self.name))
82+
raise NotFoundException("Challenge {} does not exist".format(self.name))
8383

8484
return data
8585

TikTokApi/api/sound.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def info(self, use_html=False, **kwargs) -> dict:
6363
sound_data = api.sound(id='7016547803243022337').info()
6464
```
6565
"""
66+
self.__ensure_valid()
6667
if use_html:
6768
return self.info_full(**kwargs)["musicInfo"]
6869

@@ -73,7 +74,7 @@ def info(self, use_html=False, **kwargs) -> dict:
7374
res = self.parent.get_data(path, **kwargs)
7475

7576
if res.get("statusCode", 200) == 10203:
76-
raise TikTokNotFoundError()
77+
raise NotFoundException()
7778

7879
return res["musicInfo"]["music"]
7980

@@ -89,6 +90,7 @@ def info_full(self, **kwargs) -> dict:
8990
sound_data = api.sound(id='7016547803243022337').info_full()
9091
```
9192
"""
93+
self.__ensure_valid()
9294
r = requests.get(
9395
"https://www.tiktok.com/music/-{}".format(self.id),
9496
headers={
@@ -119,6 +121,7 @@ def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]:
119121
# do something
120122
```
121123
"""
124+
self.__ensure_valid()
122125
processed = self.parent._process_kwargs(kwargs)
123126
kwargs["custom_device_id"] = processed.device_id
124127

@@ -154,6 +157,9 @@ def __extract_from_data(self):
154157
data = self.as_dict
155158
keys = data.keys()
156159

160+
if data.get("id") == "":
161+
self.id = ""
162+
157163
if "authorName" in keys:
158164
self.id = data["id"]
159165
self.title = data["title"]
@@ -166,6 +172,10 @@ def __extract_from_data(self):
166172
f"Failed to create Sound with data: {data}\nwhich has keys {data.keys()}"
167173
)
168174

175+
def __ensure_valid(self):
176+
if self.id == "":
177+
raise SoundRemovedException("This sound has been removed!")
178+
169179
def __repr__(self):
170180
return self.__str__()
171181

TikTokApi/api/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def info_full(self, **kwargs) -> dict:
105105

106106
user_props = user["props"]["pageProps"]
107107
if user_props["statusCode"] == 404:
108-
raise TikTokNotFoundError(
108+
raise NotFoundException(
109109
"TikTok user with username {} does not exist".format(self.username)
110110
)
111111

TikTokApi/api/video.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
from __future__ import annotations
22

33
from urllib.parse import urlencode
4-
54
from ..helpers import extract_video_id_from_url
6-
7-
import logging
85
from typing import TYPE_CHECKING, ClassVar, Optional
6+
from datetime import datetime
97

108
if TYPE_CHECKING:
119
from ..tiktok import TikTokApi
@@ -28,6 +26,10 @@ class Video:
2826

2927
id: Optional[str]
3028
"""TikTok's ID of the Video"""
29+
create_time: Optional[datetime]
30+
"""The creation time of the Video"""
31+
stats: Optional[dict]
32+
"""TikTok's stats of the Video"""
3133
author: Optional[User]
3234
"""The User who created the Video"""
3335
sound: Optional[Sound]
@@ -116,6 +118,8 @@ def __extract_from_data(self) -> None:
116118

117119
if "author" in keys:
118120
self.id = data["id"]
121+
self.create_time = datetime.fromtimestamp(data["createTime"])
122+
self.stats = data["stats"]
119123
self.author = self.parent.user(data=data["author"])
120124
self.sound = self.parent.sound(data=data["music"])
121125

@@ -137,7 +141,7 @@ def __str__(self):
137141

138142
def __getattr__(self, name):
139143
# Handle author, sound, hashtags, as_dict
140-
if name in ["author", "sound", "hashtags", "as_dict"]:
144+
if name in ["author", "sound", "hashtags", "stats", "create_time", "as_dict"]:
141145
self.as_dict = self.info()
142146
self.__extract_from_data()
143147
return self.__getattribute__(name)

TikTokApi/browser_utilities/browser.py

Lines changed: 46 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,24 @@
99
import re
1010
from .browser_interface import BrowserInterface
1111
from urllib.parse import parse_qsl, urlparse
12-
12+
import threading
1313
from ..utilities import LOGGER_NAME
1414
from .get_acrawler import _get_acrawler, _get_tt_params_script
15-
from playwright.sync_api import sync_playwright
16-
17-
playwright = None
15+
from playwright.async_api import async_playwright
16+
import asyncio
1817

1918
logger = logging.getLogger(LOGGER_NAME)
2019

2120

22-
def get_playwright():
23-
global playwright
24-
if playwright is None:
25-
try:
26-
playwright = sync_playwright().start()
27-
except Exception as e:
28-
raise e
29-
30-
return playwright
31-
32-
3321
class browser(BrowserInterface):
34-
def __init__(
35-
self,
22+
def __init__(self, **kwargs):
23+
pass
24+
25+
@staticmethod
26+
async def create(
3627
**kwargs,
3728
):
29+
self = browser()
3830
self.kwargs = kwargs
3931
self.debug = kwargs.get("debug", False)
4032
self.proxy = kwargs.get("proxy", None)
@@ -76,25 +68,25 @@ def __init__(
7668
if self.executable_path is not None:
7769
self.options["executable_path"] = self.executable_path
7870

79-
try:
80-
self.browser = get_playwright().webkit.launch(
81-
args=self.args, **self.options
82-
)
83-
except Exception as e:
84-
logger.critical("Webkit launch failed", exc_info=True)
85-
raise e
71+
self._thread_locals = threading.local()
72+
self._thread_locals.playwright = await async_playwright().start()
73+
self.playwright = self._thread_locals.playwright
74+
self.browser = await self.playwright.webkit.launch(
75+
args=self.args, **self.options
76+
)
77+
context = await self._create_context(set_useragent=True)
78+
page = await context.new_page()
79+
await self.get_params(page)
80+
await context.close()
8681

87-
context = self._create_context(set_useragent=True)
88-
page = context.new_page()
89-
self.get_params(page)
90-
context.close()
82+
return self
9183

92-
def get_params(self, page) -> None:
84+
async def get_params(self, page) -> None:
9385
self.browser_language = self.kwargs.get(
9486
"browser_language",
95-
page.evaluate("""() => { return navigator.language; }"""),
87+
await page.evaluate("""() => { return navigator.language; }"""),
9688
)
97-
self.browser_version = page.evaluate(
89+
self.browser_version = await page.evaluate(
9890
"""() => { return window.navigator.appVersion; }"""
9991
)
10092

@@ -112,15 +104,15 @@ def get_params(self, page) -> None:
112104

113105
self.timezone_name = self.kwargs.get(
114106
"timezone_name",
115-
page.evaluate(
107+
await page.evaluate(
116108
"""() => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }"""
117109
),
118110
)
119-
self.width = page.evaluate("""() => { return screen.width; }""")
120-
self.height = page.evaluate("""() => { return screen.height; }""")
111+
self.width = await page.evaluate("""() => { return screen.width; }""")
112+
self.height = await page.evaluate("""() => { return screen.height; }""")
121113

122-
def _create_context(self, set_useragent=False):
123-
iphone = playwright.devices["iPhone 11 Pro"]
114+
async def _create_context(self, set_useragent=False):
115+
iphone = self.playwright.devices["iPhone 11 Pro"]
124116
iphone["viewport"] = {
125117
"width": random.randint(320, 1920),
126118
"height": random.randint(320, 1920),
@@ -131,7 +123,7 @@ def _create_context(self, set_useragent=False):
131123

132124
iphone["bypass_csp"] = True
133125

134-
context = self.browser.new_context(**iphone)
126+
context = await self.browser.new_context(**iphone)
135127
if set_useragent:
136128
self.user_agent = iphone["user_agent"]
137129

@@ -174,17 +166,19 @@ def gen_verifyFp(self):
174166

175167
return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
176168

177-
def sign_url(self, url, calc_tt_params=False, **kwargs):
178-
def process(route):
179-
route.abort()
169+
async def sign_url(self, url, calc_tt_params=False, **kwargs):
170+
async def process(route):
171+
await route.abort()
180172

181173
tt_params = None
182-
context = self._create_context()
183-
page = context.new_page()
174+
context = await self._create_context()
175+
page = await context.new_page()
184176

185177
if calc_tt_params:
186-
page.route(re.compile(r"(\.png)|(\.jpeg)|(\.mp4)|(x-expire)"), process)
187-
page.goto(
178+
await page.route(
179+
re.compile(r"(\.png)|(\.jpeg)|(\.mp4)|(x-expire)"), process
180+
)
181+
await page.goto(
188182
kwargs.get("default_url", "https://www.tiktok.com/@redbull"),
189183
wait_until="load",
190184
)
@@ -212,8 +206,8 @@ def process(route):
212206

213207
url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id)
214208

215-
page.add_script_tag(content=_get_acrawler())
216-
evaluatedPage = page.evaluate(
209+
await page.add_script_tag(content=_get_acrawler())
210+
evaluatedPage = await page.evaluate(
217211
'''() => {
218212
var url = "'''
219213
+ url
@@ -227,9 +221,9 @@ def process(route):
227221
url = "{}&_signature={}".format(url, evaluatedPage)
228222

229223
if calc_tt_params:
230-
page.add_script_tag(content=_get_tt_params_script())
224+
await page.add_script_tag(content=_get_tt_params_script())
231225

232-
tt_params = page.evaluate(
226+
tt_params = await page.evaluate(
233227
"""() => {
234228
return window.genXTTParams("""
235229
+ json.dumps(dict(parse_qsl(urlparse(url).query)))
@@ -238,15 +232,12 @@ def process(route):
238232
}"""
239233
)
240234

241-
context.close()
235+
await context.close()
242236
return (verifyFp, device_id, evaluatedPage, tt_params)
243237

244-
def _clean_up(self):
245-
try:
246-
self.browser.close()
247-
except Exception:
248-
logger.exception("cleanup failed")
249-
# playwright.stop()
238+
async def _clean_up(self):
239+
await self.browser.close()
240+
await self.playwright.stop()
250241

251242
def find_redirect(self, url):
252243
self.page.goto(url, {"waitUntil": "load"})

0 commit comments

Comments
 (0)