diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index efaded62..df6606c9 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -18,7 +18,15 @@ Please add any relevant code that is giving you unexpected results.
Preferably the smallest amount of code to reproduce the issue.
+
+**SET LOGGING LEVEL TO INFO BEFORE POSTING CODE OUTPUT**
+```py
+import logging
+TikTokApi(logging_level=logging.INFO) # SETS LOGGING_LEVEL TO INFO
+# Hopefully the info level will help you debug or at least someone else on the issue
```
+
+```py
# Code Goes Here
```
diff --git a/.gitignore b/.gitignore
index 7b4f4bcd..f251d31c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,4 +26,5 @@ test2.py
build
MANIFEST
src
-.vscode
\ No newline at end of file
+.vscode
+.env
\ No newline at end of file
diff --git a/CITATION.cff b/CITATION.cff
index 79e7ddea..1ed9d758 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -5,5 +5,5 @@ authors:
orcid: "https://orcid.org/0000-0002-9467-4676"
title: "TikTokAPI"
url: "https://github.com/davidteather/tiktok-api"
-version: 5.0.0
-date-released: 2022-2-11
+version: 5.1.1
+date-released: 2022-3-21
diff --git a/README.md b/README.md
index 0df6d47b..48b95312 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,64 @@ This is an unofficial api wrapper for TikTok.com in python. With this api you ar
## Sponsors
These sponsors have paid to be placed here and beyond that I do not have any affiliation with them, the TikTokAPI package will always be free and open-source. If you wish to be a sponsor of this project check out my [GitHub sponsors page](https://github.com/sponsors/davidteather).
-[![TikAPI](https://raw.githubusercontent.com/davidteather/TikTok-Api/master/imgs/logo128.png)](https://tikapi.io/?ref=davidteather) | **[TikAPI](https://tikapi.io/?ref=davidteather)** is a paid TikTok API service providing an full out-of-the-box solution for developers, trusted by 100+ companies. [Learn more](https://tikapi.io/?ref=davidteather)
-:-------------------------:|:-------------------------:
+
+
+
+
+
+
+
+
+
## Table of Contents
- [Documentation](#documentation)
@@ -43,7 +99,9 @@ If you run into an issue please check the closed issues on the github, although
pip install TikTokApi
python -m playwright install
```
-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.
+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.
+
+If you want a quick video to listen for [TikTok Live](https://www.youtube.com/watch?v=307ijmA3_lc) events in python.
#### Docker Installation
@@ -70,17 +128,15 @@ Here's a quick bit of code to get the most recent trending videos on TikTok. The
```py
from TikTokApi import TikTokApi
-# In your web browser you will need to go to TikTok, check the cookies
-# and under www.tiktok.com s_v_web_id should exist, and use that value
-# as input to custom_verify_fp
-# Or watch https://www.youtube.com/watch?v=-uCt1x8kINQ for a visual
-api = TikTokApi(custom_verify_fp="")
-
-for trending_video in api.trending.videos(count=50):
- # Prints the author's username of the trending video.
- print(trending_video.author.username)
+# Watch https://www.youtube.com/watch?v=-uCt1x8kINQ for a brief setup tutorial
+with TikTokApi() as api:
+ for trending_video in api.trending.videos(count=50):
+ # Prints the author's username of the trending video.
+ print(trending_video.author.username)
```
+**Note**: Jupyter (ipynb) only works on linux, see [microsoft/playwright-python #178](https://github.com/microsoft/playwright-python/issues/178)
+
To run the example scripts from the repository root, make sure you use the `-m` option on python.
```sh
python -m examples.get_trending
@@ -125,10 +181,10 @@ Here's a few more examples that help illustrate the differences in the flow of t
api = TikTokApi.get_instance()
trending_videos = api.by_trending()
-#V5
-api = TikTokApi() # .get_instance no longer exists
-for trending_video in api.trending.videos():
- # do something
+#V5.1
+with TikTokApi() as api: # .get_instance no longer exists
+ for trending_video in api.trending.videos():
+ # do something
```
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.
diff --git a/TikTokApi/api/hashtag.py b/TikTokApi/api/hashtag.py
index aba2fad4..8d5b37c0 100644
--- a/TikTokApi/api/hashtag.py
+++ b/TikTokApi/api/hashtag.py
@@ -79,7 +79,7 @@ def info_full(self, **kwargs) -> dict:
data = self.parent.get_data(path, **kwargs)
if data["challengeInfo"].get("challenge") is None:
- raise TikTokNotFoundError("Challenge {} does not exist".format(self.name))
+ raise NotFoundException("Challenge {} does not exist".format(self.name))
return data
diff --git a/TikTokApi/api/sound.py b/TikTokApi/api/sound.py
index 69770c25..3cd883a2 100644
--- a/TikTokApi/api/sound.py
+++ b/TikTokApi/api/sound.py
@@ -63,6 +63,7 @@ def info(self, use_html=False, **kwargs) -> dict:
sound_data = api.sound(id='7016547803243022337').info()
```
"""
+ self.__ensure_valid()
if use_html:
return self.info_full(**kwargs)["musicInfo"]
@@ -73,7 +74,7 @@ def info(self, use_html=False, **kwargs) -> dict:
res = self.parent.get_data(path, **kwargs)
if res.get("statusCode", 200) == 10203:
- raise TikTokNotFoundError()
+ raise NotFoundException()
return res["musicInfo"]["music"]
@@ -89,6 +90,7 @@ def info_full(self, **kwargs) -> dict:
sound_data = api.sound(id='7016547803243022337').info_full()
```
"""
+ self.__ensure_valid()
r = requests.get(
"https://www.tiktok.com/music/-{}".format(self.id),
headers={
@@ -119,6 +121,7 @@ def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]:
# do something
```
"""
+ self.__ensure_valid()
processed = self.parent._process_kwargs(kwargs)
kwargs["custom_device_id"] = processed.device_id
@@ -154,18 +157,28 @@ def __extract_from_data(self):
data = self.as_dict
keys = data.keys()
+ if data.get("id") == "":
+ self.id = ""
+
if "authorName" in keys:
self.id = data["id"]
self.title = data["title"]
- if data.get("authorName") is not None:
+ if data["authorName"] is not None:
self.author = self.parent.user(username=data["authorName"])
+ self.id = data.get("id")
+ self.title = data.get("title")
+
if self.id is None:
Sound.parent.logger.error(
f"Failed to create Sound with data: {data}\nwhich has keys {data.keys()}"
)
+ def __ensure_valid(self):
+ if self.id == "":
+ raise SoundRemovedException("This sound has been removed!")
+
def __repr__(self):
return self.__str__()
@@ -178,4 +191,4 @@ def __getattr__(self, name):
self.__extract_from_data()
return self.__getattribute__(name)
- raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Sound")
+ # raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Sound")
diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py
index 15651803..d2e11de9 100644
--- a/TikTokApi/api/user.py
+++ b/TikTokApi/api/user.py
@@ -105,7 +105,7 @@ def info_full(self, **kwargs) -> dict:
user_props = user["props"]["pageProps"]
if user_props["statusCode"] == 404:
- raise TikTokNotFoundError(
+ raise NotFoundException(
"TikTok user with username {} does not exist".format(self.username)
)
diff --git a/TikTokApi/api/video.py b/TikTokApi/api/video.py
index 7285e79c..981812d4 100644
--- a/TikTokApi/api/video.py
+++ b/TikTokApi/api/video.py
@@ -1,11 +1,9 @@
from __future__ import annotations
from urllib.parse import urlencode
-
from ..helpers import extract_video_id_from_url
-
-import logging
from typing import TYPE_CHECKING, ClassVar, Optional
+from datetime import datetime
if TYPE_CHECKING:
from ..tiktok import TikTokApi
@@ -28,6 +26,10 @@ class Video:
id: Optional[str]
"""TikTok's ID of the Video"""
+ create_time: Optional[datetime]
+ """The creation time of the Video"""
+ stats: Optional[dict]
+ """TikTok's stats of the Video"""
author: Optional[User]
"""The User who created the Video"""
sound: Optional[Sound]
@@ -116,6 +118,8 @@ def __extract_from_data(self) -> None:
if "author" in keys:
self.id = data["id"]
+ self.create_time = datetime.fromtimestamp(data["createTime"])
+ self.stats = data["stats"]
self.author = self.parent.user(data=data["author"])
self.sound = self.parent.sound(data=data["music"])
@@ -137,7 +141,7 @@ def __str__(self):
def __getattr__(self, name):
# Handle author, sound, hashtags, as_dict
- if name in ["author", "sound", "hashtags", "as_dict"]:
+ if name in ["author", "sound", "hashtags", "stats", "create_time", "as_dict"]:
self.as_dict = self.info()
self.__extract_from_data()
return self.__getattribute__(name)
diff --git a/TikTokApi/browser_utilities/browser.py b/TikTokApi/browser_utilities/browser.py
index d3be03e1..689df7a3 100644
--- a/TikTokApi/browser_utilities/browser.py
+++ b/TikTokApi/browser_utilities/browser.py
@@ -9,32 +9,24 @@
import re
from .browser_interface import BrowserInterface
from urllib.parse import parse_qsl, urlparse
-
+import threading
from ..utilities import LOGGER_NAME
from .get_acrawler import _get_acrawler, _get_tt_params_script
-from playwright.sync_api import sync_playwright
-
-playwright = None
+from playwright.async_api import async_playwright
+import asyncio
logger = logging.getLogger(LOGGER_NAME)
-def get_playwright():
- global playwright
- if playwright is None:
- try:
- playwright = sync_playwright().start()
- except Exception as e:
- raise e
-
- return playwright
-
-
class browser(BrowserInterface):
- def __init__(
- self,
+ def __init__(self, **kwargs):
+ pass
+
+ @staticmethod
+ async def create(
**kwargs,
):
+ self = browser()
self.kwargs = kwargs
self.debug = kwargs.get("debug", False)
self.proxy = kwargs.get("proxy", None)
@@ -76,25 +68,25 @@ def __init__(
if self.executable_path is not None:
self.options["executable_path"] = self.executable_path
- try:
- self.browser = get_playwright().webkit.launch(
- args=self.args, **self.options
- )
- except Exception as e:
- logger.critical("Webkit launch failed", exc_info=True)
- raise e
+ self._thread_locals = threading.local()
+ self._thread_locals.playwright = await async_playwright().start()
+ self.playwright = self._thread_locals.playwright
+ self.browser = await self.playwright.webkit.launch(
+ args=self.args, **self.options
+ )
+ context = await self._create_context(set_useragent=True)
+ page = await context.new_page()
+ await self.get_params(page)
+ await context.close()
- context = self._create_context(set_useragent=True)
- page = context.new_page()
- self.get_params(page)
- context.close()
+ return self
- def get_params(self, page) -> None:
+ async def get_params(self, page) -> None:
self.browser_language = self.kwargs.get(
"browser_language",
- page.evaluate("""() => { return navigator.language; }"""),
+ await page.evaluate("""() => { return navigator.language; }"""),
)
- self.browser_version = page.evaluate(
+ self.browser_version = await page.evaluate(
"""() => { return window.navigator.appVersion; }"""
)
@@ -112,15 +104,15 @@ def get_params(self, page) -> None:
self.timezone_name = self.kwargs.get(
"timezone_name",
- page.evaluate(
+ await page.evaluate(
"""() => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }"""
),
)
- self.width = page.evaluate("""() => { return screen.width; }""")
- self.height = page.evaluate("""() => { return screen.height; }""")
+ self.width = await page.evaluate("""() => { return screen.width; }""")
+ self.height = await page.evaluate("""() => { return screen.height; }""")
- def _create_context(self, set_useragent=False):
- iphone = playwright.devices["iPhone 11 Pro"]
+ async def _create_context(self, set_useragent=False):
+ iphone = self.playwright.devices["iPhone 11 Pro"]
iphone["viewport"] = {
"width": random.randint(320, 1920),
"height": random.randint(320, 1920),
@@ -131,7 +123,7 @@ def _create_context(self, set_useragent=False):
iphone["bypass_csp"] = True
- context = self.browser.new_context(**iphone)
+ context = await self.browser.new_context(**iphone)
if set_useragent:
self.user_agent = iphone["user_agent"]
@@ -174,20 +166,21 @@ def gen_verifyFp(self):
return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
- def sign_url(self, url, calc_tt_params=False, **kwargs):
- def process(route):
- route.abort()
+ async def sign_url(self, url, calc_tt_params=False, **kwargs):
+ async def process(route):
+ await route.abort()
tt_params = None
- context = self._create_context()
- page = context.new_page()
+ context = await self._create_context()
+ page = await context.new_page()
if len(self.browser.contexts) > 1:
logger.warn(f'context count {len(self.browser.contexts)}')
if calc_tt_params:
- page.route(re.compile(r"(\.png)|(\.jpeg)|(\.mp4)|(x-expire)|(googleads)|(facebook)"), process)
+ await page.route(
+ re.compile(r"(\.png)|(\.jpeg)|(\.mp4)|(x-expire)|(googleads)|(facebook)"), process)
try:
- page.goto(
+ await page.goto(
kwargs.get("default_url", "https://www.tiktok.com/@redbull"),
wait_until="domcontentloaded",
)
@@ -220,28 +213,27 @@ def process(route):
url = "{}&verifyFp={}&device_id={}&msToken={}".format(url, verifyFp, device_id, msToken)
try:
- page.add_script_tag(content=_get_acrawler())
- evaluatedPage = page.evaluate(
+ await page.add_script_tag(content=_get_acrawler())
+ evaluatedPage = await page.evaluate(
'''() => {
var url = "'''
+ url
+ """"
var token = window.byted_acrawler.sign({url: url});
-
return token;
}"""
)
except Exception as e:
logger.exception('error adding crawler')
- context.close()
+ await context.close()
raise e
url = "{}&_signature={}".format(url, evaluatedPage)
if calc_tt_params:
try:
- page.add_script_tag(content=_get_tt_params_script())
- tt_params = page.evaluate(
+ await page.add_script_tag(content=_get_tt_params_script())
+ tt_params = await page.evaluate(
"""() => {
return window.genXTTParams("""
+ json.dumps(dict(parse_qsl(urlparse(url).query)))
@@ -253,15 +245,12 @@ def process(route):
logger.exception('error getting tt params')
context.close()
raise e
- context.close()
+ await context.close()
return (verifyFp, device_id, evaluatedPage, tt_params)
- def _clean_up(self):
- try:
- self.browser.close()
- except Exception:
- logger.exception("cleanup failed")
- # playwright.stop()
+ async def _clean_up(self):
+ await self.browser.close()
+ await self.playwright.stop()
def find_redirect(self, url):
self.page.goto(url, {"waitUntil": "load"})
diff --git a/TikTokApi/exceptions.py b/TikTokApi/exceptions.py
index 8f0d79bb..57911440 100644
--- a/TikTokApi/exceptions.py
+++ b/TikTokApi/exceptions.py
@@ -1,38 +1,29 @@
-class TikTokCaptchaError(Exception):
- def __init__(
- self,
- message="TikTok blocks this request displaying a Captcha \nTip: Consider using a proxy or a custom_verify_fp as method parameters",
- ):
- self.message = message
- super().__init__(self.message)
+class TikTokException(Exception):
+ """Generic exception that all other TikTok errors are children of."""
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
-# TODO: Update this so children are all subclasses of the generic error.
-class GenericTikTokError(Exception):
- def __init__(self, message):
- self.message = message
- super().__init__(self.message)
+class CaptchaException(TikTokException):
+ """TikTok is showing captcha"""
-class TikTokNotFoundError(Exception):
- def __init__(self, message="The requested object does not exists"):
- self.message = message
- super().__init__(self.message)
+class NotFoundException(TikTokException):
+ """TikTok indicated that this object does not exist."""
-class EmptyResponseError(Exception):
- def __init__(self, message="TikTok sent no data back"):
- self.message = message
- super().__init__(self.message)
+class EmptyResponseException(TikTokException):
+ """TikTok sent back an empty response."""
-class JSONDecodeFailure(Exception):
- def __init__(self, message="TikTok sent invalid JSON back"):
- self.message = message
- super().__init__(self.message)
+class SoundRemovedException(TikTokException):
+ """This TikTok sound has no id from being removed by TikTok."""
-class TikTokNotAvailableError(Exception):
- def __init__(self, message="The requested object is not available in this region"):
- self.message = message
- super().__init__(self.message)
+
+class InvalidJSONException(TikTokException):
+ """TikTok returned invalid JSON."""
+
+
+class NotAvailableException(TikTokException):
+ """The requested object is not available in this region."""
diff --git a/TikTokApi/helpers.py b/TikTokApi/helpers.py
index 048c40fe..1111ea64 100644
--- a/TikTokApi/helpers.py
+++ b/TikTokApi/helpers.py
@@ -27,7 +27,9 @@ def extract_tag_contents(html):
if sigi_json:
return sigi_json.group(1)
else:
- raise TikTokCaptchaError()
+ raise CaptchaException(
+ "TikTok blocks this request displaying a Captcha \nTip: Consider using a proxy or a custom_verify_fp as method parameters"
+ )
def extract_video_id_from_url(url):
diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py
index 6c06a503..d9ae9548 100644
--- a/TikTokApi/tiktok.py
+++ b/TikTokApi/tiktok.py
@@ -1,6 +1,8 @@
import json
import logging
import os
+import threading
+import asyncio
import random
import string
import time
@@ -28,22 +30,22 @@
BASE_URL = "https://m.tiktok.com/"
DESKTOP_BASE_URL = "https://www.tiktok.com/"
+_thread_lock = threading.Lock()
-class TikTokApi:
- _instance = None
- logger: ClassVar[logging.Logger] = logging.getLogger(LOGGER_NAME)
+class TikTokApi:
+ _is_context_manager = False
user = User
search = Search
sound = Sound
hashtag = Hashtag
video = Video
trending = Trending
+ logger = logging.getLogger(LOGGER_NAME)
- @staticmethod
- def __new__(
- cls,
- logging_level=logging.WARNING,
+ def __init__(
+ self,
+ logging_level: int = logging.WARNING,
request_delay: Optional[int] = None,
custom_device_id: Optional[str] = None,
generate_static_device_id: Optional[bool] = False,
@@ -111,10 +113,10 @@ class to prevent issues from arising with playwright
in other places.
"""
- if cls._instance is None:
- cls._instance = super(TikTokApi, cls).__new__(cls)
- cls._instance._initialize(
- logging_level=logging_level,
+ self.logger.setLevel(logging_level)
+
+ with _thread_lock:
+ self._initialize(
request_delay=request_delay,
custom_device_id=custom_device_id,
generate_static_device_id=generate_static_device_id,
@@ -125,9 +127,8 @@ class to prevent issues from arising with playwright
*args,
**kwargs,
)
- return cls._instance
- def _initialize(self, logging_level=logging.WARNING, **kwargs):
+ def _initialize(self, **kwargs):
# Add classes from the api folder
User.parent = self
Search.parent = self
@@ -136,15 +137,13 @@ def _initialize(self, logging_level=logging.WARNING, **kwargs):
Video.parent = self
Trending.parent = self
- self.logger.setLevel(level=logging_level)
-
# Some Instance Vars
self._executable_path = kwargs.get("executable_path", None)
if kwargs.get("custom_did") != None:
raise Exception("Please use 'custom_device_id' instead of 'custom_did'")
self._custom_device_id = kwargs.get("custom_device_id", None)
- self._user_agent = "5.0+(iPhone%3B+CPU+iPhone+OS+14_8+like+Mac+OS+X)+AppleWebKit%2F605.1.15+(KHTML,+like+Gecko)+Version%2F14.1.2+Mobile%2F15E148+Safari%2F604.1"
+ self._user_agent = "5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"
self._proxy = kwargs.get("proxy", None)
self._custom_verify_fp = kwargs.get("custom_verify_fp")
self._signer_url = kwargs.get("external_signer", None)
@@ -162,7 +161,10 @@ def _initialize(self, logging_level=logging.WARNING, **kwargs):
)
if self._signer_url is None:
- self._browser = browser(**kwargs)
+ self._browser = asyncio.get_event_loop().run_until_complete(
+ asyncio.gather(browser.create(**kwargs))
+ )[0]
+
self._user_agent = self._browser.user_agent
try:
@@ -184,6 +186,7 @@ def _initialize(self, logging_level=logging.WARNING, **kwargs):
self._height = "1080"
self._region = "US"
self._language = "en"
+ raise e from e
def get_data(self, path, subdomain="m", **kwargs) -> dict:
"""Makes requests to TikTok and returns their JSON.
@@ -216,9 +219,21 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict:
if self._signer_url is None:
kwargs["custom_verify_fp"] = verifyFp
- verify_fp, device_id, signature, tt_params = self._browser.sign_url(
- full_url, calc_tt_params=send_tt_params, **kwargs
- )
+ (
+ verify_fp,
+ device_id,
+ signature,
+ tt_params,
+ ) = asyncio.get_event_loop().run_until_complete(
+ asyncio.gather(
+ self._browser.sign_url(
+ full_url, calc_tt_params=send_tt_params, **kwargs
+ )
+ )
+ )[
+ 0
+ ]
+
user_agent = self._browser.user_agent
referrer = self._browser.referrer
else:
@@ -249,7 +264,7 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict:
csrf_token = None
if subdomain == "m":
- csrf_session_id = h.cookies["csrf_session_id"]
+ csrf_session_id = h.cookies.get("csrf_session_id")
csrf_token = h.headers["X-Ware-Csrf-Token"].split(",")[1]
kwargs["csrf_session_id"] = csrf_session_id
@@ -272,7 +287,7 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict:
"x-tt-params": tt_params,
}
- self.logger.info(f"GET: %s\n\theaders: %s", url, headers)
+ self.logger.debug(f"GET: %s\n\theaders: %s", url, headers)
r = requests.get(
url,
headers=headers,
@@ -293,7 +308,9 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict:
self._get_cookies(**kwargs),
url,
)
- raise TikTokCaptchaError()
+ raise CaptchaException(
+ "TikTok blocks this request displaying a Captcha \nTip: Consider using a proxy or a custom_verify_fp as method parameters"
+ )
# statusCode from props->pageProps->statusCode thanks @adiantek on #403
error_codes = {
@@ -337,17 +354,17 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict:
"undefined": "MEDIA_ERROR",
}
statusCode = parsed_data.get("statusCode", 0)
- self.logger.info(f"TikTok Returned: %s", json)
+ self.logger.debug(f"TikTok Returned: %s", json)
if statusCode == 10201:
# Invalid Entity
- raise TikTokNotFoundError(
+ raise NotFoundException(
"TikTok returned a response indicating the entity is invalid"
)
elif statusCode == 10219:
- # not available in this region
- raise TikTokNotAvailableError("Content not available for this region")
+ # Not available in this region
+ raise NotAvailableException("Content not available for this region")
elif statusCode != 0 and statusCode != -1:
- raise GenericTikTokError(
+ raise TikTokException(
error_codes.get(
statusCode, f"TikTok sent an unknown StatusCode of {statusCode}"
)
@@ -356,26 +373,22 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict:
return r.json()
except ValueError as e:
text = r.text
- self.logger.info("TikTok response: %s", text)
+ self.logger.debug("TikTok response: %s", text)
if len(text) == 0:
- raise EmptyResponseError(
+ raise EmptyResponseException(
"Empty response from Tiktok to " + url
) from None
else:
- self.logger.exception("Converting response to JSON failed")
- raise JSONDecodeFailure() from e
+ raise InvalidJSONException("TikTok sent invalid JSON") from e
def __del__(self):
"""A basic cleanup method, called automatically from the code"""
- try:
- self._browser._clean_up()
- except Exception:
- pass
- try:
- self._browser.stop()
- except Exception:
- pass
- TikTokApi._instance = None
+ if not self._is_context_manager:
+ self.logger.debug(
+ "TikTokAPI was shutdown improperlly. Ensure the instance is terminated with .shutdown()"
+ )
+ self.shutdown()
+ return
def external_signer(self, url, custom_device_id=None, verifyFp=None):
"""Makes requests to an external signer instead of using a browser.
@@ -463,9 +476,16 @@ def get_bytes(self, **kwargs) -> bytes:
kwargs["custom_device_id"] = processed.device_id
kwargs["custom_ms_token"] = self._custom_ms_token
if self._signer_url is None:
- verify_fp, device_id, signature, _ = self._browser.sign_url(
- calc_tt_params=False, **kwargs
- )
+ (
+ verify_fp,
+ device_id,
+ signature,
+ _,
+ ) = asyncio.get_event_loop().run_until_complete(
+ asyncio.gather(self._browser.sign_url(calc_tt_params=False, **kwargs))
+ )[
+ 0
+ ]
user_agent = self._browser.user_agent
referrer = self._browser.referrer
else:
@@ -545,29 +565,53 @@ class ProcessedKwargs:
)
def _add_url_params(self) -> str:
+ try:
+ region = self._region
+ browser_language = self._browser_language.lower()
+ timezone = self._timezone_name
+ language = self._language
+ except AttributeError as e:
+ self.logger.debug("Attribute Error on add_url_params", exc_info=e)
+ region = "US"
+ browser_language = "en-us"
+ timezone = "America/Chicago"
+ language = "en"
query = {
"aid": 1988,
"app_name": "tiktok_web",
"device_platform": "web_mobile",
- "region": self._region or "US",
+ "region": region,
"priority_region": "",
"os": "ios",
"referer": "",
"cookie_enabled": "true",
"screen_width": self._width,
"screen_height": self._height,
- "browser_language": self._browser_language.lower() or "en-us",
+ "browser_language": browser_language,
"browser_platform": "iPhone",
"browser_name": "Mozilla",
"browser_version": self._user_agent,
"browser_online": "true",
- "timezone_name": self._timezone_name or "America/Chicago",
+ "timezone_name": timezone,
"is_page_visible": "true",
"focus_state": "true",
"is_fullscreen": "false",
- "history_len": random.randint(0, 30),
- "language": self._language or "en",
+ "history_len": random.randint(1, 5),
+ "language": language,
"msToken": self._custom_ms_token
}
return urlencode(query)
+
+ def shutdown(self) -> None:
+ with _thread_lock:
+ self.logger.debug("Shutting down Playwright")
+ asyncio.get_event_loop().run_until_complete(self._browser._clean_up())
+
+ def __enter__(self):
+ with _thread_lock:
+ self._is_context_manager = True
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.shutdown()
diff --git a/docs/TikTokApi.html b/docs/TikTokApi.html
index 92d57a0c..66db6e58 100644
--- a/docs/TikTokApi.html
+++ b/docs/TikTokApi.html
@@ -69,8 +69,64 @@
These sponsors have paid to be placed here and beyond that I do not have any affiliation with them, the TikTokAPI package will always be free and open-source. If you wish to be a sponsor of this project check out my GitHub sponsors page.
- | TikAPI is a paid TikTok API service providing an full out-of-the-box solution for developers, trusted by 100+ companies. Learn more
-:-------------------------:|:-------------------------:
+
+
+
+
+
+
+
+
+
Table of Contents
@@ -115,7 +171,9 @@ Installing
python -m playwright install
-If you would prefer a video walk through of setting up this package I created a currently semi-outdated YouTube video just for that.
+If you would prefer a video walk through of setting up this package YouTube video just for that.
+
+If you want a quick video to listen for TikTok Live events in python.
Docker Installation
@@ -142,17 +200,15 @@ Quick Start Guide
from TikTokApi import TikTokApi
-# In your web browser you will need to go to TikTok, check the cookies
-# and under www.tiktok.com s_v_web_id should exist, and use that value
-# as input to custom_verify_fp
-# Or watch https://www.youtube.com/watch?v=-uCt1x8kINQ for a visual
-api = TikTokApi(custom_verify_fp="")
-
-for trending_video in api.trending.videos(count=50):
- # Prints the author's username of the trending video.
- print(trending_video.author.username)
+# Watch https://www.youtube.com/watch?v=-uCt1x8kINQ for a brief setup tutorial
+with TikTokApi() as api:
+ for trending_video in api.trending.videos(count=50):
+ # Prints the author's username of the trending video.
+ print(trending_video.author.username)
+Note: Jupyter (ipynb) only works on linux, see microsoft/playwright-python #178
+
To run the example scripts from the repository root, make sure you use the -m
option on python.
python -m examples.get_trending
@@ -197,10 +253,10 @@ Accessing Dictionary on O
api = TikTokApi.get_instance()
trending_videos = api.by_trending()
-#V5
-api = TikTokApi() # .get_instance no longer exists
-for trending_video in api.trending.videos():
- # do something
+#V5.1
+with TikTokApi() as api: # .get_instance no longer exists
+ for trending_video in api.trending.videos():
+ # do something
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.
@@ -414,4 +470,4 @@ Accessing Dictionary on O
}
});